feat: gleiches Schlagwort für Einnahme UND Ausgabe erlaubt (Art je Regel)
- CategorizationRule bekommt `kind` (expense|income|transfer); Unique jetzt (owner, match_field, pattern, kind) statt ohne kind - Konfliktprüfung/Anlage (add/update Keyword, Transfer-Keyword, gelernte Regeln) arbeiten kind-bewusst: dasselbe Wort je einmal pro Art möglich - Migration 0009 mit Backfill (kind aus Kategorie-Art bzw. is_transfer) - Tests angepasst: quer über Arten erlaubt, gleiche Art weiterhin Konflikt (71 grün) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -220,11 +220,14 @@ def _conflict(db: Session, clash: CategorizationRule) -> HTTPException:
|
||||
)
|
||||
|
||||
|
||||
def _find_any_rule(db: Session, user: User, pattern: str, exclude_id: int | None = None):
|
||||
def _find_any_rule(db: Session, user: User, pattern: str, kind: str, exclude_id: int | None = None):
|
||||
"""Sucht ein manuelles Schlagwort gleicher Art (kind). Dasselbe Wort darf je
|
||||
einmal für Ausgabe, Einnahme und Transfer existieren – Konflikt nur je Art."""
|
||||
q = db.query(CategorizationRule).filter(
|
||||
CategorizationRule.owner_id == user.id,
|
||||
CategorizationRule.match_field == MatchField.any,
|
||||
CategorizationRule.pattern == pattern,
|
||||
CategorizationRule.kind == kind,
|
||||
)
|
||||
if exclude_id is not None:
|
||||
q = q.filter(CategorizationRule.id != exclude_id)
|
||||
@@ -279,7 +282,7 @@ def update_keyword(
|
||||
if not pattern:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Leeres Schlagwort")
|
||||
if pattern != rule.pattern:
|
||||
clash = _find_any_rule(db, user, pattern, exclude_id=rule.id)
|
||||
clash = _find_any_rule(db, user, pattern, rule.kind, exclude_id=rule.id)
|
||||
if clash is not None:
|
||||
raise _conflict(db, clash)
|
||||
rule.pattern = pattern
|
||||
@@ -329,14 +332,15 @@ def add_keyword(
|
||||
pattern = normalize_text(payload.word)[:255]
|
||||
if not pattern:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Leeres Schlagwort")
|
||||
existing = _find_any_rule(db, user, pattern)
|
||||
kind = cat.type.value # "expense" | "income" – Konflikt nur innerhalb derselben Art
|
||||
existing = _find_any_rule(db, user, pattern, kind)
|
||||
if existing is not None:
|
||||
if not existing.is_transfer and existing.category_id == cat.id and existing.amount_cents == payload.amount_cents:
|
||||
return _keyword_out(existing) # schon identisch vorhanden – unverändert zurück
|
||||
raise _conflict(db, existing)
|
||||
rule = CategorizationRule(
|
||||
owner_id=user.id, match_field=MatchField.any, pattern=pattern,
|
||||
category_id=cat.id, amount_cents=payload.amount_cents,
|
||||
category_id=cat.id, amount_cents=payload.amount_cents, kind=kind,
|
||||
source=RuleSource.manual, priority=10,
|
||||
)
|
||||
db.add(rule)
|
||||
@@ -374,7 +378,7 @@ def add_transfer_keyword(
|
||||
pattern = normalize_text(payload.word)[:255]
|
||||
if not pattern:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Leeres Schlagwort")
|
||||
existing = _find_any_rule(db, user, pattern)
|
||||
existing = _find_any_rule(db, user, pattern, "transfer")
|
||||
if existing is not None:
|
||||
if existing.is_transfer and existing.amount_cents == payload.amount_cents:
|
||||
return _transfer_keyword_out(existing)
|
||||
@@ -382,7 +386,7 @@ def add_transfer_keyword(
|
||||
rule = CategorizationRule(
|
||||
owner_id=user.id, match_field=MatchField.any, pattern=pattern,
|
||||
category_id=None, amount_cents=payload.amount_cents, is_transfer=True,
|
||||
source=RuleSource.manual, priority=10,
|
||||
kind="transfer", source=RuleSource.manual, priority=10,
|
||||
)
|
||||
db.add(rule)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user