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)
|
@Convert(converter = CategorizationRule.JpaConverter.class)
|
||||||
private CategorizationRule categorizationRule;
|
private CategorizationRule categorizationRule;
|
||||||
|
|
||||||
|
private boolean isRoot;
|
||||||
|
|
||||||
private String stringValue;
|
private String stringValue;
|
||||||
|
|
||||||
private Double numericGreaterThan;
|
private Double numericGreaterThan;
|
||||||
|
@ -149,4 +151,12 @@ public class Categorization extends AbstractEntity implements UserOwned {
|
||||||
public void setRightCategorizationId(Long rightCategorizationId) {
|
public void setRightCategorizationId(Long rightCategorizationId) {
|
||||||
this.rightCategorizationId = 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_REGEX(RawTransactionValueType.STRING),
|
||||||
STRING_EQ(RawTransactionValueType.STRING),
|
STRING_EQ(RawTransactionValueType.STRING),
|
||||||
STRING_CONTAINS(RawTransactionValueType.STRING),
|
STRING_CONTAINS(RawTransactionValueType.STRING),
|
||||||
|
STRING_IS_EMPTY(RawTransactionValueType.STRING),
|
||||||
NUMERIC_GREATER_THAN(RawTransactionValueType.NUMERIC),
|
NUMERIC_GREATER_THAN(RawTransactionValueType.NUMERIC),
|
||||||
NUMERIC_LESS_THAN(RawTransactionValueType.NUMERIC),
|
NUMERIC_LESS_THAN(RawTransactionValueType.NUMERIC),
|
||||||
NUMERIC_EQUALS(RawTransactionValueType.NUMERIC),
|
NUMERIC_EQUALS(RawTransactionValueType.NUMERIC),
|
||||||
|
|
|
@ -13,13 +13,11 @@ import java.util.Collection;
|
||||||
@Repository
|
@Repository
|
||||||
public interface CategorizationRepository extends JpaRepository<Categorization, Long> {
|
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(
|
@Query(
|
||||||
value = """
|
value = """
|
||||||
SELECT cat.*
|
SELECT cat.*
|
||||||
FROM categories.categorization AS cat
|
FROM categories.categorization AS cat
|
||||||
WHERE user_id = :userId AND category_id IS NOT NULL
|
WHERE user_id = :userId
|
||||||
""",
|
""",
|
||||||
nativeQuery = true
|
nativeQuery = true
|
||||||
)
|
)
|
||||||
|
@ -31,7 +29,7 @@ public interface CategorizationRepository extends JpaRepository<Categorization,
|
||||||
childCats AS (
|
childCats AS (
|
||||||
SELECT root.*
|
SELECT root.*
|
||||||
FROM categories.categorization AS root
|
FROM categories.categorization AS root
|
||||||
WHERE root.category_id = :categoryId
|
WHERE root.category_id = :categoryId AND root.is_root = TRUE
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@ public class CategoryService {
|
||||||
|
|
||||||
// Run each category's rules for all transactions in parallel to eachother
|
// Run each category's rules for all transactions in parallel to eachother
|
||||||
final var futures = categorizations.stream()
|
final var futures = categorizations.stream()
|
||||||
|
.filter(Categorization::isRoot)
|
||||||
.collect(Collectors.groupingBy(Categorization::getCategoryId, HashMap::new, Collectors.toList()))
|
.collect(Collectors.groupingBy(Categorization::getCategoryId, HashMap::new, Collectors.toList()))
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -138,13 +139,14 @@ public class CategoryService {
|
||||||
private boolean matchesRule(final Collection<Categorization> allCategorizations, final Categorization categorization, final ProcessedTransaction processedTransaction) {
|
private boolean matchesRule(final Collection<Categorization> allCategorizations, final Categorization categorization, final ProcessedTransaction processedTransaction) {
|
||||||
return switch (categorization.getCategorizationRule()) {
|
return switch (categorization.getCategorizationRule()) {
|
||||||
// string operations
|
// 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);
|
final String fieldValue = fetchTransactionStringValue(categorization, processedTransaction);
|
||||||
|
|
||||||
yield switch (categorization.getCategorizationRule()) {
|
yield switch (categorization.getCategorizationRule()) {
|
||||||
case STRING_EQ -> fieldValue.equalsIgnoreCase(categorization.getStringValue());
|
case STRING_EQ -> fieldValue.equalsIgnoreCase(categorization.getStringValue());
|
||||||
case STRING_REGEX -> fieldValue.matches(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());
|
default -> throw new CommonFinancesException("Unsupported string rule: %s", categorization.getCategorizationRule());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -271,7 +273,7 @@ public class CategoryService {
|
||||||
public Collection<CategorizationDTO> fetchCategorizationRules(Long categoryId) {
|
public Collection<CategorizationDTO> fetchCategorizationRules(Long categoryId) {
|
||||||
final var categorizations = categorizationRepository.fetchForCategory(categoryId);
|
final var categorizations = categorizationRepository.fetchForCategory(categoryId);
|
||||||
return categorizationRepository.fetchForCategory(categoryId).stream()
|
return categorizationRepository.fetchForCategory(categoryId).stream()
|
||||||
.filter(c -> c.getCategoryId() != null)
|
.filter(Categorization::isRoot)
|
||||||
.map(c -> mapCategorization(categorizations, c))
|
.map(c -> mapCategorization(categorizations, c))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
@ -312,16 +314,17 @@ public class CategoryService {
|
||||||
categorizationRepository.deleteAllForCategory(categoryId);
|
categorizationRepository.deleteAllForCategory(categoryId);
|
||||||
|
|
||||||
return dtos.stream()
|
return dtos.stream()
|
||||||
.map(dto -> saveCategorizationRule(categoryId, userId, dto).getId())
|
.map(dto -> saveCategorizationRule(true, categoryId, userId, dto).getId())
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Categorization saveCategorizationRule(Long categoryId, Integer userId, CreateCategorizationDTO dto) {
|
private Categorization saveCategorizationRule(boolean isRoot, Long categoryId, Integer userId, CreateCategorizationDTO dto) {
|
||||||
// TODO: Avoid recursion
|
// TODO: Avoid recursion
|
||||||
|
|
||||||
final var categorization = new Categorization();
|
final var categorization = new Categorization();
|
||||||
|
|
||||||
categorization.setCategorizationRule(dto.rule());
|
categorization.setCategorizationRule(dto.rule());
|
||||||
|
categorization.setRoot(isRoot);
|
||||||
categorization.setUserId(userId);
|
categorization.setUserId(userId);
|
||||||
categorization.setRuleBasedOn(dto.ruleBasedOn());
|
categorization.setRuleBasedOn(dto.ruleBasedOn());
|
||||||
categorization.setCategoryId(categoryId);
|
categorization.setCategoryId(categoryId);
|
||||||
|
@ -331,17 +334,17 @@ public class CategoryService {
|
||||||
categorization.setNumericValue(dto.numericValue().orElse(null));
|
categorization.setNumericValue(dto.numericValue().orElse(null));
|
||||||
categorization.setTimestampGreaterThan(dto.timestampGreaterThan().orElse(null));
|
categorization.setTimestampGreaterThan(dto.timestampGreaterThan().orElse(null));
|
||||||
categorization.setTimestampLessThan(dto.timestampLessThan().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
|
// Only root rules have category id set, to differentiate them from non-roots
|
||||||
// TODO: This smells bad. Add an isRoot property instead?
|
// TODO: This smells bad. Add an isRoot property instead?
|
||||||
if (dto.left() != null) {
|
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());
|
categorization.setLeftCategorizationId(leftCat.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.right() != null) {
|
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());
|
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 "BOOLEAN_EQ":
|
||||||
case "NUMERIC_EQUALS":
|
case "NUMERIC_EQUALS":
|
||||||
case "STRING_EQ": return "equals";
|
case "STRING_EQ": return "equals";
|
||||||
|
case "STRING_IS_EMPTY": return "is empty";
|
||||||
case "TIMESTAMP_GREATER_THAN": return "is later than";
|
case "TIMESTAMP_GREATER_THAN": return "is later than";
|
||||||
case "NUMERIC_GREATER_THAN": return "is greater than";
|
case "NUMERIC_GREATER_THAN": return "is greater than";
|
||||||
case "TIMESTAMP_LESS_THAN": return "is earlier than";
|
case "TIMESTAMP_LESS_THAN": return "is earlier than";
|
||||||
|
@ -220,6 +221,7 @@ export default function CategorizationRule({ ruleData, fields, ruleTypes, onDele
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
case "STRING_IS_EMPTY": return ("");
|
||||||
case "BOOLEAN_EQ": return (
|
case "BOOLEAN_EQ": return (
|
||||||
<Grid xs={1} lg={1}>
|
<Grid xs={1} lg={1}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
Loading…
Add table
Reference in a new issue