diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Categorization.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Categorization.java index 0fa2e8c..8d501d3 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Categorization.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/entity/Categorization.java @@ -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; + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/CategorizationRule.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/CategorizationRule.java index 9a2c15c..fea625e 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/CategorizationRule.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/CategorizationRule.java @@ -8,6 +8,7 @@ public enum CategorizationRule implements PersistableEnum { 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), diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/CategorizationRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/CategorizationRepository.java index 0adcc0b..7750f90 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/CategorizationRepository.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/CategorizationRepository.java @@ -13,13 +13,11 @@ import java.util.Collection; @Repository public interface CategorizationRepository extends JpaRepository { - // 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 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 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()); } diff --git a/PersonalFinancesService/src/main/resources/db/migration/V1.16__AddCategorizationIsRoot.sql b/PersonalFinancesService/src/main/resources/db/migration/V1.16__AddCategorizationIsRoot.sql new file mode 100644 index 0000000..c42a8e5 --- /dev/null +++ b/PersonalFinancesService/src/main/resources/db/migration/V1.16__AddCategorizationIsRoot.sql @@ -0,0 +1 @@ +ALTER TABLE categories.categorization ADD COLUMN IF NOT EXISTS is_root BOOLEAN DEFAULT(FALSE); \ No newline at end of file diff --git a/frontend/src/components/categories/CategorizationRule.jsx b/frontend/src/components/categories/CategorizationRule.jsx index 7875502..2429bc9 100644 --- a/frontend/src/components/categories/CategorizationRule.jsx +++ b/frontend/src/components/categories/CategorizationRule.jsx @@ -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 /> ); + case "STRING_IS_EMPTY": return (""); case "BOOLEAN_EQ": return (