Files
bin/docs/superpowers/plans/2026-07-19-ausbaustufe-3.md
2026-07-19 22:10:23 +02:00

142 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- [x] **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.
- [x] **Step 2: FAIL** · **Step 3: Implementieren + Alembic-Autogenerate gegen Wegwerf-SQLite** (Muster Task 2 Ausbaustufe 1; Migration committen) · **Step 4: PASS, Suite grün**
- [x] **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).
- [x] **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.
- [x] **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.
- [x] **Step 2: FAIL** · **Step 3: Implementieren** (csv-Modul der stdlib; Encoding-Erkennung: BOM-Sniff + UTF-16-Heuristik über Nullbytes) · **Step 4: PASS**
- [x] **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).
- [x] **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.
- [x] **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.
- [x] **Step 2: FAIL** · **Step 3: Implementieren** · **Step 4: PASS, Suite grün**
- [x] **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).
- [x] **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".
- [x] **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.
- [x] **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.
- [x] **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).
- [x] **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.
- [x] **Step 4: FABLE-TESTAGENT**: Suite + Metrik-Vergleich vorher/nachher (Werkzeug erneut ausführen; Verbesserung dokumentieren, Zahlen ohne echte Daten). **Fable-Abnahme durch Controller.**
- [x] **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.
- [x] **Step 1:** VERSION bump + Commit `chore: Version 0.4.0` → Image-Build → `create_pod_finance.sh` (Alembic-Migration läuft im Entrypoint).
- [x] **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.
- [x] **Step 3:** Ledger + Plan-Häkchen + Commit; Hilfe-/Memory-Konsistenz.
---
## Abschluss-Checkliste
- [x] Suite grün (>= 90 Tests erwartet); alle Task-Verifikationen durch FABLE-Testagenten dokumentiert
- [x] 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
- [x] Parser-Tuning-Metriken dokumentiert (vorher/nachher), Fable-Abnahme erteilt
- [x] Keine echten Kontodaten/Secrets in git log beider Repos; CSV/PDF/expected-Dateien untracked
- [x] Plan-Häkchen gesetzt, beide Repos committet