# 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).