docs: Spec Ausbaustufe 14 (Szenario-Zeitraum, Neuer Betrag, szenario-eigene wiederkehrende Zahlungen)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 10:59:44 +02:00
co-authored by Claude Opus 5.5
parent 60cd3c4fea
commit d8dc25f2a8
@@ -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).