15 KiB
Ausbaustufe 3 — CSV-Import, Saldo-Anker, Salden-Seite, Parser-Tuning
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: CSV-Kontoumsatz-Import als primärer Import-Weg (DKB/VR/HVB-Exportformate), Konto-Saldo-Anker mit GUI-Pflege, neue Salden-Seite (Stichtag + Monatsübersicht), PDF-Parser per CSV-Ground-Truth verbessern; anschließend beaufsichtigte Vollmigration (PDF-Bestand → CSV-Basis 2025-01-01 bis heute).
Architecture: CSV-Dateien laufen durch die BESTEHENDE Import-Pipeline (Upload/Inbox → ParsedStatement → Statement/Draft → Vorschau → Confirm → Rollback). Neu: parsers/csv_formats.py erkennt die drei Bankformate am Header und liefert ParsedStatement. Saldenrechnung wird von Statement-closing auf Konto-Anker (Kontostand X am Datum Y, neue Account-Spalten via Alembic) umgestellt — rückwärts wie vorwärts vom Anker aus; Grafana-Views analog. Nutzerentscheidungen (2026-07-19): CSV primär; Anker GUI-pflegbar; Prüfschärfe je Format maximal (VR lückenlos, DKB gegen Datei-Kontostand-Anker, HVB ohne — gekennzeichnet); Migration führt der Controller beaufsichtigt durch.
Tech Stack: unverändert; Version → 0.4.0.
Global Constraints
- Repo
/home/wlfb/bin, Codefinance/; GeldbeträgeDecimal; Nutzertexte Deutsch; Suite grün vor jedem Commit (Basis: 79 passed); Commit-TrailerCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>. - DATENSCHUTZ:
tests/fixtures/*.csvund*.pdfsind echte Kontodaten (gitignored). Keine echten Daten in Commits/Reports/Code. Erwartungswerte nur in gitignortentests/fixtures/expected_*.json-Dateien. - TEST-VERIFIKATION (Nutzer-Vorgabe): Nach jeder Task-Implementierung führt ein SUBAGENT MIT FABLE-MODELL die Test-/Verifikationspassage aus (Suite + task-spezifische Checks) und berichtet unabhängig; der Implementer-Selbstbericht genügt nicht.
- UX-Regel (etabliert): Bedienelemente immer sichtbar, Nichtverfügbares disabled mit title.
- Deployment erst in Task 6.
CSV-Formatreferenz (aus Datei-Analyse 2026-07-19)
| DKB_*.csv | VR_*.csv | HVB_*.csv | |
|---|---|---|---|
| Encoding | UTF-8 mit BOM | UTF-8 mit BOM, CRLF | UTF-16 |
| Trenner | ;, Werte in " |
;, unquoted |
; |
| Kopf | 4 Metazeilen: Girokonto/IBAN; Zeitraum; „Kontostand vom TT.MM.JJJJ:" + Betrag mit €; Leerzeile — dann Spaltenkopf | Spaltenkopf in Zeile 1 | Spaltenkopf in Zeile 1 |
| Spalten | Buchungsdatum;Wertstellung;Status;Zahlungspflichtiger;Zahlungsempfängerin;Verwendungszweck;Umsatztyp;IBAN;Betrag (€);Gläubiger-ID;Mandatsreferenz;Kundenreferenz | Bezeichnung Auftragskonto;IBAN Auftragskonto;BIC Auftragskonto;Bankname Auftragskonto;Buchungstag;Valutadatum;Name Zahlungsbeteiligter;IBAN Zahlungsbeteiligter;BIC (SWIFT-Code) Zahlungsbeteiligter;Buchungstext;Verwendungszweck;Betrag;Waehrung;Saldo nach Buchung;Bemerkung;Gekennzeichneter Umsatz;Glaeubiger ID;Mandatsreferenz | Kontonummer;Buchungsdatum;Valuta;Verwendungszweck;Betrag;Waehrung |
| Datumsformat | TT.MM.JJ (zweistellig!) | TT.MM.JJJJ | TT.MM.JJJJ |
| Sortierung | absteigend | absteigend | aufsteigend |
| Konto-Zuordnung | IBAN aus Kopfzeile | IBAN Auftragskonto | Kontonummer → Konto per iban.endswith(kontonummer) |
| counterparty | Betrag<0 → Zahlungsempfängerin, sonst Zahlungspflichtiger | Name Zahlungsbeteiligter | "" (steckt im Verwendungszweck) |
| purpose | Umsatztyp + " " + Verwendungszweck | Buchungstext + " " + Verwendungszweck | Verwendungszweck |
| value_date | Wertstellung | Valutadatum | Valuta |
| Saldo-Prüfung | Datei-Kontostand als KONTO-ANKER übernehmen (Datum aus „Kontostand vom"); keine per-Datei-Prüfung (Kontostand = Exportzeitpunkt, nicht Periodenende) | Saldo nach Buchung: chronologisch je Zeile prüfen saldo_n = saldo_{n-1} + betrag_n → daraus opening/closing für ParsedStatement ableiten (balance_difference==0 greift); Anker = neuester Saldo |
keine (opening/closing None, preview „Saldo-Prüfung: nicht verfügbar") |
| Sonstiges | nur Zeilen mit Status "Gebucht" importieren | Abschluss-Zeilen (leerer Name) normal importieren | Betrag-Spalte ist maßgeblich (Text enthält Betrag redundant) |
Beträge deutsch formatiert (1.234,56, Minus führend) → parse_german_amount wiederverwenden (€-Zeichen/EUR vorher strippen).
Task 1: Konto-Saldo-Anker (Modell, Migration, Saldenrechnung, GUI, Views)
Files:
- Modify:
finance/app/models/tables.py(Account:anchor_date: Mapped[date | None],anchor_balance: Mapped[Decimal | None]= Numeric(12,2)) - Create:
finance/alembic/versions/<autogen>_konto_anker.py(autogenerate, add_column x2) - Modify:
finance/app/services/balances.py,finance/app/routers/accounts.py(PATCH um anchor-Felder),finance/app/templates/index.html(Anker-Anzeige+Pflegeformular je Konto),finance/app/models/views.py(Views ankerbasiert) - Test:
finance/tests/test_crud_api.py,finance/tests/test_gui.py
Interfaces:
-
Produces:
balances.account_balance(session, account, at: date | None = None) -> Decimal—atdefault heute. Mit Anker:anchor_balance + Σ(tx: anchor_date < booking_date <= at) − Σ(tx: at < booking_date <= anchor_date)(nur bestätigte; eine der Summen ist leer). Ohne Anker:Σ(tx: booking_date <= at). Die bisherige Statement-closing-Logik ENTFÄLLT ersatzlos (Statements bleiben als Import-Einheiten bestehen). -
PATCH /api/accounts/{id}zusätzlich:anchor_date: date | None,anchor_balance: Decimal | None(beide optional, model_fields_set-Semantik wie bei name; nur gemeinsam gesetzt oder gemeinsam null → sonst 422 „Anker braucht Datum und Betrag"). -
GUI Übersicht: je Konto Ankeranzeige („Anker: X € am TT.MM.JJJJ" oder „kein Anker") + Inline-Formular (Datum+Betrag, json-form hx-patch).
-
views.py:v_balance_history/v_balance_totalseeden pro Konto mitanchor_balance − Σ(tx > … bis anchor_date)statt Statement-opening — konkret: Basis je Konto =account_balancezum frühesten Buchungsdatum − dessen Tagesumsatz; einfachste korrekte Form: CTE mit Anker-Join und kumulativer Fenstersumme relativ zum Anker.v_projectionunverändert. -
Step 1: Failing Tests — test_crud_api: (a) Konto mit Anker (Datum D, Betrag B) + bestätigte Buchungen vor/nach D →
account_balance(at=D)==B,at=D+2daddiert spätere Buchung,at=D−2dsubtrahiert dazwischenliegende; (b) PATCH setzt/löscht Anker, 422 bei nur-einem-Feld; test_gui: Übersicht zeigt Ankertext. -
Step 2: FAIL · Step 3: Implementieren + Alembic-Autogenerate gegen Wegwerf-SQLite (Muster Task 2 Ausbaustufe 1; Migration committen) · Step 4: PASS, Suite grün
-
Step 5: FABLE-TESTAGENT verifiziert unabhängig (Suite + Anker-Randfälle: at==anchor_date, Buchung AM Ankertag zählt nicht zur Vorwärtssumme/wohl zur Rückwärtssumme — Konvention: Anker gilt per Tagesende).
-
Step 6: Commit —
feat: Konto-Saldo-Anker mit ankerbasierter Saldenrechnung
Task 2: CSV-Format-Parser
Files:
- Create:
finance/app/parsers/csv_formats.py,finance/tests/test_csv_formats.py - Modify:
finance/.gitignoreist bereits umtests/fixtures/*.csvergänzt (erledigt)
Interfaces:
-
Produces:
csv_formats.detect_csv_format(head_bytes: bytes) -> str | None(„dkb_csv"|„vr_csv"|„hvb_csv"; erkennt Encoding inkl. UTF-16/BOM + Headersignatur);csv_formats.parse_csv(path: Path) -> ParsedCsvmitParsedCsv(statement: ParsedStatement, anchor: tuple[date, Decimal] | None, balance_checkable: bool). Transaktionen chronologisch aufsteigend im ParsedStatement, Mapping exakt gemäß Formatreferenz oben. VR: Saldo-Kette prüfen, bei BruchParserError(„VR-CSV: Saldo-Kette inkonsistent bei Zeile N"); opening/closing setzen (balance_checkable=True). DKB: anchor=(Datum aus „Kontostand vom", Betrag), opening/closing=None, nur „Gebucht". HVB: alles None/False. -
ParserError bei unbekanntem Format/kaputter Struktur.
-
Step 1: Failing Tests — pro Format parametrisiert (skip wenn Datei fehlt): Zeilenzahl >0, Transaktionen aufsteigend, Beträge Decimal, VR: balance_difference(stmt)==0 und anchor gesetzt, DKB: anchor gesetzt + alle Status Gebucht + counterparty je Vorzeichen befüllt, HVB: balance_checkable False; plus
expected_csv_<fmt>.json(gitignored, Schema wie expected_*: transaction_count + 5 spot_checks) — Dateien erstellt der CONTROLLER nach Implementierung aus Stichproben. -
Step 2: FAIL · Step 3: Implementieren (csv-Modul der stdlib; Encoding-Erkennung: BOM-Sniff + UTF-16-Heuristik über Nullbytes) · Step 4: PASS
-
Step 5: CONTROLLER erstellt expected-Dateien (Stichproben aus den echten CSVs, nur lokal) · Step 6: FABLE-TESTAGENT verifiziert (Suite + eigene Stichproben-Gegenkontrolle CSV-Zeile↔geparste Transaktion für je 3 Zeilen pro Format, feldweise).
-
Step 7: Commit —
feat: CSV-Parser fuer DKB/VR/HVB-Kontoumsatz-Exporte
Task 3: CSV in die Import-Pipeline
Files:
- Modify:
finance/app/services/importer.py(Dateiendungs-Weiche: .csv → parse_csv; Anker-Autofill: bei confirm... nein: beim IMPORTParsedCsv.anchoram Statement zwischenspeichern → einfacher: importer setzt Anker DIREKT am Konto beim process, nur wennanchor_dateneuer als vorhandener Anker oder keiner existiert),finance/app/routers/imports.py(Upload akzeptiert .csv; 400-Text anpassen „Nur PDF- oder CSV-Dateien"), Inbox-Scan*.csvzusätzlich,finance/app/templates/_preview_table.html/_import_list.html(balance_ok dreiwertig: True „Saldo plausibel" / False rot / None „Saldo-Prüfung: nicht verfügbar (Format ohne Saldodaten)"),finance/app/templates/hilfe.html(CSV als primärer Weg, PDF als Fallback) - Test:
finance/tests/test_import_api.py
Interfaces:
-
Produces: preview-Response
balance_ok: bool | None. Statement.bank erhält Formatkennung („dkb_csv" etc.). Duplikatschutz unverändert über dedup_hash (funktioniert für künftige überlappende CSV-Exporte, da identische Texte). -
Anker-Regel: Import (bereits beim Draft-Anlegen) aktualisiert
account.anchor_*nur, wenn CSV-Anker-Datum ≥ bestehendes Anker-Datum oder Konto ohne Anker; manuelles Überschreiben bleibt via PATCH möglich. -
Step 1: Failing Tests — Upload einer synthetischen Mini-CSV je Format (im Test als Bytes konstruiert nach Formatreferenz — KEINE echten Daten): 201 draft, preview balance_ok-Wert je Format (VR True, HVB None), confirm übernimmt, DKB-Anker am Konto gesetzt; Upload .txt → 400; Inbox-Scan findet .csv.
-
Step 2: FAIL · Step 3: Implementieren · Step 4: PASS, Suite grün
-
Step 5: FABLE-TESTAGENT verifiziert (Suite + Upload der ECHTEN 6 Dateien gegen Test-Instanz in-memory: je 201+plausible Zeilenzahlen — Zahlen nur als Counts berichten).
-
Step 6: Commit —
feat: CSV-Import ueber bestehende Pipeline mit dreiwertiger Saldo-Pruefung
Task 4: Salden-Seite
Files:
- Create:
finance/app/templates/salden.html - Modify:
finance/app/routers/gui.py(GET /salden mit gui_session; Stichtag-Param lenient wie Filter),finance/app/templates/base.html(Nav „Salden" zwischen Buchungen und Planung),finance/app/templates/hilfe.html(Konzept-Absatz),finance/tests/test_gui.py
Interfaces:
-
GET /salden[?stichtag=YYYY-MM-DD] → Abschnitt 1 „Salden am " (Default heute): Tabelle Konto→Saldo (account_balance(at=stichtag)) + Gesamtzeile; Datumsformular (GET). Abschnitt 2 „Monatsanfangs-Salden": Zeilen = 1. jedes Monats von frühestem Buchungsmonat bis heute (absteigend), Spalten = Konten + Gesamt; Werte = account_balance(at=Monatsersten). Beträge 2 Nachkommastellen, .neg-Klasse, Konten ohne Anker mit Fußnote „ohne Anker — relative Werte".
-
Step 1: Failing Tests — /salden in beide Seiten-Tests; mit Seed (Anker + Buchungen über 3 Monate): Stichtags-Tabelle zeigt erwarteten Kontosaldo, Monatsübersicht enthält 3 Monatszeilen mit korrektem Wert für einen definierten Monatsersten.
-
Step 2: FAIL · Step 3: Implementieren · Step 4: PASS · Step 5: FABLE-TESTAGENT (Suite + Handrechnung eines Monatsanfangs-Werts gegen Seed-Daten) · Step 6: Commit —
feat: Salden-Seite mit Stichtag und Monatsuebersicht
Task 5: PDF-Parser-Tuning per CSV-Ground-Truth (Fable-Audit)
Files:
- Create:
finance/scripts/parser_vs_csv.py(lokales Vergleichswerkzeug) - Modify:
finance/app/parsers/dkb.py(ggf. vr.py/hvb.py),finance/tests/fixtures/expected_*.json(Controller, lokal)
Interfaces:
-
parser_vs_csv.py <pdf> <csv...>: matcht PDF-Transaktionen gegen CSV-Zeilen im Überlappungszeitraum über (booking_date, amount); gibt je Match Feldvergleich counterparty/purpose aus (Ähnlichkeit + Differenzen), am Ende Metrik: N matched, counterparty-Übereinstimmung x %, purpose-Kernübereinstimmung y %. Nur lokal, echte Daten nur im gitignorten Workspace. -
Step 1: Werkzeug bauen (Subagent) · Step 2: CONTROLLER-Audit (Fable): Werkzeug auf DKB/VR/HVB-Überlappung (Juni–Juli 2026) laufen lassen, konkrete strukturelle Verbesserungen identifizieren (v. a. DKB: Empfänger/Zweck-Trennung — CSV-Empfängerspalte zeigt, wo die Grenze liegt; erwartbar: Empfänger = Anfang der ersten Fortsetzungszeile bis zum ersten Schlüsselwort-Muster). Mängelliste in
.superpowers/sdd/parser-vs-csv-audit.md(gitignored). -
Step 3: Parser-Fixes (Subagent, Mängelliste als Brief) — Saldo-Gates und bestehende expected-Tests bleiben Pflicht; expected_*.json passt der CONTROLLER an, wo sich purposes verbessern.
-
Step 4: FABLE-TESTAGENT: Suite + Metrik-Vergleich vorher/nachher (Werkzeug erneut ausführen; Verbesserung dokumentieren, Zahlen ohne echte Daten). Fable-Abnahme durch Controller.
-
Step 5: Commit —
fix: PDF-Parser per CSV-Ground-Truth verbessert(generische Beschreibung). Hinweis: „möglichst gut" = Best effort; verbleibende strukturbedingte Grenzen (z. B. DKB-Fließtext) werden dokumentiert, nicht erzwungen.
Task 6: Version 0.4.0, Redeploy, beaufsichtigte Vollmigration
Files: finance/VERSION → 0.4.0; keine weiteren Code-Änderungen.
- Step 1: VERSION bump + Commit
chore: Version 0.4.0→ Image-Build →create_pod_finance.sh(Alembic-Migration läuft im Entrypoint). - Step 2 (CONTROLLER, beaufsichtigt): (a) 3 PDF-Importe zurückrollen (Rollback-Feature); (b) HVB-Anker setzen: PATCH anchor_date=2026-07-03, anchor_balance=4559.51 (aus PDF-Auszug); (c) die 6 CSVs importieren (Reihenfolge egal, je: Upload → Preview prüfen [Zeilenzahl plausibel; VR balance_ok True; DKB/HVB Kennzeichnung sichtbar] → Confirm); DKB/VR-Anker kommen automatisch aus den Dateien; (d) Endkontrolle: Buchungszahl gesamt = Summe der CSV-Zeilen (minus evtl. Nicht-Gebucht), Salden-Seite: DKB-Saldo heute == Datei-Kontostand, VR-Saldo == neuester Saldo nach Buchung, HVB plausibel gegen Anker; Grafana-Kurven ab 2025; Suite grün; Smoke der Kernseiten.
- Step 3: Ledger + Plan-Häkchen + Commit; Hilfe-/Memory-Konsistenz.
Abschluss-Checkliste
- Suite grün (>= 90 Tests erwartet); alle Task-Verifikationen durch FABLE-Testagenten dokumentiert
- Live: v0.4.0; Datenbestand vollständig aus 6 CSVs (2025-01-01 bis heute), Anker je Konto gesetzt, Salden-Seite konsistent mit CSV-Quellwerten
- Parser-Tuning-Metriken dokumentiert (vorher/nachher), Fable-Abnahme erteilt
- Keine echten Kontodaten/Secrets in git log beider Repos; CSV/PDF/expected-Dateien untracked
- Plan-Häkchen gesetzt, beide Repos committet