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:
@@ -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).
|
||||||
Reference in New Issue
Block a user