From d8dc25f2a82810f0f39469f328a1b01949eec4aff6dfe4b9e35124181b8f0396 Mon Sep 17 00:00:00 2001 From: wlfb Date: Wed, 30 Sep 2026 10:59:44 +0200 Subject: [PATCH] docs: Spec Ausbaustufe 14 (Szenario-Zeitraum, Neuer Betrag, szenario-eigene wiederkehrende Zahlungen) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/2026-09-30-ausbaustufe-14-design.md | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-30-ausbaustufe-14-design.md diff --git a/docs/superpowers/specs/2026-09-30-ausbaustufe-14-design.md b/docs/superpowers/specs/2026-09-30-ausbaustufe-14-design.md new file mode 100644 index 0000000..8b44f7f --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-ausbaustufe-14-design.md @@ -0,0 +1,188 @@ +# Design — Ausbaustufe 14: Szenario-Einträge mit Zeitraum, „Neuer Betrag", szenario-eigene wiederkehrende Zahlungen (v0.14.0) + +> Status: vom Nutzer im Chat freigegeben (2026-09-30), Abschnitte 1–3. +> Umsetzungsplan folgt unter `docs/superpowers/plans/`. Datenschutz-Regel aus +> `CLAUDE.md` gilt: konkrete Posten-Namen/Beträge aus der Live-Datenbank +> stehen nur im Chat, nicht in diesem Dokument, nicht in Tests/Commits/Ledger. + +## Anlass + +Der Nutzer wollte in einem Szenario einen bestehenden monatlichen Fixposten +ab einem Stichtag auf 50 % senken. Das ging nicht: Szenario-Einträge +(Prozent/Absolut/Entfällt) wirken über die gesamte Laufzeit, „Absolut" ist +eine Kürzung (nicht ein neuer Betrag), und ein Szenario kann keine eigenen +wiederkehrenden Zahlungen haben — nur Einmalzahlungen. Die Lücke ist eine +bewusste Auslassung aus A5 („Ende" als eigene Art statt Gültigkeitszeitraum +aller Modifikatoren). + +## Nutzerentscheidungen (Chat 2026-09-30) + +- **A — Zeitraum:** Prozent, Kürzung (€), Neuer Betrag und Entfällt bekommen + optionale Grenzen **Gültig ab** und **Gültig bis** (beide leer = + unbegrenzt). „Ende" bleibt wie bisher. +- **Neue Eintragsart „Neuer Betrag"** (ersetzt den Betrag eines Postens). + „Absolut" heißt in der GUI künftig **„Kürzung (€)"**. +- **B — szenario-eigene wiederkehrende Zahlungen** mit denselben Feldern wie + Fixposten inkl. **optionaler Kategorie**; Kategorie-Einträge treffen sie. +- **Formular geteilt** in „Neue Änderung an Bestehendem" und „Neue Zahlung + nur in diesem Szenario" (Szenario-Einmalzahlungen bekommen dabei ebenfalls + eine optionale Kategorie). +- **A14-Kandidaten** aus dem Ledger werden mitgenommen, **außer** + „figcaption nicht fett" — die Bildunterschrift der Torte bleibt fett + (ausdrücklicher Nutzerwunsch). +- Das Live-Szenario des Nutzers wird **nicht** vom Tool-Team angepasst; der + Nutzer probiert die Neuerung selbst daran aus. + +## Teil 1 — Datenmodell (eine Alembic-Revision, `down_revision = 'd5e8b3c7a410'`) + +- `scenario_modifiers`: neue Spalte `start_date DATE NULL` („Gültig ab"). + Die vorhandene Spalte `end_date` bedeutet künftig bei **allen** Arten + „Gültig bis" (inklusiv); bei `ende` bleibt sie das Enddatum des Postens. + Bestandsdaten: bisherige `percent`/`absolute`/`remove`-Zeilen haben + `end_date = NULL` (API hat es bisher genullt) → gelten unverändert + unbegrenzt. **Keine Datenmigration nötig.** +- Neue Art `set` (GUI-Label „Neuer Betrag"): `value` ist der neue Betrag + **mit Vorzeichen** (wie Fixposten-Beträge, z. B. `-137.50`). Nur mit + `target_type = "recurring"` zulässig. +- Neue Tabelle `scenario_recurring_items`: `id`, `scenario_id` (FK + `scenarios.id`, index), `name String(200)`, `amount MONEY`, + `rhythm String(20)`, `due_day Integer`, `start_date Date NULL`, + `end_date Date NULL`, `category_id` (FK `categories.id`, NULL). Modell + `ScenarioRecurringItem` in `models/tables.py`. +- `scenario_planned_items`: neue Spalte `category_id` (FK `categories.id`, + NULL). +- **Downgrade:** löscht `set`-Zeilen, setzt `end_date = NULL` für alle + Nicht-`ende`-Zeilen (alte Semantik), droppt `start_date`, + `scenario_planned_items.category_id` und `scenario_recurring_items`. + +## Teil 2 — Rechenlogik (`engine/scenario.py`, `engine/breakdown.py`) + +- `PlainModifier` bekommt `start_date: date | None = None`. +- `modified_amount(amount, category_id, recurring_id, modifiers, on: date)`: + ein Modifikator wirkt nur, wenn `(start_date is None or on >= start_date) + and (end_date is None or on <= end_date)` (bei Art `ende` wird er hier wie + bisher übersprungen — Ende wirkt nur über `effective_end`). Reihenfolge + unverändert = Anlage-Reihenfolge (ID). `set` → `amount = value`; ein + späterer `percent`/`absolute` rechnet auf dem neuen Betrag weiter. + `remove` im Zeitraum → `None` (Fälligkeit entfällt), außerhalb → normal. +- **Projektion** (`build_cashflows`): Betrag **je Fälligkeit** mit + `on = Fälligkeitsdatum` bestimmen (bisher einmal je Posten). + Einmalposten: `on = due`. +- **Aufteilung** (`breakdown_rows`): Stichtag je Fixposten = sein + Fälligkeitstag im gewählten Monat, auf die Monatslänge geklemmt (gleiche + Klemmregel wie `recurrence.occurrences`); Einmalposten: `on = due`. + Monatsausschluss über `start_date`/`effective_end` bleibt wie bisher. + Beispiel: Posten fällig am 14., Eintrag „50 % ab 16.10." → Oktober voll, + ab November halbiert. +- **Szenario-eigene wiederkehrende Zahlungen** werden in + `load_scenario_inputs` als `PlainRecurring` mit **`id = None`** geladen + (Typ `id: int | None`), damit Posten-Einträge (`target_type="recurring"`) + sie nie treffen — die IDs zweier Tabellen könnten kollidieren. + Kategorie-Einträge treffen sie über `category_id`. Sie zählen **immer**, + unabhängig von `include_recurring` (Regel wie Szenario-Einmalzahlungen). + Für „System-Plan" (`scenario=None`) gibt es sie nicht. +- Szenario-Einmalzahlungen werden mit ihrer `category_id` geladen (bisher + fest `None`). +- `BreakdownRow.recurring_id` bleibt für szenario-eigene Zahlungen `None` → + der Doppelzählungs-Hinweis (Kredit vs. Fixposten) berücksichtigt sie + nicht. +- Unberührt: Grafana „Planung nach Kategorie", „System-Plan", v_projection. + +## Teil 3 — API (`routers/scenarios.py`) + +- `ModifierIn`: `kind: Literal["percent", "absolute", "set", "remove", + "ende"]`, neu `start_date: date | None = None`. Validator (422, deutsch): + - `ende` ohne `end_date` → „Art »Ende« erfordert ein Datum" (wie bisher); + bei `ende` wird `start_date` genullt. + - `set` mit `target_type != "recurring"` → „Art »Neuer Betrag« ist nur für + Posten möglich". + - `start_date > end_date` → „»Gültig ab« liegt nach »Gültig bis«". + - `end_date` wird **nicht mehr** für Nicht-`ende`-Arten genullt. + POST/PATCH bleiben Full-Body. +- Neuer Endpunkt `/api/scenarios/{id}/recurring`: `GET` (Liste, sortiert + nach Name), `POST` (201), `PATCH /{item_id}` (Full-Body), `DELETE + /{item_id}` (204). Schema `ScenarioRecurringIn` mit den Feldern und + Validierungen von `RecurringIn` (Rhythmus-Literal, `due_day` 1–31, Ende + nicht vor Start); Kategorie-Existenz via `_check_category`-Muster (404 + „Kategorie nicht gefunden"); fremde/fehlende IDs → 404 „Wiederkehrende + Zahlung nicht gefunden"; fehlendes Szenario → 404. Keine + Doppelzählungs-Prüfung. +- `ScenarioPlannedIn`: neu `category_id: int | None = None` mit + Kategorie-Existenzprüfung. +- `delete_scenario` löscht zusätzlich `ScenarioRecurringItem`-Zeilen. + +## Teil 4 — GUI (`templates/szenarien.html`, `routers/gui.py`, `formats.py`) + +- `de_label`: `absolute → "Kürzung (€)"`, neu `set → "Neuer Betrag"`. + `modifier_kinds` = `percent, absolute, set, remove, ende`. +- **Tabelle „Einträge"**, Spalten: Was | Art | Wert/Betrag | Zeitraum | (Aktionen). + - Modifikator-Zeilen: Wert bei `percent`/`absolute`/`set`; Zeitraum + „ab TT.MM.JJJJ", „bis TT.MM.JJJJ", „TT.MM.JJJJ – TT.MM.JJJJ" oder „–"; + bei `ende` das Enddatum. + - Neue Zeilen für szenario-eigene wiederkehrende Zahlungen: Name (+ + Kategorie), Art „Wiederkehrend (monatlich, am 14.)", Betrag, Zeitraum + Start–Ende; Inline-Bearbeiten/Löschen (Muster Einmalzahlung). + - Einmalzahlungs-Zeilen zeigen ggf. die Kategorie; Edit-Zeile bekommt das + Kategorie-Feld. +- **Formular „Neue Änderung an Bestehendem"** (POST `/modifiers`): Art + (Prozent | Kürzung (€) | Neuer Betrag | Entfällt | Ende), Ziel-Typ, + Kategorie/Posten, Wert, Gültig ab, Gültig bis. + - Wert-Label dynamisch: „Prozentsatz (%)" / „Kürzung (€)" / „Neuer + Betrag (€)"; bei Entfällt/Ende gesperrt. + - Neuer Betrag: Ziel-Typ fest auf Posten (Option Kategorie gesperrt). + - Ende: „Gültig ab" gesperrt, „Gültig bis" heißt „Endet am". +- **Formular „Neue Zahlung nur in diesem Szenario"**: Art (Einmalzahlung | + Wiederkehrend), Name, Betrag, Kategorie (optional, „(keine)"), Fällig am + (nur Einmal), Rhythmus + Fälligkeitstag + Start + Ende (nur + Wiederkehrend). POST-Ziel wechselt `/planned` ↔ `/recurring` (Muster + `data-planned-url`). +- Modifikator-Edit-Zeilen bekommen „Gültig ab"/„Gültig bis" und dieselbe + Umschaltlogik. +- UX-Regel bleibt: nicht passende Felder sichtbar + `disabled` + Tooltip, + Fehler inline über den bestehenden Response-Error-Handler. +- `hilfe.html`: Szenario-Absatz um Zeitraum, Neuer Betrag, Kürzung (€) und + wiederkehrende Zahlungen ergänzen, mit neutralem Beispiel („ein Posten + wird ab 16.10. halbiert"). + +## Teil 5 — A14-Kandidaten aus dem Ledger + +1. **Nav-Überlauf bei 390 px:** `nav` darf umbrechen (`flex-wrap: wrap`, + Abstände), kein horizontales Scrollen der Seite. +2. **Umriss des gewählten Tortensegments zuletzt zeichnen:** gewähltes + Segment wird im SVG als letztes Element ausgegeben, damit sein Umriss + nicht von Nachbarn überdeckt wird. +3. **Tilgungsplan-Helfer:** die 4 Aufrufe `loan_schedule(Decimal(l.principal), + Decimal(l.annual_rate_pct), l.term_months, l.payout_date, + l.repayment_type)` (planning-Router, services/breakdown, + projection_service, planning_chart) über einen Helfer + `schedule_for(loan)` führen — verhaltensneutral. +4. **`.legende-zusatz` ohne CSS:** eigene Regel (normales Gewicht, + gedämpfte Farbe), damit der Zusatz bei gewählter Legendenzeile nicht mit + fett wird. +5. **`term_months`-Validierung** 1..1200 für Kredite (Anlegen + PATCH), + 422 inline. + +Ausdrücklich **nicht**: figcaption der Torte entfetten. + +## Teil 6 — Tests, Doku, Release + +- **Engine:** Grenzen inklusiv (Tag vor/an/nach ab bzw. bis), `set` dann + `percent`, `remove` mit Zeitraum, szenario-eigene Zahlung von + Kategorie-Eintrag getroffen / von Posten-Eintrag mit gleicher ID nicht, + zählt bei `include_recurring = false`; Aufteilungs-Stichtag (Okt voll / + Nov halbiert, Klemmung am Monatsende) — nur erfundene Beträge. +- **API:** alle 422/404-Fälle aus Teil 3, CRUD `/recurring`, + Szenario-Löschen räumt `/recurring` mit ab, Planned mit Kategorie, + `term_months`-Grenzen. +- **Migration:** Upgrade/Downgrade in Wegwerf-Postgres; Bestandseinträge + bleiben gleich. +- **GUI/JS:** Umschaltmatrix beider Formulare und der Edit-Zeilen (Stil der + bestehenden Template-/JS-Tests), Nav-CSS, Torten-Reihenfolge. +- Suite bleibt grün (Basis: 377 passed / 3 skipped). +- **Doku:** `docs/ARCHITEKTUR.md` (neue Tabelle, Endpunkt, Stichtag-Regel), + `CLAUDE.md` falls betroffen, fb-Repo (finanz-api- und + finanzberatung-Skill). +- **Ablauf:** Plan `docs/superpowers/plans/2026-09-30-ausbaustufe-14.md`, + direkt auf `main`; jeder Task mit Fable-Testagent-Gate und + Ledger-Eintrag; Release **v0.14.0** (VERSION, Backup, Redeploy, + Live-Smoke, Fable-Release-Gate).