mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-19 14:19:52 +03:00
Mark categorization root rules with new field, fix boolean equality check
This commit is contained in:
parent
3653dc5861
commit
acfbe7033a
6 changed files with 27 additions and 12 deletions
|
@ -22,6 +22,8 @@ public class Categorization extends AbstractEntity implements UserOwned {
|
|||
@Convert(converter = CategorizationRule.JpaConverter.class)
|
||||
private CategorizationRule categorizationRule;
|
||||
|
||||
private boolean isRoot;
|
||||
|
||||
private String stringValue;
|
||||
|
||||
private Double numericGreaterThan;
|
||||
|
@ -149,4 +151,12 @@ public class Categorization extends AbstractEntity implements UserOwned {
|
|||
public void setRightCategorizationId(Long rightCategorizationId) {
|
||||
this.rightCategorizationId = rightCategorizationId;
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
return isRoot;
|
||||
}
|
||||
|
||||
public void setRoot(boolean root) {
|
||||
isRoot = root;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ public enum CategorizationRule implements PersistableEnum<String> {
|
|||
STRING_REGEX(RawTransactionValueType.STRING),
|
||||
STRING_EQ(RawTransactionValueType.STRING),
|
||||
STRING_CONTAINS(RawTransactionValueType.STRING),
|
||||
STRING_IS_EMPTY(RawTransactionValueType.STRING),
|
||||
NUMERIC_GREATER_THAN(RawTransactionValueType.NUMERIC),
|
||||
NUMERIC_LESS_THAN(RawTransactionValueType.NUMERIC),
|
||||
NUMERIC_EQUALS(RawTransactionValueType.NUMERIC),
|
||||
|
|
|
@ -13,13 +13,11 @@ import java.util.Collection;
|
|||
@Repository
|
||||
public interface CategorizationRepository extends JpaRepository<Categorization, Long> {
|
||||
|
||||
// We fetch only the ones with non-null category ids
|
||||
// because ones with null category are used in AND, OR or NOT logical operations
|
||||
@Query(
|
||||
value = """
|
||||
SELECT cat.*
|
||||
FROM categories.categorization AS cat
|
||||
WHERE user_id = :userId AND category_id IS NOT NULL
|
||||
WHERE user_id = :userId
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
|
@ -31,7 +29,7 @@ public interface CategorizationRepository extends JpaRepository<Categorization,
|
|||
childCats AS (
|
||||
SELECT root.*
|
||||
FROM categories.categorization AS root
|
||||
WHERE root.category_id = :categoryId
|
||||
WHERE root.category_id = :categoryId AND root.is_root = TRUE
|
||||
|
||||
UNION ALL
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ public class CategoryService {
|
|||
|
||||
// Run each category's rules for all transactions in parallel to eachother
|
||||
final var futures = categorizations.stream()
|
||||
.filter(Categorization::isRoot)
|
||||
.collect(Collectors.groupingBy(Categorization::getCategoryId, HashMap::new, Collectors.toList()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
|
@ -138,13 +139,14 @@ public class CategoryService {
|
|||
private boolean matchesRule(final Collection<Categorization> allCategorizations, final Categorization categorization, final ProcessedTransaction processedTransaction) {
|
||||
return switch (categorization.getCategorizationRule()) {
|
||||
// string operations
|
||||
case STRING_REGEX, STRING_EQ, STRING_CONTAINS -> {
|
||||
case STRING_REGEX, STRING_EQ, STRING_CONTAINS, STRING_IS_EMPTY -> {
|
||||
final String fieldValue = fetchTransactionStringValue(categorization, processedTransaction);
|
||||
|
||||
yield switch (categorization.getCategorizationRule()) {
|
||||
case STRING_EQ -> fieldValue.equalsIgnoreCase(categorization.getStringValue());
|
||||
case STRING_REGEX -> fieldValue.matches(categorization.getStringValue());
|
||||
case STRING_CONTAINS -> fieldValue.contains(categorization.getStringValue());
|
||||
case STRING_CONTAINS -> fieldValue.toLowerCase().contains(categorization.getStringValue().toLowerCase());
|
||||
case STRING_IS_EMPTY -> fieldValue == null || fieldValue.isBlank();
|
||||
default -> throw new CommonFinancesException("Unsupported string rule: %s", categorization.getCategorizationRule());
|
||||
};
|
||||
}
|
||||
|
@ -271,7 +273,7 @@ public class CategoryService {
|
|||
public Collection<CategorizationDTO> fetchCategorizationRules(Long categoryId) {
|
||||
final var categorizations = categorizationRepository.fetchForCategory(categoryId);
|
||||
return categorizationRepository.fetchForCategory(categoryId).stream()
|
||||
.filter(c -> c.getCategoryId() != null)
|
||||
.filter(Categorization::isRoot)
|
||||
.map(c -> mapCategorization(categorizations, c))
|
||||
.toList();
|
||||
}
|
||||
|
@ -312,16 +314,17 @@ public class CategoryService {
|
|||
categorizationRepository.deleteAllForCategory(categoryId);
|
||||
|
||||
return dtos.stream()
|
||||
.map(dto -> saveCategorizationRule(categoryId, userId, dto).getId())
|
||||
.map(dto -> saveCategorizationRule(true, categoryId, userId, dto).getId())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Categorization saveCategorizationRule(Long categoryId, Integer userId, CreateCategorizationDTO dto) {
|
||||
private Categorization saveCategorizationRule(boolean isRoot, Long categoryId, Integer userId, CreateCategorizationDTO dto) {
|
||||
// TODO: Avoid recursion
|
||||
|
||||
final var categorization = new Categorization();
|
||||
|
||||
categorization.setCategorizationRule(dto.rule());
|
||||
categorization.setRoot(isRoot);
|
||||
categorization.setUserId(userId);
|
||||
categorization.setRuleBasedOn(dto.ruleBasedOn());
|
||||
categorization.setCategoryId(categoryId);
|
||||
|
@ -331,17 +334,17 @@ public class CategoryService {
|
|||
categorization.setNumericValue(dto.numericValue().orElse(null));
|
||||
categorization.setTimestampGreaterThan(dto.timestampGreaterThan().orElse(null));
|
||||
categorization.setTimestampLessThan(dto.timestampLessThan().orElse(null));
|
||||
categorization.setBooleanValue(dto.booleanValue().orElse(null));
|
||||
categorization.setBooleanValue(dto.booleanValue().orElse(false));
|
||||
|
||||
// Only root rules have category id set, to differentiate them from non-roots
|
||||
// TODO: This smells bad. Add an isRoot property instead?
|
||||
if (dto.left() != null) {
|
||||
final var leftCat = saveCategorizationRule(null, userId, dto.left());
|
||||
final var leftCat = saveCategorizationRule(false, categoryId, userId, dto.left());
|
||||
categorization.setLeftCategorizationId(leftCat.getId());
|
||||
}
|
||||
|
||||
if (dto.right() != null) {
|
||||
final var rightCat = saveCategorizationRule(null, userId, dto.right());
|
||||
final var rightCat = saveCategorizationRule(false, categoryId, userId, dto.right());
|
||||
categorization.setRightCategorizationId(rightCat.getId());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE categories.categorization ADD COLUMN IF NOT EXISTS is_root BOOLEAN DEFAULT(FALSE);
|
|
@ -80,6 +80,7 @@ export default function CategorizationRule({ ruleData, fields, ruleTypes, onDele
|
|||
case "BOOLEAN_EQ":
|
||||
case "NUMERIC_EQUALS":
|
||||
case "STRING_EQ": return "equals";
|
||||
case "STRING_IS_EMPTY": return "is empty";
|
||||
case "TIMESTAMP_GREATER_THAN": return "is later than";
|
||||
case "NUMERIC_GREATER_THAN": return "is greater than";
|
||||
case "TIMESTAMP_LESS_THAN": return "is earlier than";
|
||||
|
@ -220,6 +221,7 @@ export default function CategorizationRule({ ruleData, fields, ruleTypes, onDele
|
|||
/>
|
||||
</Grid>
|
||||
);
|
||||
case "STRING_IS_EMPTY": return ("");
|
||||
case "BOOLEAN_EQ": return (
|
||||
<Grid xs={1} lg={1}>
|
||||
<Checkbox
|
||||
|
|
Loading…
Add table
Reference in a new issue