diff --git a/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md b/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md index b5ace78..34d18ed 100644 --- a/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md +++ b/docs/superpowers/plans/2026-09-29-ausbaustufe-13.md @@ -46,7 +46,7 @@ **Interfaces:** - Produces: `app.engine.scenario.modified_amount(amount: Decimal, category_id: int | None, recurring_id: int | None, modifiers: list[PlainModifier]) -> Decimal | None`; `app.engine.scenario.effective_end(r: PlainRecurring, modifiers: list[PlainModifier]) -> date | None`; `app.services.projection_service.ScenarioInputs` (frozen dataclass: `recurring: list[PlainRecurring]`, `planned: list[PlainPlanned]`, `loans: list[Loan]` (ORM), `modifiers: list[PlainModifier]`); `app.services.projection_service.load_scenario_inputs(session: Session, scenario: Scenario | None) -> ScenarioInputs` — `None` = System-Plan (alle Fixposten, alle globalen Einmalposten, alle Kredite, keine Modifikatoren, keine Szenario-Einmalposten). -- [ ] **Step 1: Failing Tests** — `finance/tests/test_scenario_inputs.py`: +- [x] **Step 1: Failing Tests** — `finance/tests/test_scenario_inputs.py`: ```python from datetime import date @@ -119,9 +119,9 @@ def test_load_scenario_inputs_system_plan(db): assert inp.modifiers == [] ``` -- [ ] **Step 2: Rot prüfen** — `cd finance && .venv/bin/python -m pytest tests/test_scenario_inputs.py -q` → FAIL (`ImportError: cannot import name 'effective_end'`). +- [x] **Step 2: Rot prüfen** — `cd finance && .venv/bin/python -m pytest tests/test_scenario_inputs.py -q` → FAIL (`ImportError: cannot import name 'effective_end'`). -- [ ] **Step 3: Engine umbenennen** — in `finance/app/engine/scenario.py` `def _modified(` → `def modified_amount(` und `def _effective_end(` → `def effective_end(`; in `build_cashflows` die drei Aufrufe anpassen: +- [x] **Step 3: Engine umbenennen** — in `finance/app/engine/scenario.py` `def _modified(` → `def modified_amount(` und `def _effective_end(` → `def effective_end(`; in `build_cashflows` die drei Aufrufe anpassen: ```python for r in recurring: @@ -137,7 +137,7 @@ def test_load_scenario_inputs_system_plan(db): Prüfen, dass keine alten Namen übrig sind: `grep -rn "_modified\|_effective_end" app tests` → keine Treffer. -- [ ] **Step 4: `load_scenario_inputs`** — in `finance/app/services/projection_service.py` Imports ergänzen (`from dataclasses import dataclass`) und vor `run_projection` einfügen; `run_projection` bis einschließlich `flows = …` ersetzen: +- [x] **Step 4: `load_scenario_inputs`** — in `finance/app/services/projection_service.py` Imports ergänzen (`from dataclasses import dataclass`) und vor `run_projection` einfügen; `run_projection` bis einschließlich `flows = …` ersetzen: ```python @dataclass(frozen=True) @@ -199,16 +199,16 @@ def run_projection(session: Session, scenario: Scenario, horizon_days: int, Der Rest von `run_projection` (ab `proj = project(...)`) bleibt unverändert. -- [ ] **Step 5: Grün prüfen** — `.venv/bin/python -m pytest -q` → alle grün, 314 passed (310 + 4), 3 skipped; insbesondere `tests/test_projection.py`, `tests/test_projection_refresh.py`, `tests/test_scenario_engine.py` unverändert grün. +- [x] **Step 5: Grün prüfen** — `.venv/bin/python -m pytest -q` → alle grün, 314 passed (310 + 4), 3 skipped; insbesondere `tests/test_projection.py`, `tests/test_projection_refresh.py`, `tests/test_scenario_engine.py` unverändert grün. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add finance/app/engine/scenario.py finance/app/services/projection_service.py finance/tests/test_scenario_inputs.py git commit -m "refactor: Modifikator-Helfer öffentlich, load_scenario_inputs aus run_projection (A13-T1)" ``` -- [ ] **Step 7: Fable-Gate** — Faktencheck Verhaltensgleichheit: `git diff HEAD~1 -- finance/app/services/projection_service.py` zeigt nur Umzug + `order_by` (Reihenfolge ist für `build_cashflows` irrelevant, da `sorted(flows)`); `include_recurring`/`include_planned`/Szenario-Einmalposten/Szenario-Kredite exakt wie vorher; eine Projektion eines Test-Szenarios vor/nach dem Commit (z. B. per `git stash`-freiem Vergleich zweier Checkouts in einem Wegwerf-Verzeichnis) liefert identische Punkte. +- [x] **Step 7: Fable-Gate** — Faktencheck Verhaltensgleichheit: `git diff HEAD~1 -- finance/app/services/projection_service.py` zeigt nur Umzug + `order_by` (Reihenfolge ist für `build_cashflows` irrelevant, da `sorted(flows)`); `include_recurring`/`include_planned`/Szenario-Einmalposten/Szenario-Kredite exakt wie vorher; eine Projektion eines Test-Szenarios vor/nach dem Commit (z. B. per `git stash`-freiem Vergleich zweier Checkouts in einem Wegwerf-Verzeichnis) liefert identische Punkte. --- @@ -231,7 +231,7 @@ git commit -m "refactor: Modifikator-Helfer öffentlich, load_scenario_inputs au - `share(amount: Decimal, basis: Decimal) -> Decimal | None` (|amount| ÷ basis × 100, eine Nachkommastelle; `basis == 0` ⇒ `None`) - `category_rows(rows: list[BreakdownRow], categories: set[str]) -> list[BreakdownRow]` (nur Kostenzeilen der Kategorien, Reihenfolge wie `rows`) -- [ ] **Step 1: Failing Tests** — `finance/tests/test_breakdown.py`: +- [x] **Step 1: Failing Tests** — `finance/tests/test_breakdown.py`: ```python from datetime import date @@ -410,9 +410,9 @@ def test_category_rows_nur_kosten_der_kategorien(): assert category_rows(rs, {FREI}) == [] ``` -- [ ] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_breakdown.py -q` → FAIL (`ModuleNotFoundError: app.engine.breakdown`). +- [x] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_breakdown.py -q` → FAIL (`ModuleNotFoundError: app.engine.breakdown`). -- [ ] **Step 3: Implementierung** — `finance/app/engine/breakdown.py`: +- [x] **Step 3: Implementierung** — `finance/app/engine/breakdown.py`: ```python """Aufteilung (Ausbaustufe 13): Monatsbeträge je Posten für einen Monat. @@ -545,16 +545,16 @@ def category_rows(rows: list[BreakdownRow], categories: set[str]) -> list[Breakd return [r for r in rows if r.amount < 0 and r.category in categories] ``` -- [ ] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_breakdown.py -q` → PASS; Gesamtsuite grün (Anzahl steigt um die neuen Tests). +- [x] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_breakdown.py -q` → PASS; Gesamtsuite grün (Anzahl steigt um die neuen Tests). -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add finance/app/engine/breakdown.py finance/tests/test_breakdown.py git commit -m "feat: Engine breakdown – Monatsbeträge je Posten, Summen, Kategorie-Filter (A13-T2)" ``` -- [ ] **Step 6: Fable-Gate** — unabhängige Handrechnung mit eigenen Werten: jährlicher/quartalsweiser Posten inkl. Rundung auf den Cent, Annuität mit Zins (Rate aus `annuity_payment` nachrechnen), endfällig (Zinsrate und Schlussmonat), Laufzeitgrenzen und `ende`-Modifikator, Modifikator-Kombination wie in `build_cashflows`; Reinheit: `grep -n "^from\|^import" finance/app/engine/breakdown.py` zeigt nur Stdlib und `app.engine.*`; kein `float`. +- [x] **Step 6: Fable-Gate** — unabhängige Handrechnung mit eigenen Werten: jährlicher/quartalsweiser Posten inkl. Rundung auf den Cent, Annuität mit Zins (Rate aus `annuity_payment` nachrechnen), endfällig (Zinsrate und Schlussmonat), Laufzeitgrenzen und `ende`-Modifikator, Modifikator-Kombination wie in `build_cashflows`; Reinheit: `grep -n "^from\|^import" finance/app/engine/breakdown.py` zeigt nur Stdlib und `app.engine.*`; kein `float`. --- @@ -572,7 +572,7 @@ git commit -m "feat: Engine breakdown – Monatsbeträge je Posten, Summen, Kate Regeln: Werte ≤ 0 entfallen; Basis = Summe der Werte + `rest` (falls > 0); sortiert absteigend nach Wert, bei Gleichstand nach Label; benannt werden höchstens `max_named` Werte mit Anteil ≥ `min_share` (Farben `s1`…); die übrigen: genau einer ⇒ bleibt mit eigenem Label, Farbe `"uebrige"`, `merged=False`; zwei oder mehr ⇒ ein Segment `UEBRIGE` (Farbe `"uebrige"`, `merged=True`, `members` in Größenreihenfolge); `rest` (> 0) wird nie zusammengefasst, kommt als letztes Segment mit Label `rest_label` und Farbe `"frei"`; genau ein Segment ⇒ `path=None`. -- [ ] **Step 1: Failing Tests** — `finance/tests/test_pie.py`: +- [x] **Step 1: Failing Tests** — `finance/tests/test_pie.py`: ```python from decimal import Decimal @@ -653,9 +653,9 @@ def test_rest_none_oder_null_ignoriert(): assert len(pie_slices([("A", D("1"))], rest=D("0"), rest_label="X")) == 1 ``` -- [ ] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_pie.py -q` → FAIL (`ModuleNotFoundError: app.engine.pie`). +- [x] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_pie.py -q` → FAIL (`ModuleNotFoundError: app.engine.pie`). -- [ ] **Step 3: Implementierung** — `finance/app/engine/pie.py`: +- [x] **Step 3: Implementierung** — `finance/app/engine/pie.py`: ```python """Tortendiagramm-Segmente (Ausbaustufe 13), rein. @@ -735,16 +735,16 @@ def pie_slices(values: list[tuple[str, Decimal]], rest: Decimal | None = None, return slices ``` -- [ ] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_pie.py -q` → PASS; Gesamtsuite grün. +- [x] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_pie.py -q` → PASS; Gesamtsuite grün. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add finance/app/engine/pie.py finance/tests/test_pie.py git commit -m "feat: Engine pie – Tortensegmente mit Übrige-Regel und SVG-Pfaden (A13-T3)" ``` -- [ ] **Step 6: Fable-Gate** — Geometrie-Faktencheck: für 3–4 eigene Wertelisten Endpunkte der Pfade nachrechnen (12 Uhr Start, Uhrzeigersinn, `large-arc` ab > 50 %), Anteilssumme ≈ 100 % (Rundung), Übrige-Regel (3 %, max. 8 benannte, Einzelner behält Namen, max. 10 Segmente), `rest` nie zusammengefasst; ein erzeugtes SVG (Wegwerfdatei im Scratchpad, `viewBox="-1.05 -1.05 2.1 2.1"`) rendern und ansehen (z. B. per `rsvg-convert` oder Browser-Container), keine Lücken/Überlappungen. +- [x] **Step 6: Fable-Gate** — Geometrie-Faktencheck: für 3–4 eigene Wertelisten Endpunkte der Pfade nachrechnen (12 Uhr Start, Uhrzeigersinn, `large-arc` ab > 50 %), Anteilssumme ≈ 100 % (Rundung), Übrige-Regel (3 %, max. 8 benannte, Einzelner behält Namen, max. 10 Segmente), `rest` nie zusammengefasst; ein erzeugtes SVG (Wegwerfdatei im Scratchpad, `viewBox="-1.05 -1.05 2.1 2.1"`) rendern und ansehen (z. B. per `rsvg-convert` oder Browser-Container), keine Lücken/Überlappungen. --- @@ -758,7 +758,7 @@ git commit -m "feat: Engine pie – Tortensegmente mit Übrige-Regel und SVG-Pfa - Consumes: `load_scenario_inputs`, `ScenarioInputs` (Task 1); `Filters`, `PlainLoan`, `BreakdownRow`, `Summary`, `breakdown_rows`, `summarize` (Task 2); `KREDITRATEN`, `UNKATEGORISIERT` (`services/planning_chart.py`); `all_conflicts`, `Konflikt` (`services/double_count.py`); `SYSTEM_PREFIX` (`app/reserved.py`). - Produces: `app.services.breakdown.PLAN_NAME: str` (= „System-Plan"); `app.services.breakdown.Breakdown` (frozen dataclass: `month: date` (Monatserster), `rows: list[BreakdownRow]`, `summary: Summary`, `conflicts: list[Konflikt]`); `app.services.breakdown.compute_breakdown(session: Session, month: date, scenario: Scenario | None, filters: Filters) -> Breakdown`. -- [ ] **Step 1: Failing Tests** — `finance/tests/test_breakdown_service.py`: +- [x] **Step 1: Failing Tests** — `finance/tests/test_breakdown_service.py`: ```python from datetime import date @@ -851,9 +851,9 @@ def test_konflikt_nur_wenn_beide_zeilen_enthalten(db): assert compute_breakdown(db, M, sc, Filters()).conflicts == [] ``` -- [ ] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_breakdown_service.py -q` → FAIL (`ModuleNotFoundError: app.services.breakdown`). +- [x] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_breakdown_service.py -q` → FAIL (`ModuleNotFoundError: app.services.breakdown`). -- [ ] **Step 3: Implementierung** — `finance/app/services/breakdown.py`: +- [x] **Step 3: Implementierung** — `finance/app/services/breakdown.py`: ```python """Aufteilung (Ausbaustufe 13): Brücke DB -> engine/breakdown.py. @@ -907,16 +907,16 @@ def compute_breakdown(session: Session, month: date, scenario: Scenario | None, return Breakdown(month.replace(day=1), rows, summarize(rows), conflicts) ``` -- [ ] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_breakdown_service.py -q` → PASS; Gesamtsuite grün. +- [x] **Step 4: Grün prüfen** — `.venv/bin/python -m pytest tests/test_breakdown_service.py -q` → PASS; Gesamtsuite grün. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add finance/app/services/breakdown.py finance/tests/test_breakdown_service.py git commit -m "feat: Service compute_breakdown – System-Plan/Szenario, Doppelzählungs-Hinweis (A13-T4)" ``` -- [ ] **Step 6: Fable-Gate** — Faktencheck gegen `run_projection`: gleiche Datenbasis je Szenario (Flags, Szenario-Kredite, Szenario-Einmalposten, Modifikatoren); System-Plan entspricht `planning_chart.planning_monthly_totals` für einen Monat bis auf die Normalisierung der Fixposten (eigenes Beispiel mit nur monatlichen Posten + Einmalposten + Kredit: Kategoriesummen identisch); Konflikt-Filter prüft beide IDs. +- [x] **Step 6: Fable-Gate** — Faktencheck gegen `run_projection`: gleiche Datenbasis je Szenario (Flags, Szenario-Kredite, Szenario-Einmalposten, Modifikatoren); System-Plan entspricht `planning_chart.planning_monthly_totals` für einen Monat bis auf die Normalisierung der Fixposten (eigenes Beispiel mit nur monatlichen Posten + Einmalposten + Kredit: Kategoriesummen identisch); Konflikt-Filter prüft beide IDs. --- @@ -938,7 +938,7 @@ git commit -m "feat: Service compute_breakdown – System-Plan/Szenario, Doppelz - Consumes: `compute_breakdown`, `PLAN_NAME` (Task 4); `Filters`, `FREI`, `share`, `BreakdownRow` (Task 2); `pie_slices` (Task 3). - Produces: `app.formats.pct(value: Decimal | None) -> str` („12,5 %", `None` ⇒ „–"); in `routers/gui.py`: `_aufteilung_context(session, today, monat, szenario, fixposten, einmal, kredite, gesendet) -> dict` mit den Schlüsseln `scenarios`, `filters`, `plan_name`, `monat_wert`, `monat_anzeige`, `szenario_id`, `szenario_name`, `error`, `b` (Breakdown oder None), `s` (Summary), `zeilen` (Liste von `{"r": BreakdownRow, "art": str, "anteil": Decimal | None, "einnahme": bool}`), `frei_anteil`, `haupt` (Liste von Segment-Dicts `{"s": Slice, "q": str | None, "gewaehlt": bool, "zusatz": str | None}`), `haupt_titel`; `_segmente(slices) -> list[dict]`; Jinja-Makro `torte(segmente, titel)` in `_torte_macros.html`; Container-IDs `#aufteilung-torte`. -- [ ] **Step 1: Failing Tests** — `finance/tests/test_aufteilung_gui.py`: +- [x] **Step 1: Failing Tests** — `finance/tests/test_aufteilung_gui.py`: ```python import re @@ -1113,9 +1113,9 @@ def test_aufteilung_tabelle_sortierbar_tfoot_fest(client, db): assert re.search(r"\s*
", table) ``` -- [ ] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_aufteilung_gui.py tests/test_gui.py tests/test_sort_templates.py -q` → FAIL (404 / fehlender Menüpunkt). +- [x] **Step 2: Rot prüfen** — `.venv/bin/python -m pytest tests/test_aufteilung_gui.py tests/test_gui.py tests/test_sort_templates.py -q` → FAIL (404 / fehlender Menüpunkt). -- [ ] **Step 3: `pct`-Filter** — in `finance/app/formats.py` anhängen: +- [x] **Step 3: `pct`-Filter** — in `finance/app/formats.py` anhängen: ```python def pct(value) -> str: @@ -1126,7 +1126,7 @@ def pct(value) -> str: return f"{d}".replace(".", ",") + " %" ``` -- [ ] **Step 4: Router** — in `finance/app/routers/gui.py`: +- [x] **Step 4: Router** — in `finance/app/routers/gui.py`: Imports ergänzen (`import re` oben; `from app.formats import de_label, eur, pct`; `Scenario` in den `app.models.tables`-Import; neu): @@ -1224,7 +1224,7 @@ def aufteilung_page(request: Request, monat: str | None = None, return templates.TemplateResponse(request, "aufteilung.html", ctx) ``` -- [ ] **Step 5: Templates** — +- [x] **Step 5: Templates** — `finance/app/templates/_torte_macros.html`: @@ -1347,7 +1347,7 @@ def aufteilung_page(request: Request, monat: str | None = None, Admin ``` -- [ ] **Step 6: CSS** — an `finance/app/static/style.css` anhängen: +- [x] **Step 6: CSS** — an `finance/app/static/style.css` anhängen: ```css /* Aufteilung (Ausbaustufe 13). Tortenfarben: dataviz-Referenzpalette, @@ -1387,16 +1387,16 @@ def aufteilung_page(request: Request, monat: str | None = None, .hinweis-klein { font-size: 0.85rem; color: #555; } ``` -- [ ] **Step 7: Grün prüfen** — `.venv/bin/python -m pytest -q` → alle grün. +- [x] **Step 7: Grün prüfen** — `.venv/bin/python -m pytest -q` → alle grün. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add finance/app/formats.py finance/app/routers/gui.py finance/app/templates/aufteilung.html finance/app/templates/_aufteilung_torte.html finance/app/templates/_torte_macros.html finance/app/templates/base.html finance/app/static/style.css finance/tests/test_aufteilung_gui.py finance/tests/test_gui.py finance/tests/test_sort_templates.py git commit -m "feat: Seite Aufteilung – Auswahl, Kennzahlen, Tabelle, Tortendiagramm (A13-T5)" ``` -- [ ] **Step 9: Fable-Gate** — Live-Approximation: lokaler uvicorn gegen Wegwerf-SQLite (`FB_DATABASE_URL=sqlite:////