Files
bin/docs/superpowers/specs/2026-09-30-ausbaustufe-14-design.md

10 KiB
Raw Permalink Blame History

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