From 8eee62ee198a375dbac5076e7e7f24bc61a1cf2adef5b5187d798c306a1a60c1 Mon Sep 17 00:00:00 2001 From: wlfb Date: Mon, 20 Jul 2026 21:59:29 +0200 Subject: [PATCH] fix: Datenschutz-Scrub in Tests/Kommentaren + Spec-Nachtrag Vorschlags-Algorithmus Co-Authored-By: Claude Fable 5 --- ...-07-20-vorschlags-algorithmus-v2-design.md | 23 ++++++++++++++++ finance/app/services/suggestions.py | 26 +++++++++--------- finance/tests/test_planning_api.py | 27 ++++++++++--------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md b/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md index d2ec70b..f0135f9 100644 --- a/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md +++ b/docs/superpowers/specs/2026-07-20-vorschlags-algorithmus-v2-design.md @@ -79,3 +79,26 @@ Kandidatenliste berichtet (nur Chat, kein Commit). Fable-Gate je Task. **Außerhalb des Scopes:** halbjährliche Rhythmen (nicht im Datenmodell), automatische Übernahme ohne Nutzer-Klick, Einnahmen-Prognose des Geschäftskontos. + +## Nachtrag (nach Live-Release-Gate, gleiche Session) + +Das erste Live-Gate scheiterte an einem Duplikat: ein kuratierter +„variabel"-Fixposten unter Alias-Namen des Anbieters wurde vom +Bestandsabgleich (a)/(b) nicht erkannt. Daraus zwei Ergänzungen: + +- **Bestandsabgleich-Regel (c) Token-Match:** Vorschlag entfällt auch, wenn + ein Fixposten mit gleichem Rhythmus, Fälligkeitstag ±2 und mindestens + einem gemeinsamen Namens-Token (≥ 5 Zeichen, normalisiert, Split an + Nicht-Alphanumerik) existiert. +- **Volatilitäts-Hinweis:** Wurde die neueste Buchung einer Empfänger-Gruppe + durch den Betrags-Cluster-Split abgetrennt UND gehört sie zu keiner + anderen qualifizierten Serie der Gruppe, erhält der Vorschlag den Zusatz + „Beträge schwanken stark – letzte Buchung weicht ab" (keine + Unterdrückung; die Ausnahme verhindert False-Positives bei parallelen + Verträgen desselben Anbieters). + +Bewiesene Pipeline-Eigenschaft (bindend fürs Verständnis): der +Umfirmierungs-Merge kann die Vorschlagsanzahl nie ändern (Aktiv-Check/ +Fenster erledigen das allein); sein Nutzen ist Kategorie-/Historien- +Kontinuität. Nach einer Umfirmierung entsteht eine Vorschlags-Lücke, bis +der neue Name selbst die Mindestbelege erreicht. diff --git a/finance/app/services/suggestions.py b/finance/app/services/suggestions.py index fc1703e..ddba323 100644 --- a/finance/app/services/suggestions.py +++ b/finance/app/services/suggestions.py @@ -70,15 +70,17 @@ MERGE_GAP_MAX_NUM, MERGE_GAP_MAX_DEN = 16, 10 # 1.6 MERGE_DUE_DAY_TOL = 3 # Schritt 6: Faelligkeitstag-Toleranz in Tagen BESTAND_DUE_DAY_TOL = 2 # Schritt 8: Faelligkeitstag-Toleranz in Tagen -# Bestandsabgleich, Token-Match (Live-Gate A9-Fund, Nachtrag 4): kuratierte +# Bestandsabgleich, Token-Match (Live-Gate-Fund, Nachtrag 4): kuratierte # Fixposten tragen haeufig einen Alias-/Variabel-Namen, der weder Substring -# noch betragsaehnlich zum automatisch erkannten Vorschlag ist (Beispiel aus -# der echten Datenbasis: Fixposten "Mastercard-Abrechnung Volksbank -# (variabel, letzter Wert)" vs. erkannter Vorschlag "Volksbank Ulm-Biberach -# eG" - Betrag weicht um >10% ab, kein Substring-Treffer). Ein gemeinsames, -# hinreichend spezifisches Namens-Token (>=5 Zeichen, um generische Woerter -# wie "Bank" nicht faelschlich matchen zu lassen) bei gleichem Rhythmus und -# nahem Faelligkeitstag gilt als ausreichendes Indiz fuer denselben Fixposten. +# noch betragsaehnlich zum automatisch erkannten Vorschlag ist (Muster: +# ein Sammel-Fixposten fuer eine Kreditkartenabrechnung mit variablem Betrag +# unter einem Alias-Namen des Anbieters deckt den vom Algorithmus erkannten +# Vorschlag desselben Anbieters unter seinem regulaeren Empfaenger-Namen +# nicht ab, weil weder Substring noch Betrags-Toleranz greifen). Ein +# gemeinsames, hinreichend spezifisches Namens-Token (>=5 Zeichen, um +# generische Woerter wie "Bank" nicht faelschlich matchen zu lassen) bei +# gleichem Rhythmus und nahem Faelligkeitstag gilt als ausreichendes Indiz +# fuer denselben Fixposten. TOKEN_MIN_LEN = 5 # Volatilitaets-Hinweis (Live-Gate A9-Fund, Nachtrag 4): wenn der @@ -239,10 +241,10 @@ def _covered_by_existing(cand_name: str, cand_amount: Decimal, rhythm: str, due_ (c) Token-Match: gleicher Rhythmus, Faelligkeitstag-Differenz <= 2 UND mindestens ein gemeinsames Namens-Token (>=5 Zeichen) - faengt kuratierte Alias-/Variabel-Fixposten, deren Name UND Betrag stark - vom automatisch erkannten Vorschlag abweichen (Live-Gate A9-Fund: - Fixposten "Mastercard-Abrechnung Volksbank (variabel, letzter - Wert)" deckt den Vorschlag "Volksbank Ulm-Biberach eG" ab, obwohl - weder (a) noch (b) greifen).""" + vom automatisch erkannten Vorschlag abweichen (Live-Gate-Fund: ein + Sammel-Fixposten unter Alias-Namen des Anbieters deckt den + automatisch erkannten Vorschlag desselben Anbieters unter seinem + regulaeren Empfaenger-Namen ab, obwohl weder (a) noch (b) greifen).""" cand_norm = _norm(cand_name) cand_tokens = _tokens(cand_name) for item in existing: diff --git a/finance/tests/test_planning_api.py b/finance/tests/test_planning_api.py index 5cc1f38..eb9b011 100644 --- a/finance/tests/test_planning_api.py +++ b/finance/tests/test_planning_api.py @@ -329,25 +329,26 @@ def test_suggest_umfirmierung_merge_verschiebt_kategorie_mehrheit(db): assert out[0]["category_id"] == cat_a.id -# ------------------------------------------------- Nachtrag 4 (A9-Live-Gate-Fund) -# Echter Fall aus dem Live-Gate: Fixposten 44 "Mastercard-Abrechnung Volksbank -# (variabel, letzter Wert)" (monthly, due_day 7, -296.07) deckte den Vorschlag -# "Volksbank Ulm-Biberach eG" (monthly, due_day 5, -584.43) nicht ab, weil -# weder Substring- noch Betrags-Toleranz-Regel griffen. Betraege/Namen hier -# synthetisch nachgebildet (keine echten Kontodaten, siehe CLAUDE.md). +# ------------------------------------------------- Nachtrag 4 (Live-Gate-Fund) +# Live-Gate-Fund (Muster, keine echten Kontodaten - Namen/Betraege hier rein +# synthetisch): ein kuratiertes Sammel-Fixposten unter Alias-Namen des +# Anbieters ("Kreditkarten-Abrechnung ... (variabel)") deckte den vom +# Algorithmus erkannten Vorschlag desselben Anbieters unter dessen +# regulaerem Empfaenger-Namen nicht ab, weil weder Substring- noch +# Betrags-Toleranz-Regel griffen. def test_suggest_alias_recurring_item_token_match(db): acc = _acc(db) - db.add(RecurringItem(name="Mastercard-Abrechnung Volksbank (variabel, letzter Wert)", - amount=Decimal("-296.07"), rhythm="monthly", due_day=7)) - for d, a in [(date(2026, 4, 5), "-560.00"), (date(2026, 5, 5), "-580.00"), - (date(2026, 6, 5), "-590.00"), (date(2026, 7, 5), "-580.00")]: - _tx(db, acc, d, a, "Volksbank Ulm-Biberach eG") + db.add(RecurringItem(name="Kreditkarten-Abrechnung Musterbank (variabel)", + amount=Decimal("-250.00"), rhythm="monthly", due_day=7)) + for d, a in [(date(2026, 4, 5), "-560.00"), (date(2026, 5, 5), "-575.00"), + (date(2026, 6, 5), "-590.00"), (date(2026, 7, 5), "-575.00")]: + _tx(db, acc, d, a, "Musterbank Neustadt eG") db.commit() # Substring-Match (a) schlaegt fehl (kein Teilstring gemeinsam), Betrags- - # Toleranz (b) auch (-580 vs. -296.07, >10%) - erst der Token-Match (c) - # ueber das gemeinsame Token "volksbank" (Rhythmus gleich, due_day 5 vs. 7 + # Toleranz (b) auch (-575 vs. -250.00, >10%) - erst der Token-Match (c) + # ueber das gemeinsame Token "musterbank" (Rhythmus gleich, due_day 5 vs. 7 # -> Differenz 2 <= 2) deckt den Vorschlag ab. assert suggest_recurring(db, today=TODAY) == []