Mark categorization root rules with new field, fix boolean equality check

This commit is contained in:
Miroslav Vasilev 2023-12-30 20:47:11 +02:00
parent 3653dc5861
commit acfbe7033a
6 changed files with 27 additions and 12 deletions

View file

@ -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;
}
}

View file

@ -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),

View file

@ -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

View file

@ -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());
}

View file

@ -0,0 +1 @@
ALTER TABLE categories.categorization ADD COLUMN IF NOT EXISTS is_root BOOLEAN DEFAULT(FALSE);

View file

@ -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