diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2ce79f7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,154 @@ +# CLAUDE.md — Repo `~/bin` + +> Diese Datei richtet sich primär an künftige Claude-Coding-Sessions in +> diesem Repo, sekundär an Menschen. Architektur/Modul-Übersicht (Mermaid): +> **`docs/ARCHITEKTUR.md`**. Vor Code-Änderungen dort das Komponenten- und +> Sequenzdiagramm lesen — jeder dort genannte Name existiert real im Code. + +## Projektüberblick + +Finanzberatungs-Tool (Verzeichnis `finance/`): FastAPI-App (API + Web-GUI), +Postgres, Grafana-Dashboards. Importiert Kontoauszüge (PDF: VR/HVB/DKB; +CSV: VR/HVB/DKB), verwaltet Buchungen/Kategorien/Fixposten, rechnet +Kredit-Tilgungspläne und Liquiditäts-Szenarien durch. Läuft als Podman-Pod +`finance_pod` (3 Container) unter systemd `--user`, siehe +`docs/ARCHITEKTUR.md` Abschnitt 3 (Deployment). Aktuelle Version: +`finance/VERSION` (Single Source — siehe dort). + +Zugehöriges Repo `/home/wlfb/fb` ist die **Beratungsumgebung** (Skills, +Gesprächsleitfäden für die Nutzung des Tools in der Finanzberatung selbst, +eigenes `CLAUDE.md`) — nicht der Code dieses Tools. Dieses Repo (`~/bin`) +enthält Quellcode, Deployment- und Betriebsskripte. + +## Betriebskommandos + +**Test-Suite** (Basis-Erwartung: alle grün, dreistellige Anzahl): +```bash +cd finance && .venv/bin/python -m pytest -q +``` + +**Deploy / Redeploy** (baut das Image neu, erstellt Pod+Container neu, +generiert systemd-Units, aktiviert den Service, wartet auf Bereitschaft): +```bash +./create_pod_finance.sh +``` +Idempotent — erneutes Ausführen rotiert **keine** Secrets in einer +bestehenden `.env` (Ausnahme: `FB_GUI_PASSWORD_HASH` wird bei jedem Lauf mit +`FB_PASSWORD` synchron gehalten, siehe Passwort-Prozedur unten). Legacy-Pfad +`~/bin/finance/.env` wird beim ersten Lauf automatisch nach +`~/.local/share/finance_pod/.env` migriert. + +**Backup** (kalt, kurze Downtime, Zielverzeichnis optional): +```bash +./backup_finance_pod.sh [ZIELDIR] # Default: ~/backups +``` +Stoppt `pod-finance_pod.service`, sichert `~/.local/share/finance_pod` +(inkl. `.env`) via `tar czf` **im `podman unshare`-Namespace** (Postgres-Daten +gehören auf dem Host der subuid-verschobenen Container-UID und sind sonst +unlesbar), startet den Service wieder, prüft Erreichbarkeit +(`GET /login` → 200) und Archiv-Integrität (`.env`, `postgres-data`, +`grafana-data`, `data` müssen enthalten sein), setzt `chmod 600` (Archiv +enthält Secrets). Bei jedem Fehler läuft der Service danach trotzdem wieder +(EXIT-Trap). + +**Passwort ändern** (GUI + Grafana teilen sich `FB_PASSWORD`): +- **Vorzugsweg (Admin-Seite):** `/admin` in der GUI aufrufen — ändert altes + Passwort gegen Grafana (`services/admin.py::change_password`), schreibt + danach die `.env` **in-place** neu. Bestehende Sitzungen bleiben gültig + (Session-Secret ändert sich nicht). +- **Handverfahren:** `FB_PASSWORD` in + `~/.local/share/finance_pod/.env` von Hand editieren (Wert + **single-quoted** lassen!) und `./create_pod_finance.sh` erneut ausführen + — das Skript leitet `FB_GUI_PASSWORD_HASH` automatisch neu ab und + synchronisiert Grafana per `grafana cli admin reset-admin-password`. + +**Parser-Werkzeuge** (nur lokal, siehe DATENSCHUTZ unten): +```bash +cd finance +PYTHONPATH=. .venv/bin/python scripts/parser_audit.py tests/fixtures/dkb_beispiel.pdf +PYTHONPATH=. .venv/bin/python scripts/parser_vs_csv.py [...] +``` +`parser_audit.py` zeigt geparste Transaktionen + PDF-Rohtext einer Fixture. +`parser_vs_csv.py` vergleicht ein PDF-Parser-Ergebnis gegen CSV-Ground-Truth +(Betrag/Datum-Match, Gegenpartei/Verwendungszweck-Ähnlichkeit via +`difflib`). Beide geben echte Kontodaten auf der Konsole aus. + +## WICHTIGE REGELN für künftige Sessions + +**`tests/fixtures/*` = ECHTE Kontodaten — NIE committen oder zitieren.** +`finance/tests/fixtures/*.pdf`, `*.csv` und `expected_*.json` sind reale +Kontoauszüge (via `finance/.gitignore` vom Repo ausgeschlossen). In Commits, +PR-Beschreibungen, Reports, Ledger-Einträgen oder Chat-Antworten dürfen +weder Dateiinhalte noch daraus abgeleitete Beträge/Namen/IBANs auftauchen. +Ausgaben von `parser_audit.py`/`parser_vs_csv.py` sind ausschließlich zur +lokalen Fehlersuche bestimmt, niemals zum Weiterreichen. + +**Decimal, nicht float.** Geldbeträge sind überall `decimal.Decimal` +(DB-Spaltentyp `Numeric(12,2)`, siehe `models/tables.py::MONEY`). Keine +`float`-Arithmetik für Beträge einführen (Rundungsfehler). + +**Deutsch.** GUI-Texte, Fehlermeldungen, Templates, Commit-Messages und +Beratungsinhalte sind auf Deutsch. Datumsformat TT.MM.JJJJ. + +**UX-Regel: Bedienelemente sichtbar, nicht versteckt.** Formulare/Buttons +bleiben immer sichtbar und bedienbar; Sperrzustände werden über `disabled` +ausgedrückt, nicht durch Entfernen des Elements aus dem DOM. Fehler werden +inline auf derselben Seite gemeldet (Beispiel: `routers/admin.py` gibt bei +Validierungsfehlern dasselbe Template mit `error`-Kontext zurück statt +umzuleiten oder das Formular verschwinden zu lassen). + +**Fable-Testagent-Gate (Nutzer-Vorgabe).** Jeder Task in einem +Superpowers-Plan (`docs/superpowers/plans/*.md`) braucht vor dem Commit eine +Abnahme durch den Fable-Testagenten (Faktencheck/Live-Test je nach Task). +Kein Task gilt als abgeschlossen, solange dieses Gate nicht durchlaufen ist +— auch nicht bei scheinbar trivialen Doku-/Config-Änderungen. + +**Superpowers-Workflow.** Pläne liegen unter `docs/superpowers/plans/` +(ein Plan pro Ausbaustufe, z.B. `2026-07-20-ausbaustufe-4.md`), gegliedert +in Tasks mit Checkbox-Tracking. Fortschritt/Entscheidungen/offene Punkte +werden fortlaufend im Ledger `.superpowers/sdd/progress.md` festgehalten +(ein Eintrag pro abgeschlossenem Task, inkl. Commit-Range und Fable-Befund). +Vor Arbeitsbeginn an einem Task: Plan-Datei UND Ledger lesen — der Ledger +enthält oft bindende Nutzerentscheidungen und Warnungen aus früheren Tasks +(z.B. Migrations-Hinweise), die nicht im Plan selbst stehen. + +**Versionierung via `finance/VERSION`.** Einzige Versionsquelle (siehe +`docs/ARCHITEKTUR.md` Abschnitt 4). Speist Image-Tag +(`create_pod_finance.sh`), GUI-Footer und `GET /api/version` +(`app/version.py::get_version`). Bei einem Release: `VERSION` hochzählen, +bevor `create_pod_finance.sh` läuft — sonst baut das Skript ein Image mit +altem Tag. + +**`.env` unter `~/.local/share/finance_pod/`, nicht im Repo-Checkout.** +Secrets liegen unter `$BIND_DIR/.env` (`chmod 600`), niemals unter +`finance/.env` (Legacy-Pfad, wird migriert). Alle Werte darin sind +**single-quoted** zu schreiben (`KEY='wert'`) — `create_pod_finance.sh` +sourced die Datei per `set -a; . "$ENV_FILE"; set +a`, und unquoted Werte +mit `$` (z.B. der GUI-Passwort-Hash `salt$digest`) würden dabei fälschlich +shell-expandiert. Der API-Container mountet dieselbe Datei zusätzlich als +**Einzeldatei-Bind-Mount** nach `/data/.env` — das folgt dem Host-Inode, die +App muss sie **in-place** überschreiben (`open` im Modus `r+`, `flock`, +`truncate` — siehe `services/admin.py::_rewrite_env_file`), niemals über +Temp-Datei+`rename` (neuer Inode, vom laufenden Mount nicht mehr gesehen). +Host-seitige `sed`-Edits in `create_pod_finance.sh` selbst sind unkritisch, +weil das Skript die Container ohnehin bei jedem Lauf neu erstellt. + +**Disaster Recovery = `BIND_DIR`-Backup + Repo + Skript.** Für vollständige +Wiederherstellung werden **beide** gebraucht: ein Backup von +`~/.local/share/finance_pod` (Secrets + Postgres-/Grafana-Daten, via +`backup_finance_pod.sh`) UND der Repo-Checkout `~/bin` (Code für den +Image-Build, `create_pod_finance.sh`, `finance/grafana/` für die +Provisioning-Mounts). Wiederherstellung: Backup nach +`~/.local/share/finance_pod` entpacken, dann `./create_pod_finance.sh` +erneut ausführen (baut Image, erstellt Pod/Container/systemd-Units neu, +übernimmt vorhandene `.env` unverändert). Verifiziert in A2-Task 7 +(Live-DR-Probe, siehe `.superpowers/sdd/progress.md`). + +## Weiterführend + +- Architektur (Module, Funktionen, drei Mermaid-Diagramme): + `docs/ARCHITEKTUR.md` +- Pläne: `docs/superpowers/plans/` +- Fortschritts-Ledger: `.superpowers/sdd/progress.md` +- Beratungsumgebung (Nutzung des Tools, Skills): `/home/wlfb/fb` + (eigenes `CLAUDE.md` dort) diff --git a/docs/ARCHITEKTUR.md b/docs/ARCHITEKTUR.md new file mode 100644 index 0000000..584b5ad --- /dev/null +++ b/docs/ARCHITEKTUR.md @@ -0,0 +1,314 @@ +# Architektur — Finanzberatungs-Tool (`finance/`) + +> Diese Doku richtet sich **primär an künftige Claude-Coding-Sessions** in +> diesem Repo, sekundär an Menschen. Sie beschreibt den Stand nach +> Ausbaustufe 4 (v0.5.0). Jeder hier genannte Modul-/Funktionsname existiert +> real im Code unter `finance/app/` — bei Unsicherheit den Pfad öffnen statt +> zu raten. Betriebs-/Konventionsregeln stehen in `/home/wlfb/bin/CLAUDE.md`. + +## 1. Komponenten-Diagramm + +Kernachse: `routers/* → services/* → engine/* + parsers/* → models/tables.py +→ db.py`. `routers/gui.py` ist ein Sonderfall — es importiert +Handler-Funktionen direkt aus anderen Routern (`imports.py`, `planning.py`, +`scenarios.py`, `transactions.py`), um serverseitige Logik nicht zu +duplizieren. `routers/admin.py` importiert `gui_session` und `templates` +direkt aus `routers/gui.py`. + +```mermaid +flowchart TD + subgraph TPL["templates (Jinja2, app/templates/)"] + T_pages["base.html, login.html, index.html
transactions.html, salden.html
import.html, planning.html
admin.html, hilfe.html"] + end + + subgraph ROUTERS["routers/"] + R_gui["gui.py
gui_session, index, buchungen,
salden_page, import_page, planung_page"] + R_accounts["accounts.py
list_accounts, create_account,
patch_account"] + R_transactions["transactions.py
list_transactions, create_transaction,
patch_transaction, count_transactions"] + R_categories["categories.py
list_categories, create_category_rule"] + R_imports["imports.py
upload, scan_inbox, preview,
confirm, rollback"] + R_planning["planning.py
list_recurring, list_planned,
list_loans, recurring_suggestions"] + R_scenarios["scenarios.py
create_scenario, add_modifier,
project_scenario"] + R_admin["admin.py
admin_page, admin_change_password,
apply_category_rules"] + end + + subgraph SERVICES["services/"] + S_importer["importer.py
process_file, process_pdf, process_csv"] + S_balances["balances.py
account_balance(at), total_balance"] + S_categorize["categorize.py
apply_rules"] + S_suggestions["suggestions.py
suggest_recurring"] + S_projection["projection_service.py
run_projection"] + S_admin["admin.py
change_password, apply_rules_retroactively"] + end + + subgraph ENGINE["engine/ (rein, kein DB-Zugriff)"] + E_loans["loans.py
loan_schedule, annuity_payment, add_months"] + E_recurrence["recurrence.py
occurrences"] + E_projection["projection.py
project"] + E_scenario["scenario.py
build_cashflows (PlainRecurring/
PlainPlanned/PlainModifier)"] + end + + subgraph PARSERS["parsers/"] + P_base["base.py
parse_german_amount, parse_german_date"] + P_detect["detect.py
detect_bank"] + P_validate["validate.py
balance_difference, dedup_hash"] + P_vr["vr.py — parse"] + P_hvb["hvb.py — parse"] + P_dkb["dkb.py — parse"] + P_csv["csv_formats.py
detect_csv_format, parse_csv"] + P_registry["registry.py — parse_pdf"] + end + + subgraph MODELS["models/ + alembic/"] + M_tables["tables.py
Account, Statement, Transaction,
RecurringItem, PlannedItem, Loan,
Scenario, ProjectionResult, ..."] + M_views["views.py — create_views()
v_balance_history, v_balance_total,
v_monthly_by_category, v_projection"] + M_alembic["alembic/versions
b2b1f5a18a74_initial_schema
1ef6a356f028_konto_anker"] + end + + R_gui --> T_pages + R_admin --> T_pages + + R_gui --> R_imports + R_gui --> R_planning + R_gui --> R_scenarios + R_gui --> R_transactions + R_gui --> E_loans + R_gui --> E_recurrence + R_gui --> S_balances + R_admin --> R_gui + + R_accounts --> S_balances + R_transactions --> P_validate + R_transactions --> S_categorize + R_imports --> S_importer + R_planning --> E_loans + R_planning --> S_suggestions + R_scenarios --> S_projection + R_admin --> S_admin + + ROUTERS -.-> M_tables + + S_importer --> P_csv + S_importer --> P_registry + S_importer --> P_validate + S_importer --> S_categorize + S_projection --> E_loans + S_projection --> E_projection + S_projection --> E_scenario + S_projection --> S_balances + S_admin --> S_categorize + + E_scenario --> E_loans + E_scenario --> E_recurrence + + P_registry --> P_vr + P_registry --> P_hvb + P_registry --> P_dkb + P_registry --> P_detect + P_vr --> P_base + P_hvb --> P_base + P_dkb --> P_base + P_csv --> P_base + P_validate --> P_base + + SERVICES -.-> M_tables + M_alembic -.->|erzeugt Schema fuer| M_tables + M_tables -.->|Basis fuer| M_views +``` + +Nicht im Diagramm (Querschnitt, siehe Prosa unten): `app/auth.py` +(`require_auth`, `current_password_hash`, `session_valid` — an fast allen +Routern als FastAPI-Dependency), `app/config.py` (`get_settings`), +`app/db.py` (`get_session`, `get_engine`), `app/main.py` (bindet alle Router ++ `lifespan` → `create_views`), `app/version.py` (`get_version`). + +## 2. Sequenzdiagramm — Import-Fluss + +```mermaid +sequenceDiagram + participant B as Browser/GUI + participant RI as routers/imports.py + participant IM as services/importer.py + participant PA as parsers (registry.py / csv_formats.py) + participant VA as parsers/validate.py + participant CA as services/categorize.py + participant DB as Postgres + + Note over B,RI: Upload ODER Inbox-Scan + alt Datei-Upload + B->>RI: POST /api/imports/upload (PDF/CSV) + else Inbox-Scan + B->>RI: POST /api/imports/scan-inbox + Note right of RI: iteriert *.pdf und *.csv in FB_INBOX_DIR + end + + RI->>IM: process_file(session, path) + alt Endung .csv + IM->>PA: parse_csv(path) [csv_formats.detect_csv_format] + else Endung .pdf (Default) + IM->>PA: parse_pdf(path) [registry.detect_bank -> vr/hvb/dkb.parse] + end + PA-->>IM: ParsedStatement / ParsedCsv + + IM->>IM: _find_or_create_account(bank, iban) + IM->>DB: INSERT Statement(status="draft") + + opt Saldo pruefbar (PDF immer; CSV nur VR: balance_checkable=True) + IM->>VA: balance_difference(parsed) + Note right of IM: Differenz != 0 -> Statement.status="error", Abbruch + end + + loop je Buchungszeile + IM->>VA: dedup_hash(account_id, booking_date, amount, purpose) + IM->>DB: INSERT Transaction(status="draft", is_duplicate=?) + end + IM->>CA: apply_rules(session, drafts) + IM->>IM: _apply_anchor_autofill(account, anchor) + Note right of IM: nur wenn CSV-Anker-Datum >= Account.anchor_date (oder kein Anker) + IM->>DB: Datei inbox -> uploads verschieben, COMMIT + IM-->>RI: Statement (draft) + RI-->>B: 201 StatementOut + + B->>RI: GET /api/imports/{id}/preview + RI->>DB: SELECT Transactions WHERE statement_id=id + RI->>RI: balance_ok = true/false/None (dreiwertig:
None bei fehlenden Salden, z.B. HVB/DKB-CSV) + RI-->>B: PreviewOut(balance_ok, duplicates) + + alt Bestaetigen + B->>RI: POST /api/imports/{id}/confirm + loop je Draft-Transaktion (nicht is_duplicate) + RI->>DB: Dedup-Recheck: dedup_hash bereits confirmed
in ANDEREM Statement? + alt Kollision gefunden + RI->>DB: tx.is_duplicate = true (bleibt draft) + else keine Kollision + RI->>DB: tx.status = "confirmed" + end + end + RI->>DB: Statement.status = "confirmed" + RI-->>B: 200 StatementOut + else Verwerfen (Rollback) + B->>RI: POST /api/imports/{id}/rollback + Note right of RI: nur wenn Statement.status == "confirmed" + RI->>DB: DELETE Transactions + Statement + RI-->>B: 200 {deleted_transactions, statement_id} + end +``` + +## 3. Deployment-Diagramm + +```mermaid +flowchart TB + subgraph HOST["Host wlfb (rootless Podman)"] + Boot["create_pod_finance.sh
Bootstrap: FB_PASSWORD erzeugen
-> PBKDF2-Hash (FB_GUI_PASSWORD_HASH)
-> Grafana-Sync (grafana cli
admin reset-admin-password)"] + Backup["backup_finance_pod.sh [ZIELDIR]
KALT: systemctl stop -> tar czf
via 'podman unshare' (subuid-Daten)
-> systemctl start -> Readiness-Curl
-> tar -tzf Integritaet -> chmod 600"] + + subgraph SYSTEMD["systemd --user (Unit-Dateien in ~/.config/systemd/user, je chmod 600)"] + U1["pod-finance_pod.service"] + U2["container-finance-db_ctr.service"] + U3["container-finance-api_ctr.service"] + U4["container-finance-grafana_ctr.service"] + end + + subgraph POD["Pod finance_pod (podman pod)"] + API["finance-api_ctr
127.0.0.1:8096 -> 8000
uvicorn app.main:app
(entrypoint.sh: alembic upgrade head)"] + DBC["finance-db_ctr
Postgres 17.10
NUR pod-intern: localhost:5432
(kein Host-Port)"] + GRA["finance-grafana_ctr
127.0.0.1:8097 -> 3000
grafana-oss 12.1.0"] + end + + subgraph BINDDIR["Bind-Mounts ~/.local/share/finance_pod (BIND_DIR, chmod 700)"] + DATA["data/ (-v DATA_DIR:/data:Z)
inbox/, uploads/"] + ENVF[".env (-v ENV_FILE:/data/.env:Z)
EINZELDATEI-Mount folgt dem INODE!
App schreibt IN-PLACE (open r+,
flock, truncate) - NIE Temp+rename
(services/admin.py _rewrite_env_file)"] + PGD["postgres-data/"] + GRD["grafana-data/ (setgid, gid 0)"] + end + + subgraph REPO["Repo-Checkout ~/bin/finance/grafana (read-only Mounts)"] + PROV["grafana/provisioning
-> /etc/grafana/provisioning:Z,ro"] + DASH["grafana/dashboards/finanzen.json
-> /var/lib/grafana/dashboards:Z,ro"] + end + end + + Boot -->|erzeugt/aktualisiert| ENVF + Boot -->|podman generate systemd --new| SYSTEMD + Boot -->|podman run| API + Boot -->|podman run| DBC + Boot -->|podman run| GRA + Boot -.->|exec grafana cli admin reset-admin-password| GRA + + SYSTEMD -->|verwaltet Start/Stop| POD + + API -->|liest/schreibt in-place| ENVF + API -->|Daten| DATA + API -.->|SQL localhost:5432| DBC + DBC --> PGD + GRA --> GRD + GRA --> PROV + GRA --> DASH + GRA -.->|SQL read-only Rolle finance_read| DBC + API -.->|HTTP PUT .../password, siehe change_password| GRA + + Backup -->|stoppt/startet| U1 + Backup -->|tar czf BIND_DIR| BINDDIR +``` + +## 4. Zentrale Konzepte (kompakt) + +**Saldo-Anker (per Tagesende).** `Account.anchor_date` / +`Account.anchor_balance` (Migration `1ef6a356f028_konto_anker`) ersetzen die +frühere Statement-closing-Logik als Basis der Saldenrechnung. +`services/balances.py::account_balance(session, account, at)` addiert bzw. +subtrahiert bestätigte Buchungen zwischen Anker und `at`; der Anker gilt +**per Tagesende** des Ankerdatums (enthält bereits alle Buchungen bis +einschließlich diesem Tag). CSV-Importe füllen einen fehlenden oder +veralteten Anker automatisch nach (`_apply_anchor_autofill` in +`services/importer.py`) — ein manuell per PATCH gesetzter, neuerer Anker +bleibt dabei unangetastet. Die Postgres-Views `v_balance_history` / +`v_balance_total` (`models/views.py`) bilden dieselbe Anker-Semantik als +lückenlose Tagesreihe (Carry-Forward via `generate_series` + Fenster-`SUM`) +für Grafana ab. + +**`dedup_hash`.** `parsers/validate.py::dedup_hash(account_id, booking_date, +amount, purpose)` erzeugt einen deterministischen Hash je Buchung. Er wird +zweimal genutzt: (1) beim Import markiert `services/importer.py` Entwürfe +als `is_duplicate`, wenn der Hash bereits unter einer *bestätigten* +Transaktion existiert; (2) beim Bestätigen (`routers/imports.py::confirm`) +erfolgt ein **Dedup-Recheck** gegen zwischenzeitlich bestätigte Buchungen +*anderer* Statements — zwei identische Buchungen *innerhalb* desselben +Statements teilen sich zwar denselben Hash, dürfen sich aber nicht +gegenseitig ausschließen, da die in der Preview geprüfte Bilanz von beiden +abhängt. + +**Statement-Lebenszyklus.** `Statement.status` (`models/tables.py`) durchläuft +`draft` → `confirmed`, oder `draft` → `error` (Bank/Format nicht erkannt, +Saldo-Differenz, kaputte Datei). Transaktionen haben denselben +Status-Wortschatz (`draft`/`confirmed`) pro Zeile. Bestätigte Statements sind +nur noch über `rollback` (löscht Statement + Transaktionen komplett) zu +entfernen, nicht mehr über `DELETE /api/imports/{id}` (nur für Entwürfe und Fehl-Importe). + +**Szenario-Engine ist rein.** `app/engine/*` (`loans.py`, `recurrence.py`, +`projection.py`, `scenario.py`) hat **keinen** Datenbank- oder +Request-Zugriff — nur `scenario.py` importiert innerhalb der Engine von +`loans.py`/`recurrence.py`, sonst keine Abhängigkeiten auf `app.*`. Ein-/ +Ausgaben sind einfache Dataclasses (`Installment`, `PlainRecurring`, +`PlainPlanned`, `PlainModifier`, `Projection`). `services/projection_service.py` +ist die einzige Brücke: es lädt ORM-Objekte (`Loan`, `RecurringItem`, +`PlannedItem`, `Scenario`, `ScenarioModifier`), baut daraus die Plain-Objekte +und ruft `engine.scenario.build_cashflows` → `engine.projection.project`. + +**`VERSION`-Datei als Single Source.** `finance/VERSION` (ein Einzeiler, z.B. +`0.4.0`) ist die einzige Versionsquelle: `app/version.py::get_version()` +liest sie (Fallback `"0.0.0-dev"`), gespeist in den Jinja2-Footer +(`templates.env.globals["app_version"]` in `routers/gui.py`) und +`GET /api/version` (`main.py`). `Containerfile` kopiert `VERSION` ins Image; +`create_pod_finance.sh` liest dieselbe Datei für den Image-Tag +(`API_IMAGE="localhost/finance-api:$(cat "$FINANCE_DIR/VERSION")"`). Ein +Release ist somit: `VERSION` hochzählen → Image bauen → Redeploy. + +**DATENSCHUTZ: Fixtures enthalten echte Kontodaten.** `finance/tests/fixtures/` +enthält reale PDF-/CSV-Kontoauszüge (`*.pdf`, `*.csv`, `expected_*.json`) — +siehe `finance/tests/fixtures/README.md` und `finance/.gitignore` (alle drei +Muster ausgeschlossen). `scripts/parser_audit.py` und +`scripts/parser_vs_csv.py` geben Klartext-Kontodaten auf der Konsole aus und +sind ausdrücklich mit "NUR lokal verwenden" markiert — ihre Ausgabe darf +nicht in Commits, Reports, Tickets oder Chat-Antworten landen. Details und +verbindliche Regeln: siehe `CLAUDE.md`.