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:
Guido
2026-08-20 16:37:35 +02:00
co-authored by Claude Opus 4.8
parent 64cbbc8cf6
commit d1cab19264
5 changed files with 84 additions and 13 deletions
+13 -5
View File
@@ -249,11 +249,19 @@ def test_transfer_rule_and_target_guess(admin):
assert rows[1]["to_account_id"] is None
def test_transfer_keyword_conflicts_with_category_keyword(admin):
a, _ = _expense_cats(admin, 2)
admin.post(f"/api/categories/{a['id']}/keywords", json={"word": "doppelwort"})
# gleiches Wort als Transfer-Regel -> Konflikt
assert admin.post("/api/categories/transfer-keywords", json={"word": "doppelwort"}).status_code == 409
def test_same_word_across_kinds_allowed(admin):
a, b = _expense_cats(admin, 2)
inc = _income_cat(admin)
# Dasselbe Wort für eine AUSGABE- und eine EINNAHME-Kategorie -> beides erlaubt.
r1 = admin.post(f"/api/categories/{a['id']}/keywords", json={"word": "querart"})
assert r1.status_code == 201, r1.text
r2 = admin.post(f"/api/categories/{inc['id']}/keywords", json={"word": "querart"})
assert r2.status_code == 201, r2.text
assert r1.json()["id"] != r2.json()["id"]
# ... und auch als Transfer-Regel (dritte Art) erlaubt.
assert admin.post("/api/categories/transfer-keywords", json={"word": "querart"}).status_code == 201
# Aber ein zweites Ausgabe-Wort gleicher Art bleibt ein Konflikt.
assert admin.post(f"/api/categories/{b['id']}/keywords", json={"word": "querart"}).status_code == 409
def test_no_retroactive_apply(admin):