docs: Implementierungsplan Ausbaustufe 3 (CSV-Import, Saldo-Anker, Salden-Seite)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 21:12:15 +02:00
parent 6b02fa3d85
commit 41d556bcde

View File

@@ -0,0 +1,141 @@
# 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`, Code `finance/`; Geldbeträge `Decimal`; Nutzertexte Deutsch; Suite grün vor jedem Commit (Basis: 79 passed); Commit-Trailer `Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`.
- DATENSCHUTZ: `tests/fixtures/*.csv` und `*.pdf` sind echte Kontodaten (gitignored). Keine echten Daten in Commits/Reports/Code. Erwartungswerte nur in gitignorten `tests/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;Zahlungspflichtige*r;Zahlungsempfänger*in;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änger*in, sonst Zahlungspflichtige*r | 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``at` default 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_total` seeden pro Konto mit `anchor_balance Σ(tx > … bis anchor_date)` statt Statement-opening — konkret: Basis je Konto = `account_balance` zum frühesten Buchungsdatum dessen Tagesumsatz; einfachste korrekte Form: CTE mit Anker-Join und kumulativer Fenstersumme relativ zum Anker. `v_projection` unverä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+2d` addiert spätere Buchung, `at=D2d` subtrahiert 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/.gitignore` ist bereits um `tests/fixtures/*.csv` ergä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) -> ParsedCsv` mit `ParsedCsv(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 Bruch `ParserError(„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 IMPORT `ParsedCsv.anchor` am Statement zwischenspeichern → einfacher: importer setzt Anker DIREKT am Konto beim process, nur wenn `anchor_date` neuer als vorhandener Anker oder keiner existiert), `finance/app/routers/imports.py` (Upload akzeptiert .csv; 400-Text anpassen „Nur PDF- oder CSV-Dateien"), Inbox-Scan `*.csv` zusä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 <Datum>" (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 (JuniJuli 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