Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
189 lines
10 KiB
Markdown
189 lines
10 KiB
Markdown
# 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).
|