diff --git a/docs/superpowers/plans/2026-09-30-ausbaustufe-14.md b/docs/superpowers/plans/2026-09-30-ausbaustufe-14.md index b9eadb4..e8dc4b9 100644 --- a/docs/superpowers/plans/2026-09-30-ausbaustufe-14.md +++ b/docs/superpowers/plans/2026-09-30-ausbaustufe-14.md @@ -46,7 +46,7 @@ **Interfaces:** - Produces: `ScenarioModifier.start_date: date | None`; `ScenarioPlannedItem.category_id: int | None`; `ScenarioRecurringItem` (Tabelle `scenario_recurring_items`: `id`, `scenario_id`, `name`, `amount`, `rhythm`, `due_day`, `start_date`, `end_date`, `category_id`). -- [ ] **Step 1: Failing Test (SQLite-Roundtrip)** — an `finance/tests/test_models.py` anhängen: +- [x] **Step 1: Failing Test (SQLite-Roundtrip)** — an `finance/tests/test_models.py` anhängen: ```python def test_a14_szenario_felder_roundtrip(db): @@ -75,9 +75,9 @@ def test_a14_szenario_felder_roundtrip(db): assert (w.scenario_id, w.rhythm, w.due_day, w.category_id) == (sc.id, "monthly", 14, cat.id) ``` -- [ ] **Step 2: Test laufen lassen** — `cd finance && .venv/bin/python -m pytest -q tests/test_models.py::test_a14_szenario_felder_roundtrip` → FAIL (`ImportError: cannot import name 'ScenarioRecurringItem'`). +- [x] **Step 2: Test laufen lassen** — `cd finance && .venv/bin/python -m pytest -q tests/test_models.py::test_a14_szenario_felder_roundtrip` → FAIL (`ImportError: cannot import name 'ScenarioRecurringItem'`). -- [ ] **Step 3: Modelle** — in `finance/app/models/tables.py`: +- [x] **Step 3: Modelle** — in `finance/app/models/tables.py`: `ScenarioModifier` (Kommentar über `end_date` ersetzen, `start_date` ergänzen): @@ -123,7 +123,7 @@ class ScenarioRecurringItem(Base): category_id: Mapped[int | None] = mapped_column(ForeignKey("categories.id"), nullable=True) ``` -- [ ] **Step 4: Migration** — `finance/alembic/versions/e7f1a9c3b214_szenario_zeitraum.py`: +- [x] **Step 4: Migration** — `finance/alembic/versions/e7f1a9c3b214_szenario_zeitraum.py`: ```python """szenario zeitraum, neuer betrag, szenario-eigene wiederkehrende zahlungen @@ -182,7 +182,7 @@ def downgrade() -> None: op.drop_column('scenario_modifiers', 'start_date') ``` -- [ ] **Step 5: Postgres-Migrationstest** — `finance/tests/test_migration_pg.py` (skip ohne `FB_TEST_PG_URL`, Muster `test_views_pg.py`): +- [x] **Step 5: Postgres-Migrationstest** — `finance/tests/test_migration_pg.py` (skip ohne `FB_TEST_PG_URL`, Muster `test_views_pg.py`): ```python """Postgres-only: Alembic-Revision e7f1a9c3b214 (Ausbaustufe 14) hoch/runter @@ -266,9 +266,9 @@ def test_migration_a14_up_down(alembic_cfg): assert "start_date" not in {c["name"] for c in insp.get_columns("scenario_modifiers")} ``` -- [ ] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün, `test_migration_pg` skipped (Host ohne `FB_TEST_PG_URL`). Dann mit Wegwerf-Postgres (Kommando aus dem Docstring) `FB_TEST_PG_URL=… .venv/bin/python -m pytest -q tests/test_migration_pg.py tests/test_views_pg.py` → PASS; Container danach `podman stop a14-pg`. +- [x] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün, `test_migration_pg` skipped (Host ohne `FB_TEST_PG_URL`). Dann mit Wegwerf-Postgres (Kommando aus dem Docstring) `FB_TEST_PG_URL=… .venv/bin/python -m pytest -q tests/test_migration_pg.py tests/test_views_pg.py` → PASS; Container danach `podman stop a14-pg`. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add finance/app/models/tables.py finance/alembic/versions/e7f1a9c3b214_szenario_zeitraum.py \ @@ -276,7 +276,7 @@ git add finance/app/models/tables.py finance/alembic/versions/e7f1a9c3b214_szena git commit -m "feat: Szenario-Zeitraum, Neuer Betrag, szenario-eigene Zahlungen – Datenmodell + Migration (A14-T1)" ``` -- [ ] **Step 8: Fable-Gate** — Wegwerf-Postgres selbst starten; `test_migration_pg.py` + `test_views_pg.py` grün; zusätzlich `alembic upgrade head` → `alembic downgrade -1` → `alembic upgrade head` per CLI (`FB_DATABASE_URL=… .venv/bin/alembic …`) ohne Fehler; Modell ↔ Migration abgleichen (Spaltentypen, Nullbarkeit, FK-Ziele, Index-Name) per `inspect` gegen `Base.metadata`; bestätigen, dass Live-DB unberührt ist (keine Verbindung zur Produktiv-DB). +- [x] **Step 8: Fable-Gate** — Wegwerf-Postgres selbst starten; `test_migration_pg.py` + `test_views_pg.py` grün; zusätzlich `alembic upgrade head` → `alembic downgrade -1` → `alembic upgrade head` per CLI (`FB_DATABASE_URL=… .venv/bin/alembic …`) ohne Fehler; Modell ↔ Migration abgleichen (Spaltentypen, Nullbarkeit, FK-Ziele, Index-Name) per `inspect` gegen `Base.metadata`; bestätigen, dass Live-DB unberührt ist (keine Verbindung zur Produktiv-DB). --- @@ -295,7 +295,7 @@ git commit -m "feat: Szenario-Zeitraum, Neuer Betrag, szenario-eigene Zahlungen - `modified_amount(amount, category_id, recurring_id, modifiers, *, on: date) -> Decimal | None` (`on` ist Pflicht, keyword-only). - `recurrence.due_in_month(month: date, due_day: int) -> date` (Fälligkeitstag im Monat von `month`, auf Monatslänge geklemmt). -- [ ] **Step 1: Failing Tests** — an `finance/tests/test_scenario_engine.py` anhängen: +- [x] **Step 1: Failing Tests** — an `finance/tests/test_scenario_engine.py` anhängen: ```python D = Decimal @@ -407,9 +407,9 @@ In `finance/tests/test_scenario_inputs.py::test_modifikator_helfer_oeffentlich` assert modified_amount(Decimal("-100.00"), None, 2, mods, on=date(2030, 1, 1)) == Decimal("-100.00") ``` -- [ ] **Step 2: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_scenario_engine.py tests/test_breakdown.py tests/test_recurrence.py tests/test_scenario_inputs.py` → FAIL (`unexpected keyword argument 'start_date'` / `'on'`, `cannot import name 'due_in_month'`). +- [x] **Step 2: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_scenario_engine.py tests/test_breakdown.py tests/test_recurrence.py tests/test_scenario_inputs.py` → FAIL (`unexpected keyword argument 'start_date'` / `'on'`, `cannot import name 'due_in_month'`). -- [ ] **Step 3: `recurrence.py`** — nach `_clamp` ergänzen: +- [x] **Step 3: `recurrence.py`** — nach `_clamp` ergänzen: ```python def due_in_month(month: date, due_day: int) -> date: @@ -418,7 +418,7 @@ def due_in_month(month: date, due_day: int) -> date: return _clamp(month.year, month.month, due_day) ``` -- [ ] **Step 4: `scenario.py`** — Dataclasses und Helfer ersetzen: +- [x] **Step 4: `scenario.py`** — Dataclasses und Helfer ersetzen: ```python @dataclass(frozen=True) @@ -506,7 +506,7 @@ In `build_cashflows` die zwei Schleifen über `recurring`/`planned` ersetzen: flows.append((p.due, amount)) ``` -- [ ] **Step 5: `breakdown.py`** — Import `from app.engine.recurrence import STEP, due_in_month`; im Modul-Docstring den Satz „Modifikatoren wirken exakt wie in build_cashflows (gleiche Helfer)." ersetzen durch „Modifikatoren wirken wie in build_cashflows (gleiche Helfer); Stichtag je Fixposten ist sein Fälligkeitstag im Monat, je Einmalposten das Fälligkeitsdatum." In `breakdown_rows`: +- [x] **Step 5: `breakdown.py`** — Import `from app.engine.recurrence import STEP, due_in_month`; im Modul-Docstring den Satz „Modifikatoren wirken exakt wie in build_cashflows (gleiche Helfer)." ersetzen durch „Modifikatoren wirken wie in build_cashflows (gleiche Helfer); Stichtag je Fixposten ist sein Fälligkeitstag im Monat, je Einmalposten das Fälligkeitsdatum." In `breakdown_rows`: ```python amount = modified_amount(r.amount, r.category_id, r.id, modifiers, @@ -517,9 +517,9 @@ In `build_cashflows` die zwei Schleifen über `recurring`/`planned` ersetzen: amount = modified_amount(p.amount, p.category_id, None, modifiers, on=p.due) ``` -- [ ] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün. `grep -n "^from\|^import" app/engine/scenario.py app/engine/breakdown.py` zeigt nur Stdlib und `app.engine.*`. +- [x] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün. `grep -n "^from\|^import" app/engine/scenario.py app/engine/breakdown.py` zeigt nur Stdlib und `app.engine.*`. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add finance/app/engine/scenario.py finance/app/engine/breakdown.py finance/app/engine/recurrence.py \ @@ -528,7 +528,7 @@ git add finance/app/engine/scenario.py finance/app/engine/breakdown.py finance/a git commit -m "feat: Szenario-Einträge je Stichtag mit Zeitraum und Art Neuer Betrag (A14-T2)" ``` -- [ ] **Step 8: Fable-Gate** — unabhängige Handrechnung mit eigenen Werten: vierteljährlicher Posten mit Zeitraum, der eine Fälligkeit einschließt und eine ausschließt; `set` → `absolute` → `percent` in Folge; Kategorie- und Posten-Eintrag auf denselben Posten; Aufteilung eines vierteljährlichen Postens in einem Monat ohne Fälligkeit (Stichtag = Fälligkeitstag dieses Monats); Einträge ohne Datum liefern exakt dieselben Ergebnisse wie vor dem Commit (Projektion eines synthetischen Szenarios vor/nach dem Commit vergleichen, z. B. zwei Checkouts in einem Wegwerf-Verzeichnis); Reinheit + kein `float`. +- [x] **Step 8: Fable-Gate** — unabhängige Handrechnung mit eigenen Werten: vierteljährlicher Posten mit Zeitraum, der eine Fälligkeit einschließt und eine ausschließt; `set` → `absolute` → `percent` in Folge; Kategorie- und Posten-Eintrag auf denselben Posten; Aufteilung eines vierteljährlichen Postens in einem Monat ohne Fälligkeit (Stichtag = Fälligkeitstag dieses Monats); Einträge ohne Datum liefern exakt dieselben Ergebnisse wie vor dem Commit (Projektion eines synthetischen Szenarios vor/nach dem Commit vergleichen, z. B. zwei Checkouts in einem Wegwerf-Verzeichnis); Reinheit + kein `float`. --- @@ -543,7 +543,7 @@ git commit -m "feat: Szenario-Einträge je Stichtag mit Zeitraum und Art Neuer B - Consumes: `ScenarioRecurringItem`, `ScenarioModifier.start_date`, `ScenarioPlannedItem.category_id` (Task 1); `PlainRecurring(id=None, …)`, `PlainModifier(..., start_date=…)`, `build_cashflows` (Task 2); `app.routers.planning.RecurringIn`, `app.routers.planning._check_category` (bestehend). - Produces: `GET/POST /api/scenarios/{id}/recurring`, `PATCH/DELETE /api/scenarios/{id}/recurring/{item_id}` (Schema `RecurringIn`, Antwort `ScenarioRecurringOut` = `RecurringIn` + `id` + `scenario_id`); `ModifierIn.start_date`, `kind` inkl. `set`; `ScenarioPlannedIn.category_id`. -- [ ] **Step 1: Failing Tests (Service)** — an `finance/tests/test_scenario_inputs.py` anhängen (Imports oben um `build_cashflows` aus `app.engine.scenario` und `ScenarioRecurringItem`, `Category` aus `app.models.tables` ergänzen): +- [x] **Step 1: Failing Tests (Service)** — an `finance/tests/test_scenario_inputs.py` anhängen (Imports oben um `build_cashflows` aus `app.engine.scenario` und `ScenarioRecurringItem`, `Category` aus `app.models.tables` ergänzen): ```python def test_szenario_zahlung_zaehlt_immer_und_id_kollision(db): @@ -589,7 +589,7 @@ def test_zeitraum_und_kategorie_werden_geladen(db): assert [(p.name, p.category_id) for p in inp.planned] == [("E", cat.id)] ``` -- [ ] **Step 2: Failing Tests (API)** — in `finance/tests/test_crud_api.py`: +- [x] **Step 2: Failing Tests (API)** — in `finance/tests/test_crud_api.py`: Bestehende Erwartungen an die neue Semantik anpassen: in `test_modifier_ende_validierung` den letzten Block ersetzen durch @@ -728,9 +728,9 @@ def test_scenario_planned_mit_kategorie(client, db): assert r.status_code == 200 and r.json()["category_id"] is None ``` -- [ ] **Step 3: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_scenario_inputs.py tests/test_crud_api.py` → FAIL. +- [x] **Step 3: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_scenario_inputs.py tests/test_crud_api.py` → FAIL. -- [ ] **Step 4: `load_scenario_inputs`** — Import `ScenarioRecurringItem` ergänzen; im Szenario-Zweig (nach dem `if scenario is None: … return`) den Block der Szenario-Einmalzahlungen ersetzen und Szenario-Zahlungen sowie `start_date` ergänzen: +- [x] **Step 4: `load_scenario_inputs`** — Import `ScenarioRecurringItem` ergänzen; im Szenario-Zweig (nach dem `if scenario is None: … return`) den Block der Szenario-Einmalzahlungen ersetzen und Szenario-Zahlungen sowie `start_date` ergänzen: ```python # Szenario-eigene Zahlungen zählen IMMER für dieses Szenario - @@ -760,7 +760,7 @@ und die Modifikator-Liste: Docstring von `load_scenario_inputs` um „keine Szenario-Zahlungen" ergänzen: „… keine Modifikatoren, keine Szenario-Einmalposten und keine szenario-eigenen wiederkehrenden Zahlungen." -- [ ] **Step 5: `routers/scenarios.py`** — Importe: +- [x] **Step 5: `routers/scenarios.py`** — Importe: ```python from app.models.tables import (Loan, ProjectionPoint, ProjectionResult, @@ -868,9 +868,9 @@ def delete_scenario_recurring(scenario_id: int, item_id: int, Prüfen, dass `app.routers.planning` `app.routers.scenarios` nicht importiert (kein Zirkelimport): `grep -n "scenarios" finance/app/routers/planning.py` → keine Import-Zeile. -- [ ] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün. +- [x] **Step 6: Tests laufen lassen** — `.venv/bin/python -m pytest -q` → alle grün. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add finance/app/services/projection_service.py finance/app/routers/scenarios.py \ @@ -878,7 +878,7 @@ git add finance/app/services/projection_service.py finance/app/routers/scenarios git commit -m "feat: API für Szenario-Zeitraum, Neuer Betrag und szenario-eigene wiederkehrende Zahlungen (A14-T3)" ``` -- [ ] **Step 8: Fable-Gate** — Live-Approximation gegen lokalen uvicorn mit Wegwerf-SQLite (`FB_DATABASE_URL=sqlite://///a14.sqlite`, `FB_API_KEY=test`, `.venv/bin/alembic upgrade head`, `.venv/bin/uvicorn app.main:app --port 8199`): per curl ein Fixposten (monatlich am 14., erfundener Betrag), Szenario mit „Prozent 50 ab <16. eines Monats>" und einer szenario-eigenen wiederkehrenden Zahlung mit Kategorie; `POST /api/scenarios/{id}/project` → Tiefpunkt/Serie gegen eigene Handrechnung; alle 422/404-Fälle aus Step 2 per curl nachstellen (deutsche Texte in `detail`); `DELETE` des Szenarios hinterlässt 0 Zeilen in `scenario_recurring_items`; danach aufräumen. +- [x] **Step 8: Fable-Gate** — Live-Approximation gegen lokalen uvicorn mit Wegwerf-SQLite (`FB_DATABASE_URL=sqlite://///a14.sqlite`, `FB_API_KEY=test`, `.venv/bin/alembic upgrade head`, `.venv/bin/uvicorn app.main:app --port 8199`): per curl ein Fixposten (monatlich am 14., erfundener Betrag), Szenario mit „Prozent 50 ab <16. eines Monats>" und einer szenario-eigenen wiederkehrenden Zahlung mit Kategorie; `POST /api/scenarios/{id}/project` → Tiefpunkt/Serie gegen eigene Handrechnung; alle 422/404-Fälle aus Step 2 per curl nachstellen (deutsche Texte in `detail`); `DELETE` des Szenarios hinterlässt 0 Zeilen in `scenario_recurring_items`; danach aufräumen. --- @@ -893,7 +893,7 @@ git commit -m "feat: API für Szenario-Zeitraum, Neuer Betrag und szenario-eigen - Consumes: Endpunkte aus Task 3 (`/modifiers` mit `start_date`, `/planned` mit `category_id`, `/recurring`); `ScenarioRecurringItem` (Task 1). - Produces: `window.FBSzenario = {zielTyp(select), aenderungArt(select), zahlungArt(select), init(root)}`; Template-Kontext `row.recurring_items`, `rhythms`; `DE_LABELS["absolute"] = "Kürzung (€)"`, `DE_LABELS["set"] = "Neuer Betrag"`. -- [ ] **Step 1: Failing Tests (Python)** — `finance/tests/test_formats.py::test_de_label_bekannte_und_unbekannte_werte`: Zeile `absolute` → `assert de_label("absolute") == "Kürzung (€)"`, neue Zeile `assert de_label("set") == "Neuer Betrag"`, Schlüsselmenge um `"set"` ergänzen. +- [x] **Step 1: Failing Tests (Python)** — `finance/tests/test_formats.py::test_de_label_bekannte_und_unbekannte_werte`: Zeile `absolute` → `assert de_label("absolute") == "Kürzung (€)"`, neue Zeile `assert de_label("set") == "Neuer Betrag"`, Schlüsselmenge um `"set"` ergänzen. In `finance/tests/test_gui.py`: den bestehenden Import aus `app.models.tables` um `Category` und `ScenarioRecurringItem` ergänzen. In `test_szenario_eintraege_tabelle` die letzte Zeile ersetzen durch `assert "FBSzenario.aenderungArt" in r and "onEntryArtChange" not in r`. In `test_neuer_eintrag_formular_struktur` `assert 'class="entry-form"' in r` ersetzen durch `assert 'class="entry-form aenderung-form"' in r and 'class="entry-form zahlung-form"' in r` (beide Formulare tragen jetzt zwei Klassen). In `test_statische_dateien_mit_versionskennung` die Dateiliste um `"szenarien.js"` ergänzen. Neue Tests anhängen: @@ -949,7 +949,7 @@ def test_szenario_eintraege_zeitraum_und_wiederkehrend(client, db): assert 'id="sri-row-' in r and 'id="sri-edit-' in r ``` -- [ ] **Step 2: Failing Tests (JS)** — `finance/tests/js/szenarien.dom.test.js`: +- [x] **Step 2: Failing Tests (JS)** — `finance/tests/js/szenarien.dom.test.js`: ```js 'use strict'; @@ -1070,7 +1070,7 @@ test('Zahlung: Einmal → Wiederkehrend → Einmal', { skip: !JSDOM }, () => { }); ``` -- [ ] **Step 3: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_gui.py tests/test_formats.py` → FAIL. JS per Podman (Host hat kein node): +- [x] **Step 3: Tests laufen lassen** — `.venv/bin/python -m pytest -q tests/test_gui.py tests/test_formats.py` → FAIL. JS per Podman (Host hat kein node): ```bash podman run --rm -v "$PWD":/w:Z -w /w docker.io/library/node:22-alpine \ @@ -1079,9 +1079,9 @@ podman run --rm -v "$PWD":/w:Z -w /w docker.io/library/node:22-alpine \ Erwartet: `szenarien.dom.test.js` FAIL (Datei `szenarien.js` fehlt). -- [ ] **Step 4: `formats.py`** — in `DE_LABELS` `"absolute": "Kürzung (€)"` und nach `"absolute"` neu `"set": "Neuer Betrag"`. +- [x] **Step 4: `formats.py`** — in `DE_LABELS` `"absolute": "Kürzung (€)"` und nach `"absolute"` neu `"set": "Neuer Betrag"`. -- [ ] **Step 5: `static/szenarien.js`** +- [x] **Step 5: `static/szenarien.js`** ```js /* Szenarien-Seite (Ausbaustufe 14): Umschaltlogik der Formulare @@ -1158,7 +1158,7 @@ Erwartet: `szenarien.dom.test.js` FAIL (Datei `szenarien.js` fehlt). })(); ``` -- [ ] **Step 6: `base.html`** — nach der `doppelzaehlung.js`-Zeile: +- [x] **Step 6: `base.html`** — nach der `doppelzaehlung.js`-Zeile: ```html @@ -1170,7 +1170,7 @@ und nach `if (window.FBDoppel) { window.FBDoppel.init(document); }`: if (window.FBSzenario) { window.FBSzenario.init(document); } ``` -- [ ] **Step 7: `gui.py`** — Import `ScenarioRecurringItem` ergänzen. In `_scenario_rows` nach `planned_items`: +- [x] **Step 7: `gui.py`** — Import `ScenarioRecurringItem` ergänzen. In `_scenario_rows` nach `planned_items`: ```python recurring_items = session.execute( @@ -1181,7 +1181,7 @@ und nach `if (window.FBDoppel) { window.FBDoppel.init(document); }`: und `"recurring_items": recurring_items,` ins Row-Dict. In `szenarien_page`: `"modifier_kinds": ["percent", "absolute", "set", "remove", "ende"],` und neu `"rhythms": ["monthly", "quarterly", "yearly"],`. -- [ ] **Step 8: `szenarien.html`** — direkt nach `{% block content %}` das Makro: +- [x] **Step 8: `szenarien.html`** — direkt nach `{% block content %}` das Makro: ```jinja {% macro zeitraum(von, bis) -%} @@ -1412,9 +1412,9 @@ Den gesamten Bereich von `

Einträge

` bis vor `
` ersetzen durch: Im `