ce5f83823f
Deploy App / deploy (push) Successful in 1m52s
Haushaltsform, Personen (Name/Alter) und Startjahr wandern vom Szenario auf den Plan. Das Pensionsalter bleibt szenario-eigen -- es ist der Kern jedes Frueh-/Spaetpensionierungs-Szenarios. Neue Tabelle PlanPerson; Person behaelt nur Rolle + Pensionsalter; Plan bekommt householdType und startYear. Der Rechenkern bleibt unberuehrt: toPlanInput fuegt beide Ebenen wieder zu einem unveraenderten PlanInput zusammen. 43 Golden Tests unveraendert. Nebeneffekt: Ein Ist-Satz trifft jetzt in ALLEN Szenarien dasselbe Planjahr -- vorher war das nicht garantiert. Wiederherstellen einer Version setzt nur noch Szenario-Eigenes zurueck. Profil-Dialog kennzeichnet plan-weite vs. szenario-eigene Felder. Zwei Tests spielen echte V6-Daten ein und pruefen die Uebernahme. Spezifikation 0.23 (210 -> 212). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3863 lines
235 KiB
Markdown
3863 lines
235 KiB
Markdown
# FPT – Financial Planning Tool
|
||
## Funktionale und Technische Spezifikation
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||
| **Version** | 0.23 |
|
||
| **Datum** | 2026-07-18 |
|
||
| **Status** | Lebendes Dokument |
|
||
| **Codestand** | Arbeitsstand nach `1d046e9` inkl. zwei Monte-Carlo-Fragestellungen (Branch `main`) |
|
||
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1–v5) im Ordner `Info Dateien` – diese sind ab Version 0.1 dieses Dokuments obsolet |
|
||
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
|
||
|
||
---
|
||
|
||
## Änderungshistorie
|
||
|
||
| Version | Datum | Autor | Änderung |
|
||
|---|---|---|---|
|
||
| 0.23 | 2026-07-20 | Claude (Opus 4.8) | **V7: Der Haushalt liegt am Plan, die Annahmen am Szenario.** **Haushaltsform**, **Personen** (Name, Alter) und **Planstartjahr** wandern vom Szenario auf den **Plan**; das **Pensionsalter** bleibt szenario-eigen (es ist der Kern jedes Früh-/Spätpensionierungs-Szenarios), ebenso Inflation und Cash-Anfangswert. Begründung: Diese Angaben beschreiben den Haushalt, nicht eine Planungsvariante – unterscheiden sie sich, ist es ein anderer **Plan**, kein anderes Szenario. Neue Tabelle `PlanPerson` (Rolle, Name, Alter je Plan); `Person` behält nur noch Rolle und Pensionsalter; `Plan` bekommt `householdType` und `startYear`. **Der Rechenkern bleibt unberührt:** `toPlanInput()` fügt Plan- und Szenario-Ebene wieder zu einem unveränderten `PlanInput` zusammen, die 43 Golden Tests laufen durch. **Nebeneffekt, der ein reales Problem löst:** Weil das Startjahr nun plan-weit ist, landet ein erfasster Ist-Satz für 2031 in **allen** Szenarien zwingend auf demselben Planjahr – vorher war das nicht garantiert. Das **Wiederherstellen einer Szenario-Version** setzt folgerichtig nur noch das Szenario-Eigene zurück; plan-weite Angaben über eine Version *eines* Szenarios zu überschreiben, hätte die übrigen stillschweigend mitverändert. Der Profil-Dialog kennzeichnet neu je Feld, ob es **plan-weit** oder **nur dieses Szenario** gilt. Die Migration übernimmt die Werte aus dem **Basisszenario**; zwei neue Tests spielen dafür echte V6-Daten ein und prüfen die Übernahme inkl. abweichender Nebenszenarien (210 → 212). |
|
||
| 0.22 | 2026-07-20 | Claude (Opus 4.8) | **Fehlerbehebung: Cash-Vorbelegung im Ist-Wizard.** Der Wizard für die effektiven Werte zeigte als geplanten Cash-Bestand den Stand am **Phasenende** statt am gewählten Stichtag -- in einer Phase von 2026 bis 2036 also für 2031 den Wert von 2036. Ursache: Der Dialog las `cashBridge.cashEnd`, weil `computePlan` den Cash-Bestand bisher nur **je Phase** auswies. `YearPoint` trägt neu ein Feld `cash` (Stand am Jahresende), analog zu `wealthNominal`; der Wizard liest daraus. Die Vorbelegung der ELEMENTE war nie betroffen -- die stammte schon immer aus dem Jahresverlauf. Zwei Regressionstests decken den gemeldeten Fall ab (208 -> 210). Rein additiv, die 43 Golden Tests laufen unverändert. |
|
||
| 0.21 | 2026-07-20 | Claude (Opus 4.8) | **Effektive Werte / Plan-Ist-Vergleich** (Roadmap Nr. 5, neue Kapitel 3.9 und 9.29). Macht aus dem Planer ein Monitoring-Werkzeug. Neuer Knopf **«Effektive Werte»** auf Plan-Ebene: Liste der Erfassungen plus Wizard in zwei Schritten (Stichtag, dann alle Elemente **aller** Szenarien inkl. **Cash**, Einkommen und Ausgaben, vorbelegt mit dem Planwert für dieses Jahr). Ein Ist-Satz hängt am **Plan**, nicht am Szenario – die Wirklichkeit ist dieselbe, egal wogegen man sie hält; die Zuordnung läuft über die Herkunfts-Kette `sourceElementId`. Das exakte Datum steht in Liste und Zeitachse, für die Rechnung zählt nur die **Jahreszahl**. **Zweiter Rechenlauf:** `computePlan` nimmt neu `{ actuals }`; die Werte schnappen in **jedem** erfassten Jahr auf die Realität und laufen von dort planmässig weiter (Lücken fallen auf die Plandaten zurück). Ohne die Option verhält sich die Funktion exakt wie bisher – die 43 Golden Tests laufen unverändert. Der Sprung wird als eigene Brückenposition `actualsCorrection` geführt (Vermögens- **und** Cash-Brücke): Eine Planabweichung ist keine Rendite, und ohne diese Zeile ginge die Zerlegung im Ist-Jahr nicht mehr auf. **Matrix:** zweiter Umschalter «Plan» / «Effektiv»; im Ist-Modus steht neben dem Wert die **Abweichung** zum Plan, farbig – bewusst kein «beide», das wären mit nominal/real acht Zahlen je Zelle (Begründung 9.29). **Zeitachse:** Marker je erfasstem Jahr, der jüngste farbig, ältere blass. **Alle vier Analysewerkzeuge** erhalten eine einheitliche Leiste (nominal/real als **Einfach**auswahl, Plan/Effektiv); im Vermögensverlauf kommt die Planlinie **gestrichelt** als Referenz dazu, max. vier Serien. **Monte-Carlo:** Der Zielbetrag dreht mit real/nominal mit und wird entsprechend beschriftet; eine Zeile weist aus, ab welchem Jahr simuliert wird – die Jahre davor sind durch Ist-Werte belegt und werden nicht gewürfelt. Das Startjahr ist **abgeleitet, nicht eingebbar**: Ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst wurden. Ein Ist-Satz erzeugt **keine** Szenario-Version – er ist eine Beobachtung, keine Planänderung. Neue Tabelle `ActualsSet` (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter `/api/plans/<id>/actuals`, neue Module `actuals.ts` und `dataview.ts`; 27 Tests ergänzt (181 → 208). |
|
||
| 0.20 | 2026-07-19 | Claude (Opus 4.8) | **Raten über Lebensphasen übernehmen** (neues Kapitel 3.6.11) und **Rate in der Verlaufsgrafik**. (1) Ändert man ein **Ratenfeld**, fragt das Bearbeitungspanel neu nach der Reichweite: **nur diese Phase** (Vorgabe, bisheriges Verhalten), **diese + folgende** oder **alle Phasen**. Anlass war, dass eine geänderte Rendite bisher nur für die eine Phase galt und viermal eingetippt werden musste. Als Ratenfelder gelten `expectedReturn` (PK, 3a, Sonstiges Vermögen), `valueGrowth` und `interestRate` (Immobilie) sowie `teuerungsausgleich` (Einkommen, Ausgaben); AHV und Schulden haben keine. Die Rückfrage erscheint **inline und erst beim Speichern wirksam**, nicht als Modal – das Zahlenfeld löst bei jedem Tastendruck aus, ein Dialog erschiene bei «5.2» viermal. Sie erscheint nur bei **tatsächlich veränderten** Raten («nicht gesetzt» und 0 gelten als gleich). Beim Übertragen bleiben die **übrigen Werte der Zielphasen erhalten** – der Endpunkt ersetzt den ganzen Werte-Satz, ein blosses Kopieren des Entwurfs hätte dort Beträge, Sparraten und Bezüge gelöscht (durch Test abgesichert). Phasen, in denen der Wert schon stimmt, werden übersprungen. Kein neuer Schreibpfad: ein PUT je Zielphase über den bestehenden Endpunkt, alle in einer Bearbeitungssitzung und damit in **einer** Nebenversion. (2) `ElementYearPoint` führt neu ein Feld **`rate`** mit – additiv, es wird nur durchgereicht, was die Rechnung ohnehin benutzt; die 43 Golden Tests laufen unverändert. Die Verlaufsgrafik der Element-Detailansicht zeigt die Rate damit auf einer **zweiten Y-Achse rechts** in Prozent, als **Stufenlinie** (innerhalb einer Phase konstant, Sprung an der Phasengrenze). Neues Modul `ratefields.ts`; 17 Tests ergänzt (164 → 181). Keine DB- oder API-Änderung. |
|
||
| 0.19 | 2026-07-19 | Claude (Opus 4.8) | **Versionierung und Änderungshistorie je Szenario** (neues Kapitel 3.8). Jedes Szenario trägt eine Version **A.B**: **B** entsteht automatisch, **A** manuell mit Pflichtkommentar. **Der zentrale Entwurfsentscheid:** FPT hat keinen Speichern-Knopf – jede Änderung schreibt sofort, ein Assistenten-Durchlauf macht ~14 Schreibvorgänge, ein Verteil-Klick einen je Zielelement. Eine Version je Schreibvorgang wäre ein Tastenprotokoll gewesen; stattdessen werden alle Schreibvorgänge innerhalb von **10 Minuten zu einer** Nebenversion zusammengefasst, inhaltlich unveränderte Stände erzeugen gar keine, und verschiedene Benutzer laufen nie in einer Version zusammen. Eine Version hält den **vollständigen** Zustand als JSON in der Form `PlanInput` – dadurch ist die **Versionsauswahl in allen vier Analysewerkzeugen** (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren) fast kostenlos; bei Monte-Carlo **je Szenario einzeln**, weil dort mehrere gleichzeitig laufen. **Wiederherstellen** ist ungefährlich gebaut: Es legt den zurückgesetzten Stand selbst als neue Version an («Wiederhergestellt aus A.B»), löscht also nichts, und **erhält die IDs** von Phasen und Elementen – sonst verlören alle Kind-Szenarien ihre Diff-Basis und zeigten schlagartig alles als «neu». Wo ein Bezug trotzdem bricht (der alte Stand kannte das Element noch nicht), **warnt der Dialog vorher namentlich**. Die destruktive Logik liegt als reine Funktion `planRestore` vor und ist dort getestet; `versioning-db.ts` führt sie nur aus. Ein **statischer Wächter-Test** liest alle Route-Dateien und verlangt, dass jeder schreibende Endpunkt eine Version auslöst – eine vergessene Stelle wäre eine stille Lücke. Neue Tabelle `ScenarioVersion` + `Scenario.currentMajor` (Migration gegen echtes Postgres verifiziert), neue Endpunkte unter `/api/scenarios/<id>/versions`. Neue Kapitel 3.8 und 9.28; 25 Tests ergänzt (139 → 164). |
|
||
| 0.18 | 2026-07-19 | Claude (Opus 4.8) | **Live-Simulation** (Roadmap Nr. 22). Neuer Button und Dialog als Zweispalter: links Schieberegler, rechts eine wählbare Grafik, darüber eine Kennzahlenleiste. Dreht man an einem Regler, wird der Plan **sofort** neu gerechnet – ohne für jede Variante eine Szenario-Kopie anzulegen. **Keine eigene Rechenlogik:** Die Regler benutzen dieselben Transformationen wie der Tornado (`applyDriver`), können also gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse. Neu ist nur `applyElementDriver` – dieselbe Verschiebung auf ein **einzelnes** Element statt auf eine ganze Kategorie: Standardmässig gibt es einen Sammelregler «Rendite», ein Klick auf «Renditen einzeln aufschlüsseln» ersetzt ihn durch je einen Regler pro Anlage (der Sammelregler wird dabei **entfernt**, nicht ergänzt, sonst zählte eine Bewegung doppelt; ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben). Der unveränderte Plan wird als **Referenzlinie** mitgezeichnet, und die Kennzahlenleiste weist Endvermögen nominal/real **mit Differenz zum Plan** aus sowie – als eigene Karte – ob das Kapital reicht; ein gekippter Plan ist einer Verlaufslinie sonst nicht anzusehen. Gemessene Laufzeit von `computePlan`: **0.2 ms** auf einem 60-Jahres-Plan mit 10 Elementen, also rund 1 % des 16-ms-Frame-Budgets – deshalb wird synchron gerechnet, **ohne Debounce und ohne Worker**. Anders als der Tornado haben die Regler **Standardbereiche** (Begründung des scheinbaren Widerspruchs zu 9.18: neues Kapitel 9.27), beide Enden editierbar. **Das Pensionsalter fehlt weiterhin** (9.18, eigener Roadmap-Punkt); «Als Szenario speichern» ist bewusst zurückgestellt, ersatzweise zeigt der Dialog die aktive Einstellung als lesbare Zeile. Die Vermögensaufteilung wurde als `AllocationChart` aus dem Dashboard herausgelöst, damit beide sie nutzen. Neue Kapitel 4.15 und 9.27; 15 Tests ergänzt (124 → 139). Keine API-, DB- oder Schreib-Änderung – das Feature liest ausschliesslich. Nebenbei dieselbe vom Umlaut-Sweep verstümmelte Hex-Farbe (`#7c3aed`) wie in 0.17, diesmal im Dashboard. |
|
||
| 0.17 | 2026-07-19 | Claude (Opus 4.8) | **Monte-Carlo: zwei Welten, vier Fälle.** Behebt einen Darstellungs-Widerspruch: Zuvor konnte «Planung 69 % erreicht» neben «Ziel 3 Mio nur 41 %» stehen, obwohl 3 Mio unter dem Plan-Endbetrag von 3.7 Mio lag – die beiden Zahlen stammten aus **verschiedenen simulierten Welten**. Neu läuft die Simulation **immer zweimal** (historische Renditen / geplante Werte, gemeinsamer Seed) und liest aus **jeder** Verteilung **beide** Schwellen ab: Plan-Endbetrag und Zielbetrag. Fall 1 und Fall 3 stammen damit aus derselben Verteilung, wodurch ein tieferes Ziel **nie** unwahrscheinlicher sein kann als ein höheres – der Widerspruch ist strukturell ausgeschlossen (Test). Zweite Korrektur: Der Nullpunkt für das Urteil ist **nicht 50 %**, sondern **Fall 2** (derselbe Schwellwert in der eigenen geplanten Welt); durch den Volatilitäts-Drag liegt der je nach Streuung bei 27–48 %. Verglichen wird Fall 1 gegen Fall 2 mit ± 5 pp Toleranzband → «zurückhaltend / realistisch / zu optimistisch». Darstellung: Fall 1 prominent mit Urteil, Fall 3+4 als Satzpaar untergeordnet, Fall 2 und die Mediane klein als Referenz. Der Drei-Wege-Umschalter aus 0.16 entfällt; historische Mittelwerte **und** Zielbetrag sind jetzt beide Pflicht. Technisch: `MonteCarloResult.finalWealthSorted` (alle Endvermögen sortiert) plus neuer Helfer `probabilityAtLeast` (Binärsuche) – vier Zahlen aus zwei Läufen statt vier Läufen. Kapitel 4.12.7 und 9.26 neu gefasst; 3 Tests ergänzt (121 → 124). Keine Änderung am Rechenkern. |
|
||
| 0.16 | 2026-07-19 | Claude (Opus 4.8) | **Monte-Carlo mit zwei Fragestellungen** (Roadmap Nr. 46). Ein Umschalter oben trennt: **«Planung prüfen»** (Fall 1, wie bisher) würfelt um die **historischen** Renditen und prüft gegen den **Planungs-Endbetrag** (read-only) – «wie realistisch ist meine Planung?». **«Ziel prüfen»** (Fall 2, neu) würfelt um die **geplanten** Werte aus dem Plan und prüft gegen einen **manuellen Zielbetrag** – «erreiche ich mein Ziel?»; hier sind keine historischen Mittelwerte nötig, nur die Streuung. **«Beides»** rechnet beide Durchgänge gleichzeitig (gemeinsamer Seed). Neue Ergebnis-**Deutungstexte** je Fall (weich formuliert wegen des Volatilitäts-Drags). Der Fächer stammt aus dem historischen Durchgang; der Zielbetrag ist in Fall 2 **einer für alle** Szenarien. `runMonteCarloMulti` nimmt neu die Inflation **je Szenario** (`inflationMeanFor`); neuer Helfer `plannedReturnOf`. Kapitel 4.12 überarbeitet, 4.12.7 und 9.26 neu; 8 Tests ergänzt (119 → 121). Nebenbei eine vom Umlaut-Sweep verstümmelte Hex-Farbe (`#7c3aed`) korrigiert. Keine Änderung am Rechenkern. |
|
||
| 0.15 | 2026-07-19 | Claude (Opus 4.8) | **Plan-Assistent überarbeitet** (Schritt 2 und 4). Rein an der Oberfläche, keine Änderung an Berechnung, Datenmodell oder API. **(Schritt 2 – Lebensphasen):** Die Lebenslinie zerfällt neu an den **fixen Pensionierungszeitpunkten** in Abschnitte (neues reines Modul `phaseplan.ts`, `planSegments`): Erwerb (alle arbeiten), Misch (eine pensioniert, eine arbeitet), Pension (alle pensioniert) – jeweils mit **kurzer Definition**. In den durch eine Pensionierung **fest begrenzten** Abschnitten verteilt der Nutzer beliebig viele Phasen mit **+/Papierkorb** und **eigenem Namen je Phase**; eine Live-Summe erzwingt, dass die Phasendauern exakt aufgehen («Weiter» ist bis dahin gesperrt). Der letzte Pensions-Abschnitt ist **offen** (Lebensdauer frei). Die Anzahl Abschnitte wird **abgeleitet** – Einzelplan: 2 (Erwerb, Pension); Paar mit unterschiedlichem Pensionsalter: 3. Neue **Zeitachse** mit Pensionierungs-Flaggen und nummerierter Beschriftung **unter** dem Balken (auch kurze Phasen bleiben lesbar). Behebt den Fehler, dass die Erwerbsphase zuvor beliebig über die Pensionierung hinaus gesetzt werden konnte. **(Schritt 4 – Vorsorge & Vermögen):** bei Paaren aufgeteilt in **Gemeinsam / Person A / Person B**; PK und 3a sind je Person, Wertschriften/Wohneigentum/Schulden je Bereich (gemeinsam oder pro Person). Neue Kapitel 3.2.8 überarbeitet; 8 Tests ergänzt (111 → 119). |
|
||
| 0.14 | 2026-07-18 | Claude (Opus 4.8) | **Phasenkopf überarbeitet und zwei Verteil-Werkzeuge.** (1) **Zweizeilige Wertdarstellung:** Im Anzeigemodus «Beide» steht der Realwert neu in Klammern in einer **eigenen Zeile** unter dem nominalen Wert statt daneben – im Phasenkopf *und* in den Matrix-Zellen. Der Pfeil wiederholt sich auf der zweiten Zeile, damit der Bezug Start → Ende erhalten bleibt. Nebeneffekt: Die Spalten werden schmaler, wodurch **jede Kennzahl umbruchfrei** (`whitespace-nowrap`) dargestellt werden kann. (2) Die Kennzahl heisst korrekt **«Sparquote»** bzw. **«Verzehrquote»** statt «Quote»/«Verzehr». (3) Neuer Block **«Verfügbares Kapital»** im Phasenkopf (ab Phase 2, nur wenn > 0): der beim Übergang zugeflossene Topf mit «davon verteilt» und «Rest auf Cash». (4) Zwei neue Werkzeuge als eigene Popups: **«Kapital verteilen»** (Zusatzeinlagen in PK/3a/Vermögen, Sonderamortisation, Sofort-Tilgung) und **«Sparquote/Bezug verteilen»** (jährliche Raten), beide mit **Live-Vorschau** über eine erneute `computePlan`-Rechnung im Browser – die angezeigte Wirkung ist dadurch per Konstruktion exakt die spätere, inklusive aller Kappungen. Der Quoten-Dialog weist neben erstem und letztem Jahr die **absolute Quote über die ganze Phase** aus und warnt, wenn die Quote über die Phase sinkt. Neues reines Modul `distribution.ts`. Neue Kapitel 3.6.9, 3.6.10, 9.25; 3.6.1 und 3.6.3 überarbeitet. 8 Tests ergänzt (103 → 111). **Keine Änderung an Berechnung, Datenmodell oder API** – beide Werkzeuge schreiben ausschliesslich bestehende Felder über bestehende Endpunkte. |
|
||
| 0.13 | 2026-07-18 | Claude (Fable 5) | **UI-Gesamtumbau** – rein an der Oberfläche, Berechnung, Datenmodell und API-Semantik unverändert. **(A) Fundament:** durchgehende **Du-Form** und **echte Umlaute** in allen sichtbaren Texten (inkl. API-Fehlermeldungen); neue UI-Primitiven in `ui.tsx` (Button, Modal mit ESC/Fokus-Falle/Animation, Bestätigungs-Dialog statt `window.confirm`, Toasts statt `alert`, Skeleton-Loader, EmptyState); eigene **Attention-Farbe** (Amber) für offene Entscheide, getrennt vom Akzent; Micro-Interactions mit `prefers-reduced-motion`-Fallback. **(B) Onboarding (Roadmap Nr. 10):** geführter **Plan-Assistent** in fünf Schritten (reine Orchestrierung bestehender Endpunkte, Einkommen bewusst pro Person – räumt die 9.9-Falle aus), **Beispielplan mit einem Klick** (Übergänge absichtlich offen – die Ampel lehrt sich selbst), **interaktive Tour** über die Planansicht, abgeleitete **«Nächste Schritte»**-Karte. **(C) Struktur:** Einzel-Bearbeitungen laufen neu über ein rechtes **Inspector-Panel** statt Modals (Matrix bleibt sichtbar; Klick auf andere Zelle wechselt den Inhalt); **Phasenkopf entschlackt** auf vier Kern-Infos (Rest wohnt in der Detailansicht aus 0.11); Matrix mit eigenem Scrollbereich und **beidachsig fixierten Köpfen**; Sidebar-Gruppen «Meine Pläne»/«Wissen» («So rechnet FPT», Systemparameter); Terminologie-Fix «Szenario-Profil» statt «Plan-Einstellungen»; Aktions-Icons auch ohne Hover sichtbar (Touch). **(D) Extras:** **Sparklines** je Element-Zeile (aus den 0.11-Verlaufswerten, keine Neuberechnung), **Befehls-Palette** (Ctrl/Cmd+K), Ruin-Banner verlinkt auf die Einflussfaktoren. Neue Kapitel 3.2.8, 3.7.6–3.7.9, 9.23, 9.24; 9.17 bereinigt (der `Selection`-Rest und der ProfileMenu-Lint-Fehler sind behoben – `npm run lint` ist erstmals fehlerfrei). Testbestand unverändert 103. |
|
||
| 0.12 | 2026-07-18 | Claude (Opus 4.8) | **Lesbarkeit der Wasserfälle, Verkaufspreis-Abgleich und Erklärung wirkungsloser Tornado-Treiber.** (1) Die beiden Wasserfälle werden **nicht mehr mit Recharts** gezeichnet, sondern als eigene liegende Darstellung: Verbindungslinien zwischen den Balken, Wertbeschriftung an jedem Schritt, Abschnitts-Überschriften („Am Übergang" / „Innerhalb der Phase") und eine aufklappbare Tabelle mit **laufendem Zwischenstand**. Anlass war, dass die bisherige Darstellung faktisch nicht lesbar war – die Zahlen waren korrekt, die Grafik nicht. (2) Der Restposten beider Brücken wird bei Abweichung neu als **Fehlermeldung** ausgewiesen statt als beiläufige „Rundungsdifferenz"; eine nicht aufgehende Zerlegung ist ein Rechenfehler und kein Schönheitsproblem. (3) **Verkaufspreis einer Immobilie** wird beim Wechsel auf „Verkaufen" neu mit dem **modellierten Verkehrswert** vorbelegt; der Dialog weist Verkehrswert und Abweichung aus und warnt ab 10 % Differenz (Kap. 3.5.8, 9.22). Damit fällt auf, wenn angenommene Wertsteigerung und erwarteter Verkaufspreis nicht zusammenpassen. (4) Der Tornado erklärt neu **Nullbalken** statt sie stumm zu zeigen – insbesondere den Fall, dass die Immobilien-Wertsteigerung bei einem Verkauf nachweislich wirkungslos ist (`ineffectiveReason`, Kap. 4.13.5). Neue Kapitel 3.5.8, 4.13.5, 9.22; 11 Tests ergänzt (92 → 103), darunter die Invariante `residual === 0` über sieben Plankonstellationen. Keine DB-Änderung, keine Änderung an der Berechnung. |
|
||
| 0.11 | 2026-07-18 | Claude (Opus 4.8) | **Detailansichten (Roadmap Nr. 43)** und **vollständige Offenlegung der Berechnungslogiken (Roadmap Nr. 41)**. (1) Neue **Systemparameter-Ansicht** in der Seitenleiste: alle fest hinterlegten Grössen mit Wert, Bedeutung, Herleitung, Quelle und Stand – als strukturierte Daten aus `constants.ts`, also aus derselben Quelle, aus der gerechnet wird. (2) **Nur-Lese-Detailansicht** je Element und je Lebensphase über ein Expand-Icon: Element mit Verlaufsgrafik über **alle Planjahre** (dafür führt `computePlan` neu `ElementPhaseComputed.yearly` je Element mit), Phase mit Vermögensaufteilung und **zwei Wasserfällen**. (3) Die **Wasserfälle** sind bewusst getrennt: Der Vermögens-Wasserfall zeigt nur echte Zu- und Abgänge (Quote, Kapitalerträge, Wertsteigerung, PK-Beiträge, Steuern, Verrentung, Einmalposten); Sparraten, Amortisationen und Investitionen sind **Umbuchungen** und erscheinen ausschliesslich im Cash-Wasserfall – als Vermögensabgang gezeichnet würden sie einen Verlust vortäuschen, den es nicht gibt. Neue Strukturen `WealthBridge` / `CashBridge` inkl. Restposten als Kontrollgrösse. (4) **Rechenweg-Protokoll**: `computePlan(plan, sample?, { explain })` protokolliert die Schritte, die es ohnehin ausführt – Formel, eingesetzte Zahlen, Ergebnis und Hinweis auf geltende Vereinfachungen. Abdeckung über **alle** Ebenen (Element je Phase, Element je Übergang, Phasen-Kennzahlen, Plan-Ebene). Standardmässig aus, damit die Monte-Carlo-Simulation unberührt bleibt. Jeder Rechenweg verlinkt in das passende Kapitel dieser Spezifikation; ein Test prüft, dass alle Verweise eine existierende Überschrift treffen. Neue Kapitel 3.6.7, 3.6.8, 4.14, 9.20, 9.21; 12 Tests ergänzt (80 → 92). Keine DB-Änderung; die 43 Golden Tests laufen unverändert. |
|
||
| 0.10 | 2026-07-18 | Claude (Opus 4.8) | **Szenario-Vergleich in der Monte-Carlo-Simulation** und **Sensitivitätsanalyse / Tornado** (Roadmap Nr. 20). (1) Die MC-Simulation rechnet neu **mehrere Szenarien desselben Plans in einem Lauf**. Die historischen Annahmen werden dabei nur **einmal je logischem Element** erfasst – die Zuordnung über die Herkunfts-Kette `sourceElementId`, dieselbe Grundlage wie beim Diff (neue Funktionen `resolveRootElementId`, `buildElementGroups`, `paramsForScenario`, `runMonteCarloMulti`). Alle Szenarien laufen mit **demselben Seed** (Common Random Numbers), damit Unterschiede strukturell und nicht zufällig sind. Der **Zielbetrag bleibt szenario-eigen** (vorbelegt mit dem jeweils geplanten Endvermögen) – nur so misst die Erfolgswahrscheinlichkeit, wie oft ein Szenario sein *eigenes* Versprechen hält. Ergebnis als Vergleichstabelle plus Median-Linien je Szenario; bei einem einzelnen Szenario unverändert der bisherige Fächer. (2) Neuer Bereich **Einflussfaktoren** (eigener Button, eigener Dialog) mit einem **Tornado-Chart** nach dem One-at-a-time-Verfahren: neues reines Modul `sensitivity.ts` mit sieben Treibern, je Treiber an-/abwählbar und mit **pflichtiger, frei definierbarer Bandbreite ohne Default**. Das **Pensionsalter ist bewusst nicht enthalten** (Begründung: 9.18). Neue Kapitel 3.6.6, 4.12.6, 4.13, 9.18; 20 Tests ergänzt (60 → 80). Keine DB-Änderung, keine Verhaltensänderung bestehender Pläne. **Ausserdem vier Dokumentationsfehler korrigiert:** Kap. 1.2 nannte noch Monte-Carlo, Hypothekarzinsen und Immobilien-Wertsteigerung als nicht umgesetzt (seit 0.5/0.7 vorhanden); Kap. 4.6.5 endete mit einem widersprüchlichen Restsatz zur fehlenden Wertsteigerung; Kap. 5.2/5.3/5.5.2/8.1 waren bei Migrationszahl, Dateiliste, Komponentenliste und Testzahlen veraltet; der Glossar-Eintrag „Szenario" beschrieb noch das Modell vor V6. |
|
||
| 0.9 | 2026-07-18 | Claude (Opus 4.8) | **UI-Umbau und Planstart.** (1) Die Grafiken liegen neu im eigenen Bereich **Grafiken** (Dialog, Button oben) statt unter der Matrix. (2) Kennzahl **Geschätzter Nachlass** entfernt – sie war identisch mit dem nominalen Endvermögen. (3) **Monte-Carlo-Button** nach oben zu den Szenario-Aktionen verschoben. (4) Neues Profilfeld **Planstart (Jahr)** (`Scenario.startYear`, Migration; reine Anzeige, Berechnung bleibt in relativen Jahren) – erscheint auf der Zeitachse. (5) Zeitachse zeigt neu die **Lebensphasen als Segmente** (Breite = Dauer, Einfärbung nach Phasentyp, Jahresspanne). (6) **Lebensphase bearbeiten** neu als Popup statt Panel unter der Tabelle. (7) **Vermögensverlauf** über **alle Jahre** statt nur über die Phasengrenzen – dafür führt `computePlan` das Vermögen neu pro Jahr mit (`YearPoint.wealthNominal/wealthReal`). Zwei Tests ergänzt (58 → 60). |
|
||
| 0.8 | 2026-07-18 | Claude (Opus 4.8) | **Szenario-Hierarchie (V6)** – grösste Umstrukturierung bisher. Der **Plan** ist neu ein schlanker Behälter ohne Finanzdaten; die berechenbare Einheit ist das **Szenario**, das Grundprofil (inkl. **Pensionsalter** → Frühpensionierungs-Szenarien), Phasen und Elemente trägt. Jeder Plan erhält beim Anlegen automatisch ein **Basisszenario**; weitere Szenarien entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als **Baum** darunter (Sidebar zeigt die Verschachtelung). Kopierte Phasen/Elemente tragen Herkunfts-Verweise (`sourcePhaseId`, `sourceElementId`) – darauf beruht die **Abweichungs-Markierung**: geändert = gelb, neu = grün, entfernt = graue Geisterzeile (eigene Theme-Tokens für alle drei Farbschemata). Charts vergleichen neu die Geschwister-Szenarien. Datenmodell: neue Tabelle `Plan`, bisheriger `Plan` → `Scenario` (IDs erhalten), `planId` → `scenarioId` in Person/Phase/FinancialElement. API neu unter `/api/scenarios/*`. Neue Kapitel 2.1, 3.2, 4.13; 9 Diff-Tests + Migrations-Test (48 → 58). **Migration mit echtem Postgres (PGlite) verifiziert**, inkl. verschachtelter Szenarien und Cascade. |
|
||
| 0.7 | 2026-07-17 | Claude (Opus 4.8) | **Monte-Carlo-Simulation** (Roadmap Nr. 19, Stufe A). Button in der Planansicht → Dialog mit Erklärung, Eingaben und Ergebnis. Statt einer festen Rendite/Inflation werden tausende Zufallspfade gerechnet; ausgewiesen werden **Ruinwahrscheinlichkeit**, **Erfolgswahrscheinlichkeit** (P(Endvermögen ≥ Zielbetrag)) und ein **Fächer** (10 %/Median/90 %) plus die deterministische Linie. Läuft komplett im Browser (`computePlan` ist rein). Modell: fettschwänzige Verteilung (Student-t, ν=5), gemeinsamer Marktschock (ρ=0.7), Böden 0 % für PK/3a und −100 % sonst. Pro renditetragendem Element und für die Inflation je: historischer Ø (Pflicht), Streuungsstufe, σ. Neue Datei `montecarlo.ts` + optionaler `sample`-Parameter in `computePlan` (Inflation neu als kumulatives Array; deterministisch identisch). Neues Kapitel 4.12; 7 MC-Tests + Refactor-Absicherung (41 → 48). Keine Verhaltensänderung, keine DB-Änderung. |
|
||
| 0.6 | 2026-07-17 | Claude (Opus 4.8) | **Teilverkauf** von Sonstigem Vermögen (Roadmap Nr. 42) und **Sonderamortisation** der Hypothek (Roadmap Nr. 15). `OTHER_ASSET` am Übergang neu: Halten / Verkaufen / **Teilverkauf** – ein Betrag fliesst ins Cash (erscheint als „Kapitalzufluss" im Phasenkopf), der Rest bleibt investiert. `REAL_ESTATE` im Halten-Fall neu mit **Einmaltilgung** aus dem Cash (analog zur Sofort-Tilgung bei Schulden; erscheint als „Kapitalinvestition"). Damit lässt sich die indirekte Amortisation via 3a mechanisch nachbilden. Die eigentliche Roadmap Nr. 15 (Steuerwirkung der indirekten Amortisation) bleibt mit dem Steuer-Bündel 13/14/23 zurückgestellt – siehe 9.14. Kapitel 4.9.3/4.9.4 ergänzt. Fünf Regressionstests (36 → 41). Keine Verhaltensänderung für bestehende Pläne. |
|
||
| 0.5 | 2026-07-17 | Claude (Opus 4.8) | **Netto/Brutto geklärt** (Roadmap Nr. 9, reduziert) und **Immobilien-Modul erweitert** (Roadmap Nr. 8). Einkommen ist neu explizit als **Nettolohn** definiert (Label und Hilfetext); für die AHV rechnet das Tool intern mit `AHV_GROSS_FROM_NET_FACTOR = 1.12` auf den Bruttolohn hoch – die AHV bemisst sich am Brutto, die bisherige Netto-Basis unterschätzte die Rente um bis zu ~1'900/Jahr. Immobilie neu mit **Hypothekarzins** (% der Restschuld, sinkt mit der Amortisation, mit Doppelzählungs-Schalter) und **Wertsteigerung** (auf die **Liegenschaft**, nicht auf das Eigenkapital – Hebeleffekt). Grundstückgewinnsteuer bemisst sich neu explizit am ursprünglichen Kaufpreis. Keine Aufschlüsselung bei Einkommen oder Ausgaben, keine Steuerschätzung (Begründung: 9.14). Neues Kapitel 4.4.5; 4.6.5 und 4.9.4 überarbeitet; Abschnitt 9 um zwei Punkte ergänzt. Sechs Regressionstests (30 → 36). **Verhaltensänderung:** siehe 9.13. |
|
||
| 0.4 | 2026-07-17 | Claude (Opus 4.8) | **AHV-Rente einkommensabhängig** (Roadmap Nr. 3) und **Fortschreibungs-Warnhinweis** (Roadmap Nr. 4). Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) über das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente; verifiziert gegen die amtliche Tabelle 318.117.1 (51/51 Zeilen). Prüfung der Beitragskarriere am Pensions-Übergang, mit Zusatzfeldern für die Jahre vor Planbeginn (ab Alter 21). Alles **real** gerechnet. Neue Konstanten `AHV_MIN_MONTHLY_FULL`, `AHV_PENSION_MONTHS`, `AHV_CONTRIBUTION_START_AGE`; `AHV_MAX_ANNUAL_SINGLE` neu abgeleitet. Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen existieren. Neue Kapitel 3.5.6, 4.4; Abschnitt 9 um zwei Punkte ergänzt. Zwölf Regressionstests (18 → 30). **Verhaltensänderung:** siehe 9.10. |
|
||
| 0.3 | 2026-07-16 | Claude (Opus 4.8) | **Einmalige Sonderein-/ausgaben** umgesetzt (Roadmap Nr. 1). Der Cash-Übergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 übernehmen · einmaliger Zufluss · einmalige Kosten · beides. Beträge gehen direkt aufs Cash-Konto. Zufluss wird nominal erfasst (real angezeigt) mit optionalem Steuersatz (Default 0 %), Kosten werden real erfasst (nominal angezeigt); beide bleiben aus der Sparquote heraus. Der Entscheid startet unbeantwortet und zählt im „offen"-Badge mit. Neue Spalte `Phase.cashTransition` (JSON) + Migration, neue Route `PUT /api/phases/<id>/cash-transition`, eigene Kennzahlen im Phasenkopf. Fünf Regressionstests ergänzt (13 → 18). Neue Kapitel 3.5.5, 4.9.6; Abschnitt 9 um zwei Grenzen ergänzt. |
|
||
| 0.2 | 2026-07-16 | Claude (Opus 4.8) | Drei Fixes umgesetzt und dokumentiert: **(1)** `Phase.inflationRate` ersatzlos entfernt (DB-Migration, API, Typen, UI) – die Inflation liegt seit V5 plan-weit; das Feld war wirkungslos. **(2)** Amortisation und Tilgung stoppen neu, sobald Hypothek bzw. Schuld abbezahlt sind – sie belasten danach weder Cash noch Sparquote (Kap. 4.6.5, 4.6.7, 4.7, 4.8). **(3)** Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezügen vor der Pensionierung, inkl. Steuerfeld und Netto-Vorschau im UI (Kap. 3.5.2, 4.9.1, 4.9.2). Kennzahl `plannedSaveRate` ist neu die Rate des **ersten** Phasenjahres. Drei Regressionstests ergänzt (10 → 13). Abschnitt 9 neu nummeriert (erledigte Punkte entfernt, „Sparraten werden nicht indexiert" ergänzt). |
|
||
| 0.1 | 2026-07-16 | Claude (Opus 4.8) | Erstfassung. Vollständige Neuerstellung aus dem Code (Stand `f768e01`). Ersetzt die bisherigen FDD/TDD-Dokumente v1–v5 vollständig. |
|
||
|
||
> **Pflegehinweis:** Dieses Dokument ist ein lebendes Dokument. Bei jeder Aktualisierung wird
|
||
> (a) die Änderungshistorie um eine Zeile ergänzt, (b) die Versionsnummer im Kopf hochgezählt
|
||
> und (c) der referenzierte Codestand (Commit) aktualisiert.
|
||
>
|
||
> Die Versionsnummer ist ein **Zähler nach dem Punkt**, keine Dezimalzahl: auf 0.9 folgt 0.10,
|
||
> darauf 0.11 und so weiter. Ein Sprung auf 1.0 ist damit dem Erreichen eines echten
|
||
> Meilensteins vorbehalten und passiert nicht als Nebeneffekt des Hochzählens.
|
||
|
||
---
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
1. [Einleitung und Abgrenzung](#1-einleitung-und-abgrenzung)
|
||
2. [Fachliche Grundkonzepte](#2-fachliche-grundkonzepte)
|
||
3. [Funktionale Spezifikation](#3-funktionale-spezifikation)
|
||
4. [Berechnungsmodell](#4-berechnungsmodell)
|
||
5. [Technische Spezifikation](#5-technische-spezifikation)
|
||
6. [API-Referenz](#6-api-referenz)
|
||
7. [Betrieb und Deployment](#7-betrieb-und-deployment)
|
||
8. [Qualitätssicherung](#8-qualitätssicherung)
|
||
9. [Bekannte Einschränkungen und Modellentscheide](#9-bekannte-einschränkungen-und-modellentscheide)
|
||
10. [Glossar](#10-glossar)
|
||
|
||
---
|
||
|
||
# 1. Einleitung und Abgrenzung
|
||
|
||
## 1.1 Zweck des Tools
|
||
|
||
Das FPT ist eine Webanwendung zur persönlichen Finanzplanung über Lebensabschnitte hinweg,
|
||
ausgelegt auf Schweizer Verhältnisse (AHV, Pensionskasse, Säule 3a, Grundstückgewinnsteuer).
|
||
Ein Benutzer bildet seine finanzielle Situation als Kette von **Lebensphasen** ab, hinterlegt
|
||
**finanzielle Elemente** (Einkommen, Ausgaben, Vorsorge, Immobilien, Vermögen, Schulden) und
|
||
trifft an den **Übergängen** zwischen den Phasen bewusste Entscheide (halten, verkaufen,
|
||
Kapital beziehen, verrenten). Das Tool rechnet daraus Jahr für Jahr das Vermögen, den
|
||
Cash-Bestand, Spar- bzw. Verzehrquoten sowie ein allfälliges Ruinalter fort.
|
||
|
||
## 1.2 Was das Tool nicht ist
|
||
|
||
Aus dem Code direkt ableitbare Abgrenzungen:
|
||
|
||
- **Keine Steuerberechnung** ausser den drei explizit modellierten Sätzen
|
||
(Kapitalbezugssteuer, Grundstückgewinnsteuer, PK-Umwandlungssatz). Einkommens- und
|
||
Vermögenssteuern sind nicht modelliert und müssen vom Benutzer in den Ausgaben erfasst werden
|
||
(Begründung: [9.14](#914-keine-steuerschätzung)).
|
||
- **Keine Nebenkosten, kein Eigenmietwert, keine Mieteinnahmen** bei Immobilien
|
||
(siehe [9.3](#93-immobilien-was-noch-fehlt)).
|
||
- **Keine automatische Deckung von Liquiditätslücken.** Negatives Cash wird gemeldet, aber nicht
|
||
korrigiert (siehe [9.1](#91-cash-wird-nicht-automatisch-ausgeglichen)).
|
||
- **Keine Mehrbenutzer-Kollaboration.** Pläne gehören genau einem Benutzer.
|
||
|
||
> **Hinweis zur Dokumenthistorie:** Bis Version 0.9 stand hier zusätzlich „keine
|
||
> Monte-Carlo-Simulation", „keine Hypothekarzinsen" und „keine Wertentwicklung von Immobilien".
|
||
> Alle drei sind seit Version 0.5 bzw. 0.7 umgesetzt ([4.4.5](#445-netto-brutto-umrechnung-für-die-ahv),
|
||
> [4.6.5](#465-real_estate-immobilie), [4.12](#412-monte-carlo-simulation)); die Abgrenzung war
|
||
> versehentlich stehen geblieben.
|
||
|
||
Die deterministische Rechnung bleibt der Kern des Tools; Stochastik kommt ausschliesslich in der
|
||
**Monte-Carlo-Simulation** ([4.12](#412-monte-carlo-simulation)) und – als reine Was-wäre-wenn-
|
||
Rechnung – in der **Sensitivitätsanalyse** ([4.13](#413-sensitivitätsanalyse-tornado)) dazu.
|
||
Beide verändern die gespeicherten Plandaten nicht.
|
||
|
||
## 1.3 Kernprinzip: Plan als selbsttragende Einheit
|
||
|
||
Seit dem V3-Rework (Migration `20260713150000_profile_to_plan_v3`) trägt **jeder Plan sein
|
||
eigenes Grundprofil**: Haushaltsform, Personen (Alter, Pensionsalter, Name) und
|
||
Inflationsannahme. Es gibt keine übergeordnete Haushalts-Entität mehr. Ein Szenario ist
|
||
deshalb eine vollständige Deep-Copy und vom Ursprungsplan unabhängig veränderbar.
|
||
|
||
Referenz: `prisma/schema.prisma` Zeilen 8–10, `src/lib/types.ts` Zeilen 38–49.
|
||
|
||
---
|
||
|
||
# 2. Fachliche Grundkonzepte
|
||
|
||
## 2.1 Die Ebenen
|
||
|
||
```
|
||
User
|
||
└── Plan (Behälter: nur Name — KEINE Finanzdaten)
|
||
└── Scenario[] (die berechenbare Einheit)
|
||
│ Grundprofil: Haushaltsform, Personen, Inflation, Cash-Startwert
|
||
│ isBase = genau eines je Plan · parentScenarioId = Baum + Vergleichsbasis
|
||
├── Person[] (1 bei SINGLE, 2 bei COUPLE — je Szenario eigen!)
|
||
├── Phase[] (Kette 1..n; sourcePhaseId = Gegenstück in der Vorlage)
|
||
└── FinancialElement[] (szenario-weit; sourceElementId = Gegenstück in der Vorlage)
|
||
├── ElementPhaseValue[] (Werte je Phase, JSON)
|
||
└── ElementTransitionValue[] (Entscheide je Übergang, JSON)
|
||
```
|
||
|
||
**Zwei Designprinzipien tragen dieses Modell:**
|
||
|
||
**(1) Ein finanzielles Element ist über alle Lebensphasen hinweg dieselbe Entität.** Eine
|
||
Pensionskasse „PK Arbeitgeber" existiert einmal pro Szenario; sie hat pro Phase einen Werte-Satz
|
||
und pro Phasenübergang einen Entscheid-Satz. Dadurch bleibt die Identität eines
|
||
Vermögensgegenstands über die Zeit erhalten – Voraussetzung für die Fortschreibung (Carry) und
|
||
die Vermögensaufteilungs-Grafik.
|
||
|
||
**(2) Das Grundprofil liegt am Szenario, nicht am Plan** (V6). Nur so lassen sich die
|
||
wertvollsten Szenario-Fragen abbilden – allen voran ein abweichendes **Pensionsalter**
|
||
(„Was, wenn ich mit 62 statt 65 aufhöre?"), das in `Person` steckt. Wäre das Profil geteilt,
|
||
wären Frühpensionierungs-Szenarien unmöglich.
|
||
|
||
Der **Plan** ist damit ein reiner Behälter: Er bündelt Szenarien und trägt den Eigentümer
|
||
(`userId`). Ownership von Szenario/Phase/Element läuft über die Kette
|
||
`Scenario → Plan → User`.
|
||
|
||
Referenz: `prisma/schema.prisma`.
|
||
|
||
## 2.2 Die Matrix als Leitmetapher
|
||
|
||
Die Hauptansicht ist eine Tabelle:
|
||
|
||
- **Zeilen** = finanzielle Elemente, gruppiert nach Kategorie; zuoberst die systemseitige,
|
||
read-only Zeile **Cash**.
|
||
- **Spalten** = abwechselnd **Phasenspalten** und **Übergangsspalten**
|
||
(Phase 1 → Übergang → Phase 2 → Übergang → Phase 3 …). Der letzten Phase folgt keine
|
||
Übergangsspalte.
|
||
- **Zellen** = anklickbar; Phasenzellen öffnen die Werte-Eingabe, Übergangszellen den
|
||
Entscheid-Dialog.
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 111–120 (Spaltenaufbau), 333–477 (Matrix).
|
||
|
||
## 2.3 Phasentyp – abgeleitet, nicht gespeichert
|
||
|
||
Der Typ einer Phase wird **nie gespeichert**, sondern in jeder Berechnung aus Alter und
|
||
Pensionsalter der Personen abgeleitet:
|
||
|
||
| Bedingung (zu Phasenbeginn) | Typ |
|
||
|---|---|
|
||
| alle Personen `startAge < retirementAge` | `ERWERB` |
|
||
| alle Personen `startAge >= retirementAge` | `PENSION` |
|
||
| gemischt | `MIXED` |
|
||
|
||
Referenz: `src/lib/calculations.ts` Zeilen 158–172; Kommentar in `prisma/schema.prisma` Zeilen 94–95.
|
||
|
||
## 2.4 Cash als Ausgleichskonto
|
||
|
||
Cash ist kein vom Benutzer erfassbares Element, sondern das systemseitige Ausgleichskonto:
|
||
|
||
- Es startet mit `Plan.initialCash` (Phase 1).
|
||
- Es nimmt jährlich die **Spar-/Verzehrquote** (Einkommen − nominale Ausgaben) auf.
|
||
- Es finanziert die **geplanten Sparraten** (3a-Beiträge, Sparbeiträge, Amortisationen, Tilgungen).
|
||
- Es empfängt die **Bezugsraten** aus Sonstigem Vermögen.
|
||
- Es empfängt an Übergängen **Kapitalzuflüsse** (Verkäufe, PK-/3a-Bezüge) und finanziert
|
||
**Sofort-Tilgungen** sowie **Zusatzinvestitionen** der Folgephase.
|
||
- Es nimmt an Übergängen **einmalige Sonderein-/ausgaben** auf (Erbschaft, Poolbau, Autokauf) –
|
||
siehe [3.5.5](#355-cash-übergang-einmalige-sonderein-ausgaben).
|
||
- Es darf **negativ werden** – dies ist die Definition einer Liquiditätslücke und wird rot
|
||
markiert, aber nicht automatisch korrigiert.
|
||
|
||
Cash ist die einzige Zeile der Matrix ohne `FinancialElement`-Datensatz. Zwei Zellen sind
|
||
dennoch bearbeitbar: die **erste** Phasenzelle (Cash-Anfangswert) und **jede Übergangszelle**
|
||
(einmalige Sonderein-/ausgaben).
|
||
|
||
Referenz: `src/lib/calculations.ts` (Jahresschleife und Übergang).
|
||
|
||
---
|
||
|
||
# 3. Funktionale Spezifikation
|
||
|
||
## 3.1 Authentifizierung und Benutzerkonto
|
||
|
||
### 3.1.1 Registrierung
|
||
|
||
- Öffentlich zugänglich (kein Einladungscode, keine Freischaltung).
|
||
- Benutzername: Muster `^[a-zA-Z0-9._-]{3,32}$` – 3 bis 32 Zeichen, Buchstaben, Zahlen, Punkt,
|
||
Unterstrich, Bindestrich. Muss plan-übergreifend eindeutig sein (`User.username @unique`).
|
||
- Passwort: mindestens 6 Zeichen; wird mit bcrypt (Cost-Faktor 12) gehasht.
|
||
- Bei Erfolg wird sofort eine Session gesetzt (Login inbegriffen), HTTP 201.
|
||
- Bei belegtem Benutzernamen: HTTP 409 mit Meldung „Dieser Benutzername ist bereits vergeben."
|
||
|
||
Referenz: `src/lib/users.ts` Zeilen 6–22, `src/app/api/auth/register/route.ts`.
|
||
|
||
### 3.1.2 Anmeldung
|
||
|
||
- Benutzername + Passwort; Prüfung via `bcrypt.compare`.
|
||
- Fehlermeldung ist bewusst unspezifisch: „Benutzername oder Passwort falsch." (HTTP 401) –
|
||
verrät nicht, ob der Benutzer existiert.
|
||
- Bei Erfolg: JWT (HS256, Payload `{ userId }`, Gültigkeit 30 Tage) im HttpOnly-Cookie
|
||
`fpt_session` (`sameSite=lax`, `secure` nur in Produktion, `maxAge` 30 Tage).
|
||
|
||
Referenz: `src/app/api/auth/login/route.ts`, `src/lib/auth.ts`.
|
||
|
||
### 3.1.3 Abmeldung
|
||
|
||
`POST /api/auth/logout` löscht das Cookie. Da die Session zustandslos ist (JWT), bleibt ein
|
||
bereits kopiertes Token bis zum Ablauf technisch gültig – es gibt keine serverseitige
|
||
Token-Sperrliste.
|
||
|
||
### 3.1.4 Passwortänderung
|
||
|
||
Im Profilmenü. Erfordert das aktuelle Passwort; das neue Passwort muss ≥ 6 Zeichen haben und
|
||
wird im Dialog gegen eine Wiederholung geprüft (Client-seitig). Nach Erfolg erscheint 1.2 s lang
|
||
„Passwort geändert.", dann schliesst der Dialog.
|
||
|
||
Referenz: `src/app/api/auth/change-password/route.ts`, `src/components/ProfileMenu.tsx` Zeilen 117–140.
|
||
|
||
### 3.1.5 Zugriffsschutz
|
||
|
||
Zweistufig:
|
||
|
||
1. **Middleware** (`src/middleware.ts`, Edge-Runtime): schützt alle Pfade ausser
|
||
`/login`, `/api/auth/login`, `/api/auth/register`, `/_next/*`, `/favicon*`.
|
||
Ohne gültiges Token → API-Aufrufe erhalten HTTP 401, Seitenaufrufe werden nach
|
||
`/login?next=<pfad>` umgeleitet.
|
||
2. **Ownership-Check in jeder API-Route**: `getCurrentUserId()` plus eine Abfrage, die den
|
||
Datensatz nur zurückgibt, wenn er dem Benutzer gehört (`getOwnedPlan`, `getOwnedPhase`,
|
||
`getOwnedElement` in `src/lib/queries.ts`). Ein fremder Datensatz führt zu HTTP 404
|
||
(nicht 403) – die Existenz wird nicht preisgegeben.
|
||
|
||
## 3.2 Plan-Verwaltung
|
||
|
||
### 3.2.1 Plan erstellen
|
||
|
||
Der «+»-Knopf öffnet eine Auswahl mit drei Wegen:
|
||
|
||
| Weg | Für wen | Was passiert |
|
||
|---|---|---|
|
||
| **Geführt erstellen** (empfohlen) | neue Nutzer | der Assistent aus [3.2.8](#328-geführter-assistent-und-beispielplan) |
|
||
| **Leer starten** | geübte Nutzer | der bisherige Dialog (unten) – nur Grundprofil, keine Phasen/Elemente |
|
||
| **Beispielplan ansehen** | Erkunden | legt einen fiktiven, voll ausgefüllten Plan an ([3.2.8](#328-geführter-assistent-und-beispielplan)) |
|
||
|
||
Der Dialog «Leer starten» fragt Name plus das vollständige Grundprofil:
|
||
|
||
| Feld | Typ | Default | Wertebereich |
|
||
|---|---|---|---|
|
||
| Name des Plans | Text | „Basisplan" | 1–120 Zeichen |
|
||
| Haushaltsform | Auswahl | `SINGLE` | `SINGLE` / `COUPLE` |
|
||
| Name je Person | Text (optional) | leer | ≤ 60 Zeichen |
|
||
| Aktuelles Alter | Zahl | 35 | 0–120 |
|
||
| Pensionierungsalter | Zahl | 65 | 30–100 |
|
||
| Erwartete Inflationsrate (%) | Zahl | 1.5 | −20 bis 50 |
|
||
|
||
Konsistenzregel: `SINGLE` erfordert genau eine Person, `COUPLE` genau zwei (Person A und B).
|
||
Verletzung → HTTP 400 mit Klartextmeldung.
|
||
|
||
Referenz: `src/components/PlanProfileFields.tsx` Zeilen 1012–1018, `src/app/api/plans/route.ts`
|
||
Zeilen 6–35.
|
||
|
||
Ein neu erstellter Plan hat **keine Phasen und keine Elemente**; `initialCash` ist 0.
|
||
|
||
### 3.2.2 Grundprofil ändern
|
||
|
||
Über „Einstellungen" in der Planansicht. Wichtig: Die Personen werden serverseitig in einer
|
||
Transaktion **gelöscht und neu angelegt** (`deleteMany` + `create`). Die Person-IDs ändern sich
|
||
dadurch. Da Elemente über `ownerRole` (nicht über `personId`) zugeordnet sind, bleibt die
|
||
Zuordnung erhalten.
|
||
|
||
Ein Wechsel von `COUPLE` auf `SINGLE` entfernt Person B. Elemente mit `ownerRole = PERSON_B`
|
||
bleiben in der Datenbank bestehen, finden aber keinen Owner mehr – siehe
|
||
[9.2](#92-verwaiste-person_b-elemente).
|
||
|
||
Referenz: `src/app/api/plans/[planId]/route.ts` Zeilen 55–70.
|
||
|
||
### 3.2.3 Cash-Anfangswert
|
||
|
||
Klick auf die erste Zelle der Cash-Zeile öffnet den Dialog „Cash-Anfangswert". Wertebereich
|
||
0 bis 1'000'000'000, wird auf ganze Franken gerundet.
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 1130–1156, `src/app/api/plans/[planId]/route.ts` Zeile 36.
|
||
|
||
### 3.2.4 Plan löschen
|
||
|
||
Aus der Übersichtskachel oder der Planansicht, mit Browser-`confirm()`. Löscht per Datenbank-Cascade
|
||
Personen, Phasen, Elemente und alle Werte. Szenarien, die auf diesen Plan als `parentPlanId`
|
||
zeigen, werden **nicht** gelöscht – ihre `parentPlanId` wird auf `NULL` gesetzt
|
||
(`onDelete: SetNull`).
|
||
|
||
Referenz: `prisma/schema.prisma` Zeile 82.
|
||
|
||
### 3.2.5 Szenarien
|
||
|
||
Beim Anlegen eines Plans entsteht **automatisch das Basisszenario** (`isBase = true`,
|
||
Name „Basisszenario"). Der beim Anlegen erfasste Profilteil (Haushaltsform, Personen,
|
||
Inflation) landet dort, der Name am Plan.
|
||
|
||
Ein weiteres Szenario ist eine **vollständige Kopie eines beliebigen bestehenden Szenarios**
|
||
(nicht nur des Basisszenarios). Kopiert werden Grundprofil, alle Personen, alle Phasen, alle
|
||
Elemente sowie sämtliche Phasen- und Übergangswerte.
|
||
|
||
Gesetzt werden dabei:
|
||
- `parentScenarioId` = das kopierte Szenario → ergibt den **Baum** in der Seitenleiste **und**
|
||
die **Vergleichsbasis** für die Abweichungs-Markierung
|
||
- `sourcePhaseId` / `sourceElementId` je kopierter Phase bzw. Element → die **Identität**, über
|
||
die der Diff „dieselbe Zelle" wiederfindet
|
||
|
||
Da jede Kopie wieder kopierbar ist, entstehen **Sub-Szenarien** beliebiger Tiefe; die
|
||
Seitenleiste rückt sie entsprechend ein.
|
||
|
||
**Löschen:** Ein Szenario lässt sich löschen, das **Basisszenario nicht** (dafür den ganzen Plan
|
||
löschen). Das Löschen eines Plans entfernt per Cascade alle seine Szenarien.
|
||
|
||
Referenz: `src/app/api/scenarios/[scenarioId]/copy/route.ts`.
|
||
|
||
### 3.2.6 Abweichungs-Markierung (Diff)
|
||
|
||
Jedes Szenario ausser dem Basisszenario wird **live gegen sein Eltern-Szenario** verglichen.
|
||
Abweichende Stellen sind in der Matrix farblich markiert:
|
||
|
||
| Zustand | Darstellung |
|
||
|---|---|
|
||
| **geändert** | gelb hinterlegt (`--diff` / `--diff-soft`) |
|
||
| **neu** (in der Vorlage nicht vorhanden) | grün hinterlegt + Badge „neu" (`--diff-added`) |
|
||
| **entfernt** (in der Vorlage vorhanden, hier gelöscht) | graue **Geisterzeile**, durchgestrichen (`--diff-removed`) |
|
||
|
||
Markiert werden: Phasenzellen, Übergangszellen, Element-Zeilen (Name/Zuordnung), Phasenköpfe
|
||
(Name/Dauer), der Cash-Anfangswert, Cash-Übergänge und das Grundprofil-Banner. Über der Matrix
|
||
steht die Gesamtzahl der Abweichungen.
|
||
|
||
Die Farben sind **semantische Tokens** und für Hell, Dunkel und Warm getrennt abgestimmt – ein
|
||
fester Gelbwert würde im Dunkelschema unbrauchbar aussehen.
|
||
|
||
**Zwei bewusste Eigenschaften:**
|
||
- Der Vergleich läuft gegen das **direkte Eltern-Szenario**, nicht immer gegen die Basis. Bei
|
||
einem Sub-Szenario ist das genau „was habe ich gegenüber der Vorlage geändert".
|
||
- Der Diff ist **live**: Ändert man die Vorlage, verschiebt sich die Markierung im Kind
|
||
rückwirkend (siehe [9.16](#916-diff-ist-live-gegen-die-vorlage)).
|
||
|
||
Referenz: `src/lib/diff.ts`.
|
||
|
||
### 3.2.7 Planstart (Kalenderjahr)
|
||
|
||
Das Grundprofil enthält das Feld **Planstart (Jahr)** – das Kalenderjahr, in dem Jahr 1 der
|
||
Planung liegt (`Scenario.startYear`). Es dient **ausschliesslich der Darstellung**: Zeitachse
|
||
und Grafiken beschriften damit Jahre statt nur Alter. Die Berechnung rechnet unverändert in
|
||
**relativen** Jahren ab Planbeginn – `startYear` fliesst in keine Formel ein.
|
||
|
||
Beim Anlegen wird das laufende Jahr vorbelegt; bestehende Szenarien wurden per Migration darauf
|
||
gesetzt. Kalenderjahr eines Planjahrs: `startYear + (Jahr − 1)`.
|
||
|
||
### 3.2.8 Geführter Assistent und Beispielplan
|
||
|
||
**Der Plan-Assistent** (Roadmap Nr. 10: «Schritt für Schritt statt leerer Matrix») fragt in
|
||
fünf Schritten in Alltagssprache: (1) Grundprofil, (2) Lebensphasen, (3) Einkommen und Ausgaben
|
||
plus Kontostand, (4) Vorsorge und Vermögen, (5) Zusammenfassung.
|
||
|
||
**Schritt 2 ist an den fixen Pensionierungszeitpunkten ausgerichtet.** Das Pensionsalter jeder
|
||
Person ist ein Fixpunkt auf der Lebenslinie; dazwischen entstehen Abschnitte mit konstantem
|
||
Erwerbsstatus (reines Modul `phaseplan.ts`, `planSegments`):
|
||
|
||
| Abschnitt | Bedeutung | Länge |
|
||
|---|---|---|
|
||
| **Erwerb** | alle arbeiten | fest (bis zur ersten Pensionierung) |
|
||
| **Misch** | eine Person pensioniert, eine arbeitet | fest (zwischen den Pensionierungen) |
|
||
| **Pension** | alle pensioniert | **offen** (Lebensdauer frei) |
|
||
|
||
Die **Anzahl** Abschnitte wird abgeleitet, nicht vorgegeben: Einzelplan → 2 (Erwerb, Pension);
|
||
Paar mit gleichem Pensionsalter → 2 (keine Mischzeit); Paar mit unterschiedlichem Pensionsalter
|
||
→ 3. Ist eine Person bei Planbeginn bereits pensioniert, beginnt die Linie mit einem Misch- oder
|
||
Pensions-Abschnitt.
|
||
|
||
In jedem **fest begrenzten** Abschnitt verteilt der Nutzer beliebig viele Phasen (mit
|
||
+/Papierkorb und eigenem Namen je Phase); eine **Live-Summe** erzwingt, dass die Phasendauern
|
||
exakt der festen Länge entsprechen – «Weiter» bleibt gesperrt, bis es aufgeht. Das ist zwingend:
|
||
Die Berechnung leitet den Phasentyp am Phasenbeginn ab und **kappt jede Phase am nächsten
|
||
Pensionsereignis** ([2.3](#23-phasentyp--abgeleitet-nicht-gespeichert)) – eine Phase, die eine
|
||
Pensionierung überspannt, gäbe es nicht. Der frühere Assistent liess die Erwerbsphase beliebig
|
||
über das Pensionsalter hinaus setzen; das ist damit behoben. Der **offene** Pensions-Abschnitt
|
||
nimmt beliebige Dauern (Summe = geplante Restlebensdauer).
|
||
|
||
Eine **Zeitachse** zeigt den proportionalen Verlauf mit den Pensionierungs-Fixpunkten als
|
||
Flaggen; die Phasen sind nummeriert und **unter** dem Balken beschriftet, damit auch kurze
|
||
Phasen lesbar bleiben.
|
||
|
||
**Schritt 4 (Vorsorge & Vermögen)** ist bei Paaren in **Gemeinsam / Person A / Person B**
|
||
aufgeteilt. Pensionskasse und Säule 3a sind immer persönlich (personengebundene Kategorien);
|
||
Wertschriften, Wohneigentum und Schulden lassen sich gemeinsam **oder** je Person erfassen.
|
||
|
||
Zwei bewusste Entscheide bleiben:
|
||
- **Einkommen wird pro Person erfasst**, nie als «Gemeinsam» – in Paar-Plänen zählt
|
||
Haushalts-Einkommen nicht für die AHV ([9.9](#99-gemeinsames-einkommen-zählt-bei-paaren-nicht-für-die-ahv));
|
||
der Assistent räumt diese Falle von Anfang an aus.
|
||
- Technisch ist der Assistent **reine Orchestrierung bestehender Endpunkte** (Plan → Phase 1 →
|
||
Elemente samt Werten → Folgephasen; die Reihenfolge stellt sicher, dass die Phasen-Route die
|
||
Folgephasen korrekt vorbelegt). Kein neuer Endpunkt, keine Berechnungsänderung. Grenze: 9.23.
|
||
|
||
**Der Beispielplan** («Beispiel: Alex Muster», `src/lib/demoplan.ts`) ist ein fiktiver, voll
|
||
ausgefüllter Plan per Ein-Klick – ebenfalls reine Orchestrierung. Die Übergangs-Entscheide
|
||
bleiben **absichtlich offen**: Der neue Nutzer sieht die Ampel («N offen») in Aktion und lernt
|
||
das wichtigste Konzept am Beispiel statt aus einer Erklärung.
|
||
|
||
Nach dem ersten Öffnen eines Plans mit Phasen startet einmalig die **Tour**
|
||
([3.7.8](#378-tour-und-nächste-schritte)).
|
||
|
||
## 3.3 Lebensphasen
|
||
|
||
### 3.3.1 Phase anlegen
|
||
|
||
Neue Phasen werden **immer am Ende der Kette** angehängt (`sequenceNumber = Anzahl + 1`).
|
||
|
||
**Automatische Dauer-Kappung:** Die Dauer wird ans nächste Pensionsereignis gekappt. Formel
|
||
(`maxPhaseDuration`): für jede Person, die zu Phasenbeginn noch erwerbstätig ist, gilt
|
||
`retirementAge − (age + yearsBefore)`; das Minimum dieser Werte ist die Obergrenze. Ist keine
|
||
Person mehr erwerbstätig, gibt es keine Obergrenze (`null`). Diese Kappung ist im Dialog
|
||
sichtbar („max. N") **und** wird serverseitig erzwungen.
|
||
|
||
Fachliche Begründung: Eine Phase darf keine Pensionierung überspannen, weil der Phasentyp
|
||
und die AHV-/PK-Renten am Phasenbeginn ausgewertet werden.
|
||
|
||
**Default-Dauer:** die Kappung, sonst 10 Jahre.
|
||
**Default-Name:** Phase 1 → „Erste Lebensphase"; sonst „Pensionsphase" wenn zu Phasenbeginn
|
||
mindestens eine Person pensioniert ist, sonst „Erwerbsphase".
|
||
|
||
**Vorbelegung der Elemente:** Beim Anlegen einer Phase wird für jedes noch aktive Element
|
||
(nicht `SOLD`, nicht `SETTLED`) ein `ElementPhaseValue` mit den **editierbaren** Feldern der
|
||
Vorphase erzeugt (`buildCarryData`):
|
||
|
||
| Kategorie | Übernommene Felder |
|
||
|---|---|
|
||
| `INCOME`, `EXPENSE` | nur `teuerungsausgleich` (Basis wird live fortgeschrieben) |
|
||
| `AHV` | `gapYears: 0` |
|
||
| `PENSION_FUND`, `PILLAR_3A`, `OTHER_ASSET` | `annualContribution`, `expectedReturn` |
|
||
| `REAL_ESTATE` | `purchasePrice`, `amortization` (Resthypothek wird live fortgeschrieben) |
|
||
| `OTHER_DEBT` | `annualRepayment` |
|
||
|
||
Bestände (PK-/3a-/Vermögenswert, Resthypothek, Restschuld) werden **bewusst nicht als Snapshot
|
||
gespeichert**, sondern in jeder Berechnung live aus der Vorphase fortgeschrieben. Damit wirken
|
||
sich nachträgliche Änderungen an frühen Phasen automatisch auf alle Folgephasen aus.
|
||
|
||
Referenz: `src/app/api/plans/[planId]/phases/route.ts`.
|
||
|
||
### 3.3.2 Phase bearbeiten
|
||
|
||
Klick auf einen Phasenkopf öffnet ein **Popup** („Lebensphase: <Name>") mit Bezeichnung und
|
||
Dauer – konsistent zu allen anderen Eingaben (Element-Zellen, Übergänge). Speichern schliesst
|
||
das Popup. Die Dauer wird auch hier gekappt. Eine phasenspezifische Inflationsrate gibt es nicht;
|
||
das Panel weist darauf hin: „Die Inflationsrate gilt plan-weit und wird in den Plan-Einstellungen
|
||
gesetzt."
|
||
|
||
Referenz: `src/components/PhaseDetail.tsx`.
|
||
|
||
### 3.3.3 Phase löschen
|
||
|
||
**Nur die letzte Phase** kann gelöscht werden – serverseitig geprüft (HTTP 400: „Nur die letzte
|
||
Phase kann gelöscht werden."). Damit bleibt die Kette der `sequenceNumber` lückenlos. Der
|
||
Löschen-Button erscheint im Detail-Panel nur bei der letzten Phase.
|
||
|
||
Referenz: `src/app/api/phases/[phaseId]/route.ts` Zeilen 55–77.
|
||
|
||
## 3.4 Finanzielle Elemente
|
||
|
||
### 3.4.1 Kategorien und Personenzuordnung
|
||
|
||
| Kategorie | Label | Zuordnung |
|
||
|---|---|---|
|
||
| `INCOME` | Einkommen | frei: Gemeinsam / Person A / Person B |
|
||
| `EXPENSE` | Ausgaben | frei |
|
||
| `AHV` | AHV | **zwingend genau eine Person** |
|
||
| `PENSION_FUND` | Pensionskasse | **zwingend genau eine Person** |
|
||
| `PILLAR_3A` | Säule 3a | **zwingend genau eine Person** |
|
||
| `REAL_ESTATE` | Immobilie | frei |
|
||
| `OTHER_ASSET` | Sonstiges Vermögen | frei |
|
||
| `OTHER_DEBT` | Sonstige Schulden | frei |
|
||
|
||
Die Vorsorge-Kategorien (`PERSON_ONLY_CATEGORIES`) sind zwingend personengebunden, weil AHV-Rente,
|
||
PK-Verrentung und 3a-Bezug am **individuellen** Pensionierungszeitpunkt hängen. Wird für diese
|
||
Kategorien kein `PERSON_A`/`PERSON_B` übergeben → HTTP 400.
|
||
|
||
Für die übrigen Kategorien gilt: fehlt die Zuordnung, wird serverseitig `HOUSEHOLD` gesetzt.
|
||
|
||
Referenz: `src/lib/elements.ts` Zeilen 19–23, `src/app/api/plans/[planId]/elements/route.ts` Zeilen 43–54.
|
||
|
||
### 3.4.2 Element anlegen
|
||
|
||
Der Dialog „Finanzielles Element" fragt Kategorie, Zuordnung, Bezeichnung **und direkt die
|
||
Werte der ersten Lebensphase** ab. Nach dem Anlegen (`POST .../elements`) werden die Werte per
|
||
`PUT .../phase/<firstPhaseId>` gespeichert, sofern mindestens ein Feld gesetzt wurde. Der
|
||
Bezeichnungs-Default ist das Kategorie-Label.
|
||
|
||
`orderIndex` = bisheriges Maximum + 1; bestimmt die Reihenfolge innerhalb der Kategoriegruppe.
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 799–947.
|
||
|
||
### 3.4.3 Element bearbeiten und löschen
|
||
|
||
Ein Klick auf eine **Phasenzelle** öffnet den Dialog „Lebensphase: <Name>" mit den
|
||
kategorie- und kontextabhängigen Feldern (siehe [3.4.4](#344-feldkatalog-je-kategorie)) sowie
|
||
dem Button „Element löschen". Löschen entfernt das Element **aus allen Phasen**
|
||
(Browser-`confirm()`, dann Cascade auf `ElementPhaseValue` und `ElementTransitionValue`).
|
||
|
||
Umbenennen ist per API möglich (`PATCH /api/elements/<id>`), im aktuellen UI aber nicht
|
||
angebunden.
|
||
|
||
### 3.4.4 Feldkatalog je Kategorie
|
||
|
||
Die angezeigten Felder hängen von drei Kontextgrössen ab:
|
||
- **`carried`** – ob der Basiswert aus der Vorphase fortgeschrieben wird (ab Phase 2)
|
||
- **`ownerWorking`** – ob der zugeordnete Besitzer in dieser Phase erwerbstätig ist
|
||
- **`durationYears`** – Phasendauer (begrenzt z. B. die Ausfalljahre)
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` Zeilen 124–340.
|
||
|
||
#### INCOME (Einkommen)
|
||
|
||
**Einkommen ist der NETTOLOHN** – der Betrag, der nach allen Lohnabzügen (AHV/ALV,
|
||
Pensionskasse, NBU) tatsächlich aufs Konto kommt. Das ist die für den Cash-Fluss richtige
|
||
Grösse und die, in der man denkt.
|
||
|
||
| Feld | JSON | Semantik |
|
||
|---|---|---|
|
||
| Jahreseinkommen NETTO, nominal (erstes Jahr) | `amount` | Basiswert Jahr 1. Ab Phase 2 vorbelegt mit dem fortgeschriebenen Wert, bewusst änderbar (Teilzeit, Beförderung). |
|
||
| ≈ real (heutige Kaufkraft) | – | Read-only Info: `amount / deflatorStart` |
|
||
| Nominale Lohnerhöhung (%/Jahr) | `teuerungsausgleich` | Default 0 %. 0 % = nominal gleichbleibend, real sinkend. |
|
||
|
||
Für die **AHV** rechnet das Tool intern auf den Bruttolohn zurück – siehe
|
||
[4.4.5](#445-netto-brutto-umrechnung-für-die-ahv). Warum die Definition überhaupt nötig war:
|
||
Für den Cash-Fluss sind beide Konventionen gleichwertig (`brutto − Ausgaben inkl. Abzüge`
|
||
≡ `netto − Ausgaben ohne Abzüge`), weshalb die Ambiguität lange folgenlos blieb. Mit der
|
||
einkommensabhängigen AHV hängt daran aber eine Rente.
|
||
|
||
#### EXPENSE (Ausgaben)
|
||
|
||
Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."
|
||
|
||
| Feld | JSON | Semantik |
|
||
|---|---|---|
|
||
| Jahresausgaben REAL (erstes Jahr) | `amount` | Basiswert in heutiger Kaufkraft. |
|
||
| ≈ nominal (in diesem Jahr) | – | Read-only Info: `amount * deflatorStart` |
|
||
| Reale Mehrausgaben (%/Jahr) | `teuerungsausgleich` | **Zusätzlich** zur Inflation. 0 % = gleicher Lebensstandard. |
|
||
|
||
Die Asymmetrie (Einkommen nominal, Ausgaben real) ist der Kern des V5-Modells: Man kennt seinen
|
||
Lohn nominal, aber seinen Lebensstandard real.
|
||
|
||
#### AHV
|
||
|
||
| Zustand | Anzeige |
|
||
|---|---|
|
||
| Besitzer erwerbstätig | Eingabefeld **Geplante Ausfalljahre** (`gapYears`), 0 bis Phasendauer. Hilfetext: „Jedes Ausfalljahr kuerzt die spaetere Rente um 1/44." |
|
||
| Besitzer pensioniert | Nur Hinweistext – die Rente wird automatisch berechnet. |
|
||
|
||
#### PENSION_FUND (Pensionskasse)
|
||
|
||
| Zustand | Felder |
|
||
|---|---|
|
||
| erwerbstätig, Phase 1 | **Aktueller PK-Wert** (`currentValue`), **Jährliche Einzahlung** (`annualContribution`), **Erwartete Rendite** (`expectedReturn`) |
|
||
| erwerbstätig, ab Phase 2 | **Startwert (fortgeschrieben)** (read-only), **Zusatzeinlage aus Kapital** (`additionalInvestment`), Einzahlung, Rendite |
|
||
| pensioniert | Hinweistext (Rente aus Umwandlungssatz bzw. „Vollständig bezogen") |
|
||
|
||
Wichtig (Hilfetext im UI): Die PK-Einzahlung „Fliesst NICHT in die Sparquote ein (bereits in den
|
||
Ausgaben beruecksichtigt)" – Lohnabzüge sind im Nettoeinkommen bereits weg.
|
||
|
||
#### PILLAR_3A (Säule 3a)
|
||
|
||
Wie PK, aber:
|
||
- Die Einzahlung **zählt** zur Sparquote (verlässt das Cash).
|
||
- Das Feld ist auf `PILLAR_3A_MAX_ANNUAL` = **7'258 CHF** (2026, mit PK) hart geklammert.
|
||
- Bei Pensionierung: „Die Saeule 3a wird beim Pensions-Uebergang vollstaendig bezogen."
|
||
|
||
#### REAL_ESTATE (Immobilie)
|
||
|
||
| Feld | JSON | Semantik |
|
||
|---|---|---|
|
||
| Kaufpreis | `purchasePrice` | nur Phase 1 / Neukauf; ab Phase 2 read-only fortgeschrieben |
|
||
| Hypothek | `mortgage` | dito |
|
||
| Startwert Netto (fortgeschrieben) | – | read-only ab Phase 2: **Verkehrswert** − Resthypothek |
|
||
| Resthypothek (fortgeschrieben) | – | read-only ab Phase 2 |
|
||
| Amortisation (CHF/Jahr) | `amortization` | endet, sobald die Hypothek abbezahlt ist |
|
||
| **Hypothekarzins (%/Jahr)** | `interestRate` | Zinssatz auf der **Restschuld** |
|
||
| **Hypothekarzins-Betrag (Beginn → Ende)** | – | read-only: Zinsbetrag im ersten und im letzten Jahr der Phase |
|
||
| **Geschätzte Wertsteigerung (%/Jahr)** | `valueGrowth` | wirkt auf die **Liegenschaft** |
|
||
| **Sind die Zinsen bereits in den Ausgaben enthalten?** | `interestHandling` | `INCLUDED` (Default) / `ADD` |
|
||
|
||
Der Zinsbetrag sinkt automatisch mit der Amortisation – das kann kein manueller
|
||
Ausgabenposten. Beispiel: Hypothek 1'000'000, Amortisation 10'000/Jahr, Zins 1 %, 10 Jahre →
|
||
Anzeige `10'000 → 9'100` (Jahr 1 auf 1'000'000, Jahr 10 auf 910'000).
|
||
|
||
Der Schalter `interestHandling` verhindert die Doppelzählung: Bestehende Pläne haben die
|
||
Zinsen im Ausgabenbetrag, deshalb ist `INCLUDED` der Default und das Tool zieht **nichts** ab.
|
||
Erst `ADD` rechnet die Zinsen dazu – dann gehören sie aus dem Ausgabenbetrag entfernt. Der
|
||
Schalter sitzt bewusst auf der **Immobilie** und nicht auf dem Ausgaben-Element: Bei mehreren
|
||
Ausgaben-Elementen wäre sonst unklar, welches die Zinsen trägt (und zwei auf „Ja" würden
|
||
doppelt zählen).
|
||
|
||
#### OTHER_ASSET (Sonstiges Vermögen)
|
||
|
||
| Feld | JSON | Semantik |
|
||
|---|---|---|
|
||
| Startwert / Startwert (fortgeschrieben) | `startValue` | ab Phase 2 read-only |
|
||
| Zusatzinvestition aus Kapital | `additionalInvestment` | nur ab Phase 2 |
|
||
| Erwartete Rendite (%/Jahr) | `expectedReturn` | |
|
||
| Jährlicher Sparbeitrag | `annualContribution` | fliesst ins Vermögen, verlässt das Cash → **geplante Sparrate** |
|
||
| Jährliche Bezugsrate | `annualWithdrawal` | Entnahme aus dem Vermögen ins Cash → **geplante Verzehrrate** |
|
||
|
||
Die Bezugsrate ist das Instrument für den **laufenden** Kapitalverzehr *innerhalb* einer Phase.
|
||
Für einen **einmaligen** Zugriff *am Übergang* gibt es zusätzlich den Teilverkauf (3.5.2, 4.9.3).
|
||
|
||
#### OTHER_DEBT (Sonstige Schulden)
|
||
|
||
| Feld | JSON | Semantik |
|
||
|---|---|---|
|
||
| Restschuld / (fortgeschrieben) | `startValue` | ab Phase 2 read-only |
|
||
| Jährliche Tilgung | `annualRepayment` | verlässt das Cash → geplante Sparrate |
|
||
|
||
Schulden gehen mit **negativem** Vorzeichen ins Vermögen ein.
|
||
|
||
## 3.5 Übergänge
|
||
|
||
### 3.5.1 Konzept
|
||
|
||
Zwischen zwei Phasen liegt ein Übergang. Er ist der Ort, an dem einmalige Entscheide getroffen
|
||
werden. Fünf Kategorien haben Übergangs-Entscheide (`TRANSITION_CATEGORIES`):
|
||
`PENSION_FUND`, `PILLAR_3A`, `REAL_ESTATE`, `OTHER_ASSET`, `OTHER_DEBT`. Dazu kommt der
|
||
**Cash-Entscheid** (siehe [3.5.5](#355-cash-übergang-einmalige-sonderein-ausgaben)), der an
|
||
jedem Übergang zu treffen ist.
|
||
|
||
Für `INCOME`, `EXPENSE` und `AHV` erscheint: „Fuer diese Kategorie gibt es im Uebergang keine
|
||
Eingaben."
|
||
|
||
### 3.5.2 Normaler Übergang vs. Pensions-Übergang
|
||
|
||
Ein Übergang ist für ein Element ein **Pensions-Übergang**, wenn dessen Besitzer in der Von-Phase
|
||
erwerbstätig und in der Ziel-Phase pensioniert ist (`isRetirementTransition`). Nur
|
||
personenzugeordnete Elemente (nicht `HOUSEHOLD`) können das auslösen.
|
||
|
||
| Kategorie | Normaler Übergang (Vorbezug) | Pensions-Übergang |
|
||
|---|---|---|
|
||
| `PENSION_FUND` | Bezug? Kein Bezug / Bezug + Bruttobetrag (max. Endwert) + **Kapitalbezugssteuer (%)** | **Bezugsart**: Rente / Kapitalbezug / Kombination |
|
||
| `PILLAR_3A` | Bezug? Kein Bezug / Bezug + Bruttobetrag + **Kapitalbezugssteuer (%)** | Nur **Kapitalbezugssteuer (%)** – Bezug ist zwingend vollständig |
|
||
| `REAL_ESTATE` | Halten (+ optionale **Sonderamortisation**) / Verkaufen (+ Verkaufspreis, Grundstückgewinnsteuer) | identisch |
|
||
| `OTHER_ASSET` | Halten / Verkaufen / **Teilverkauf** (+ Betrag) | identisch |
|
||
| `OTHER_DEBT` | Sofortige Tilgung (CHF) | identisch |
|
||
|
||
Ein **Vorbezug** (vor der Pensionierung, z. B. für Wohneigentum oder Selbstständigkeit) ist wie
|
||
der Bezug bei Pensionierung kapitalbezugssteuerpflichtig. Der eingegebene Betrag ist der
|
||
**Bruttobezug**: er wird in voller Höhe dem Vorsorgekapital entnommen, ins Cash fliesst der
|
||
Betrag nach Abzug der Steuer. Der Dialog zeigt die resultierende Netto-Auszahlung als
|
||
read-only Vorschau an.
|
||
|
||
Bei PK-Bezugsart:
|
||
- **Rente**: Feld Umwandlungssatz (Default 6 %)
|
||
- **Kapitalbezug**: Feld Kapitalbezugssteuer (Default 8 %)
|
||
- **Kombination**: beide Felder plus „Davon Kapitalbezug (CHF)", geklammert am Endwert
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` Zeilen 342–462.
|
||
|
||
### 3.5.3 Ampel-Logik: „offene" Entscheide
|
||
|
||
Ein Entscheid gilt als **beantwortet** (`isTransitionAnswered`), wenn das jeweilige
|
||
Entscheidungsfeld gesetzt ist:
|
||
|
||
| Kategorie | Beantwortet, wenn |
|
||
|---|---|
|
||
| `REAL_ESTATE`, `OTHER_ASSET` | `decision` gesetzt (inkl. `PARTIAL`) |
|
||
| `PENSION_FUND` | Pensions-Übergang: `payoutMode` gesetzt; sonst: `withdrawalMode` gesetzt |
|
||
| `PILLAR_3A` | Pensions-Übergang: **immer** beantwortet; sonst: `withdrawalMode` gesetzt |
|
||
| **Cash** | `mode` gesetzt (`isCashTransitionAnswered`) |
|
||
| alle anderen | immer beantwortet |
|
||
|
||
Der Übergangs-Spaltenkopf zeigt entweder „N offen" (Akzentfarbe) oder „geprüft" (grün, Häkchen).
|
||
Offene Zellen sind farblich hervorgehoben und zeigen „?".
|
||
|
||
**Ein Element ist am Übergang inaktiv** (`transitionInactive`), wenn es bereits verkauft/getilgt
|
||
ist **oder** wenn es eine PK/3a ist, deren Besitzer schon zu Beginn der Von-Phase pensioniert war
|
||
(dann ist bereits bezogen/verrentet). Inaktive Zellen zeigen „–" und sind nicht anklickbar.
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 200–232, `src/components/ElementDetail.tsx` Zeilen 107–120.
|
||
|
||
### 3.5.4 Geführter Übergang (Review-Dialog)
|
||
|
||
Klick auf einen Übergangs-Spaltenkopf öffnet „Übergang prüfen: <Von> → <Nach>". Der Dialog
|
||
listet zuoberst den **Cash-Entscheid** (einmalige Sonderein-/ausgaben, betrifft jeden Übergang)
|
||
und darunter **alle** noch aktiven Elemente der Übergangs-Kategorien mit ihren Entscheidfeldern
|
||
und kontextabhängigen Hinweisen:
|
||
|
||
- PK/3a, normaler Übergang: „Hier könnten Sie optional Kapital beziehen."
|
||
- PK, Pensionierung: „Pensionierung: Bezugsart wählen (Rente / Kapital / Kombination)."
|
||
- 3a, Pensionierung: „Wird bei Pensionierung vollständig bezogen."
|
||
|
||
„Alle speichern" schreibt jeden Entscheid einzeln per `PUT`. Entscheidend: Die Formulare sind mit
|
||
`withTransitionDefaults` vorbelegt (Halten / Kein Bezug / Rente), damit ein blosses Speichern den
|
||
**sichtbaren** Default auch tatsächlich persistiert und die Ampel auf grün geht.
|
||
|
||
Referenz: `src/components/PlanView.tsx` (`TransitionReviewDialog`), `src/components/ElementDetail.tsx`
|
||
(`withTransitionDefaults`).
|
||
|
||
### 3.5.5 Cash-Übergang: einmalige Sonderein-/ausgaben
|
||
|
||
Einmalige Ereignisse (Erbschaft, Poolbau, Autokauf, grössere Anschaffung) werden **nicht** als
|
||
finanzielles Element modelliert, sondern als Entscheid auf dem **Cash-Konto am Phasenübergang**.
|
||
Sie belasten bzw. speisen das Cash direkt.
|
||
|
||
Der Entscheid hat vier Ausprägungen (`CashTransitionMode`):
|
||
|
||
| Modus | Bedeutung | Felder |
|
||
|---|---|---|
|
||
| `NONE` | **1:1 übernehmen** – Cash läuft unverändert weiter (Default) | – |
|
||
| `INFLOW` | **Einmaliger Zufluss** | Bezeichnung, Betrag (nominal), Steuer (%) |
|
||
| `OUTFLOW` | **Einmalige Kosten** | Bezeichnung, Betrag (real) |
|
||
| `BOTH` | Zufluss **und** Kosten am selben Übergang | beide Feldgruppen |
|
||
|
||
**Erfassungs-Konventionen** – bewusst analog zu den laufenden Flows:
|
||
|
||
- **Zufluss: nominal erfasst, real angezeigt** (wie Einkommen). Man kennt den Betrag, der
|
||
effektiv aufs Konto kommt. Der Realwert erscheint read-only als Info.
|
||
- **Kosten: real erfasst, nominal angezeigt** (wie Ausgaben). Man denkt „ein Pool kostet heute
|
||
20'000"; die Inflation rechnet daraus den Betrag zum Ereigniszeitpunkt. Der Nominalwert
|
||
erscheint read-only als Info.
|
||
- **Steuersatz nur beim Zufluss**, Default 0 % (Erbschaften an direkte Nachkommen sind in den
|
||
meisten Kantonen steuerfrei). Ins Cash fliesst der Betrag nach Abzug der Steuer.
|
||
|
||
**Weder Zufluss noch Kosten gehen in die Spar-/Verzehrquote.** Sie sind keine laufenden Flows;
|
||
eine Erbschaft von 250'000 würde die Quote zu einem sinnlosen Ausschlag treiben. Sie wirken
|
||
ausschliesslich auf das Cash und damit auf Vermögensverlauf, Endvermögen und Ruinalter.
|
||
|
||
**Bedienung:** Klick auf eine Übergangszelle der Cash-Zeile öffnet den Dialog „Uebergang: Cash".
|
||
Die Zelle zeigt `1:1`, `+100'000`, `−20'000` bzw. `+100'000 / −20'000`, solange offen ein `?`.
|
||
Der Entscheid ist Teil des geführten Übergangs (3.5.4) und zählt im „offen"-Badge mit – eine
|
||
neu angelegte Phase erzeugt damit automatisch einen offenen Cash-Entscheid am neuen Übergang.
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` (`CashTransitionFields`), `src/components/PlanView.tsx`
|
||
(`CashTransitionDialog`).
|
||
|
||
### 3.5.6 AHV-Prüfung am Pensions-Übergang
|
||
|
||
Die AHV-Rente hängt vom **massgebenden durchschnittlichen Jahreseinkommen (mdJE)** über die
|
||
ganze Beitragsdauer ab. Diese Grösse kann das Tool nicht allein aus dem Plan bestimmen: Die
|
||
Beitragspflicht beginnt mit 21, der Plan aber erst beim heutigen Alter. Bei einer 45-jährigen
|
||
Person liegen 24 Beitragsjahre vor dem Planbeginn.
|
||
|
||
Deshalb ist die Beitragskarriere **am Pensions-Übergang** zu prüfen – dort, wo bereits die
|
||
PK-Bezugsart und der 3a-Bezug entschieden werden. Der Dialog zeigt:
|
||
|
||
| Feld | Art |
|
||
|---|---|
|
||
| Geplantes Durchschnittseinkommen (aus dem Plan) | read-only, real |
|
||
| Beitragsjahre im Plan | read-only |
|
||
| Durchschnittseinkommen vor Planbeginn (real) | Eingabe – **nur wenn Alter bei Planbeginn > 21** |
|
||
| Ausfalljahre vor Planbeginn | Eingabe – **nur wenn Alter bei Planbeginn > 21** |
|
||
| Massgebendes durchschnittliches Jahreseinkommen | read-only, live berechnet |
|
||
| Resultierende AHV-Rente pro Jahr | read-only, live berechnet |
|
||
|
||
Die beiden Eingabefelder erscheinen also nur, wenn sie fachlich gebraucht werden. Die Zelle
|
||
zeigt `Geprueft` bzw. `?`; der Entscheid zählt im „offen"-Badge mit. An allen **anderen**
|
||
Übergängen ist die AHV-Zelle inaktiv (`–`).
|
||
|
||
**Wichtig zum Feld „vor Planbeginn":** Der Wert ist **real** (heutige Kaufkraft). Der
|
||
AHV-Kontoauszug listet Einkommen historisch-nominal – ein Lohn von 2008 steht dort mit dem
|
||
Betrag von 2008 und wäre zu tief. Die Zahl stammt idealerweise aus der **Rentenvorausberechnung**,
|
||
dort ist die Aufwertung bereits enthalten. Der Hilfetext im Feld sagt das.
|
||
|
||
**Sonderfall „bei Planbeginn bereits pensioniert":** Dann gibt es keinen Pensions-Übergang. Die
|
||
gleichen Felder erscheinen stattdessen in der **AHV-Phasenzelle der ersten Phase**, zusammen mit
|
||
der resultierenden Rente als Live-Vorschau.
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` (`AhvReviewFields`), Formeln in
|
||
`src/lib/calculations.ts` (`ahvMdje`, `ahvAnnualPension`).
|
||
|
||
### 3.5.7 Warnhinweis bei Änderungen in früheren Phasen
|
||
|
||
Seit dem V3-Rework werden Werte **live fortgeschrieben** (Endwert = Startwert der Folgephase).
|
||
Eine Änderung in einer frühen Phase wirkt damit bis ans Planende durch – oft unbemerkt. Seit
|
||
Roadmap Nr. 3 gilt das verschärft: Ein geändertes Einkommen in Phase 1 verschiebt über das mdJE
|
||
auch die AHV-Rente in Phase 5.
|
||
|
||
Beim Bearbeiten einer Phase, der noch Phasen folgen, erscheint deshalb ein rot abgesetzter
|
||
Warnhinweis mit der Anzahl betroffener Folgephasen. Er erscheint in:
|
||
|
||
- **Phasenzellen** (Werte eines Elements in einer Phase)
|
||
- **Phasen-Detail** (Bezeichnung/Dauer – eine geänderte Dauer verschiebt alle Folgephasen)
|
||
|
||
Nicht in Übergangs-Dialogen (dort ist die Wirkung auf die Folgephase offensichtlich) und nicht
|
||
beim Anlegen eines Elements (dort gibt es noch nichts zu überschreiben).
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` (`CarryWarning`).
|
||
|
||
### 3.5.8 Verkaufspreis und modellierter Verkehrswert
|
||
|
||
Das Modell führt zwei Immobilienwerte getrennt: den **Verkehrswert**, der mit `valueGrowth`
|
||
wächst, und den **ursprünglichen Kaufpreis** als Basis der Grundstückgewinnsteuer
|
||
([4.6.5](#465-real_estate-immobilie)). Beim Verkauf zählt jedoch ausschliesslich der vom
|
||
Benutzer **erfasste Verkaufspreis** ([4.9.4](#494-real_estate)).
|
||
|
||
Daraus ergab sich eine stille Inkonsistenz: Man konnte 2 % jährliche Wertsteigerung annehmen
|
||
und die Immobilie trotzdem zum Kaufpreis verkaufen, ohne dass das Tool widersprach.
|
||
|
||
Deshalb gilt seit Version 0.12:
|
||
|
||
- Beim Wechsel auf **Verkaufen** wird der Verkaufspreis mit dem **modellierten Verkehrswert
|
||
am Phasenende** vorbelegt – aber nur, wenn noch keiner erfasst ist (bestehende Pläne bleiben
|
||
unverändert).
|
||
- Der Dialog zeigt den Verkehrswert daneben read-only an und beziffert die **Abweichung** in
|
||
Franken und Prozent.
|
||
- Ab **10 %** Abweichung wird der Hinweis rot abgesetzt, mit der Aufforderung zu prüfen, ob
|
||
Wertsteigerungsannahme und erwarteter Verkaufspreis zusammenpassen.
|
||
|
||
Der erfasste Preis bleibt **massgebend** – die Vorbelegung ist eine Hilfe, keine Bevormundung.
|
||
Ein bewusst abweichender Preis (Notverkauf, Liebhaberpreis, Verkauf an Nachkommen) bleibt
|
||
möglich. Die Berechnung ist unverändert.
|
||
|
||
Referenz: `src/components/ElementDetail.tsx` (`ElementTransitionFields`, `REAL_ESTATE`).
|
||
|
||
## 3.6 Auswertung und Visualisierung
|
||
|
||
### 3.6.1 Anzeigemodus nominal / beide / real
|
||
|
||
Ein Umschalter oben in der Planansicht steuert die Darstellung aller Geldbeträge in Matrix und
|
||
Phasenköpfen:
|
||
|
||
| Modus | Darstellung |
|
||
|---|---|
|
||
| Nominal | `1'234'567` |
|
||
| Beide | nominal oben, real in Klammern **darunter** (siehe unten) |
|
||
| Real | `890'123` |
|
||
|
||
Im Modus **Beide** steht der Realwert seit 0.14 in einer **eigenen Zeile** unter dem nominalen
|
||
Wert, nicht mehr daneben. Bei Start-/Endwerten wiederholt sich der Pfeil, damit der zeitliche
|
||
Bezug erhalten bleibt:
|
||
|
||
```
|
||
30'000 → 10'000
|
||
(29'557) → (7'430)
|
||
```
|
||
|
||
Grund: Nebeneinander wird die Zeile so lang, dass die Spalten unnötig breit werden und
|
||
Kennzahlen umbrechen. Untereinander bleiben die Spalten schmal – erst dadurch lässt sich jede
|
||
Kennzahl **umbruchfrei** darstellen. Die Regel gilt im Phasenkopf **und** in den Matrix-Zellen;
|
||
in den Modi «Nominal» und «Real» bleibt alles einzeilig.
|
||
|
||
„real" bedeutet kaufkraftbereinigt auf den **Planbeginn**: `nominal / Deflator`. Die Wahl wird in
|
||
`localStorage` unter `fpt-value-mode` gespeichert.
|
||
|
||
Es werden zwei verschiedene Deflatoren verwendet – siehe [4.5.3](#453-die-drei-deflatoren).
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 665–699.
|
||
|
||
### 3.6.2 Zeitachse
|
||
|
||
Horizontale Achse über das Alter (von jüngster Person bis Planende) mit:
|
||
- den **Lebensphasen als Segmente**: Breite proportional zur Dauer, Einfärbung nach Phasentyp
|
||
(Erwerb kräftig, Misch mittel, Pension hell), beschriftet mit Name, Dauer und – sofern ein
|
||
Planstart gesetzt ist – der **Jahresspanne** (z. B. „2026–2046")
|
||
- Flaggen-Marker je Person am Pensionsalter (Farbe: Person A indigo, Person B hellblau)
|
||
- rotem „Ruin <Alter>"-Marker, falls zutreffend
|
||
- Alters- **und Jahres**-Beschriftung an beiden Enden
|
||
|
||
Die Kalenderjahre stammen aus dem Profilfeld **Planstart** (`Scenario.startYear`). Ist es nicht
|
||
gesetzt, zeigt die Achse nur Alter – die Berechnung ist davon nie betroffen (siehe 3.2.7).
|
||
|
||
Referenz: `src/components/Timeline.tsx`.
|
||
|
||
### 3.6.3 Phasenkopf-Kennzahlen
|
||
|
||
Seit 0.13 ist der Phasenkopf bewusst **schlank** – zwölf Kennzahlen in 10-Pixel-Schrift liest
|
||
niemand (Progressive Disclosure):
|
||
|
||
| Kennzahl | Bedeutung |
|
||
|---|---|
|
||
| Name + Status-Icon | grünes Häkchen oder rotes Warnsymbol (Liquiditätslücke) |
|
||
| Typ-Badge + Dauer | Erwerb / Pension / Misch, „N J." |
|
||
| Alter je Person | `<Name> <StartAlter> → <EndAlter>` |
|
||
| **Verfügbares Kapital** | ab Phase 2 und nur wenn > 0: Topf, davon verteilt, Rest auf Cash ([3.6.9](#369-verfügbares-kapital-im-phasenkopf)) |
|
||
| **Sparquote** bzw. **Verzehrquote** | Einkommen − Ausgaben; Label wechselt auf „Verzehrquote", wenn Jahr 1 negativ; rot bei Verzehr |
|
||
| Vermögen | Start → Ende (inkl. Cash), hervorgehoben |
|
||
| Einmalposten | nur als Kurzhinweis (Bezeichnung), wenn vorhanden |
|
||
|
||
Die beiden Blöcke «Verfügbares Kapital» und «Sparquote» tragen je einen Knopf, der das passende
|
||
Verteil-Werkzeug öffnet ([3.6.10](#3610-verteil-werkzeuge)).
|
||
|
||
Alles Weitere – Einkommen/Ausgaben Jahr 1 → letztes Jahr, geplante Spar-/Verzehrrate,
|
||
Kapitalzufluss und -investitionen, die vollen Einmalposten – wohnt in der
|
||
**Phasen-Detailansicht** (seit 0.11, [3.6.7](#367-detailansichten-je-element-und-je-lebensphase)),
|
||
erreichbar über das Expand-Icon im Kopf. Die Kennzahlen selbst sind unverändert berechnet; nur
|
||
der Anzeigeort hat gewechselt.
|
||
|
||
Referenz: `src/components/PlanView.tsx` Zeilen 701–769.
|
||
|
||
### 3.6.4 Analyse-Bereich „Grafiken"
|
||
|
||
Die Auswertungen liegen **nicht** unter der Matrix, sondern in einem eigenen Bereich: Der Button
|
||
**Grafiken** in der oberen Aktionsleiste (neben „Neues Szenario aus diesem" und
|
||
„Monte-Carlo-Simulation") öffnet sie als breiten Dialog. So bleibt die Matrix die ruhige
|
||
Hauptansicht.
|
||
|
||
**Kennzahl-Karten:** Endvermögen nominal und Endvermögen real. (Die frühere Karte „Geschätzter
|
||
Nachlass" ist entfallen – sie war rechnerisch identisch mit dem nominalen Endvermögen und
|
||
suggerierte eine zusätzliche Information, die es nicht gab.)
|
||
|
||
**Grafik 1 – Einkommen vs. Ausgaben pro Jahr** (`SparquoteChart`): Ein Datenpunkt pro Jahr
|
||
über alle Phasen. Grüne Linie = Einkommen nominal (inkl. Renten), rote Linie = Ausgaben nominal,
|
||
graue gestrichelte Linie = Ausgaben real. Die Fläche zwischen Einkommen und nominalen Ausgaben
|
||
ist grün (Sparquote) oder rot (Verzehr) eingefärbt. Der Keil zwischen roter und grauer Linie ist
|
||
anschaulich „das, was die Inflation frisst".
|
||
|
||
**Grafik 2 – Vermögensverlauf nach Alter** (`WealthChart`): Liniendiagramm über das Alter von
|
||
Person A. Je Szenario eine durchgezogene Linie (nominal) und eine gestrichelte (real).
|
||
Datenpunkte: **jedes Planjahr** (nicht nur die Phasengrenzen) – dadurch werden Verläufe
|
||
*innerhalb* einer Phase sichtbar, etwa das Abschmelzen im Kapitalverzehr. Grundlage ist
|
||
`YearPoint.wealthNominal/wealthReal` (4.10). Über Checkboxen lassen sich die **Geschwister-
|
||
Szenarien überlagern**; deren Daten werden bei Bedarf nachgeladen und im Client
|
||
zwischengespeichert.
|
||
|
||
**Grafik 3 – Vermögensaufteilung pro Phase**: Gestapeltes Balkendiagramm mit zwei Balken je Phase
|
||
(Beginn / Ende). Gestapelt werden alle Elemente der Kategorien PK, 3a, Immobilie, Sonstiges
|
||
Vermögen, die irgendwann einen positiven Wert haben. Stapelung nach `elementId` (nicht Name),
|
||
damit gleichnamige Elemente nicht kollidieren. Negative Werte werden auf 0 geklammert.
|
||
|
||
Referenz: `src/components/Dashboard.tsx`, `src/components/WealthChart.tsx`, `src/components/SparquoteChart.tsx`.
|
||
|
||
### 3.6.5 CSV-Export
|
||
|
||
`GET /api/plans/<planId>/export` liefert eine semikolon-getrennte CSV, eine Zeile pro Phase:
|
||
|
||
```
|
||
Phase;Typ;Dauer;Einkommen (Beginn);Ausgaben (Beginn);Quote (Beginn);Quote (Ende);Cash (Ende);Endvermoegen (nominal);Endvermoegen (real)
|
||
```
|
||
|
||
Bei vorhandenem Ruin folgt eine Schlusszeile `Ruin: Kapital aufgebraucht mit Alter <N>`.
|
||
Dateiname = Planname, nicht-alphanumerische Zeichen durch `_` ersetzt.
|
||
|
||
Referenz: `src/lib/calculations.ts` Zeilen 620–648.
|
||
|
||
### 3.6.6 Analyse-Bereich „Einflussfaktoren"
|
||
|
||
Der Button **Einflussfaktoren berechnen** in der oberen Aktionsleiste (neben „Grafiken" und
|
||
„Monte-Carlo-Simulation") öffnet die Sensitivitätsanalyse als eigenen Dialog. Aufbau bewusst
|
||
analog zur Monte-Carlo-Simulation:
|
||
|
||
1. **Erklärung** – was die Analyse beantwortet, wie sie rechnet, was man davon hat, und die zwei
|
||
ehrlichen Grenzen (Bandbreiten-Abhängigkeit, keine Wechselwirkungen).
|
||
2. **Zielgrösse** – Endvermögen real (Default) oder nominal.
|
||
3. **Parameter** – je Treiber eine Checkbox; erst angehakt erscheinen die beiden Pflichtfelder
|
||
„tief" und „hoch" in der Einheit des Treibers, mit Hilfe-Bubble zu plausiblen Bandbreiten.
|
||
Nicht anwendbare Treiber werden gar nicht erst angezeigt.
|
||
4. **Ergebnis** – Basisfall, Tornado-Chart und Tabelle.
|
||
|
||
Der Dialog ist bewusst **nicht** Teil des Grafiken-Bereichs: Dieser ist reine Ausgabe, während der
|
||
Tornado Pflichteingaben braucht. Ein Hinweis am Ende der Parameterliste benennt, warum das
|
||
**Pensionsalter** nicht enthalten ist (siehe [9.18](#918-tornado-was-der-chart-nicht-leistet)).
|
||
|
||
Referenz: `src/components/SensitivityDialog.tsx`, `src/components/AppShell.tsx`.
|
||
|
||
### 3.6.7 Detailansichten je Element und je Lebensphase
|
||
|
||
Jede Element-Zeile und jeder Phasenkopf trägt oben rechts ein **Expand-Icon** (erscheint beim
|
||
Überfahren). Es öffnet eine **Nur-Lese-Detailansicht** mit Reitern.
|
||
|
||
**Element** – zwei Reiter:
|
||
|
||
| Reiter | Inhalt |
|
||
|---|---|
|
||
| Verlauf | Liniendiagramm über **alle Planjahre**, nicht nur die Phasengrenzen. Bei einer Immobilie drei Reihen (Eigenkapital, Verkehrswert, Restschuld). Zusätzlich die wirksame **Rate** auf einer zweiten Y-Achse rechts in Prozent – als **Stufenlinie**, weil sie innerhalb einer Phase konstant ist und an der Phasengrenze springt; eine interpolierte Kurve würde einen gleitenden Übergang suggerieren, den die Berechnung nicht macht. Die Achse erscheint nur, wenn das Element überhaupt eine Rate trägt. Darunter eine Tabelle Beginn/Ende je Lebensphase. |
|
||
| Rechenweg | Die Herleitung je Phase und je Übergang (siehe [4.14](#414-verlaufswerte-brücken-und-rechenwege)) |
|
||
|
||
**Lebensphase** – drei Reiter:
|
||
|
||
| Reiter | Inhalt |
|
||
|---|---|
|
||
| Übersicht | Typ, Dauer, Vermögen Beginn/Ende; Vermögensaufteilung als gestapelter Balken (Beginn und Ende) |
|
||
| Wasserfall | Vermögens- und Cash-Wasserfall ([4.14.2](#4142-die-beiden-wasserfälle)) |
|
||
| Rechenweg | Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp, Cash-Übergang |
|
||
|
||
Der Phasenkopf reagiert bereits auf Klick (öffnet das Bearbeiten-Popup); das Expand-Icon stoppt
|
||
deshalb die Ereignis-Weitergabe, sonst gingen beide Dialoge gleichzeitig auf.
|
||
|
||
Zusätzlich öffnet der Button **Rechenwege** in der Aktionsleiste die **plan-weiten** Grössen:
|
||
Deflatoren, AHV-Beitragskarriere je Person und Ruinalter.
|
||
|
||
Referenz: `src/components/DetailView.tsx`, `src/components/PlanView.tsx`.
|
||
|
||
### 3.6.8 Systemparameter-Ansicht
|
||
|
||
Eigener Eintrag in der Seitenleiste (über der SPEZIFIKATION). Zeigt alle fest hinterlegten
|
||
Systemgrössen, gruppiert nach AHV, Vorsorge und Steuern, je mit **Wert, Bedeutung, Herleitung,
|
||
Quelle und Stand**.
|
||
|
||
Die Einträge stammen aus `SYSTEM_PARAMETERS` in `src/lib/constants.ts` – **derselben Datei, aus
|
||
der die Berechnung liest**. Ein Auseinanderlaufen von angezeigtem und gerechnetem Wert ist damit
|
||
konstruktiv ausgeschlossen. Abgeleitete Grössen wie `AHV_MAX_ANNUAL_SINGLE` zeigen ihre
|
||
Herleitung (`2 × R0 × 13`) statt nur das Ergebnis.
|
||
|
||
Referenz: `src/components/SystemParametersView.tsx`, `src/lib/constants.ts`.
|
||
|
||
### 3.6.9 Verfügbares Kapital im Phasenkopf
|
||
|
||
Für die Planung einer Phase ist die zentrale Frage: **Wie viel Kapital steht überhaupt zur
|
||
Verfügung?** Der Phasenkopf weist das ab Phase 2 als eigenen Block aus (nur wenn > 0):
|
||
|
||
| Zeile | Bedeutung |
|
||
|---|---|
|
||
| **Verfügbares Kapital** | der gesamte Topf beim Übergang in diese Phase |
|
||
| davon verteilt | Zusatzeinlagen + Sonderamortisation + Sofort-Tilgung |
|
||
| Rest auf Cash | was auf dem Cash-Konto liegen bleibt (= `cashStart`) |
|
||
|
||
Der Topf ist vollständig aus der **Cash-Brücke** ([4.14.2](#4142-die-beiden-wasserfälle))
|
||
ableitbar – es braucht keine zusätzliche Berechnung:
|
||
|
||
```
|
||
Topf = Cash-Ende der Vorphase + Kapitalzufluss + einmaliger Zufluss − einmalige Kosten
|
||
= cashStart + Investitionen + Sofort-Tilgungen
|
||
```
|
||
|
||
**Nur ab Phase 2:** In der ersten Phase ignoriert die Berechnung `additionalInvestment` – dort
|
||
tragen die Elemente ihren Startwert direkt ([4.6.3](#463-pension_fund)). Ein «Verteilen» hätte
|
||
dort eine andere Bedeutung, deshalb wird es gar nicht erst angeboten.
|
||
|
||
Referenz: `src/lib/distribution.ts` (`capitalPot`).
|
||
|
||
### 3.6.10 Verteil-Werkzeuge
|
||
|
||
Zwei Popups, erreichbar über je einen Knopf im Phasenkopf. Beide schreiben **ausschliesslich
|
||
bestehende Felder** über die bestehenden Endpunkte – an der Berechnung ändert sich nichts.
|
||
|
||
**«Kapital verteilen»** verteilt den Topf aus 3.6.9 auf:
|
||
|
||
| Ziel | geschriebenes Feld | liegt an |
|
||
|---|---|---|
|
||
| PK, Säule 3a, Sonstiges Vermögen | `additionalInvestment` | **dieser** Phase |
|
||
| Immobilie | `extraAmortization` | dem **Übergang davor** |
|
||
| Sonstige Schulden | `immediateRepayment` | dem **Übergang davor** |
|
||
|
||
Dass zwei verschiedene Objekte beschrieben werden, ist eine Folge des Datenmodells: Die
|
||
Zusatzeinlage ist ein Phasenwert, Sonderamortisation und Sofort-Tilgung sind Übergangs-Entscheide.
|
||
Beide zehren aber vom selben Topf. Betragsfelder werden dabei in die bestehenden Daten
|
||
**hineingemischt** – vorhandene Entscheide (`decision`, `salePrice`, `payoutMode` …) bleiben
|
||
erhalten.
|
||
|
||
Was nicht verteilt wird, **bleibt automatisch auf dem Cash** – dafür braucht es keine Logik, das
|
||
ist das Verhalten des Modells. Wird mehr verteilt als vorhanden, startet die Folgephase mit
|
||
negativem Cash; der Dialog weist das als Liquiditätslücke aus.
|
||
|
||
**«Sparquote verteilen»** (bzw. **«Bezug verteilen»** bei Verzehr) verteilt die laufende Quote auf
|
||
jährliche Raten: `annualContribution` (3a, Sonstiges Vermögen), `annualWithdrawal` (Sonstiges
|
||
Vermögen), `amortization` (Immobilie), `annualRepayment` (Schulden).
|
||
|
||
> **Die Pensionskasse fehlt hier bewusst.** Ihr Beitrag stammt aus dem Bruttolohn und belastet
|
||
> das Cash-Konto nicht ([4.6.3](#463-pension_fund)) – er lässt sich also gar nicht aus der Quote
|
||
> verteilen.
|
||
|
||
Der Dialog weist **drei** Bezugsgrössen aus: Quote im ersten Jahr, im letzten Jahr und –
|
||
entscheidend – die **absolute Quote über die ganze Phase**. Letztere ist die Grösse, gegen die
|
||
sich eine flache Jahresrate sinnvoll verteilen lässt (siehe [9.25](#925-die-quote-ist-kein-fester-betrag)).
|
||
|
||
**Live-Vorschau:** Beide Dialoge kopieren den Plan mit den Entwurfswerten und rechnen ihn erneut
|
||
durch `computePlan` – im Browser, ohne API-Aufruf. Die angezeigte Wirkung ist dadurch **per
|
||
Konstruktion exakt die spätere**, inklusive aller Kappungen (Bezugsrate am Bestand, Amortisation
|
||
an der Restschuld). Eine Nebenrechnung im UI hätte hier dieselbe Driftgefahr wie bei den
|
||
Rechenwegen ([4.14.3](#4143-rechenweg-protokoll)).
|
||
|
||
Beide Dialoge zeigen den **Fortschreibungs-Warnhinweis** ([3.5.7](#357-warnhinweis-bei-änderungen-in-früheren-phasen)),
|
||
wenn Folgephasen existieren.
|
||
|
||
Referenz: `src/components/DistributionDialogs.tsx`, `src/lib/distribution.ts`.
|
||
|
||
### 3.6.11 Raten über Lebensphasen übernehmen
|
||
|
||
Werte liegen **je Lebensphase** vor. Bei Beträgen ist das richtig – das Einkommen ändert sich,
|
||
der Vermögensstand ohnehin. Bei **Raten** ist es meist nicht gemeint: Wer die erwartete Rendite
|
||
seines ETF auf 5 % setzt, meint fast nie «nur in Phase 2». Ohne Übernahme muss derselbe Wert
|
||
vier- oder fünfmal eingetippt werden, und dabei wird zuverlässig eine Phase übersehen.
|
||
|
||
Ändert man ein Ratenfeld, erscheint deshalb im Bearbeitungspanel eine Rückfrage mit drei
|
||
Möglichkeiten:
|
||
|
||
| Auswahl | Wirkung |
|
||
|---|---|
|
||
| **Nur diese Phase** (Vorgabe) | bisheriges Verhalten, andere Phasen bleiben unberührt |
|
||
| **Diese + folgende** | ab der bearbeiteten Phase vorwärts – Vergangenes bleibt stehen |
|
||
| **Alle Phasen** | der Wert gilt für den ganzen Plan |
|
||
|
||
Als **Ratenfelder** gelten:
|
||
|
||
| Feld | Kategorien |
|
||
|---|---|
|
||
| `expectedReturn` | Pensionskasse, Säule 3a, Sonstiges Vermögen |
|
||
| `valueGrowth` | Immobilie (Wertsteigerung) |
|
||
| `interestRate` | Immobilie (Hypothekarzins) |
|
||
| `teuerungsausgleich` | Einkommen (Lohnentwicklung), Ausgaben (reale Mehrausgaben) |
|
||
|
||
**AHV und Schulden haben keine**: Die AHV-Rente folgt der amtlichen Formel
|
||
([4.4](#44-ahv-rente)), Schulden tragen ihren Zins nicht als eigenes Feld.
|
||
|
||
**Vier Entwurfsentscheide:**
|
||
|
||
**Die Rückfrage erscheint beim Bearbeiten, nicht als Modal.** Das Zahlenfeld löst bei *jedem
|
||
Tastendruck* aus – ein Dialog erschiene bei der Eingabe «5.2» viermal. Stattdessen taucht die
|
||
Auswahl inline unter den Feldern auf, sobald sich eine Rate tatsächlich vom gespeicherten Wert
|
||
unterscheidet, und wird beim **Speichern** ausgeführt.
|
||
|
||
**Nur veränderte Raten lösen sie aus.** Wer bloss einen Betrag anpasst, bekommt keine
|
||
Rückfrage. «Nicht gesetzt» und «0» gelten dabei als gleich – sonst meldete schon das Öffnen
|
||
eines Panels mit leerem Feld eine Änderung.
|
||
|
||
**Die Zielphasen behalten ihre übrigen Werte.** Der Endpunkt ersetzt den *ganzen* Werte-Satz
|
||
einer Phase. Würde man den Entwurf der bearbeiteten Phase einfach hinüberkopieren, verlöre jede
|
||
andere Phase ihre Beträge, Sparraten und Bezüge. Übertragen wird deshalb ausschliesslich das
|
||
geänderte Ratenfeld, in die bestehenden Werte hineingemischt. Ein Test sichert genau das ab.
|
||
|
||
**Phasen, in denen der Wert schon stimmt, werden übersprungen** – das spart Schreibvorgänge und
|
||
verhindert eine Version, obwohl sich inhaltlich nichts geändert hat.
|
||
|
||
Technisch ist die Übernahme ein Schreibvorgang **je Zielphase** über den bestehenden Endpunkt;
|
||
es gibt keinen neuen Schreibpfad. Alle fallen in dieselbe Bearbeitungssitzung und ergeben
|
||
deshalb **eine** Nebenversion, nicht eine je Phase ([3.8.1](#381-eine-version-je-sitzung--nicht-je-änderung)).
|
||
|
||
Referenz: `src/lib/ratefields.ts`, `src/components/ElementDetail.tsx`.
|
||
|
||
## 3.7 Bedienoberfläche
|
||
|
||
### 3.7.1 Layout
|
||
|
||
- **Sidebar** (Desktop ab `lg` fix, mobil als Overlay): Logo, Ctrl-K-Hinweis, „Übersicht",
|
||
Gruppe **«Meine Pläne»** (Szenario-Baum, «+»), Gruppe **«Wissen»** («So rechnet FPT» =
|
||
gerenderte Spezifikation, «Systemparameter»).
|
||
- **Header**: Menü-Button (mobil), Plantitel, Profilmenü.
|
||
- **Hauptbereich**: Übersicht (Plan-Kacheln bzw. EmptyState mit «Geführt starten» /
|
||
«Beispielplan») oder Planansicht (Umschalter, Zeitachse, Szenario-Profil-Leiste,
|
||
«Nächste Schritte», Ruin-Banner, Matrix, Inspector-Panel rechts).
|
||
- Die **Matrix** hat einen eigenen Scrollbereich (max. 75 vh): Phasen-Köpfe (oben) und
|
||
Elementnamen (links) bleiben beim Scrollen in **beiden Achsen** fixiert.
|
||
- Alle Aktions-Icons (Kopieren, Löschen, Detailansicht) sind auch ohne Hover leicht sichtbar –
|
||
auf Touch-Geräten gibt es kein Hover.
|
||
|
||
### 3.7.2 Farbschemata
|
||
|
||
Drei Themes: **Hell**, **Dunkel**, **Warm** (cremefarben, Koralle-Akzent). Wahl im Profilmenü,
|
||
persistiert in `localStorage` (`fpt-theme`), gesetzt als `data-theme` am `<html>`. Ohne
|
||
gespeicherte Wahl folgt die Oberfläche `prefers-color-scheme`. Ein Inline-Script im `<head>`
|
||
setzt das Attribut vor dem ersten Paint (verhindert FOUC).
|
||
|
||
Alle Farben laufen über semantische CSS-Variablen (`--bg`, `--surface`, `--accent`, `--danger`, …).
|
||
|
||
Referenz: `src/lib/theme.ts`, `src/app/globals.css`, `src/app/layout.tsx` Zeilen 1181–1196.
|
||
|
||
### 3.7.3 Geldeingabefelder
|
||
|
||
Die `MoneyInput`-Komponente ist ein spezialisiertes Betragsfeld:
|
||
- Unfokussiert Anzeige mit Apostroph-Tausendertrennung (`1'234'567`), fokussiert reine Ziffern
|
||
- Ein Default-Wert 0 wird beim Fokussieren geleert, sonst der Text markiert
|
||
- Pfeiltasten: ↑/↓ = ±1, Shift+↑/↓ = ±100
|
||
- Pfeil-Buttons mit **Klick-und-Halten-Beschleunigung**: nach 400 ms Wiederholung im 70-ms-Takt,
|
||
Schrittweite wächst mit der Haltedauer (1 → 10 → 100 → 1'000 → 10'000)
|
||
- Optionale harte Klammerung über `min` (Default 0) und `max`
|
||
|
||
**Zahlenformat:** Bewusst nicht über `toLocaleString("de-CH")`, weil dessen Trennzeichen das
|
||
typografische Apostroph (U+2019) ist. Das Tool verwendet durchgehend das gerade Apostroph.
|
||
|
||
Referenz: `src/components/FormField.tsx` Zeilen 60–200, `src/lib/format.ts`.
|
||
|
||
### 3.7.4 Hilfe-Bubbles
|
||
|
||
Feldbeschriftungen können ein Info-Icon tragen (`InfoBubble`), das per Hover oder Klick einen
|
||
Erklärtext einblendet. Wird durchgehend für die fachlich heiklen Felder verwendet.
|
||
|
||
### 3.7.5 Reaktivität
|
||
|
||
Die Anwendung rechnet **nicht im Client**. Jede Änderung führt zu einem `PUT`/`PATCH`/`POST`,
|
||
gefolgt von `onChanged()` → `loadDetail(planId, silent = true)`. Der „silent"-Refresh lädt Plan
|
||
und Berechnung neu, ohne die `PlanView` zu demontieren – so bleibt die Scrollposition nach dem
|
||
Schliessen eines Popups erhalten.
|
||
|
||
Referenz: `src/components/AppShell.tsx`.
|
||
|
||
### 3.7.6 Sprache und UI-Primitiven
|
||
|
||
Seit 0.13 gilt durchgehend die **Du-Form** (wie moderne CH-Finanz-Apps) und es werden überall
|
||
**echte Umlaute** verwendet – zuvor mischte das UI «Übergang prüfen» mit «Uebersicht» und
|
||
Sie- mit Du-Anrede. Der Sweep umfasst auch die API-Fehlermeldungen (z. B. „Nur die letzte
|
||
Phase kann gelöscht werden.").
|
||
|
||
Alle Bausteine liegen zentral in `src/components/ui.tsx`:
|
||
|
||
| Baustein | Ersetzt / leistet |
|
||
|---|---|
|
||
| `Button` | vier Varianten (primary/secondary/danger/ghost), einheitliche Hover-/Active-Zustände |
|
||
| `Modal` | Dialog mit ESC, Fokus-Falle, `aria-modal`, Einblend-Animation |
|
||
| `ConfirmProvider` / `useConfirm` | ersetzt `window.confirm()` – Browser-Systemdialoge folgen keinem Farbschema |
|
||
| `ToastProvider` / `useToast` | ersetzt `alert()`; Erfolgs- und Fehlermeldungen unten rechts |
|
||
| `Skeleton` / `PlanSkeleton` | Struktur-Platzhalter statt «Lädt…»-Text |
|
||
| `EmptyState` | einheitliche leere Zustände mit Erklärung und Aktion |
|
||
| `InspectorShell` | das rechte Bearbeiten-Panel (3.7.7) |
|
||
|
||
Mehrere Schichten (Panel + Dialog) teilen sich einen **ESC-Stapel**: ESC schliesst immer nur
|
||
die oberste. Animationen (150–220 ms) respektieren `prefers-reduced-motion`.
|
||
|
||
**Attention-Farbe:** Offene Entscheide tragen neu einen eigenen Amber-Ton (`--attention`,
|
||
je Farbschema abgestimmt) statt der Akzentfarbe – «hier fehlt eine Eingabe» und «hier kannst
|
||
du klicken» sind damit unterscheidbar.
|
||
|
||
### 3.7.7 Inspector-Panel statt Modals
|
||
|
||
Alle **Einzel-Bearbeitungen** – Phasenzelle, Übergangszelle, Cash-Übergang, Cash-Anfangswert,
|
||
Phase (Name/Dauer), Szenario-Profil – öffnen sich als rechtes Seitenpanel (`InspectorShell`,
|
||
26 rem, mobil volle Breite) statt als Modal. Bewusst **ohne Backdrop**: Die Matrix bleibt
|
||
sichtbar und klickbar, ein Klick auf eine andere Zelle wechselt einfach den Panel-Inhalt –
|
||
der Kontext, den Modals nehmen, bleibt erhalten. Genau ein Panel kann offen sein (eine
|
||
`Panel`-Union ersetzt die früheren Einzel-Zustände); der `key` erzwingt beim Wechsel den
|
||
Neuaufbau des Formulars wie zuvor bei den Dialogen.
|
||
|
||
**Modals bleiben** für Erstell-Flows (Element, Phase, Plan, Assistent), den geführten
|
||
Übergang (mehrere Objekte auf einmal), die Analysen und die Detailansichten.
|
||
|
||
Der frühere Dialog «Plan-Einstellungen» heisst im Panel korrekt **«Szenario-Profil»** – er
|
||
bearbeitet seit V6 das Szenario, nicht den Plan.
|
||
|
||
### 3.7.8 Tour und «Nächste Schritte»
|
||
|
||
Die **Tour** (`Tour.tsx`) startet einmalig beim ersten Öffnen eines Plans mit Phasen
|
||
(localStorage `fpt-tour-done`) und führt in bis zu sechs Schritten über Profil, Zeitachse,
|
||
Matrix, Übergänge, Cash und Analysen – als Karte am unteren Rand plus pulsierender Rahmen um
|
||
das Ziel (`data-tour`-Attribute). Schritte ohne vorhandenes Ziel werden übersprungen; der
|
||
«Tour»-Knopf in der Werkzeugleiste startet sie jederzeit neu. Grenze: 9.24.
|
||
|
||
Die Karte **«Nächste Schritte»** über der Matrix leitet aus den vorhandenen Daten ab, was
|
||
sinnvollerweise als Nächstes ansteht – offene Übergangs-Entscheide (mit Direktsprung in den
|
||
geführten Übergang), fehlendes Einkommen/fehlende Ausgaben, ein Plan, der vor der
|
||
Pensionierung endet, und bei Ruin der Sprung zu den Einflussfaktoren. Kein eigener Zustand,
|
||
kein API-Aufruf; die Karte verschwindet von selbst, wenn nichts offen ist. Auch das
|
||
**Ruin-Banner** verlinkt neu auf die Einflussfaktoren («Wirksamsten Hebel finden»).
|
||
|
||
### 3.7.9 Befehls-Palette und Sparklines
|
||
|
||
**Ctrl/Cmd+K** öffnet die Befehls-Palette (`CommandPalette.tsx`): Springen zu jedem
|
||
Plan/Szenario und die wichtigsten Aktionen (Analysen, neues Szenario, neuer Plan), mit
|
||
Tastatur-Navigation – der Effizienz-Pfad für geübte Nutzer.
|
||
|
||
Jede Element-Zeile der Matrix trägt eine **Sparkline** (`Sparkline.tsx`): der Wertverlauf
|
||
über alle Planjahre als Mini-SVG, gespeist aus den per-Jahr-Werten der Detailansichten
|
||
(`ElementPhaseComputed.yearly`, seit 0.11) – keine Neuberechnung, reine Darstellung. Flache
|
||
Verläufe werden nicht gezeichnet (keine Information).
|
||
|
||
## 3.8 Versionierung und Änderungshistorie
|
||
|
||
Jedes **Szenario** trägt eine Version **A.B** und eine vollständige Änderungshistorie. Zugang
|
||
über den Knopf **«Änderungshistorie»** in der Szenario-Leiste.
|
||
|
||
| | Bedeutung | Entsteht |
|
||
|---|---|---|
|
||
| **B** (Nebenversion) | ein Bearbeitungsstand | **automatisch**, eine je Bearbeitungssitzung |
|
||
| **A** (Hauptversion) | ein bewusst gesetzter Meilenstein | **manuell**, mit Pflichtkommentar; setzt B auf 0 |
|
||
|
||
### 3.8.1 Eine Version je Sitzung – nicht je Änderung
|
||
|
||
FPT hat **keinen Speichern-Knopf**: Jede Änderung schreibt sofort. Eine Version je
|
||
Schreibvorgang wäre deshalb ein Tastenprotokoll und keine Historie – ein Durchlauf des
|
||
Plan-Assistenten macht rund **14** Schreibvorgänge, ein Klick im Verteil-Dialog einen **je
|
||
Zielelement**.
|
||
|
||
Stattdessen werden alle Schreibvorgänge innerhalb eines **Zeitfensters von 10 Minuten** zu
|
||
**einer** Nebenversion zusammengefasst: Der erste legt sie an, alle weiteren aktualisieren
|
||
sie. Zwei Sicherungen ergänzen das:
|
||
|
||
- **Unverändert = keine Version.** Ergibt ein Schreibvorgang inhaltlich denselben Stand
|
||
(Dialog geöffnet und unverändert geschlossen), entsteht nichts. Verglichen wird über eine
|
||
Serialisierung mit **sortierten Schlüsseln** – ohne das würden identische Stände als
|
||
verschieden gelten, weil die Reihenfolge der Werte aus der Datenbank-Zeilenfolge stammt.
|
||
- **Benutzerwechsel trennt immer.** Änderungen verschiedener Benutzer laufen nie in einer
|
||
Version zusammen, auch nicht innerhalb des Fensters – Vorbereitung auf die spätere Freigabe
|
||
an einen Finanzberater.
|
||
|
||
Eine **Hauptversion** wird nie zusammengefasst und nie nachträglich verändert; die nächste
|
||
Änderung beginnt bei A.1.
|
||
|
||
### 3.8.2 Was die Historie zeigt
|
||
|
||
Je Version: **wer** (Benutzername), **wann** (Zeitpunkt der letzten Änderung dieser Sitzung)
|
||
und – bei Hauptversionen – der **Kommentar**. Die oberste Zeile ist der aktuelle Stand.
|
||
|
||
Zwei Aktionen je Version:
|
||
|
||
- **Anzeigen** – ein Nur-Lese-Fenster mit der Matrix dieses Standes ([VersionMatrix](#552-komponenten)),
|
||
Phasen als Spalten, Elemente als Zeilen, dazu Endvermögen und Ruinalter. Bewusst **nicht**
|
||
die Bearbeitungs-Matrix: Inspector, Übergangs-Ampeln und Diff-Markierung haben für einen
|
||
alten Stand keine Bedeutung.
|
||
- **Wiederherstellen** – setzt das Szenario vollständig auf diesen Stand zurück.
|
||
|
||
### 3.8.3 Wiederherstellen
|
||
|
||
Zwei Eigenschaften machen den Knopf ungefährlich:
|
||
|
||
**Es geht nichts verloren.** Der wiederhergestellte Stand wird selbst als **neue Version**
|
||
festgehalten, mit dem Kommentar «Wiederhergestellt aus A.B». Die bisherige Historie bleibt
|
||
vollständig – auch alles, was nach dem Zielstand kam.
|
||
|
||
**IDs bleiben erhalten.** Phasen und Elemente behalten ihre IDs (der Snapshot trägt sie mit).
|
||
Das ist keine Kosmetik: Kind-Szenarien zeigen über `sourceElementId` / `sourcePhaseId` auf
|
||
genau diese IDs, und daran hängt die Abweichungs-Markierung
|
||
([3.2.6](#326-abweichungs-markierung-diff)). Würden neue IDs entstehen, erschiene in
|
||
jedem Kind-Szenario schlagartig **alles als «neu»** statt als «geändert».
|
||
|
||
Restlos vermeiden lässt sich das nicht: Geht man auf einen Stand zurück, in dem ein Element
|
||
noch gar nicht existierte, auf das ein Kind verweist, bricht dieser eine Bezug zwangsläufig.
|
||
Der Dialog **prüft das vorher** und benennt die betroffenen Szenarien namentlich, statt es
|
||
stillschweigend zu tun.
|
||
|
||
### 3.8.4 Analyse auf einer bestimmten Version
|
||
|
||
Alle vier Analysewerkzeuge – **Grafiken**, **Live-Simulation**, **Monte-Carlo** und
|
||
**Einflussfaktoren** – haben oben einen Wähler **«Berechnungsgrundlage»**: aktueller Stand
|
||
(Vorgabe) oder eine festgehaltene Version.
|
||
|
||
In der **Monte-Carlo-Simulation** ist die Auswahl **zweidimensional**: Sie rechnet mehrere
|
||
Szenarien gleichzeitig, und der Wähler hängt deshalb an der jeweiligen Szenario-Zeile – man
|
||
kann Szenario A in Version 1.2 gegen Szenario B in Version 2.0 stellen.
|
||
|
||
Das war fast kostenlos: Der Snapshot ist bereits ein **`PlanInput`**, also genau der Typ, den
|
||
alle Werkzeuge ohnehin entgegennehmen. Es genügt, den Arbeitsstand durch den Snapshot zu
|
||
ersetzen; das Gerechnete entsteht lokal über `computePlan`.
|
||
|
||
**Eine Ausnahme ist beschriftet:** Der CSV-Export liest immer das Szenario aus der Datenbank
|
||
und liefert deshalb den **aktuellen** Stand, nicht die betrachtete Version. Der Knopf sagt das
|
||
in diesem Fall ausdrücklich.
|
||
|
||
### 3.8.5 Vollständigkeit der Historie
|
||
|
||
Die Versionierung hängt daran, dass **jeder** inhaltsverändernde Endpunkt sie auslöst
|
||
(`touchScenario`). Ein vergessener Pfad fiele nicht auf – er erzeugte still keine Version, und
|
||
die Lücke bemerkte man erst Wochen später. Ein **Test liest deshalb alle Route-Dateien** und
|
||
verlangt, dass jeder schreibende Endpunkt entweder `touchScenario` aufruft oder mit Begründung
|
||
in einer Ausnahmeliste steht; verwaiste Ausnahmen meldet er ebenfalls.
|
||
|
||
Das Festhalten einer Version ist bewusst **fehlertolerant**: Schlägt es fehl, wird es
|
||
protokolliert, die eigentliche Änderung des Nutzers aber nicht zurückgewiesen. Die Historie
|
||
ist Begleitinformation, nicht der Zweck der Anfrage.
|
||
|
||
Referenz: `src/lib/versioning.ts` (reine Logik), `src/lib/versioning-db.ts` (Datenbank),
|
||
`src/components/VersionHistoryDialog.tsx`, `src/components/VersionMatrix.tsx`,
|
||
`src/components/VersionPicker.tsx`.
|
||
|
||
## 3.9 Effektive Werte (Plan-/Ist-Vergleich)
|
||
|
||
Roadmap Nr. 5. Macht aus dem Planer ein **Monitoring-Werkzeug**: Was ist tatsächlich
|
||
eingetreten, und was heisst das für den Rest der Planung?
|
||
|
||
Knopf **«Effektive Werte»** auf Plan-Ebene → Liste der bisherigen Erfassungen → Wizard in zwei
|
||
Schritten.
|
||
|
||
### 3.9.1 Ein Ist-Satz gehört zum Plan, nicht zum Szenario
|
||
|
||
Das tatsächliche PK-Guthaben am 18.8.2026 ist **eine Zahl** – unabhängig davon, gegen welches
|
||
Szenario man sie hält. Ein Ist-Satz hängt deshalb am `Plan` und wird über dieselbe
|
||
Herkunfts-Kette (`sourceElementId`) auf die szenario-eigenen Element-IDs abgebildet, die auch
|
||
der Diff ([3.2.6](#326-abweichungs-markierung-diff)) und die Monte-Carlo-Gruppierung benutzen.
|
||
Erfasst wird also je **Wurzel-Element**.
|
||
|
||
### 3.9.2 Der Wizard
|
||
|
||
**Schritt 1 – Stichtag.** Exaktes Datum (z. B. 18. August 2026) plus optionale Notiz. Das
|
||
Datum erscheint in der Liste und auf der Zeitachse; für die Rechnung zählt **nur die
|
||
Jahreszahl**, weil der Rechenkern in ganzen Jahren ab Planbeginn arbeitet. Der Dialog sagt
|
||
das ausdrücklich.
|
||
|
||
**Schritt 2 – Werte.** Alle Elemente **aller Szenarien** dieses Plans, zusammengefasst auf
|
||
ihre Wurzel, dazu das **Cash-Konto**. Vorbelegt mit dem Stand, den der Plan für dieses Jahr
|
||
vorsieht (aus dem Basisszenario; fehlt das Element dort, aus dem erstbesten Szenario, das es
|
||
kennt). Der Nutzer überschreibt nur, was tatsächlich abweicht.
|
||
|
||
Zwei Arten von Werten, die sich verschieden verhalten:
|
||
|
||
| Art | Elemente | Wirkung |
|
||
|---|---|---|
|
||
| **Bestand** | PK, 3a, Sonstiges Vermögen, Schulden, Cash | ersetzt den laufenden Stand |
|
||
| **Verkehrswert + Schuld** | Immobilie | zwei Felder: Wert und Resthypothek getrennt |
|
||
| **Fluss** | Einkommen, Ausgaben | nominaler **Jahresbetrag**; ersetzt die Basis für alle Folgejahre |
|
||
|
||
**Die AHV erscheint nur, wenn die Rente zum Stichtag bereits läuft.** Vorher gibt es keinen
|
||
Stand, den man ablesen könnte – die Rente folgt der amtlichen Formel aus der Beitragskarriere
|
||
([4.4](#44-ahv-rente)).
|
||
|
||
**Ist-Werte erfassen Werte, keine Entscheide.** Wenn der Plan die Immobilie verkauft, du sie
|
||
aber behalten hast, lässt sich das hier nicht ausdrücken – dafür ist ein Szenario da.
|
||
|
||
### 3.9.3 Die zweite Berechnung
|
||
|
||
Der Plan-Lauf bleibt **unangetastet**. Parallel läuft ein zweiter mit derselben Mechanik, aber
|
||
korrigierter Ausgangsbasis: In jedem Jahr, für das ein Ist-Satz erfasst wurde, schnappen die
|
||
Werte auf die Realität und laufen von dort planmässig weiter. **Alle** Sätze gehen ein, nicht
|
||
nur der jüngste.
|
||
|
||
Beispiel aus der Anforderung: Fonds startet 2020 mit 100'000 bei 5 %. Ohne Ist-Daten steht
|
||
2022 rechnerisch 115'763. Wird für 2022 ein Ist-Wert von 120'000 erfasst, rechnet die Ist-Sicht
|
||
ab dort weiter und steht 2024 bei 132'300. Kommt für 2024 ein Wert von 140'000 dazu, springt
|
||
sie erneut. Genau das ist als Test hinterlegt.
|
||
|
||
**Lücken fallen auf die Plandaten zurück.** Ein Element ohne erfassten Ist-Wert läuft
|
||
unverändert auf seiner Planlinie weiter – man muss nicht alles wissen, um etwas zu erfassen.
|
||
|
||
Technisch: `computePlan(plan, sample?, { actuals })`. Ohne die Option verhält sich die
|
||
Funktion exakt wie bisher; die 43 Golden Tests laufen unverändert.
|
||
|
||
**Der Sprung ist keine Rendite.** Die Differenz zwischen Plan und Wirklichkeit wird als eigene
|
||
Brückenposition `actualsCorrection` geführt (Vermögens- **und** Cash-Brücke). Würde man sie den
|
||
Kapitalerträgen zuschlagen, erschiene ein Planrückstand als Anlageverlust – und die Zerlegung
|
||
ginge im Ist-Jahr nicht mehr auf ([3.6.8](#367-detailansichten-je-element-und-je-lebensphase)).
|
||
|
||
### 3.9.4 Anzeige in der Matrix
|
||
|
||
Ein zweiter Umschalter neben nominal/real/beide, aber mit nur **zwei** Möglichkeiten:
|
||
|
||
| Auswahl | Wirkung |
|
||
|---|---|
|
||
| **Plan** | wie bisher |
|
||
| **Effektiv** | die Ist-Zahlen, jeweils mit der **Abweichung** zum Plan daneben |
|
||
|
||
Warum keine dritte Möglichkeit «beide»: Plan und Ist als Rohwerte nebeneinander wären bei
|
||
zusätzlich aktivem nominal/real **acht Zahlen je Zelle**. Stattdessen zeigt die Ist-Ansicht den
|
||
Wert und daneben klein die Differenz, grün oder rot ([9.29](#929-acht-zahlen-je-zelle--und-wie-wir-sie-vermeiden)).
|
||
Der Umschalter erscheint nur, wenn überhaupt Ist-Werte erfasst sind.
|
||
|
||
**Nur die Abweichung trägt Farbe.** Die Beträge selbst bleiben neutral – sonst wird die Matrix
|
||
zum Ampelteppich, in dem nichts mehr heraussticht.
|
||
|
||
### 3.9.5 Zeitachse
|
||
|
||
Je erfasstem Jahr ein Marker. Der **jüngste** ist farbig, ältere blass – sie sind überholt,
|
||
aber nicht bedeutungslos. Ohne gesetztes Planstartjahr entfallen die Marker, weil es dann
|
||
keinen Kalenderbezug gibt.
|
||
|
||
### 3.9.6 Die vier Analysewerkzeuge
|
||
|
||
Grafiken, Live-Simulation, Monte-Carlo und Einflussfaktoren erhalten dieselbe Leiste:
|
||
**Werte** (nominal/real) und **Grundlage** (Plan/Effektiv), dazu die schon bestehende
|
||
Versionswahl. «Effektiv» ist deaktiviert, solange nichts erfasst ist.
|
||
|
||
**Nominal/real ist in den Werkzeugen eine Einfachauswahl**, kein «beide» – anders als in der
|
||
Matrix. Begründung siehe [9.29](#929-acht-zahlen-je-zelle--und-wie-wir-sie-vermeiden).
|
||
|
||
Besonderheiten:
|
||
|
||
- **Vermögensverlauf:** Im Ist-Modus kommt die reine Planlinie **gestrichelt** als Referenz
|
||
dazu. Maximal vier Serien.
|
||
- **Monte-Carlo:** Der Zielbetrag **dreht mit** der gewählten Grösse (real/nominal) und wird
|
||
entsprechend beschriftet – sonst prüft man einen nominalen Zielbetrag gegen ein reales
|
||
Endvermögen. Ausserdem eine Zeile: *Simuliert ab ‹Jahr›; die Jahre davor sind durch deine
|
||
effektiven Werte belegt und werden nicht gewürfelt.* Das Startjahr ist **abgeleitet, nicht
|
||
eingebbar** – ein frei gesetztes Jahr würde Jahre als sicher behandeln, die nie erfasst
|
||
wurden.
|
||
- **Einflussfaktoren:** Mit Ist-Werten wirken die Treiber nur noch auf die nicht belegten
|
||
Jahre. Die Balken fallen dadurch zu Recht kürzer aus.
|
||
|
||
### 3.9.7 Verhältnis zur Versionierung
|
||
|
||
Ein Ist-Satz ist eine **Beobachtung, keine Planänderung**: Er erzeugt **keine** Szenario-Version
|
||
([3.8](#38-versionierung-und-änderungshistorie)), und es gibt kein Wiederherstellen. Löschen
|
||
entfernt ihn ersatzlos; der Plan bleibt unberührt. Die beiden Historien bleiben getrennt.
|
||
|
||
Referenz: `src/lib/actuals.ts`, `src/lib/dataview.ts`, `src/components/ActualsDialog.tsx`,
|
||
`src/components/AnalysisControls.tsx`.
|
||
|
||
---
|
||
|
||
# 4. Berechnungsmodell
|
||
|
||
> Dies ist der fachliche Kern des Tools. Der gesamte Abschnitt beschreibt
|
||
> `src/lib/calculations.ts`, Funktion `computePlan(plan: PlanInput): PlanComputed`.
|
||
> Die Funktion ist **rein** (keine I/O, keine Datenbank) und dadurch isoliert testbar.
|
||
|
||
## 4.1 Ablauf im Überblick
|
||
|
||
```
|
||
für jede Phase i (nach sequenceNumber sortiert):
|
||
1. Personen-Infos berechnen (Alter, erwerbstätig?), Phasentyp ableiten
|
||
2. AHV: Ausfalljahre kumulieren, Renten der Pensionierten berechnen, plafonieren
|
||
3. Element-Setup: je Element Startwerte/Raten bestimmen, in Arbeitslisten einordnen
|
||
4. Investitionen vom Cash abziehen → cashStart
|
||
5. Jahresschleife t = 1..duration:
|
||
Flows indexieren, Vermögen verzinsen, Bezugsraten entnehmen,
|
||
Cash fortschreiben, Ruin prüfen, YearPoint anfügen
|
||
6. Endwerte je Element setzen, Phasen-Kennzahlen zusammenstellen
|
||
7. Übergang: Entscheide anwenden, Carry aktualisieren, Cash der Folgephase bilden
|
||
```
|
||
|
||
## 4.2 Zustand über Phasengrenzen: der Carry
|
||
|
||
Zwischen den Phasen wird pro Element ein `Carry`-Objekt fortgeschrieben:
|
||
|
||
| Feld | Bedeutung |
|
||
|---|---|
|
||
| `status` | `ACTIVE` / `SOLD` / `SETTLED` |
|
||
| `value` | Aktiven-Saldo (PK / 3a / Sonstiges Vermögen) am Ende der Vorphase |
|
||
| `mortgage` | Immobilie: Resthypothek |
|
||
| `owed` | Schulden: Restschuld (positiv geführt) |
|
||
| `pkPensionAnnual` | PK: jährliche Rente nach Verrentung |
|
||
| `flowBasis` | Einkommen/Ausgaben: indexierter Basiswert der nächsten Phase |
|
||
| `hasCarry` | ob überhaupt eine Vorphase existiert (steuert read-only vs. Eingabe im UI) |
|
||
|
||
Zusätzlich laufen phasenübergreifend mit: `cashCarryIn`, `cumulativeInflation`,
|
||
`gapYearsByPerson`, `yearsBefore`, `ruinAge`, `incomingInflow`, `incomingImmediateRepay`.
|
||
|
||
Referenz: `src/lib/calculations.ts` Zeilen 103–115, 132–147.
|
||
|
||
## 4.3 Personen und Phasentyp
|
||
|
||
Für jede Person und jede Phase:
|
||
|
||
```
|
||
startAge = person.age + yearsBefore
|
||
endAge = startAge + duration
|
||
working = startAge < retirementAge
|
||
retiresAtStart = startAge === retirementAge
|
||
```
|
||
|
||
`yearsBefore` ist die Summe der Dauern aller vorangehenden Phasen. **Alter wird also relativ
|
||
gezählt** – es gibt keine Kalenderdaten im Modell, nur „Jahre ab Planbeginn".
|
||
|
||
`maxDurationYears` = Minimum über `retirementAge − startAge` aller noch erwerbstätigen Personen
|
||
(nur positive Werte), sonst `null`.
|
||
|
||
## 4.4 AHV-Rente
|
||
|
||
Die Rente hängt an zwei Achsen: der **Beitragsdauer** (Rentenskala 44) und dem **massgebenden
|
||
durchschnittlichen Jahreseinkommen** (mdJE).
|
||
|
||
### 4.4.1 Die amtliche Rentenformel (Skala 44)
|
||
|
||
Alle Schwellen sind Vielfache von `R0`, dem Mindestbetrag der vollen Monatsrente
|
||
(`AHV_MIN_MONTHLY_FULL = 1'260`). Mit `x = mdJE / (12 × R0)`:
|
||
|
||
```
|
||
mdJE ≤ 12 × R0 (= 15'120) → Rente = R0 (Mindestrente 1'260)
|
||
12 × R0 < mdJE ≤ 36 × R0 → Rente = R0 × (0.74 + 0.26 × x) Formel 1
|
||
36 × R0 < mdJE < 72 × R0 → Rente = R0 × (1.04 + 0.16 × x) Formel 2
|
||
mdJE ≥ 72 × R0 (= 90'720) → Rente = 2 × R0 (Maximalrente 2'520)
|
||
```
|
||
|
||
Der Wendepunkt liegt bei `36 × R0 = 45'360` → 1'915/Monat; beide Formelteile sind dort stetig.
|
||
|
||
**Quelle und Verifikation:** BSV/MAS „Berechnungsvorschriften der AHV/IV-Renten", gültig ab
|
||
1.1.2026 (liefert die Schwellenstruktur `12/36/72 × R0`), und die amtliche Tabelle
|
||
`318.117.1 – Monatliche Vollrenten, Skala 44`. Die implementierte Formel reproduziert **alle 51
|
||
Zeilen dieser Tabelle exakt**; Stützstellen sind als Golden Tests hinterlegt (Kap. 8.2).
|
||
|
||
Die Funktion `ahvMonthlyFullPension(mdJE)` rechnet bewusst **stetig**. Amtlich wird das mdJE auf
|
||
Vielfache von `1.2 × R0` gestuft (daher die 51 Tabellenzeilen); für eine Planung ist der stetige
|
||
Wert näher an der Wahrheit, die Abweichung liegt unter 20/Monat.
|
||
|
||
### 4.4.2 Beitragskarriere und mdJE
|
||
|
||
Pro Person wird über die Phasen hinweg akkumuliert (`AhvCareer`):
|
||
|
||
| Feld | Bedeutung |
|
||
|---|---|
|
||
| `plannedAvgGrossIncome` | reales **Brutto**-Durchschnittseinkommen der Beitragsjahre **im Plan** (siehe 4.4.5) |
|
||
| `planYears` | Beitragsjahre im Plan = Σ (Phasendauer − Ausfalljahre der Phase) |
|
||
| `yearsBeforePlan` | `max(0, Alter bei Planbeginn − 21)` |
|
||
| `gapYearsInPlan` | Summe der Ausfalljahre im Plan |
|
||
|
||
Das mdJE ist der **gewichtete Mittelwert über alle Beitragsjahre**:
|
||
|
||
```
|
||
yearsBefore = max(0, yearsBeforePlan − gapYearsBefore)
|
||
mdJE = (avgIncomeBefore × yearsBefore + plannedAvgIncome × planYears)
|
||
/ (yearsBefore + planYears)
|
||
```
|
||
|
||
Ausfalljahre reduzieren die **Gewichtung** (und die Skala), nicht das Durchschnittseinkommen –
|
||
genau wie in der echten AHV: Wer zwei Jahre aussetzt, hat deswegen kein tieferes
|
||
Durchschnittseinkommen, aber weniger Beitragsjahre.
|
||
|
||
### 4.4.3 Warum real gerechnet wird
|
||
|
||
Sämtliche Einkommen gehen **real** (Kaufkraft bei Planbeginn) in das mdJE ein, und die
|
||
Schwellen sind heutige Werte. Das ist kein Vereinfachungs-, sondern ein Genauigkeitsentscheid:
|
||
Die echte AHV **wertet vergangene Einkommen auf** (Lohnindex) **und indexiert die Schwellen**
|
||
(Mischindex). Beide Bewegungen heben sich in realer Betrachtung weitgehend auf – wer nominal
|
||
mittelt und gegen heutige Schwellen hält, vergleicht Franken von 2046 mit Schwellen von 2026 und
|
||
überschätzt die Rente systematisch.
|
||
|
||
> **Grössenordnung:** 45-jährig, 85'000 Lohn, +1.5 %/Jahr bei 2 % Inflation, 20 Erwerbsjahre.
|
||
> Nominal gemittelt: mdJE 98'276 → Maximalrente 32'760. Real gemittelt: mdJE 81'156 → 31'096.
|
||
> Differenz 1'664/Jahr, über 25 Rentenjahre rund 41'600 – und der Fehler geht immer nach oben.
|
||
|
||
Die reale Berechnung eines Phasen-Durchschnitts erfolgt analytisch (`avgRealFlow`) als
|
||
geometrische Reihe mit `q = (1 + Lohnerhöhung) / (1 + Inflation)`.
|
||
|
||
**Bekannte Unschärfe:** Die Schwellen folgen dem Mischindex, die Aufwertung dem Lohnindex. Da
|
||
Löhne langfristig schneller steigen als Preise, ist die Deflationierung mit der Preisinflation
|
||
leicht **konservativ**. Bewusst in Kauf genommen, statt eine dritte Indexannahme einzuführen.
|
||
|
||
### 4.4.4 Woher die Karriere-Werte kommen
|
||
|
||
| Situation | Quelle |
|
||
|---|---|
|
||
| Person retires innerhalb des Plans | `transitionValues` des AHV-Elements am Pensions-Übergang |
|
||
| Person bei Planbeginn bereits pensioniert | `phaseValues` des AHV-Elements in der ersten Phase |
|
||
| Prüfung noch nicht erfolgt | Fallback: `avgIncomeBefore = plannedAvgIncome` |
|
||
|
||
Der Fallback ist bewusst gewählt: Ohne erfassten Wert gilt der geplante Durchschnitt als
|
||
Schätzung für die Jahre davor – exakt der Wert, den der Dialog vorbelegt. Ein Fallback auf 0
|
||
würde die Rente still und massiv zu tief rechnen (bei einer 45-jährigen Person auf rund 45 %).
|
||
|
||
Einkommen wird einer Person nur zugerechnet, wenn das `INCOME`-Element ihr zugeordnet ist. Bei
|
||
einem **Einzelplan** zählt „Gemeinsam" (`HOUSEHOLD`) zur Person A; bei einem **Paar-Plan** nicht
|
||
(siehe [9.9](#99-gemeinsames-einkommen-zählt-bei-paaren-nicht-für-die-ahv)).
|
||
|
||
### 4.4.5 Netto-Brutto-Umrechnung für die AHV
|
||
|
||
Das Tool erfasst das Einkommen **netto**, die AHV bemisst sich am **Bruttolohn**. Beim Aufbau
|
||
der Karriere wird deshalb hochgerechnet:
|
||
|
||
```
|
||
plannedAvgGrossIncome = avgRealFlow(...) × AHV_GROSS_FROM_NET_FACTOR // = 1.12
|
||
```
|
||
|
||
Sämtliche Werte in `AhvCareer`, im Prüf-Dialog und im mdJE sind damit **brutto** – auch das
|
||
Feld „Durchschnittliches Bruttoeinkommen vor Planbeginn", das der Benutzer aus der
|
||
Rentenvorausberechnung übernimmt (die ohnehin brutto-basiert ist). Eine Einheit im ganzen
|
||
Ablauf, keine Umrechnung an der Feldgrenze.
|
||
|
||
**Herleitung des Faktors** (Arbeitnehmer-Abzüge in % des Bruttolohns):
|
||
|
||
| Abzug | Satz | Bemerkung |
|
||
|---|---|---|
|
||
| AHV/IV/EO | 5.30 % | 10.6 % total, hälftig geteilt |
|
||
| ALV | 1.10 % | 2.2 % total, hälftig geteilt, bis 148'200 |
|
||
| NBU | ~1.00 % | variiert 0.4–1.6 %, teils vom Arbeitgeber getragen |
|
||
| PK | ~2–5 % | BVG-Altersgutschrift 7/10/15/18 % auf den **koordinierten** Lohn (Brutto − 26'460, max. 90'720), Arbeitnehmer ≥ die Hälfte |
|
||
|
||
Total ~9–13 % je nach Alter und Lohn → Faktor `1/(1−q)` = **1.10 bis 1.16**, Mittel **1.12**.
|
||
|
||
**Warum ein fixer Faktor genügt:** Das mdJE ist selbst ein Durchschnitt über die ganze
|
||
Beitragskarriere (44 Jahre). Der altersabhängige PK-Satz mittelt sich dabei heraus – ein
|
||
Karriere-Faktor gegen einen Karriere-Durchschnitt ist konzeptionell stimmig, nicht bloss eine
|
||
Näherung. Die Restunschärfe (~3 %) ist deutlich kleiner als der Fehler, den die Umrechnung
|
||
behebt (~11 %, siehe 9.13).
|
||
|
||
**Grenzen:** Überobligatorische PK-Pläne, vom Arbeitgeber getragene NBU, Selbstständige und
|
||
Löhne über 148'200 (ALV sinkt auf 0.5 %) weichen ab. Der Faktor ist heute eine nicht
|
||
sichtbare Konstante; mit Roadmap Nr. 41 wird er in der Formel-Erklärung ausgewiesen.
|
||
|
||
Quellen: BSV „Beträge gültig ab 1.1.2026" (Koordinationsabzug 26'460, obere Limite 90'720,
|
||
„keine Änderung gegenüber 2025"); AHV-Merkblätter 2.01 (AHV/IV/EO) und 2.08 (ALV).
|
||
|
||
### 4.4.6 Jahresrente, Skala und Plafonierung
|
||
|
||
```
|
||
factor = max(0, (44 − Ausfalljahre total) / 44) // Rentenskala 44
|
||
rente = round(ahvMonthlyFullPension(mdJE) × 13 × factor)
|
||
```
|
||
|
||
`AHV_PENSION_MONTHS = 13` – seit 1.1.2026 gibt es die **13. Altersrente** (Art. 34bis AHVG).
|
||
Die Formel liefert Monatsrenten; der Jahresbetrag ist deshalb `× 13`, nicht `× 12`.
|
||
`AHV_MAX_ANNUAL_SINGLE` ist neu abgeleitet: `2 × R0 × 13 = 32'760`.
|
||
|
||
**Ehepaar-Plafonierung** – nur bei `householdType = COUPLE` **und** wenn für **beide** Personen
|
||
eine Rente vorliegt:
|
||
|
||
```
|
||
cap = 32'760 × 1.5 = 49'140
|
||
falls (renteA + renteB) > cap:
|
||
beide Renten proportional kürzen: rente × cap / summe
|
||
```
|
||
|
||
Die Rente ist danach **nominal fix** – sie wird über die Phasen hinweg nicht indexiert und
|
||
verliert damit real an Kaufkraft (siehe [9.11](#911-ahv-rente-wird-nach-der-pensionierung-nicht-indexiert)).
|
||
Sie fliesst in `renteTotal` und wird im Einkommen mitgeführt.
|
||
|
||
Referenz: `src/lib/calculations.ts` (`ahvMonthlyFullPension`, `ahvMdje`, `ahvAnnualPension`),
|
||
`src/lib/constants.ts`.
|
||
|
||
## 4.5 Nominal, real und die Deflatoren
|
||
|
||
### 4.5.1 Das V5-Modell
|
||
|
||
| Grösse | Erfassung | Indexierung über die Phasenjahre |
|
||
|---|---|---|
|
||
| Einkommen | **nominal** | `basis × (1 + Lohnerhöhung)^(t−1)` |
|
||
| Renten (AHV, PK) | **nominal** | keine – konstant |
|
||
| Ausgaben | **real** | real: `basis × (1 + reale Mehrausgaben)^(t−1)`, dann **× kumulierte Inflation** |
|
||
|
||
Die Inflation ist seit V5 **plan-weit** (`plan.inflationRateDefault`) und gilt einheitlich für
|
||
alle Phasen; eine phasenspezifische Überschreibung existiert nicht (mehr).
|
||
|
||
### 4.5.2 Kumulierte Inflation
|
||
|
||
```
|
||
cumInflStart(Phase 1) = 1
|
||
cumInflStart(Phase n) = cumInflStart(Phase n−1) × (1 + infl/100)^duration(n−1)
|
||
```
|
||
|
||
### 4.5.3 Die drei Deflatoren
|
||
|
||
Ein subtiler, aber wichtiger Punkt: Bestandswerte und Flow-Werte haben am Phasenende **nicht
|
||
denselben** Deflator, weil ein Flow im Jahr `duration` anfällt, ein Bestand aber **nach** dem
|
||
Jahr `duration` gemessen wird.
|
||
|
||
| Deflator | Formel | Verwendung |
|
||
|---|---|---|
|
||
| `cumulativeInflationStart` | s. o. | Bestände zu Phasenbeginn, Flows im Jahr 1 |
|
||
| `cumulativeInflationEnd` | `cumInflStart × (1+infl)^duration` | Bestände am Phasenende (Cash, Vermögen) |
|
||
| `flowDeflatorEnd` | `cumInflStart × (1+infl)^(duration−1)` | Flow-Endwerte (Einkommen, Ausgaben, Quote) – **eine Kaufkraft-Stufe weniger** |
|
||
|
||
Referenz: `src/lib/calculations.ts` Zeilen 427–429; Anwendung in `src/components/PlanView.tsx` Zeilen 686–687, 716–717.
|
||
|
||
## 4.6 Element-Setup je Kategorie
|
||
|
||
Für jedes Element (sortiert nach `orderIndex`) wird ein `ElementPhaseComputed` erzeugt.
|
||
Vorab-Abbruch: Ist der Carry-Status `SOLD` → Notiz „Verkauft"; ist er `SETTLED` und die Kategorie
|
||
`OTHER_DEBT` → „Getilgt". Solche Elemente werden nicht weiter gerechnet.
|
||
|
||
### 4.6.1 INCOME / EXPENSE
|
||
|
||
```
|
||
idx = phaseData.teuerungsausgleich ?? 0
|
||
baseValue = hasCarry ? round(carry.flowBasis) : round(phaseData.amount)
|
||
basis = !hasCarry → round(phaseData.amount)
|
||
phaseData.amount ist Zahl → round(phaseData.amount) // bewusster Override
|
||
sonst → baseValue // live vererbt
|
||
carry.flowBasis = basis × (1 + idx/100)^duration // für die Folgephase
|
||
```
|
||
|
||
**Die Vererbungsregel** (`src/components/ElementDetail.tsx` Zeilen 480–490): Beim Speichern wird
|
||
das Feld `amount` **gelöscht**, wenn es exakt dem fortgeschriebenen Wert entspricht. Dadurch
|
||
bleibt der Wert „live vererbt" – eine spätere Änderung in einer früheren Phase wirkt sich weiter
|
||
durch. Nur ein bewusst abweichender Wert wird fix gespeichert.
|
||
|
||
Man beachte den Exponenten-Unterschied: der **Endwert** der Phase nutzt `duration − 1`
|
||
(letztes Jahr), der **Carry** für die Folgephase nutzt `duration` (ein Jahr weiter).
|
||
|
||
### 4.6.2 AHV
|
||
|
||
Erwerbstätig → nur Zusammenfassung („N Ausfalljahre" / „Keine Ausfalljahre"), kein Wert.
|
||
Pensioniert → `startValue = endValue = rente`, Summand in `renteTotal`.
|
||
|
||
### 4.6.3 PENSION_FUND
|
||
|
||
| Fall | Verhalten |
|
||
|---|---|
|
||
| pensioniert, `pkPensionAnnual > 0` | Rente: `startValue = endValue = pkPensionAnnual`, Summand in `renteTotal` |
|
||
| pensioniert, keine Rente | Notiz „Vollständig bezogen" |
|
||
| erwerbstätig | `base = hasCarry ? carry.value : phaseData.currentValue`; `topUp = hasCarry ? additionalInvestment : 0`; `start = base + topUp`; Rate = `annualContribution` |
|
||
|
||
Die PK-Rate wird **nicht** zu `fixedRatesTotal` addiert – sie belastet das Cash nicht.
|
||
`topUp` wird ab Phase 2 zu `investmentsFromCash` addiert.
|
||
|
||
### 4.6.4 PILLAR_3A
|
||
|
||
Identisch zu PK, mit zwei Unterschieden:
|
||
- Die Rate **wird** zu `fixedRatesTotal` addiert (belastet das Cash).
|
||
- Ist der Besitzer pensioniert, gibt es nie eine Rente, nur „Vollständig bezogen".
|
||
|
||
### 4.6.5 REAL_ESTATE (Immobilie)
|
||
|
||
```
|
||
purchase = hasCarry ? carry.propertyPurchase : round(phaseData.purchasePrice) // Kaufpreis
|
||
valueStart = hasCarry ? carry.propertyValue : round(phaseData.purchasePrice) // Verkehrswert
|
||
mortgage = hasCarry ? carry.mortgage : round(phaseData.mortgage)
|
||
amort = round(phaseData.amortization) // wird JÄHRLICH am Restsaldo gekappt
|
||
equity = valueStart − mortgage → startValue, wealthStart
|
||
```
|
||
|
||
**Kaufpreis und Verkehrswert laufen getrennt.** Der Verkehrswert wächst mit `valueGrowth`, der
|
||
Kaufpreis bleibt der ursprüngliche – er ist die Basis der Grundstückgewinnsteuer beim Verkauf
|
||
(4.9.4). Beide werden über die Phasen fortgeschrieben (`carry.propertyValue`,
|
||
`carry.propertyPurchase`); die Hypothek läuft als Saldo in der Jahresschleife (4.7).
|
||
|
||
**Die Wertsteigerung wirkt auf die Liegenschaft, nicht auf das Eigenkapital.** Das ist der
|
||
Hebel, der Wohneigentum finanziell auszeichnet: 1 % von 1'000'000 sind 10'000 im Jahr – bei
|
||
einem Eigenkapital von 100'000 also 10 % darauf. Würde man die Wertsteigerung auf das
|
||
Eigenkapital rechnen, ergäbe dieselbe Annahme nur 1'000 im Jahr:
|
||
|
||
> Kaufpreis 1'000'000 · Hypothek 900'000 · EK 100'000 · 1 %/J. · Amortisation 10'000/J. · 10 J.
|
||
> → korrekt **304'622**; auf das Eigenkapital gerechnet nur **210'462**. Differenz **94'160**,
|
||
> und sie wächst mit dem Belehnungsgrad.
|
||
|
||
Der angezeigte Elementwert bleibt das **Eigenkapital** (`Verkehrswert − Restschuld`); nur die
|
||
Basis der Verzinsung ist die Liegenschaft.
|
||
|
||
Falls **nicht** fortgeschrieben und **nicht** Phase 1 (= Neukauf in einer späteren Phase):
|
||
`investmentsFromCash += max(0, equity)` – das Eigenkapital wird aus dem Cash finanziert.
|
||
|
||
### 4.6.6 OTHER_ASSET
|
||
|
||
```
|
||
base = hasCarry ? carry.value : round(phaseData.startValue)
|
||
topUp = hasCarry ? round(phaseData.additionalInvestment) : 0
|
||
start = base + topUp
|
||
rate = round(phaseData.annualContribution) → fixedRatesTotal += rate
|
||
withdrawal = round(phaseData.annualWithdrawal) → plannedWithdrawTotal += withdrawal
|
||
```
|
||
|
||
### 4.6.7 OTHER_DEBT
|
||
|
||
```
|
||
owedStart = hasCarry ? carry.owed : round(phaseData.startValue)
|
||
repay = round(phaseData.annualRepayment) // wird JÄHRLICH am Restsaldo gekappt
|
||
startValue = −owedStart // negatives Vorzeichen im Vermögen
|
||
```
|
||
|
||
Wie bei der Immobilie ist die Restschuld ein **laufender Saldo**; ist sie getilgt, entfällt die
|
||
Tilgungsrate.
|
||
|
||
## 4.7 Die Jahresschleife
|
||
|
||
Zunächst wird der Cash-Startwert gebildet:
|
||
|
||
```
|
||
cash = cashCarryIn − (isFirstPhase ? 0 : investmentsFromCash)
|
||
cashStart = cash
|
||
```
|
||
|
||
Die Investitionen werden also **am Phasenanfang** abgezogen. Grund (Kommentar im Code, Fix in
|
||
Commit `f768e01`): Der Cash-Startwert zeigt damit den Bestand **nach** den Investitionen – die
|
||
investierten Mittel erscheinen im Vermögen und nicht doppelt auch im Cash.
|
||
|
||
Dann für `t = 1 .. duration`:
|
||
|
||
```
|
||
// 1. Einkommen (nominal)
|
||
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t−1))
|
||
|
||
// 2. Ausgaben (real → nominal) + Hypothekarzins
|
||
inflFactor = cumInflStart × (1 + infl/100)^(t−1)
|
||
expenseRealBase = Σ (exp.basis × (1 + exp.idx/100)^(t−1))
|
||
interestNominal = Σ (re.mortgage × re.interestRate/100) // nur wo interestHandling = ADD
|
||
expenseNominal = expenseRealBase × inflFactor + interestNominal
|
||
expenseReal = expenseRealBase + interestNominal / inflFactor
|
||
|
||
// 3. Quote
|
||
quote = incomeFlow − expenseNominal
|
||
|
||
// 4. YearPoint anlegen (year, age Person A, income, expenseNominal, expenseReal);
|
||
// wealthNominal/wealthReal werden nach Schritt 8 nachgetragen
|
||
|
||
// 5. Vermögen: verzinsen, Sparbeitrag, Bezugsrate
|
||
für jedes Asset a:
|
||
grown = a.value × (1 + a.r/100) + a.rate // Zins zuerst, dann Einzahlung (nachschüssig)
|
||
w = min(a.withdrawal, max(0, grown)) // Bezug am Bestand gekappt
|
||
a.value = grown − w
|
||
cashFromWithdraw += w
|
||
|
||
// 6. Amortisation und Tilgung – jeweils am Restsaldo gekappt
|
||
debtRates = 0
|
||
für jede Immobilie re:
|
||
pay = min(re.amort, re.mortgage) // nie mehr als die Restschuld
|
||
re.mortgage −= pay
|
||
debtRates += pay
|
||
re.value ×= (1 + re.growth/100) // Wertsteigerung auf die LIEGENSCHAFT
|
||
für jede Schuld d:
|
||
pay = min(d.repay, d.owed)
|
||
d.owed −= pay
|
||
debtRates += pay
|
||
falls t === 1 → plannedSaveRate = fixedRatesTotal + debtRates
|
||
|
||
// 7. Cash fortschreiben
|
||
cash += quote − fixedRatesTotal − debtRates + cashFromWithdraw
|
||
falls cash < 0 → cashNegative = true
|
||
|
||
// 8. Ruin prüfen (Gesamtvermögen zum Jahresende)
|
||
total = cash + Σ asset.value + Σ (re.value − re.mortgage) + Σ (−d.owed)
|
||
yearPoint.wealthNominal = round(total) // Grundlage des Verlaufs je Jahr
|
||
yearPoint.wealthReal = round(total / cumInfl[Jahr])
|
||
falls ruinAge === null und total < 0 → ruinAge = age(Person A) + yearsBefore + t
|
||
```
|
||
|
||
**Wichtige Details:**
|
||
- Verzinsung ist **nachschüssig**: der Sparbeitrag des Jahres wird nicht mitverzinst.
|
||
- **Amortisation und Tilgung enden mit der Schuld.** Hypothek und Restschuld sind laufende
|
||
Salden; die Rate ist pro Jahr auf den Restsaldo gekappt (`min(rate, saldo)`). Ist die Schuld
|
||
abbezahlt, fliesst kein Franken mehr ab – weder aus dem Cash noch in die Sparrate. Im letzten
|
||
Zahlungsjahr wird nur noch der Restbetrag fällig, nicht die volle Rate.
|
||
- `plannedSaveRate` (Kopf-Kennzahl) ist die tatsächliche Rate des **ersten** Phasenjahres. In
|
||
späteren Jahren kann sie tiefer liegen, wenn eine Schuld ausläuft.
|
||
- `cashNegative` wird gesetzt, sobald der Cash-Bestand **irgendwann innerhalb** der Phase unter 0
|
||
fällt – auch wenn er am Phasenende wieder positiv ist.
|
||
- Der Ruin bezieht sich auf das **Gesamtvermögen inkl. Immobilien**, nicht auf das Cash.
|
||
- `ruinAge` wird nur **einmal** gesetzt (erstes Auftreten, plan-weit).
|
||
|
||
Referenz: `src/lib/calculations.ts` Zeilen 365–425.
|
||
|
||
## 4.8 Endwerte und Phasen-Kennzahlen
|
||
|
||
```
|
||
Einkommen: startValue = basis
|
||
endValue = basis × (1 + idx/100)^(duration−1)
|
||
Ausgaben: startValue = basis × cumInflStart
|
||
endValue = basis × (1 + idx/100)^(duration−1) × flowDeflatorEnd
|
||
Assets: endValue = a.value (nach der Jahresschleife)
|
||
Immobilie: endValue = re.value − re.mortgage (Verkehrswert inkl. Wertsteigerung, minus Restschuld)
|
||
Schulden: endValue = −d.owed (0, falls getilgt; + Notiz „Wird getilgt")
|
||
```
|
||
|
||
Aggregate:
|
||
|
||
```
|
||
startWealthNominal = Σ Element-Startwerte + cashStart
|
||
endWealthNominal = Σ Element-Endwerte + cashEnd
|
||
endWealthReal = endWealthNominal / cumulativeInflationEnd
|
||
isConsumption = quotaStart < 0
|
||
incomplete = cashNegative // „roter Status" = Liquiditätslücke
|
||
capitalInflow = incomingInflow // aus dem Übergang IN diese Phase
|
||
capitalInvest = investmentsFromCash + incomingImmediateRepay
|
||
oneOffInflow = incomingOneOffInflow // einmaliger Zufluss (netto nach Steuer)
|
||
oneOffOutflow = incomingOneOffOutflow // einmalige Kosten (nominal)
|
||
```
|
||
|
||
## 4.9 Der Übergang
|
||
|
||
Nach jeder Phase (auch nach der letzten) läuft die Übergangs-Logik. Sie liest
|
||
`transitionValues[phase.id]` – der Übergang ist also am **Von**-Phasen-Schlüssel gespeichert.
|
||
|
||
```
|
||
ownerRetiresNext = owner existiert
|
||
∧ nextPhase existiert
|
||
∧ owner ist in DIESER Phase erwerbstätig
|
||
∧ owner.age + yearsBefore + duration >= owner.retirementAge
|
||
```
|
||
|
||
`INCOME`/`EXPENSE` sowie nicht-aktive Elemente überspringen die Logik (nur `hasCarry = true`).
|
||
|
||
### 4.9.1 PENSION_FUND
|
||
|
||
**Pensions-Übergang** (`ownerRetiresNext`), `value = ec.endValue`, Default-Modus `PENSION`:
|
||
|
||
| `payoutMode` | Wirkung |
|
||
|---|---|
|
||
| `CAPITAL` | `txInflow += round(value × (1 − capitalTaxRate/100))`; `carry.value = 0`; `pkPensionAnnual = 0` |
|
||
| `PENSION` | `carry.pkPensionAnnual = round(value × conversionRate / 100)`; `carry.value = 0` |
|
||
| `COMBI` | `capital = min(value, capitalAmount)`; `txInflow += round(capital × (1 − tax/100))`; `pkPensionAnnual = round((value − capital) × conversionRate / 100)`; `carry.value = 0` |
|
||
|
||
**Normaler Übergang (Vorbezug)** – brutto entnommen, netto ins Cash:
|
||
```
|
||
withdrawal = min(ec.endValue, round(td.withdrawal)) // brutto
|
||
carry.value = ec.endValue − withdrawal
|
||
txInflow += round(withdrawal × (1 − capitalTaxRate/100))
|
||
```
|
||
|
||
### 4.9.2 PILLAR_3A
|
||
|
||
**Pensions-Übergang**: immer vollständiger Bezug –
|
||
`txInflow += round(ec.endValue × (1 − capitalTaxRate/100))`; `carry.value = 0`.
|
||
**Normaler Übergang (Vorbezug)**: wie PK – Bruttoentnahme, Netto-Zufluss nach
|
||
Kapitalbezugssteuer.
|
||
|
||
### 4.9.3 OTHER_ASSET
|
||
|
||
| `decision` | Wirkung |
|
||
|---|---|
|
||
| `SELL` | `txInflow += ec.endValue`; `carry.status = "SOLD"` (kein Steuerabzug) |
|
||
| `PARTIAL` | `part = min(ec.endValue, partialSaleAmount)`; `txInflow += part`; `carry.value = ec.endValue − part` – Rest bleibt **aktiv** und wächst weiter |
|
||
| `HOLD` / sonst | `carry.value = ec.endValue` |
|
||
|
||
Der Teilverkauf (Roadmap Nr. 42) fliesst über `txInflow` ins Cash der Folgephase und erscheint
|
||
dort im Phasenkopf als **„Kapitalzufluss"** (`capitalInflow`) – das „zu investierende Kapital".
|
||
Er ist die **einmalige** Entnahme am Übergang; die laufende Entnahme *während* einer Phase
|
||
bleibt die jährliche Bezugsrate (`annualWithdrawal`, 4.6.6). Beide dürfen koexistieren.
|
||
|
||
### 4.9.4 REAL_ESTATE
|
||
|
||
Gelesen werden die laufenden Werte aus der Jahresschleife (`re.value`, `re.mortgage`,
|
||
`re.purchase`).
|
||
|
||
`decision = "SELL"`:
|
||
```
|
||
gain = max(0, salePrice − re.purchase) // URSPRÜNGLICHER Kaufpreis, nicht der Verkehrswert
|
||
tax = gain × (saleTaxRate / 100)
|
||
txInflow += round(salePrice − re.mortgage − tax)
|
||
carry.status = "SOLD"
|
||
```
|
||
Der Nettoerlös ist Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer. Ein
|
||
Verlustverkauf erzeugt keine Steuer (`gain` bei 0 geklammert).
|
||
|
||
**Die Steuer bemisst sich am ursprünglichen Kaufpreis**, nicht am zwischenzeitlich gestiegenen
|
||
Verkehrswert – deshalb führt das Modell beide getrennt (4.6.5). Nicht modelliert sind
|
||
wertvermehrende Investitionen und die Haltedauer-Abstufung (Roadmap Nr. 23).
|
||
|
||
**Halten (`decision ≠ SELL`)** – mit optionaler **Sonderamortisation** (Roadmap Nr. 15):
|
||
```
|
||
extra = min(re.mortgage, extraAmortization) // am Restsaldo gekappt
|
||
txImmediateRepay += extra // Einmaltilgung aus dem Cash
|
||
carry.mortgage = re.mortgage − extra
|
||
carry.propertyValue / carry.propertyPurchase // fortgeschrieben
|
||
```
|
||
|
||
Die Einmaltilgung bringt `REAL_ESTATE` auf Augenhöhe mit `OTHER_DEBT` (dort schon
|
||
`immediateRepayment`, 4.9.5): Sie senkt die Restschuld und damit die künftige Zinslast und
|
||
erscheint im Phasenkopf als **„Kapitalinvestition"** (`capitalInvest`). Damit lässt sich die
|
||
**indirekte Amortisation** mechanisch nachbilden – 3a wachsen lassen, bei Pensionierung ins
|
||
Cash beziehen, dann per Sonderamortisation die Hypothek tilgen.
|
||
|
||
Die **Steuerwirkung** der indirekten Amortisation (3a-Abzug, erhaltener Zinsabzug) ist bewusst
|
||
**nicht** modelliert; sie gehört zum zurückgestellten Steuer-Bündel (siehe 9.14).
|
||
|
||
### 4.9.5 OTHER_DEBT
|
||
|
||
```
|
||
carry.owed = −ec.endValue
|
||
immediate = min(carry.owed, round(td.immediateRepayment))
|
||
falls immediate > 0:
|
||
carry.owed −= immediate
|
||
txImmediateRepay += immediate
|
||
falls carry.owed === 0 → carry.status = "SETTLED"
|
||
```
|
||
|
||
### 4.9.6 Cash: einmalige Sonderein-/ausgaben
|
||
|
||
Gelesen wird `phase.cashTransition` – der Entscheid hängt an der **Von**-Phase. Er wird nur
|
||
ausgewertet, **wenn eine Folgephase existiert**; nach der letzten Phase gibt es keinen Übergang,
|
||
ein dort erfasster Betrag bleibt wirkungslos.
|
||
|
||
Der Umrechnungskurs zwischen real und nominal ist an dieser Grenze `cumulativeInflation`, also
|
||
der **Bestands-Deflator am Phasenende** (vgl. [4.5.3](#453-die-drei-deflatoren)) – denn Cash ist
|
||
ein Bestand, und das Ereignis fällt exakt auf die Grenze.
|
||
|
||
```
|
||
mode = cashTransition.mode ?? "NONE"
|
||
|
||
falls mode ∈ {INFLOW, BOTH}: // nominal erfasst
|
||
brutto = round(inflowAmount)
|
||
txOneOffInflow = round(brutto × (1 − inflowTaxRate/100))
|
||
|
||
falls mode ∈ {OUTFLOW, BOTH}: // real erfasst
|
||
txOneOffOutflow = round(outflowAmount × cumulativeInflation)
|
||
```
|
||
|
||
Beide Grössen fliessen ausschliesslich ins Cash der Folgephase (4.9.7) und **nicht** in die
|
||
Jahresschleife – damit bleiben sie per Konstruktion aus Einkommen, Ausgaben und Quote heraus.
|
||
Ein Zufluss/eine Kostenposition, die das Cash unter 0 drückt, wird über den bestehenden
|
||
Startwert-Check der Folgephase (`cashNegative = cash < 0`) automatisch als Liquiditätslücke
|
||
erkannt.
|
||
|
||
### 4.9.7 Abschluss des Übergangs
|
||
|
||
```
|
||
cashCarryIn = cashEnd + txInflow + txOneOffInflow − txImmediateRepay − txOneOffOutflow
|
||
incomingInflow = txInflow // Kopf-Kennzahlen der Folgephase
|
||
incomingImmediateRepay = txImmediateRepay
|
||
incomingOneOffInflow = txOneOffInflow // inkl. Bezeichnung
|
||
incomingOneOffOutflow = txOneOffOutflow // inkl. Bezeichnung
|
||
yearsBefore += duration
|
||
```
|
||
|
||
## 4.10 Ergebnisstruktur
|
||
|
||
```ts
|
||
PlanComputed {
|
||
phases: PhaseComputed[] // alle Kennzahlen je Phase, inkl. elements[]
|
||
yearly: YearPoint[] // ein Punkt pro Jahr über alle Phasen (inkl. Vermögen je Jahr)
|
||
nachlass: number // = endWealthNominal der letzten Phase, sonst 0
|
||
ruinAge: number | null // Alter Person A beim ersten Gesamtvermögen < 0
|
||
}
|
||
```
|
||
|
||
## 4.11 Systemparameter
|
||
|
||
Zentral in `src/lib/constants.ts` geführt, weil sie sich periodisch durch Bundesanpassungen ändern:
|
||
|
||
| Konstante | Wert | Bedeutung |
|
||
|---|---|---|
|
||
| `AHV_MIN_MONTHLY_FULL` | 1'260 | **R0** – Mindestbetrag der vollen Monatsrente (Skala 44). Alle Schwellen der Rentenformel sind Vielfache davon |
|
||
| `AHV_PENSION_MONTHS` | 13 | Rentenzahlungen pro Jahr – 13. Altersrente ab 1.1.2026 |
|
||
| `AHV_CONTRIBUTION_START_AGE` | 21 | Beitragspflicht ab 1. Januar nach dem 20. Geburtstag |
|
||
| `AHV_MAX_ANNUAL_SINGLE` | 32'760 | **abgeleitet**: `2 × R0 × 13` |
|
||
| `AHV_GROSS_FROM_NET_FACTOR` | 1.12 | Netto → Brutto für die AHV; Herleitung siehe [4.4.5](#445-netto-brutto-umrechnung-für-die-ahv) |
|
||
| `AHV_COUPLE_CAP_FACTOR` | 1.5 | Ehepaar-Plafonierung: 150 % der Einzel-Maximalrente |
|
||
| `AHV_FULL_CONTRIBUTION_YEARS` | 44 | Volle Beitragsdauer (Rentenskala 44) |
|
||
| `PILLAR_3A_MAX_ANNUAL` | 7'258 | Max. 3a-Beitrag/Jahr für PK-Versicherte (2026) |
|
||
| `DEFAULT_PK_CONVERSION_RATE` | 6 % | Umwandlungssatz |
|
||
| `DEFAULT_CAPITAL_TAX_RATE` | 8 % | Kapitalbezugssteuer |
|
||
| `DEFAULT_PROPERTY_GAINS_TAX_RATE` | 20 % | Grundstückgewinnsteuer |
|
||
|
||
Die drei Default-Sätze werden **sowohl als UI-Vorschlag als auch in der Berechnung als Fallback**
|
||
verwendet (`num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)`). Grund laut Code-Kommentar: Damit
|
||
ein nicht angetippter Wert nicht fälschlich als 0 gerechnet wird.
|
||
|
||
## 4.12 Monte-Carlo-Simulation
|
||
|
||
Die deterministische Berechnung nimmt pro Anlage *eine* feste Rendite und *eine* feste Inflation
|
||
an. Real schwanken beide. Die Monte-Carlo-Simulation (Roadmap Nr. 19, Stufe A, `montecarlo.ts`)
|
||
würfelt viele tausend mögliche Verläufe und weist die **Erfolgswahrscheinlichkeit** des Plans aus.
|
||
|
||
### 4.12.1 Die Nahtstelle in `computePlan`
|
||
|
||
`computePlan(plan, sample?)` nimmt optional ein `PlanSample`:
|
||
|
||
```ts
|
||
interface PlanSample {
|
||
inflation: number[]; // Inflation %/Jahr (Index 0 = Jahr 1)
|
||
assetReturn: (elementId: string, year: number) => number; // Rendite %/Jahr (1-basiert)
|
||
}
|
||
```
|
||
|
||
Ohne `sample` rechnet die Funktion **exakt wie bisher** (die geplanten Annahmen). Mit `sample`
|
||
liefert sie einen einzelnen simulierten Pfad. Voraussetzung war eine Umstellung der Inflation
|
||
auf ein **kumulatives Deflator-Array** `cumInfl[]` (statt der geschlossenen `(1+i)^t`-Formel),
|
||
damit die Inflation pro Jahr variieren kann – deterministisch bitgenau identisch, durch die
|
||
Golden Tests abgesichert. Die AHV-Karriere bleibt bewusst auf der festen Plan-Inflation (sie
|
||
ist eine Real-Grösse auf Planungsbasis, sie wird nicht mitgewürfelt).
|
||
|
||
**Architektur-Vorteil:** `computePlan` ist eine reine Funktion ohne Server-Abhängigkeiten und
|
||
läuft damit **im Browser**. Die gesamte Simulation rechnet client-seitig – null Serverlast.
|
||
~10'000 Läufe in rund 1 Sekunde; die Ausführung gibt alle 500 Läufe die Kontrolle ab
|
||
(Fortschrittsbalken, keine eingefrorene Oberfläche).
|
||
|
||
### 4.12.2 Das statistische Modell
|
||
|
||
Pro Jahr ein **gemeinsamer Marktschock** `z_markt`; je Element und Jahr:
|
||
|
||
```
|
||
rendite = mittelwert + σ × (ρ × z_markt + √(1−ρ²) × z_eigen)
|
||
```
|
||
|
||
- `z_markt`, `z_eigen`: **standardisierte Student-t** (ν = 5) – „fette Ränder", damit
|
||
Extremcrashs realistisch häufig auftreten. Eine Normalverteilung macht ein −40%-Jahr zu einem
|
||
1-in-250-Ereignis; real ist es ~1-in-15. Standardisiert auf Einheitsvarianz → die eingegebene
|
||
Standardabweichung σ bleibt die tatsächliche.
|
||
- `ρ = 0.7` → Korrelation zweier riskanter Anlagen ≈ 0.5: **alle riskanten Anlagen fallen im
|
||
Crash gemeinsam.** Unabhängiges Würfeln würde das Absturzrisiko systematisch unterschätzen.
|
||
- **Böden:** `max(0, rendite)` für PK/3a (schreiben keine negative Rendite gut),
|
||
`max(−100, rendite)` sonst.
|
||
|
||
Die Inflation wird analog gezogen (eigener Student-t-Schock, Mittelwert + σ), unabhängig vom
|
||
Marktschock. Der Zufallsgenerator ist **seedbar** (reproduzierbare Läufe).
|
||
|
||
### 4.12.3 Zwei Renditezahlen — und warum
|
||
|
||
Pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie) gibt es im Simulations-Dialog
|
||
**zwei** Renditen mit verschiedenen Rollen:
|
||
|
||
| Zahl | Rolle |
|
||
|---|---|
|
||
| **Geplante Rendite** (im Plan) | zeichnet die deterministische Linie = der *Zielbalken* |
|
||
| **Historische Ø-Rendite** (im Dialog, Pflicht) | der Mittelpunkt, um den die Simulation streut |
|
||
|
||
Ohne diese Trennung wäre die Kennzahl „P(erreiche mein geplantes Endvermögen)" **immer ~50 %**,
|
||
egal welche Rendite man annimmt (der Zielbetrag wüchse ja mit). Erst weil die Simulation um die
|
||
*historische* Rendite streut, während der Zielbalken auf der *geplanten* steht, wird ein
|
||
konservativer Plan (tiefe Planannahme) korrekt mit einer höheren Erfolgsquote belohnt als ein
|
||
optimistischer. Analog auf Plan-Ebene für die Inflation.
|
||
|
||
**Ehrliche Grenze (im Dialog ausgewiesen):** Die Simulation misst das Risiko *um deine Annahmen
|
||
herum* – sie beurteilt **nicht**, ob deine Mittelwerte realistisch sind. Ein zu optimistischer
|
||
Mittelwert liefert eine beruhigende, aber unrealistische Erfolgsquote (siehe 9.15).
|
||
|
||
### 4.12.4 Streuungsstufen
|
||
|
||
| Stufe (Rendite) | σ | Beispiele | | Stufe (Inflation) | σ |
|
||
|---|---|---|---|---|---|
|
||
| Sehr niedrig | 3 % | Staatsanleihen, Geldmarkt | | Sehr niedrig | 1 % |
|
||
| Niedrig | 6 % | Immobilien, defensive Mischportfolios | | Niedrig | 2 % |
|
||
| Moderat | 15 % | breit diversifizierte Aktien-ETFs/Fonds | | Manuell | frei |
|
||
| Hoch | 25 % | Einzelaktien, Branchen-/Schwellenländerfonds | | | |
|
||
| Sehr hoch | 55 % | Kryptowährungen, hochspekulative Anlagen | | | |
|
||
| Manuell | frei | eigene Eingabe | | | |
|
||
|
||
Default-Stufe je Typ: PK → sehr niedrig · 3a/Immobilie → niedrig · Sonstiges Vermögen → moderat.
|
||
Bei Inflation gibt es bewusst nur zwei Stufen (höhere wären Hyperinflations-Annahmen).
|
||
|
||
Quellen der σ-Werte: Anleihen ~6 %, globale Aktien ~15–18 %, Schweizer Immobilien(fonds) ~2 %,
|
||
Bitcoin ~54 %, Schweizer Inflation SD der letzten 20 J. ~1 %. Belege: BSV/Weltbank sowie
|
||
Markt-/Volatilitätsstatistiken (recherchiert 2026-07-17).
|
||
|
||
### 4.12.5 Ergebnis
|
||
|
||
| Kennzahl | Bedeutung |
|
||
|---|---|
|
||
| **Ruinwahrscheinlichkeit** | Anteil der Läufe mit `ruinAge !== null` (Vermögen fällt vor Planende unter 0) |
|
||
| **Erfolgswahrscheinlichkeit** | Anteil der Läufe mit Endvermögen ≥ Zielbetrag (nominal, vorbelegt mit dem geplanten Nachlass) |
|
||
| **Fächer** | je Alterspunkt (Phasengrenzen) das 10-/50-/90-Perzentil des Vermögens; dazu die deterministische Planungslinie |
|
||
|
||
Der Median liegt typischerweise **unter** der deterministischen Linie – der „Volatilitäts-Drag"
|
||
(`geometrisch ≈ arithmetisch − σ²/2`) macht sichtbar, dass die glatte Ein-Zahl-Planung schon
|
||
leicht zu optimistisch ist. Pflichtfelder: ohne die historischen Ø-Werte startet die Simulation
|
||
nicht.
|
||
|
||
### 4.12.6 Mehrere Szenarien im Vergleich
|
||
|
||
Der Dialog rechnet auf Wunsch **mehrere Szenarien desselben Plans in einem Lauf**. Drei
|
||
Entscheide machen den Vergleich überhaupt aussagekräftig.
|
||
|
||
**(1) Eine Parametereingabe je logischem Element.** Die MC-Parameter hängen an der `elementId`,
|
||
und Element-IDs sind szenario-spezifisch – eine Kopie bekommt neue IDs. Ohne Zuordnung müsste
|
||
dieselbe Anlage pro Szenario erneut erfasst werden. Das wäre nicht nur mühsam, es würde den
|
||
Vergleich **zerstören**: Mit 5 % im einen und 6 % im anderen Szenario vergleicht man die
|
||
Eingaben statt der Szenarien.
|
||
|
||
Die Zuordnung läuft über die Herkunfts-Kette `sourceElementId` – dieselbe Grundlage wie beim
|
||
Diff ([3.2.6](#326-abweichungs-markierung-diff)). `resolveRootElementId` folgt ihr bis zum
|
||
Ursprung; alle Elemente mit derselben Wurzel bilden eine **Gruppe** und teilen einen
|
||
Parametersatz. Deshalb lädt der Dialog beim Öffnen **alle** Szenarien des Plans, nicht nur die
|
||
ausgewählten: Nur so löst sich die Kette auch über ein übersprungenes Zwischen-Szenario auf
|
||
(Basis → S1 → S2 bei Auswahl von Basis und S2). Ein Element, das es nur in einem Szenario gibt,
|
||
bildet eine eigene Gruppe und wird im Dialog entsprechend gekennzeichnet.
|
||
|
||
**(2) Gemeinsamer Seed.** Alle Szenarien eines Laufs verwenden denselben Zufalls-Seed
|
||
(*Common Random Numbers*). Ohne das wären kleine Unterschiede blosses Rauschen: Bei 1'000 Läufen
|
||
beträgt der Standardfehler der Erfolgswahrscheinlichkeit rund 1.5 Prozentpunkte – zwei identische
|
||
Szenarien könnten 87 % und 90 % zeigen. Mit gemeinsamem Seed teilen **strukturgleiche** Szenarien
|
||
exakt dieselben Marktpfade, und die Unterschiede sind rein strukturell. Einschränkung: Die Pfade
|
||
sind nur dort identisch, wo die Struktur es ist – abweichende Laufzeit oder Elementzahl verschiebt
|
||
die Ziehungsreihenfolge.
|
||
|
||
**(3) Zielbetrag je Szenario.** Der Zielbetrag ist bewusst **nicht** gemeinsam, sondern je
|
||
Szenario mit dessen geplantem Endvermögen vorbelegt (einzeln editierbar). Damit misst die
|
||
Erfolgswahrscheinlichkeit, wie oft ein Szenario **sein eigenes Versprechen** hält.
|
||
|
||
> **Warum das der entscheidende Punkt ist:** In der Simulation wird die *geplante* Rendite
|
||
> vollständig durch die gewürfelte ersetzt. Unterscheiden sich zwei Szenarien **nur** in der
|
||
> geplanten Rendite (5 % vs. 6 %), sind ihre simulierten Verteilungen **identisch** – gleicher
|
||
> Median, gleicher Fächer, gleiche Ruinwahrscheinlichkeit. Der einzige Unterschied ist der
|
||
> Zielbetrag. Mit einem gemeinsamen Zielbetrag zeigte der Vergleich zwei identische Zeilen; mit
|
||
> szenario-eigenem Zielbetrag zeigt er die eigentliche Aussage: Das pessimistisch geplante
|
||
> Szenario erreicht sein tieferes Ziel häufiger und ist damit das belastbarere. Durch einen Test
|
||
> abgedeckt (Kap. 8.2).
|
||
|
||
Folge für die Darstellung: Die **Ruinwahrscheinlichkeit** ist zielbetrags-unabhängig und damit
|
||
die direkt vergleichbare Kennzahl; die Erfolgswahrscheinlichkeit bezieht sich je Zeile auf eine
|
||
andere Messlatte. Deshalb steht der Zielbetrag als **eigene Spalte** in der Vergleichstabelle.
|
||
|
||
**Darstellung:** eine Vergleichstabelle (Szenario, Ziel, Erfolg, Ruin, P10/Median/P90) als
|
||
Hauptinstrument, dazu ein Chart mit der **Median-Linie je Szenario**. Übereinandergelegte
|
||
10–90 %-Bänder wären unlesbar; der vollständige Fächer inklusive deterministischer Linie erscheint
|
||
deshalb nur, wenn **genau ein** Szenario ausgewählt ist – dann verhält sich der Dialog exakt wie
|
||
zuvor.
|
||
|
||
**Laufzeit:** Die Szenarien laufen sequenziell, der Fortschritt weist Szenario und Gesamtanteil
|
||
aus. Die Schätzung skaliert mit der Anzahl Szenarien.
|
||
|
||
Referenz: `src/lib/montecarlo.ts` (`resolveRootElementId`, `buildElementGroups`,
|
||
`paramsForScenario`, `runMonteCarloMulti`), `src/components/MonteCarloDialog.tsx`.
|
||
|
||
### 4.12.7 Zwei Welten, vier Fälle
|
||
|
||
Die Simulation läuft **immer zweimal** – einmal um die **historischen** Renditen, einmal um die
|
||
**geplanten** Werte des Plans. Aus jeder der beiden Verteilungen werden **zwei Schwellen**
|
||
abgelesen: der **Plan-Endbetrag** (read-only, der geplante Nachlass des Szenarios) und der
|
||
**Zielbetrag** (manuell, einer für alle Szenarien). Das ergibt vier Zahlen:
|
||
|
||
| | Schwelle **Plan-Endbetrag** | Schwelle **Zielbetrag** |
|
||
|---|---|---|
|
||
| Welt **historisch** (Eingabe) | **Fall 1** – Realismus der Planung | **Fall 3** – Ziel, wenn der Markt wie früher läuft |
|
||
| Welt **geplant** (aus dem Plan) | **Fall 2** – Nullpunkt für das Urteil | **Fall 4** – Ziel, wenn der Markt wie geplant läuft |
|
||
|
||
Das baut auf der **Trennung der zwei Renditezahlen** aus [4.12.3](#4123-zwei-renditezahlen--und-warum)
|
||
auf. Die **Streuung ist in beiden Welten dieselbe**; nur der Mittelpunkt unterscheidet sie. In der
|
||
geplanten Welt kommt der Mittelpunkt je Element aus `plannedReturnOf` (erste Phase, bei Immobilien
|
||
die Wertsteigerung) und die Inflation aus `plan.inflationRateDefault`.
|
||
|
||
**Warum zwei Läufe und nicht vier.** Eine Wahrscheinlichkeit P(Endvermögen ≥ X) hängt nur von der
|
||
Verteilung und von X ab. Wird das gesamte Endvermögen eines Laufs sortiert aufbewahrt
|
||
(`finalWealthSorted`), lässt sich jede Schwelle nachträglich per Binärsuche ablesen
|
||
(`probabilityAtLeast`). Zwei Läufe genügen also für vier Zahlen – halbe Rechenzeit, und vor allem:
|
||
|
||
**Fall 1 und Fall 3 stammen aus derselben Verteilung.** Liegt der Zielbetrag unter dem
|
||
Plan-Endbetrag, ist Fall 3 damit **zwingend ≥ Fall 1**. Der Widerspruch, der die alte Darstellung
|
||
unlesbar machte («Planung 69 %, Ziel 3 Mio nur 41 %» – zwei verschiedene Welten gegeneinander
|
||
gestellt), ist strukturell nicht mehr möglich. Ein Test sichert das ab.
|
||
|
||
**Der Nullpunkt ist Fall 2, nicht 50 %.** Naiv erwartet man, dass der eigene Plan-Endbetrag in der
|
||
eigenen geplanten Welt in ~50 % der Fälle erreicht wird. Das ist falsch: Der Volatilitäts-Drag
|
||
drückt den Median unter den deterministischen Pfad, und zwar umso stärker, je höher die Streuung
|
||
(gemessen: 37 % bei σ = 15 %, 27 % bei σ = 25 %; siehe [9.26](#926-fall-2-ist-der-nullpunkt--nicht-50-)).
|
||
Fall 2 misst genau diesen Nullpunkt und wird deshalb als **Referenz** ausgewiesen, gegen die Fall 1
|
||
verglichen wird – beides derselbe Schwellwert, nur andere Renditeannahme.
|
||
|
||
**Urteil** (Toleranzband ± 5 Prozentpunkte):
|
||
|
||
| Fall 1 vs. Fall 2 | Urteil |
|
||
|---|---|
|
||
| mehr als 5 pp höher | Planung **zurückhaltend** gerechnet |
|
||
| innerhalb ± 5 pp | Planung **realistisch** gerechnet |
|
||
| mehr als 5 pp tiefer | Planung tendenziell **zu optimistisch** |
|
||
|
||
**Darstellung** (bei einem einzelnen Szenario):
|
||
- **Prominent:** Fall 1 als grosse Prozentzahl mit dem Urteil.
|
||
- **Untergeordnet:** Fall 3 und 4 als Satzpaar, dessen Wortwahl automatisch dreht, je nachdem
|
||
welche Welt besser dasteht («Wenn der Markt so ‹schlecht› läuft, wie du es in deiner Planung
|
||
vorgibst, … bei X %. Läuft er so ‹gut› wie in der Vergangenheit, … bei Y %.»).
|
||
- **Kleingedruckt:** Fall 2 als Referenz plus der Median je Welt.
|
||
|
||
Die Tabelle zeigt je Szenario `Fall 1 (Fall 2)` und `Fall 3 (Fall 4)`. **Ruin**, **Bandbreite** und
|
||
**Fächer** stammen immer aus der historischen Welt, weil das das ehrliche Risikobild ist.
|
||
|
||
**Eingaben:** Da beide Welten immer laufen, sind die historischen Mittelwerte **und** der
|
||
Zielbetrag stets Pflichtfelder. Der frühere Drei-Wege-Umschalter entfällt.
|
||
|
||
**Technik:** kein Eingriff in den Rechenkern. Beide Durchgänge laufen mit **demselben Seed**
|
||
(Common Random Numbers), damit der Unterschied zwischen den Welten nicht Rauschen ist.
|
||
|
||
Referenz: `src/lib/montecarlo.ts` (`finalWealthSorted`, `probabilityAtLeast`, `plannedReturnOf`),
|
||
`src/components/MonteCarloDialog.tsx`.
|
||
|
||
## 4.13 Sensitivitätsanalyse (Tornado)
|
||
|
||
Die Monte-Carlo-Simulation würfelt alle Unsicherheiten gleichzeitig und beantwortet „wie
|
||
wahrscheinlich geht mein Plan auf?". Die Sensitivitätsanalyse (Roadmap Nr. 20, `sensitivity.ts`)
|
||
beantwortet die komplementäre Frage: **„Welche meiner Annahmen entscheidet überhaupt über das
|
||
Ergebnis?"**
|
||
|
||
### 4.13.1 Verfahren
|
||
|
||
**One-at-a-time (OAT):**
|
||
|
||
```
|
||
base = Zielgrösse(Plan)
|
||
für jeden ausgewählten Treiber d:
|
||
lowResult = Zielgrösse(applyDriver(Plan, d, d.low))
|
||
highResult = Zielgrösse(applyDriver(Plan, d, d.high))
|
||
swing = |highResult − lowResult|
|
||
sortiere absteigend nach swing → Trichterform, längster Balken zuoberst
|
||
```
|
||
|
||
Alle übrigen Parameter bleiben dabei auf dem Planwert. Das sind 2 Aufrufe je Treiber – bei
|
||
sieben Treibern 14 `computePlan`-Aufrufe, also Millisekunden. Wie die Monte-Carlo-Simulation
|
||
läuft alles **im Browser**; `applyDriver` ist rein und lässt den Ausgangsplan unberührt.
|
||
|
||
**Zielgrösse** ist das Endvermögen der letzten Phase, wahlweise **real** (Default,
|
||
kaufkraftbereinigt) oder nominal. Das Ruinalter wäre als Balkengrösse untauglich, weil es in
|
||
vielen Plänen `null` ist.
|
||
|
||
### 4.13.2 Die Treiber und ihre Einheiten
|
||
|
||
Die Einheit ist je Treiber verschieden und lässt sich nicht vereinheitlichen, ohne fachlich
|
||
falsch zu werden:
|
||
|
||
| Treiber | Einheit | Wirkung |
|
||
|---|---|---|
|
||
| Ausgaben | **relativ %** | skaliert `amount` aller `EXPENSE`-Elemente |
|
||
| Rendite (PK, 3a, Sonstiges Vermögen) | **Δ Prozentpunkte** | verschiebt `expectedReturn` |
|
||
| Lebensdauer | **Δ Jahre** | verlängert/verkürzt die **letzte** Phase (min. 1 Jahr) |
|
||
| Inflation | **absolut %** | setzt `inflationRateDefault` |
|
||
| Einkommen | **relativ %** | skaliert `amount` aller `INCOME`-Elemente |
|
||
| Lohnentwicklung | **Δ Prozentpunkte** | verschiebt `teuerungsausgleich` der `INCOME`-Elemente |
|
||
| Wertsteigerung der Immobilie | **Δ Prozentpunkte** | verschiebt `valueGrowth` |
|
||
|
||
Die Begründungen im Einzelnen:
|
||
- **Absolut** nur bei der Inflation – es gibt genau einen plan-weiten Wert.
|
||
- **Δ Prozentpunkte** bei den Renditen, weil die Elemente je eigene Sätze tragen. Ein absolutes
|
||
„3 % bis 7 %" würde die PK auf ETF-Rendite plätten.
|
||
- **Relativ %** bei Einkommen und Ausgaben, weil die Elemente je eigene Beträge tragen.
|
||
- Immobilien-Wertsteigerung ist ein **eigener** Treiber und nicht Teil von „Rendite", damit sie
|
||
nicht doppelt zählt.
|
||
|
||
Die Skalierung von Einkommen/Ausgaben greift nur dort, wo `amount` gesetzt ist. Das ist korrekt
|
||
und beabsichtigt: Ab Phase 2 ist der Wert in der Regel live vererbt ([4.6.1](#461-income--expense)),
|
||
und die Fortschreibung leitet ihn aus dem skalierten Basiswert ab – die Skalierung wirkt damit
|
||
automatisch über alle Folgephasen.
|
||
|
||
Ein Treiber erscheint nur, wenn der Plan passende Elemente enthält (`applies`).
|
||
|
||
### 4.13.3 Bandbreiten sind Pflicht – ohne Default
|
||
|
||
Je Treiber gibt der Benutzer eine tiefe und eine hohe Ausprägung an; **Vorgabewerte gibt es
|
||
bewusst nicht**. Grund: Die Balkenlänge hängt direkt von diesen Bandbreiten ab. Ein stiller
|
||
Default würde nicht hinterfragt, und das Ranking wäre dann eine Aussage über unsere Vorgabe
|
||
statt über den Plan – dieselbe Begründung wie bei den Monte-Carlo-Mittelwerten
|
||
([9.15](#915-monte-carlo-misst-risiko-um-die-annahmen-nicht-deren-richtigkeit)).
|
||
|
||
Die Hilfe-Bubble je Treiber nennt stattdessen plausible Grössenordnungen. Entscheidend ist, die
|
||
Bandbreiten **ähnlich plausibel** zu wählen, nicht ähnlich gross: „±10 % Inflation" (1.5 → 1.65 %)
|
||
und „±10 % Ausgaben" sind völlig ungleich wahrscheinlich.
|
||
|
||
Zwei weitere Regeln: mindestens **zwei** Treiber (ein Tornado ist eine Rangliste – ein einzelner
|
||
Balken ordnet nichts), und tiefer und hoher Wert dürfen nicht identisch sein (Spannweite 0).
|
||
|
||
### 4.13.4 Darstellung
|
||
|
||
Waagrechtes Balkendiagramm, je Balken die Spanne `min…max` der Zielgrösse, senkrechte
|
||
Referenzlinie beim Basisfall, sortiert nach Spannweite. Darunter eine Tabelle mit der
|
||
eingegebenen Bandbreite, den beiden Ergebniswerten und der Spannweite.
|
||
|
||
Die **Richtung kann sich umkehren** – tiefe Ausgaben ergeben ein hohes Endvermögen. Der Balken
|
||
spannt deshalb über `min…max`; welche Eingabe zu welchem Ende gehört, zeigt die Tabelle.
|
||
|
||
Referenz: `src/lib/sensitivity.ts`, `src/components/SensitivityDialog.tsx`.
|
||
|
||
### 4.13.5 Wirkungslose Treiber werden erklärt
|
||
|
||
Ein Balken mit Spannweite 0 ohne Erklärung ist die schlechteste Antwort – der Benutzer hält
|
||
ihn für einen Fehler. `computeTornado` hängt deshalb an jeden Nullbalken eine Begründung
|
||
(`ineffectiveReason`).
|
||
|
||
Der wichtigste Fall ist die **Immobilien-Wertsteigerung bei einem Verkauf**. Der Verkaufserlös
|
||
ist `Verkaufspreis − Hypothek − Grundstückgewinnsteuer` und hängt damit am erfassten
|
||
Verkaufspreis, **nicht** am modellierten Verkehrswert. Wird die Immobilie vor Planende
|
||
verkauft, wird die aufgelaufene Wertsteigerung an dieser Stelle verworfen – der Treiber kann
|
||
das Endvermögen dann rechnerisch nicht mehr beeinflussen.
|
||
|
||
Erkannt wird das daran, dass in der letzten Phase **alle** `REAL_ESTATE`-Elemente den Status
|
||
`SOLD` tragen. Andernfalls greift ein allgemeiner Hinweis. Durch Tests abgedeckt: gehalten →
|
||
Spannweite > 0 ohne Hinweis; verkauft → Spannweite 0 mit Begründung.
|
||
|
||
Verwandt: Der Verkaufspreis-Abgleich im Übergangs-Dialog ([3.5.8](#358-verkaufspreis-und-modellierter-verkehrswert))
|
||
setzt an derselben Stelle an, nur früher – er verhindert, dass die Annahmen überhaupt
|
||
auseinanderlaufen.
|
||
|
||
## 4.14 Verlaufswerte, Brücken und Rechenwege
|
||
|
||
Dieses Kapitel beschreibt, was `computePlan` über die reinen Ergebniswerte hinaus mitführt –
|
||
die Grundlage der Detailansichten (Roadmap Nr. 43) und der Transparenz-Offenlegung (Nr. 41).
|
||
|
||
### 4.14.1 Verlaufswerte je Element
|
||
|
||
`ElementPhaseComputed.yearly` hält einen Punkt **pro Phasenjahr** je Element (Jahr, Alter, Wert;
|
||
bei Immobilien zusätzlich Verkehrswert und Restschuld).
|
||
|
||
Der Grund ist derselbe wie beim Vermögensverlauf in Version 0.9: Nur Start- und Endwert zu kennen
|
||
reicht nicht. Eine Anlage mit 5 % über 20 Jahre wächst **konvex** – eine Gerade zwischen den
|
||
Phasengrenzen wäre sichtbar falsch und würde den Zinseszins optisch unterschlagen. Der Punkt wird
|
||
am **Jahresende** erfasst, nach Verzinsung und Tilgung, konsistent zum `YearPoint`.
|
||
|
||
### 4.14.2 Die beiden Wasserfälle
|
||
|
||
Ein Wasserfall zerlegt eine Veränderung in ihre Summanden. Der entscheidende Punkt ist, **welche
|
||
Posten dazugehören** – und hier liegt eine Falle:
|
||
|
||
> **Sparraten, Amortisationen und Zusatzinvestitionen sind Umbuchungen, keine Verluste.** Eine
|
||
> 3a-Einzahlung verlässt das Cash und erhöht im selben Zug das 3a-Guthaben; das Vermögen bleibt
|
||
> unverändert. Eine Amortisation senkt das Cash und die Hypothek – das Eigenkapital steigt sogar.
|
||
> Als Abgang im Vermögens-Wasserfall gezeichnet würden diese Posten eine Vermögensminderung
|
||
> vortäuschen, die es nicht gibt.
|
||
|
||
Deshalb gibt es **zwei** Zerlegungen mit unterschiedlichen Fragestellungen:
|
||
|
||
**`WealthBridge` – „Warum hat sich mein Vermögen so entwickelt?"**
|
||
|
||
```
|
||
Vermögen Ende Vorphase
|
||
+ einmaliger Zufluss − einmalige Kosten ⎫
|
||
− Steuern am Übergang (Kapitalbezug, Grundstückgewinn) ⎬ Vermögensänderungen
|
||
− verrentetes PK-Kapital (verlässt die Bilanz) ⎪ AN der Phasengrenze
|
||
± Verkaufspreis minus Verkehrswert ⎭
|
||
= Vermögen zu Phasenbeginn
|
||
+ Spar-/Verzehrquote (Summe über alle Phasenjahre) ⎫
|
||
+ Kapitalerträge (PK, 3a, Sonstiges Vermögen) ⎬ INNERHALB der Phase
|
||
+ Wertsteigerung der Liegenschaft ⎪
|
||
+ PK-Beiträge ⎭
|
||
= Vermögen am Phasenende
|
||
```
|
||
|
||
Zwei Posten verdienen eine Erläuterung:
|
||
- **PK-Beiträge sind ein echter Zugang.** Sie belasten das Cash nicht (im Nettolohn bereits
|
||
abgezogen), erhöhen aber das Vorsorgekapital – anders als 3a-Beiträge, die aus dem Cash fliessen
|
||
und deshalb reine Umbuchung sind.
|
||
- **Verrentetes PK-Kapital verlässt die Bilanz.** Bei der Verrentung wird Kapital in einen
|
||
Rentenstrom umgewandelt; der Saldo fällt auf 0. Ohne diesen Posten ginge die Brücke am
|
||
Pensions-Übergang nicht auf.
|
||
|
||
**`CashBridge` – „Wohin ist mein Cash geflossen?"**
|
||
|
||
```
|
||
Cash Ende Vorphase (Phase 1: Cash-Anfangswert)
|
||
+ Kapitalzufluss + einmaliger Zufluss
|
||
− Sofort-Tilgung/Sonderamortisation − einmalige Kosten
|
||
− Investitionen am Phasenanfang
|
||
= Cash zu Phasenbeginn
|
||
+ Spar-/Verzehrquote − Sparraten − Amortisationen/Tilgungen + Bezugsraten
|
||
= Cash am Phasenende
|
||
```
|
||
|
||
Beide Strukturen führen einen **Restposten** (`residual`) mit: die Differenz zwischen dem
|
||
gerechneten Endwert und der Summe der Summanden. Er ist die eingebaute Selbstkontrolle – ist die
|
||
Zerlegung vollständig und richtig, muss er **exakt 0** sein.
|
||
|
||
`src/lib/bridges.test.ts` nagelt das über **sieben Plankonstellationen** fest (Ansparen mit 3a
|
||
und Schuldentilgung, Pensionierung mit Verrentung und 3a-Bezug, PK-Kapitalbezug, Immobilie
|
||
gehalten, Immobilie verkauft, einmalige Sonderein-/ausgaben, Sofort-Tilgung mit
|
||
Sonderamortisation) – je Phase für beide Brücken, zusätzlich der Abgleich der Kontrollpunkte
|
||
gegen `startWealthNominal` / `endWealthNominal` / `cashStart` / `cashEnd`.
|
||
|
||
Im UI wird ein Restposten über 2 Franken als **Fehlermeldung** ausgewiesen, nicht als beiläufige
|
||
Rundungsnotiz: Eine Brücke, die nicht aufgeht, ist ein Rechenfehler und kein Darstellungsproblem.
|
||
|
||
### 4.14.2.1 Darstellung der Wasserfälle
|
||
|
||
Die Wasserfälle werden **nicht mit Recharts** gezeichnet. Ein Wasserfall lebt von drei Dingen,
|
||
die dort nicht ohne Weiteres zu bekommen sind:
|
||
|
||
- **Verbindungslinien** zwischen den Balken – ohne sie sieht man nicht, dass jeder Balken dort
|
||
ansetzt, wo der vorherige aufhört, und die Grafik zerfällt in unverbundene Rechtecke.
|
||
- **Wertbeschriftung** an jedem Schritt, statt Beträge aus der Achse zu schätzen.
|
||
- **Unterscheidung von Zwischenstand und Veränderung.** Ein Zwischenstand („Vermögen
|
||
Phasenbeginn") ist ein absoluter Wert ab Null, eine Veränderung („Kapitalerträge") setzt auf dem
|
||
laufenden Saldo auf. Sehen beide gleich aus, ist die Grafik nicht lesbar.
|
||
|
||
Die Darstellung ist deshalb eine eigene HTML/CSS-Konstruktion und **liegend** statt stehend – die
|
||
Beschriftungen sind lang und müssten stehend gedreht werden; liegend ist es ausserdem konsistent
|
||
zum Tornado. Abschnitts-Überschriften trennen „Am Übergang in diese Phase" von „Innerhalb der
|
||
Phase". Posten mit Wert 0 werden gar nicht erst gezeichnet.
|
||
|
||
Darunter steht aufklappbar eine **Tabelle mit laufendem Zwischenstand**. Bei sieben bis zwölf
|
||
Schritten mit stark unterschiedlichen Grössenordnungen ist sie der Grafik schlicht überlegen –
|
||
die Grafik zeigt das Verhältnis, die Tabelle die Zahl.
|
||
|
||
### 4.14.3 Rechenweg-Protokoll
|
||
|
||
`computePlan(plan, sample?, { explain: true })` legt zu jedem Ergebnis die Herleitung ab:
|
||
|
||
```ts
|
||
interface TraceStep { label; formula?; substituted?; result; unit?; note? }
|
||
interface Trace { title; specAnchor?; steps: TraceStep[] }
|
||
```
|
||
|
||
`formula` ist die abstrakte Regel, `substituted` dieselbe Regel **mit den eingesetzten Zahlen**,
|
||
`note` benennt eine an dieser Stelle geltende Vereinfachung.
|
||
|
||
**Der Architekturentscheid dahinter ist der wichtigste Teil dieses Kapitels.** Die naheliegende
|
||
Alternative wäre, die Formeln im UI nachzurechnen und anzuzeigen. Das ergäbe eine **zweite
|
||
Implementierung jeder Formel** – und damit die Möglichkeit, dass die angezeigte Herleitung still
|
||
von der tatsächlichen Rechnung abdriftet. Bei einem Tool, dessen Kernversprechen die rechnerische
|
||
Korrektheit ist, wäre das die gefährlichste Variante überhaupt: Ein Nutzer, der nachrechnet und
|
||
eine Abweichung findet, verliert mehr Vertrauen, als eine Black Box je gekostet hätte.
|
||
|
||
Deshalb entstehen die Schritte **innerhalb** von `computePlan`, als Nebenprodukt der Rechnung, die
|
||
ohnehin läuft. Die Erklärung *ist* die Rechnung.
|
||
|
||
Zwei Konsequenzen daraus:
|
||
- **Standardmässig aus.** Die Monte-Carlo-Simulation ruft `computePlan` zehntausendfach auf und
|
||
darf von der Protokollierung nichts merken. Ein Test prüft, dass `explain` die Ergebniswerte
|
||
nicht verschiebt.
|
||
- **Die Arithmetik wird nicht umgestellt.** Wo für das Protokoll eine Zwischengrösse gebraucht
|
||
wird (etwa der Renditeanteil eines Jahres), wird sie als **Differenz** abgeleitet statt die
|
||
Formel umzuformen – `a × (1 + r)` und `a + a × r` sind in Gleitkomma-Arithmetik nicht bitgleich.
|
||
Die 43 Golden Tests laufen unverändert.
|
||
|
||
**Abdeckung.** Vollständig über alle Ebenen: Element je Phase (Einkommen, Ausgaben, PK, 3a,
|
||
Immobilie, Sonstiges Vermögen, Schulden, AHV-/PK-Renten), Element je Übergang (Bezugsarten,
|
||
Verkauf inkl. Grundstückgewinnsteuer, Teilverkauf, Sonderamortisation, Sofort-Tilgung),
|
||
Phasen-Kennzahlen (Quote, Spar-/Verzehrrate, Cash-Fortschreibung, Vermögen, Phasentyp,
|
||
Cash-Übergang) und Plan-Ebene (Deflatoren, AHV-Karriere je Person, Ruinalter).
|
||
|
||
**Verweis in die Spezifikation.** Jeder Trace trägt optional einen `specAnchor` auf das zugehörige
|
||
Kapitel dieses Dokuments; die App springt von dort in die eingebaute SPEZIFIKATION-Ansicht (die
|
||
Anker erzeugt `rehype-slug`). Ein Test liest `SPEZIFIKATION.md` und prüft, dass **jeder** Verweis
|
||
eine existierende Überschrift trifft – sonst würden die Links bei einer Umbenennung still ins
|
||
Leere zeigen.
|
||
|
||
**Wo die Erklärung gerechnet wird.** Nicht auf dem Server: `computePlan` ist rein und läuft im
|
||
Browser (wie schon bei Monte Carlo und Sensitivitätsanalyse), und der Client hält den
|
||
`PlanInput` ohnehin. Die Detailansicht rechnet die erklärte Fassung beim Öffnen lokal – kein
|
||
API-Umbau, keine grössere Antwort, keine Serverlast, und per Konstruktion identisch zum
|
||
Serverergebnis.
|
||
|
||
Referenz: `src/lib/calculations.ts`, `src/components/DetailView.tsx`.
|
||
|
||
## 4.15 Live-Simulation (Was-wäre-wenn-Regler)
|
||
|
||
Roadmap Nr. 22. Beantwortet weder «welche Annahme entscheidet» (das ist der Tornado,
|
||
[4.13](#413-sensitivitätsanalyse-tornado)) noch «wie wahrscheinlich ist das» (das ist
|
||
Monte-Carlo, [4.12](#412-monte-carlo-simulation)), sondern schlicht: **«Wie sieht mein Plan aus,
|
||
wenn ich hier drehe?»** – sofort, und ohne für jede Variante eine Szenario-Kopie anzulegen.
|
||
|
||
Eigener Button **«Live-Simulation»** in der Szenario-Leiste, Dialog als Zweispalter: links die
|
||
Regler, rechts die Grafik, darüber eine Kennzahlenleiste.
|
||
|
||
### 4.15.1 Keine eigene Rechenlogik
|
||
|
||
Die Regler benutzen **dieselben Transformationen wie der Tornado** (`applyDriver`). Damit kann
|
||
die Live-Simulation gar nicht etwas anderes zeigen als die Einflussfaktoren-Analyse – beide
|
||
bewegen den Plan identisch. Es entsteht kein zweiter, potenziell abweichender Rechenweg.
|
||
|
||
Neu hinzu kommt nur `applyElementDriver(plan, elementId, deltaPp)`: dieselbe Verschiebung, aber
|
||
auf **ein einzelnes** Element statt auf eine ganze Kategorie (bei Immobilien auf `valueGrowth`
|
||
statt `expectedReturn`). Element-IDs sind innerhalb eines Szenarios eindeutig; die
|
||
Herkunfts-Verkettung `sourceElementId` aus der Monte-Carlo-Simulation braucht es hier **nicht**,
|
||
weil die Live-Simulation immer nur auf **einem** Szenario läuft.
|
||
|
||
### 4.15.2 Sammelregler und Aufschlüsselung
|
||
|
||
Standardmässig gibt es **einen** Rendite-Regler für alle Anlagen – das hält das Panel ruhig und
|
||
entspricht dem Tornado. Ein Klick auf **«Renditen einzeln aufschlüsseln»** ersetzt ihn durch je
|
||
einen Regler pro renditetragendem Element (PK, 3a, Sonstiges Vermögen, Immobilie). Erst dann
|
||
lässt sich die eigentliche Spielfrage stellen: *Was, wenn mein ETF schlechter läuft, die PK aber
|
||
wie geplant?*
|
||
|
||
Der Sammelregler wird beim Aufklappen **entfernt**, nicht bloss ergänzt – sonst würde eine
|
||
Bewegung doppelt zählen. Aus demselben Grund werden die Rendite-Regler beim Umschalten
|
||
zurückgesetzt. Ein Test sichert ab, dass beide Ansichten denselben Plan beschreiben: Alle
|
||
Elemente einzeln um +1 pp zu heben ergibt exakt dasselbe Endvermögen wie der Sammelregler auf
|
||
+1 pp.
|
||
|
||
**Bewusst nicht aufschlüsselbar sind Ausgaben und Einkommen.** «Alle Ausgaben ±20 %» ist die
|
||
Frage, die man tatsächlich stellt; Ausgaben-Elemente sind typischerweise viele kleine Posten,
|
||
deren Einzelregler das Panel fluten würden, ohne eine bessere Frage zu ermöglichen.
|
||
|
||
**Das Pensionsalter fehlt weiterhin** – aus demselben Grund wie beim Tornado
|
||
([9.18](#918-tornado-was-der-chart-nicht-leistet)): Es liesse sich nicht verschieben,
|
||
ohne die Phasengrenzen mitzuziehen. Ersatzweise gibt es **Lebensdauer** (letzte Phase
|
||
verlängern/verkürzen), was das Langlebigkeitsrisiko abdeckt, nicht aber die Frühpensionierung.
|
||
|
||
### 4.15.3 Referenz und Kennzahlen
|
||
|
||
Eine wandernde Linie ohne Anker ist wertlos – «ist 2.9 Mio jetzt viel oder wenig?». Deshalb:
|
||
|
||
- Der **unveränderte Plan** wird im Vermögensverlauf als blasse Referenzlinie mitgezeichnet.
|
||
- Darüber steht eine **Kennzahlenleiste** mit Endvermögen nominal und real, jeweils mit der
|
||
Differenz zum Plan (`3'660'683 → 2'880'100, −780'583`).
|
||
- Eine dritte Karte meldet, ob das **Kapital reicht** oder in welchem Alter es aufgebraucht ist.
|
||
Das ist die wichtigste Einzelinformation und einer Verlaufslinie nicht zuverlässig anzusehen:
|
||
Ein Plan kann optisch plausibel aussehen und trotzdem zwischendurch unter null fallen.
|
||
|
||
Die Grafik zeigt **wann** sich etwas ändert, die Leiste **wie viel**.
|
||
|
||
Rechts stehen drei Grafiken zur Wahl: **Vermögensverlauf** (mit Referenzlinie),
|
||
**Vermögensaufteilung** je Phase und **Einkommen vs. Ausgaben**. Die beiden letzteren zeigen nur
|
||
den simulierten Stand – ein zweiter gestapelter Balkensatz wäre nicht mehr lesbar; darauf weist
|
||
der Dialog hin.
|
||
|
||
### 4.15.4 Laufzeit: synchron, ohne Debounce
|
||
|
||
Gemessen an einem Plan über 60 Jahre mit 10 Elementen braucht `computePlan` rund **0.2 ms**.
|
||
Bei 60 fps stehen 16 ms je Bild zur Verfügung – die Rechnung kostet also etwa **1 %** des
|
||
Budgets. Deshalb wird bei **jeder** Reglerbewegung synchron neu gerechnet: kein Debounce, kein
|
||
Web Worker, keine Ladeanzeige. Der Engpass ist das Neuzeichnen der Grafik, nicht die Mathematik.
|
||
|
||
### 4.15.5 Nichts wird gespeichert
|
||
|
||
Die Live-Simulation **schreibt nicht** – keine API, keine Datenbank, kein Schreibpfad. Genau das
|
||
ist der Punkt der Roadmap-Anforderung («ohne für jede Variante eine Szenario-Kopie anzulegen»).
|
||
|
||
Ein **«Als neues Szenario speichern»** ist bewusst **noch nicht** umgesetzt: Reglerwerte in echte
|
||
Element- und Phasenwerte zurückzuschreiben hiesse viele einzelne Schreibvorgänge und einen neuen
|
||
Schreibpfad – eine eigene Ausbaustufe. Als Behelf zeigt der Dialog die **aktive Einstellung** als
|
||
lesbare Zeile («Rendite −1.5 pp · Ausgaben +10 % · Lebensdauer +5 J.»), die sich von Hand in ein
|
||
echtes Szenario übertragen lässt.
|
||
|
||
Referenz: `src/lib/livesim.ts`, `src/lib/sensitivity.ts` (`applyElementDriver`,
|
||
`tunableElements`), `src/components/LiveSimDialog.tsx`, `src/components/AllocationChart.tsx`.
|
||
|
||
---
|
||
|
||
# 5. Technische Spezifikation
|
||
|
||
## 5.1 Technologie-Stack
|
||
|
||
| Bereich | Technologie | Version |
|
||
|---|---|---|
|
||
| Framework | Next.js (App Router) | 16.2.10 |
|
||
| UI | React | 19.2.4 |
|
||
| Sprache | TypeScript | ^5 |
|
||
| Styling | Tailwind CSS | ^4 (via `@tailwindcss/postcss`) |
|
||
| Icons | lucide-react | ^1.24.0 |
|
||
| Charts | recharts | ^3.9.2 |
|
||
| ORM | Prisma | ^7.8.0 (Client-Output nach `src/generated/prisma`) |
|
||
| Datenbank | PostgreSQL | 16-alpine |
|
||
| DB-Treiber | `pg` + `@prisma/adapter-pg` | ^8.22.0 / ^7.8.0 |
|
||
| Validierung | Zod | ^4.4.3 |
|
||
| Auth | jose (JWT) + bcryptjs | ^6.2.3 / ^3.0.3 |
|
||
| Tests | Vitest | ^4.1.10 |
|
||
| Build | Docker (multi-stage), `output: "standalone"` | |
|
||
|
||
> **Hinweis für Entwickler:** Gemäss `AGENTS.md` weicht diese Next.js-Version von verbreiteten
|
||
> Konventionen ab. Vor Änderungen ist der relevante Guide unter `node_modules/next/dist/docs/`
|
||
> zu konsultieren.
|
||
|
||
## 5.2 Verzeichnisstruktur
|
||
|
||
```
|
||
FPT/
|
||
├── prisma/
|
||
│ ├── schema.prisma Datenmodell
|
||
│ └── migrations/ 15 Migrationen (chronologisch, siehe 5.4.6)
|
||
├── src/
|
||
│ ├── app/
|
||
│ │ ├── api/ Route Handlers (siehe Kapitel 6)
|
||
│ │ ├── login/page.tsx Login-/Registrierseite
|
||
│ │ ├── page.tsx Einstiegsseite (lädt /api/auth/me → AppShell)
|
||
│ │ ├── layout.tsx Root-Layout, Theme-Init-Script, Metadata
|
||
│ │ └── globals.css Tailwind + semantische Farb-Tokens (3 Themes)
|
||
│ ├── components/ 23 React-Komponenten (alle "use client")
|
||
│ ├── generated/prisma/ Generierter Prisma-Client (nicht editieren)
|
||
│ ├── lib/ Domänenlogik (siehe 5.3)
|
||
│ └── middleware.ts Zugriffsschutz (Edge-Runtime)
|
||
├── Info Dateien/ Fachdokumente, Roadmap (historisch)
|
||
├── docker-compose.yml, Dockerfile, docker-entrypoint.sh
|
||
└── .gitea/workflows/deploy.yaml CI/CD
|
||
```
|
||
|
||
## 5.3 Schichtenmodell und `src/lib`
|
||
|
||
Ein bewusster Entkopplungs-Entscheid (`src/lib/types.ts` Zeilen 1–4): Die Berechnungslogik
|
||
arbeitet auf **eigenen Domänentypen**, nicht auf den generierten Prisma-Typen. Dadurch ist
|
||
`computePlan` ohne Datenbank testbar.
|
||
|
||
```
|
||
Prisma-Modelle (DB)
|
||
│ toPlanInput() ← queries.ts: parst + validiert die JSON-Felder
|
||
▼
|
||
PlanInput (types.ts) ← reine Domänentypen
|
||
│ computePlan() ← calculations.ts: pure function
|
||
▼
|
||
PlanComputed ← an den Client geliefert
|
||
```
|
||
|
||
| Datei | Verantwortung |
|
||
|---|---|
|
||
| `calculations.ts` | Berechnungskern + CSV-Export. Keine I/O. |
|
||
| `elements.ts` | Kategorien, Labels, Reihenfolge, JSON-Payload-Typen, Zod-Schemas, `num()` |
|
||
| `types.ts` | Domänentypen für API und Berechnung |
|
||
| `constants.ts` | Schweizer Systemparameter |
|
||
| `montecarlo.ts` | Monte-Carlo-Simulation (Sampler + Treiber), Szenario-Vergleich und Element-Gruppierung über die Herkunfts-Kette. Keine I/O, läuft im Browser. |
|
||
| `sensitivity.ts` | Sensitivitätsanalyse / Tornado: Treiber-Katalog, Parameter-Transformationen, `computeTornado`; zusätzlich `applyElementDriver` / `tunableElements` für die einzeln regelbaren Element-Renditen. Rein, läuft im Browser. |
|
||
| `actuals.ts` | Effektive Werte: Zuordnung auf die Szenario-Elemente über die Herkunfts-Kette, Einspielen in den Rechenkern, Bestand/Fluss. Rein. |
|
||
| `dataview.ts` | Bündelt Plan-Sicht und Ist-Sicht für Matrix, Grafiken und Analysewerkzeuge. Rein. |
|
||
| `ratefields.ts` | Ratenfelder je Kategorie, Reichweite der Übernahme über Lebensphasen, Aufbau der Schreibvorgänge (Kap. 3.6.11). Rein. |
|
||
| `versioning.ts` | Versionierung: Nummerierung A.B, Zusammenfassung je Sitzung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien. Rein, ohne I/O. |
|
||
| `versioning-db.ts` | Datenbank-Anbindung der Versionierung: Snapshot festhalten, Hauptversion, Wiederherstellen. Führt den Bauplan aus `versioning.ts` nur aus. |
|
||
| `livesim.ts` | Live-Simulation: Reglerkatalog mit Standardbereichen, Anwenden mehrerer Regler, Kennzahlen (Kap. 4.15). Rein, läuft im Browser. |
|
||
| `distribution.ts` | Kapitaltopf und Quoten-Zerlegung, Anwenden von Entwurfswerten für die Verteil-Werkzeuge (Kap. 3.6.9/3.6.10). Rein. |
|
||
| `phaseplan.ts` | Ableitung der Lebensabschnitte (Erwerb/Misch/Pension) aus den fixen Pensionierungszeitpunkten für den Assistenten (Kap. 3.2.8). Rein. |
|
||
| `constants.ts` (erweitert) | zusätzlich `SYSTEM_PARAMETERS`: dieselben Werte maschinenlesbar mit Bedeutung, Herleitung, Quelle und Stand – Grundlage der Systemparameter-Ansicht |
|
||
| `diff.ts` | Abweichungs-Erkennung eines Szenarios gegen sein Eltern-Szenario (Kap. 3.2.6) |
|
||
| `queries.ts` | Prisma-Includes, `toPlanInput()`, Ownership-Abfragen |
|
||
| `db.ts` | Prisma-Singleton (Global-Cache im Dev, verhindert Connection-Leak bei HMR) |
|
||
| `auth.ts` | JWT erzeugen/prüfen (Edge-kompatibel via `jose`) |
|
||
| `session.ts` | `getCurrentUserId()` aus dem Cookie (Node-Runtime) |
|
||
| `users.ts` | Registrierung, Credential-Prüfung, Passwortwechsel (bcrypt). **Nie aus der Middleware importieren** – Edge-Runtime hat keinen DB-Zugriff. |
|
||
| `format.ts` | `formatChf` / `parseChfInput` |
|
||
| `theme.ts` | Theme-Verwaltung (localStorage + `data-theme`) |
|
||
| `api-client.ts` | Typisierter `fetch`-Wrapper mit einheitlicher Fehlerextraktion |
|
||
|
||
## 5.4 Datenmodell
|
||
|
||
### 5.4.1 Tabellen
|
||
|
||
**User**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `username` | String | **unique** |
|
||
| `passwordHash` | String | bcrypt, Cost 12 |
|
||
| `createdAt` | DateTime | `now()` |
|
||
|
||
**Person**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||
| `role` | `PersonRole` | `PERSON_A` \| `PERSON_B` |
|
||
| `name` | String? | optional |
|
||
| `age` | Int | aktuelles Alter |
|
||
| `retirementAge` | Int | **szenario-eigenes** Pensionsalter |
|
||
| | | `@@unique([scenarioId, role])` |
|
||
|
||
**Plan** (Behälter – trägt keine Finanzdaten)
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `userId` | String | FK → User, **Cascade** |
|
||
| `name` | String | |
|
||
| `createdAt` / `updatedAt` | DateTime | |
|
||
|
||
**Scenario** (die berechenbare Einheit)
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `planId` | String | FK → Plan, **Cascade** |
|
||
| `name` | String | |
|
||
| `isBase` | Boolean | Default false; genau eines je Plan ist `true` |
|
||
| `parentScenarioId` | String? | FK → Scenario (Self-Relation „ScenarioTree"), **SetNull** – Baum **und** Vergleichsbasis |
|
||
| `householdType` | `HouseholdType` | `SINGLE` \| `COUPLE` |
|
||
| `inflationRateDefault` | Float | szenario-weite Inflation in % |
|
||
| `initialCash` | Float | Default 0 |
|
||
| `startYear` | Int? | Kalenderjahr des Planbeginns – **nur Darstellung** (siehe 3.2.7) |
|
||
| `createdAt` / `updatedAt` | DateTime | |
|
||
|
||
**Phase**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||
| `sequenceNumber` | Int | 1-basiert, lückenlos |
|
||
| `name` | String | |
|
||
| `durationYears` | Int | 1–80 |
|
||
| `cashTransition` | Json? | Cash-Entscheid beim Übergang **nach** dieser Phase (siehe 5.4.5) |
|
||
| `sourcePhaseId` | String? | Gegenstück in der Vorlage (**lose** Referenz, kein FK) – Diff-Grundlage |
|
||
| `createdAt` / `updatedAt` | DateTime | |
|
||
| | | `@@unique([scenarioId, sequenceNumber])` |
|
||
|
||
`cashTransition` liegt an der Phase und nicht in `ElementTransitionValue`, weil Cash kein
|
||
`FinancialElement` ist und damit keine `elementId` besitzt. Die Verschlüsselung folgt derselben
|
||
Logik wie dort: Der Übergang gehört der **Von**-Phase.
|
||
|
||
**FinancialElement**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||
| `category` | `ElementCategory` | 8 Werte |
|
||
| `name` | String | |
|
||
| `ownerRole` | `OwnerRole?` | `PERSON_A` \| `PERSON_B` \| `HOUSEHOLD` |
|
||
| `orderIndex` | Int | Default 0 |
|
||
| `sourceElementId` | String? | Gegenstück in der Vorlage (**lose** Referenz, kein FK) – Diff-Grundlage |
|
||
| `createdAt` | DateTime | |
|
||
|
||
Die Herkunfts-Verweise sind bewusst **lose** (kein Fremdschlüssel): Wird das Gegenstück in der
|
||
Vorlage gelöscht, soll die Kopie bestehen bleiben und im Diff einfach als „neu" gelten – ein
|
||
Cascade wäre hier falsch.
|
||
|
||
**ElementPhaseValue**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `elementId` | String | FK → FinancialElement, **Cascade** |
|
||
| `phaseId` | String | FK → Phase, **Cascade** |
|
||
| `data` | Json | Payload gemäss `PhaseData` |
|
||
| | | `@@unique([elementId, phaseId])` |
|
||
|
||
**ElementTransitionValue**
|
||
|
||
| Feld | Typ | Constraints |
|
||
|---|---|---|
|
||
| `id` | String | PK, `cuid()` |
|
||
| `elementId` | String | FK → FinancialElement, **Cascade** |
|
||
| `fromPhaseId` | String | FK → Phase, **Cascade** |
|
||
| `data` | Json | Payload gemäss `TransitionData` |
|
||
| | | `@@unique([elementId, fromPhaseId])` |
|
||
|
||
### 5.4.2 Warum JSON?
|
||
|
||
Die kategoriespezifischen Felder liegen als JSON, weil sich sonst pro Kategorie eine eigene
|
||
Tabelle (oder eine breite Sparse-Tabelle) ergäbe. Die Typisierung und Validierung findet in der
|
||
**Applikationsschicht** statt (`src/lib/elements.ts`): `PhaseData` / `TransitionData` als
|
||
TypeScript-Interfaces, `phaseDataSchema` / `transitionDataSchema` als Zod-Schemas an der
|
||
API-Grenze.
|
||
|
||
Die Interfaces sind bewusst **tolerant** getippt (alle Felder optional): die Berechnung liest
|
||
defensiv über `num(value, fallback)`, das UI zeigt kontextabhängig nur die relevanten Felder.
|
||
|
||
Referenz: `prisma/schema.prisma` Zeilen 4–6, `src/lib/elements.ts` Zeilen 48–51.
|
||
|
||
### 5.4.3 JSON-Payload `PhaseData`
|
||
|
||
| Feld | Kategorien | Zod-Regel |
|
||
|---|---|---|
|
||
| `amount` | INCOME, EXPENSE | ≥ 0 |
|
||
| `teuerungsausgleich` | INCOME, EXPENSE | −20 bis 50 |
|
||
| `gapYears` | AHV | Integer ≥ 0 |
|
||
| `avgIncomeBefore` | AHV – nur wenn bei Planbeginn **bereits pensioniert** | ≥ 0, **real** |
|
||
| `gapYearsBefore` | AHV – dito | Integer 0–50 |
|
||
| `currentValue` | PENSION_FUND, PILLAR_3A | ≥ 0 |
|
||
| `startValue` | OTHER_ASSET, OTHER_DEBT | ≥ 0 |
|
||
| `expectedReturn` | PK, 3a, OTHER_ASSET | −50 bis 100 |
|
||
| `annualContribution` | PK, 3a, OTHER_ASSET | ≥ 0 |
|
||
| `annualWithdrawal` | OTHER_ASSET | ≥ 0 |
|
||
| `additionalInvestment` | PK, 3a, OTHER_ASSET (ab Phase 2) | ≥ 0 |
|
||
| `purchasePrice` | REAL_ESTATE | ≥ 0 |
|
||
| `mortgage` | REAL_ESTATE | ≥ 0 |
|
||
| `amortization` | REAL_ESTATE | ≥ 0 |
|
||
| `interestRate` | REAL_ESTATE – Hypothekarzins %/Jahr | 0–20 |
|
||
| `interestHandling` | REAL_ESTATE – Doppelzählungs-Schalter | `INCLUDED` (Default) \| `ADD` |
|
||
| `valueGrowth` | REAL_ESTATE – Wertsteigerung %/Jahr auf die Liegenschaft | −20 bis 20 |
|
||
| `annualRepayment` | OTHER_DEBT | ≥ 0 |
|
||
|
||
### 5.4.4 JSON-Payload `TransitionData`
|
||
|
||
| Feld | Kategorien | Zod-Regel |
|
||
|---|---|---|
|
||
| `reviewed` | AHV (Pensions-Übergang) – Beitragskarriere geprüft | Boolean |
|
||
| `avgIncomeBefore` | AHV (Pensions-Übergang) | ≥ 0, **real** |
|
||
| `gapYearsBefore` | AHV (Pensions-Übergang) | Integer 0–50 |
|
||
| `withdrawalMode` | PK, 3a (normal) | `NONE` \| `AMOUNT` |
|
||
| `withdrawal` | PK, 3a (normal) | ≥ 0, **brutto** |
|
||
| `payoutMode` | PK (Pensionierung) | `CAPITAL` \| `PENSION` \| `COMBI` |
|
||
| `capitalAmount` | PK (COMBI) | ≥ 0 |
|
||
| `conversionRate` | PK | 0–20 |
|
||
| `capitalTaxRate` | PK + 3a, **sowohl Vorbezug als auch Pensionierung** | 0–100 |
|
||
| `decision` | REAL_ESTATE, OTHER_ASSET | `HOLD` \| `SELL` \| `PARTIAL` |
|
||
| `salePrice` | REAL_ESTATE | ≥ 0 |
|
||
| `partialSaleAmount` | OTHER_ASSET (Teilverkauf) | ≥ 0 |
|
||
| `extraAmortization` | REAL_ESTATE (Sonderamortisation) | ≥ 0 |
|
||
| `saleTaxRate` | REAL_ESTATE | 0–100 |
|
||
| `immediateRepayment` | OTHER_DEBT | ≥ 0 |
|
||
|
||
### 5.4.5 JSON-Payload `CashTransitionData`
|
||
|
||
Liegt in `Phase.cashTransition`. Validierung über `cashTransitionSchema`.
|
||
|
||
| Feld | Bedeutung | Zod-Regel |
|
||
|---|---|---|
|
||
| `mode` | Entscheid | `NONE` \| `INFLOW` \| `OUTFLOW` \| `BOTH` |
|
||
| `inflowLabel` | Bezeichnung des Zuflusses (z. B. „Erbschaft") | ≤ 120 Zeichen |
|
||
| `inflowAmount` | Betrag **nominal** | ≥ 0 |
|
||
| `inflowTaxRate` | Steuer auf den Zufluss, Default 0 % | 0–100 |
|
||
| `outflowLabel` | Bezeichnung der Kosten (z. B. „Poolbau") | ≤ 120 Zeichen |
|
||
| `outflowAmount` | Betrag **real** (heutige Kaufkraft) | ≥ 0 |
|
||
|
||
Pro Übergang ist **genau ein** Zufluss und **eine** Kostenposition möglich – siehe
|
||
[9.7](#97-nur-ein-zufluss-und-eine-kostenposition-pro-übergang).
|
||
|
||
Alle Schemas verwenden `.strip()` – **unbekannte Felder werden verworfen**, nicht abgelehnt.
|
||
Beim Lesen aus der DB gilt zusätzlich: schlägt `safeParse` fehl, wird `{}` zurückgegeben
|
||
(`parsePhaseData` / `parseTransitionData`) – korrupte Daten führen also nie zu einem Absturz,
|
||
sondern zu leeren Werten.
|
||
|
||
### 5.4.6 Migrationshistorie
|
||
|
||
| Migration | Inhalt |
|
||
|---|---|
|
||
| `20260708171600_init` | Initiales Schema |
|
||
| `20260709010000_add_transition_automation` | Übergangs-Automatik |
|
||
| `20260709120000_rework_realestate` | Immobilien-Überarbeitung |
|
||
| `20260711090000_multi_user` | User-Entität, Ownership |
|
||
| `20260713100000_element_model_rework` | Elemente auf Plan-Ebene, Phase-/Transition-Werte als JSON |
|
||
| `20260713150000_profile_to_plan_v3` | Grundprofil von Household auf Plan verschoben |
|
||
| `20260714120000_person_name` | `Person.name` |
|
||
| `20260715120000_plan_initial_cash` | `Plan.initialCash` |
|
||
| `20260716210000_drop_phase_inflation_rate` | `Phase.inflationRate` entfernt (Inflation ist plan-weit) |
|
||
| `20260716230000_phase_cash_transition` | `Phase.cashTransition` (JSONB) für einmalige Sonderein-/ausgaben |
|
||
| `20260718090000_plan_scenario_hierarchy` | **V6**: `Plan` → `Scenario` (IDs erhalten), neuer Behälter `Plan`, `planId` → `scenarioId`, Herkunfts-Verweise |
|
||
| `20260718140000_scenario_start_year` | `Scenario.startYear` (Kalenderjahr des Planbeginns), bestehende auf das laufende Jahr gesetzt |
|
||
| `20260719210000_scenario_versioning` | Tabelle `ScenarioVersion` (Snapshot als JSONB, A.B eindeutig je Szenario) und `Scenario.currentMajor` |
|
||
| `20260720090000_actuals` | Tabelle `ActualsSet` (effektive Werte je Plan, Werte als JSONB, Cash separat) |
|
||
| `20260720140000_plan_level_profile` | **V7**: Haushaltsform, Personen (Name/Alter) und Startjahr vom Szenario auf den Plan; neue Tabelle `PlanPerson`; `Person` behaelt nur das Pensionsalter. Datenübernahme aus dem Basisszenario. |
|
||
|
||
**Zur V6-Migration:** Sie benennt die bisherige `Plan`-Tabelle in `Scenario` um – dadurch
|
||
bleiben alle IDs und damit sämtliche Kind-Fremdschlüssel gültig. Für jedes bisherige
|
||
Wurzel-Element entsteht ein neuer Behälter (`'plan_' || id`, deterministisch ableitbar, daher
|
||
ohne Hilfstabelle); der bisherige Plan-Name wandert dorthin, das Szenario heisst „Basisszenario".
|
||
Bestehende Szenarien werden per rekursivem CTE demselben Behälter zugeordnet – auch mehrfach
|
||
verschachtelte. Ein Sicherheitsnetz fängt verwaiste Szenarien ab und macht sie eigenständig.
|
||
|
||
Die Migration wurde **vor dem Deploy gegen echtes PostgreSQL verifiziert** (PGlite, in-process):
|
||
alle Vorgänger-Migrationen einspielen, realistische Daten inkl. verschachtelter Szenarien
|
||
anlegen, migrieren, Ergebnis und Cascade prüfen.
|
||
|
||
## 5.5 Frontend-Architektur
|
||
|
||
### 5.5.1 Datenfluss
|
||
|
||
```
|
||
page.tsx (Client)
|
||
└─ GET /api/auth/me → username
|
||
└─ AppShell
|
||
├─ GET /api/plans → Planliste (Sidebar, Kacheln)
|
||
├─ GET /api/plans/<id> → { plan: PlanInput, computed: PlanComputed }
|
||
│ └─ PlanView (Matrix, Dialoge) → onChanged() → silent reload
|
||
│ └─ Dashboard (Kennzahlen, 3 Grafiken, Vergleich, Export)
|
||
└─ Dialoge: PlanDialog, ScenarioDialog
|
||
```
|
||
|
||
Es gibt **keinen State-Management-Layer** (kein Redux/Zustand/React Query). Der Zustand lebt in
|
||
`AppShell` (Planliste, ausgewählter Plan, Detail) und lokal in den Dialogen. Nach jeder Mutation
|
||
wird der Plan neu geladen; die Berechnung kommt immer vom Server.
|
||
|
||
### 5.5.2 Komponenten
|
||
|
||
| Komponente | Zeilen | Rolle |
|
||
|---|---|---|
|
||
| `AppShell` | 433 | Layout, Sidebar, Planliste, Laden, Plan-/Szenario-Dialoge |
|
||
| `PlanView` | 1304 | Matrix, Spaltenaufbau, Kontextbildung, alle Zell-Dialoge |
|
||
| `ElementDetail` | 548 | Feldgruppen je Kategorie/Kontext, Speicherlogik, Übergangs-Defaults |
|
||
| `Dashboard` | 187 | Kennzahlkarten, 3 Grafiken, Planvergleich, CSV-Link |
|
||
| `FormField` | 275 | `NumberField`, `MoneyInput`/`MoneyField`, `TextField`, `SelectField` |
|
||
| `ProfileMenu` | 166 | Benutzer, Theme-Wahl, Passwortwechsel, Logout |
|
||
| `Timeline` | 108 | Zeitachse mit Pensions- und Ruin-Markern |
|
||
| `WealthChart` | 106 | Vermögensverlauf, mehrere Serien |
|
||
| `PlanProfileFields` | 101 | Wiederverwendete Grundprofil-Felder |
|
||
| `PhaseDetail` | 95 | Phase bearbeiten/löschen |
|
||
| `SparquoteChart` | 64 | Einkommen/Ausgaben pro Jahr mit Spar-/Verzehrbändern |
|
||
| `MonteCarloDialog` | ~600 | Monte-Carlo-Dialog: Erklärung, Szenario-Auswahl, Eingaben, Lauf, Vergleichstabelle + Fächer |
|
||
| `VersionHistoryDialog` | ~290 | Änderungshistorie: Liste, Hauptversion festlegen, Anzeigen, Wiederherstellen mit Warnung ([3.8](#38-versionierung-und-änderungshistorie)) |
|
||
| `VersionMatrix` | ~110 | Nur-Lese-Matrix eines festgehaltenen Standes |
|
||
| `VersionPicker` | ~140 | Wahl der Berechnungsgrundlage in den vier Analysewerkzeugen |
|
||
| `LiveSimDialog` | ~330 | Live-Simulation: Regler links, Grafikwahl rechts, Kennzahlenleiste mit Differenz zum Plan ([4.15](#415-live-simulation-was-wäre-wenn-regler)) |
|
||
| `AllocationChart` | ~80 | Gestapelte Vermögensaufteilung je Phase; aus dem Dashboard herausgelöst, damit die Live-Simulation sie mitbenutzen kann |
|
||
| `SensitivityDialog` | ~330 | Einflussfaktoren: Erklärung, Treiber-Auswahl mit Bandbreiten, Tornado + Tabelle |
|
||
| `DetailView` | ~470 | Detailansichten Element/Phase, Wasserfall-Darstellung, Rechenweg-Blöcke, plan-weite Rechenwege |
|
||
| `SystemParametersView` | ~90 | Systemparameter mit Wert, Bedeutung, Herleitung, Quelle und Stand |
|
||
| `SpecView` | 65 | Rendert `SPEZIFIKATION.md` (via `/api/spec`) als lesbares Dokument, inkl. Sprungmarken aus den Rechenwegen |
|
||
| `InfoBubble` | 28 | Hilfe-Tooltip |
|
||
| `ui` | ~370 | UI-Primitiven: Button, Modal, InspectorShell, Confirm, Toast, Skeleton, EmptyState ([3.7.6](#376-sprache-und-ui-primitiven)/[3.7.7](#377-inspector-panel-statt-modals)) |
|
||
| `PlanWizard` | ~560 | Geführter Plan-Assistent in fünf Schritten, abschnittsbasierte Phasenplanung ([3.2.8](#328-geführter-assistent-und-beispielplan)) |
|
||
| `Tour` | ~140 | Interaktive Kurz-Tour über die Planansicht ([3.7.8](#378-tour-und-nächste-schritte)) |
|
||
| `CommandPalette` | ~130 | Befehls-Palette Ctrl/Cmd+K ([3.7.9](#379-befehls-palette-und-sparklines)) |
|
||
| `DistributionDialogs` | ~460 | Verteil-Werkzeuge für Kapital und Spar-/Verzehrquote ([3.6.10](#3610-verteil-werkzeuge)) |
|
||
| `Sparkline` | ~45 | Mini-Verlaufskurve je Element-Zeile ([3.7.9](#379-befehls-palette-und-sparklines)) |
|
||
|
||
### 5.5.3 Wiederverwendungsmuster
|
||
|
||
`ElementPhaseFields` und `ElementTransitionFields` sind aus `ElementDetail` **exportiert** und
|
||
werden an drei Stellen wiederverwendet: im Zell-Dialog, im Erstell-Dialog („Werte (erste
|
||
Lebensphase)") und im geführten Übergangs-Review. Dadurch gibt es genau eine Definition der
|
||
Felder je Kategorie.
|
||
|
||
Der `CellContext` ist die einheitliche Kontext-Schnittstelle dieser Feldgruppen; er wird in
|
||
`PlanView` durch `buildPhaseContext` / `buildTransitionContext` aus der Berechnung befüllt.
|
||
|
||
### 5.5.4 React-Detail: der `key` auf Dialogen
|
||
|
||
Zell-Dialoge tragen einen `key` aus `elementId` + `phaseId`. Grund (Kommentar Zeilen 479–480):
|
||
Beim Wechsel von Zelle zu Zelle wird ein Neuaufbau erzwungen, damit der lokale Formularzustand
|
||
nicht vom vorher geöffneten Element übrig bleibt.
|
||
|
||
## 5.6 Sicherheit
|
||
|
||
| Aspekt | Umsetzung |
|
||
|---|---|
|
||
| Passwortspeicherung | bcrypt, Cost 12 |
|
||
| Session | JWT HS256, 30 Tage, HttpOnly-Cookie, `sameSite=lax`, `secure` in Produktion |
|
||
| Secret | `SESSION_SECRET` aus der Umgebung; Fehlen wirft beim ersten Zugriff |
|
||
| Autorisierung | Middleware (grob) + Ownership-Query je Route (fein) |
|
||
| Information Disclosure | Fremde/nicht existierende Ressourcen → einheitlich 404; Login-Fehler unspezifisch |
|
||
| Eingabevalidierung | Zod an jeder API-Grenze; `.strip()` gegen Mass-Assignment |
|
||
| SQL-Injection | Prisma (parametrisiert) |
|
||
| XSS | React-Escaping; einziges `dangerouslySetInnerHTML` ist das statische Theme-Init-Script |
|
||
| CSRF | Kein Token. Schutz beruht allein auf `sameSite=lax` – siehe 9.4 |
|
||
|
||
## 5.7 Konfiguration
|
||
|
||
| Variable | Zweck |
|
||
|---|---|
|
||
| `DATABASE_URL` | Postgres-Connection-String |
|
||
| `SESSION_SECRET` | JWT-Signaturschlüssel (z. B. `openssl rand -hex 32`) |
|
||
| `POSTGRES_PASSWORD` | Nur für docker-compose: Passwort des `db`-Containers |
|
||
| `NODE_ENV` | Steuert u. a. das `secure`-Flag des Cookies und den Prisma-Global-Cache |
|
||
|
||
Es gibt **kein** konfiguriertes Login-Passwort: Konten werden über die Registrierung angelegt.
|
||
|
||
---
|
||
|
||
# 6. API-Referenz
|
||
|
||
Alle Routen liefern JSON. Fehlerformat einheitlich: `{ "error": "<Meldung>" }` (bei
|
||
Zod-Fehlern in `POST /api/plans`: `{ "error": <flatten()-Objekt> }`).
|
||
Alle Routen ausser `login`/`register` erfordern ein gültiges Session-Cookie.
|
||
|
||
## 6.1 Authentifizierung
|
||
|
||
| Methode | Pfad | Body | Antwort |
|
||
|---|---|---|---|
|
||
| POST | `/api/auth/register` | `{ username, password }` | 201 `{ ok, username }` + Cookie · 400 Validierung · 409 Name vergeben |
|
||
| POST | `/api/auth/login` | `{ username, password }` | 200 `{ ok, username }` + Cookie · 400 · 401 |
|
||
| POST | `/api/auth/logout` | – | 200 `{ ok }`, Cookie gelöscht |
|
||
| GET | `/api/auth/me` | – | 200 `{ user: { id, username, createdAt } }` · 401 · 404 |
|
||
| POST | `/api/auth/change-password` | `{ currentPassword, newPassword }` | 200 `{ ok }` · 400 · 401 |
|
||
|
||
## 6.2 Pläne
|
||
|
||
### `GET /api/plans`
|
||
Liste der eigenen Pläne, sortiert nach `createdAt` aufsteigend.
|
||
```json
|
||
{ "plans": [ { "id", "name", "parentPlanId", "branchFromPhaseId", "createdAt",
|
||
"phases": [ { "id", "name", "sequenceNumber" } ] } ] }
|
||
```
|
||
|
||
### `POST /api/plans`
|
||
```json
|
||
{ "name": "Basisplan", "householdType": "COUPLE", "inflationRateDefault": 1.5,
|
||
"persons": [ { "role": "PERSON_A", "name": "Anna", "age": 40, "retirementAge": 65 },
|
||
{ "role": "PERSON_B", "name": null, "age": 38, "retirementAge": 64 } ] }
|
||
```
|
||
Validierung: `name` 1–120; `inflationRateDefault` −20…50; `persons` 1–2 Einträge;
|
||
`age` 0–120; `retirementAge` 30–100; `name` je Person ≤ 60. Zusätzlich Konsistenzregel
|
||
SINGLE=1 / COUPLE=2 Personen. Legt Plan **und Basisszenario** an.
|
||
→ 201 `{ plan: { id }, scenario: { id } }`
|
||
|
||
`GET /api/plans` liefert die Pläne inkl. Szenario-Kopfdaten:
|
||
```json
|
||
{ "plans": [ { "id", "name", "createdAt",
|
||
"scenarios": [ { "id", "planId", "name", "isBase", "parentScenarioId" } ] } ] }
|
||
```
|
||
|
||
### `PATCH /api/plans/<planId>`
|
||
`{ name }` – der Plan trägt nur noch den Namen. → 200 `{ plan: { id, name } }`
|
||
|
||
### `DELETE /api/plans/<planId>`
|
||
→ 200 `{ ok: true }`, Cascade über alle Szenarien (inkl. Versionen und Ist-Sätzen).
|
||
|
||
### `GET /api/plans/<planId>/actuals`
|
||
Alle erfassten Ist-Sätze, neueste zuerst ([3.9](#39-effektive-werte-plan-ist-vergleich)):
|
||
```json
|
||
{ "sets": [ { "id", "recordedOn": "2026-08-18", "year": 2026, "comment",
|
||
"cash", "values": { "<rootElementId>": { "value", "mortgage" } },
|
||
"author", "createdAt" } ] }
|
||
```
|
||
|
||
### `POST /api/plans/<planId>/actuals`
|
||
`{ recordedOn: "JJJJ-MM-TT", comment?, cash?, values }` – `year` wird aus dem Datum abgeleitet.
|
||
→ 201 `{ set: { id, year } }`
|
||
|
||
### `DELETE /api/plans/<planId>/actuals/<setId>`
|
||
→ 200 `{ ok: true }`. Ein Ist-Satz ist eine Beobachtung – es gibt weder Versionierung noch
|
||
Wiederherstellung.
|
||
|
||
## 6.3 Szenarien
|
||
|
||
### `GET /api/scenarios/<scenarioId>`
|
||
Liefert Eingabe, Berechnung **und die Vergleichsbasis** in einem Zug:
|
||
```json
|
||
{ "plan": <PlanInput>, "computed": <PlanComputed>,
|
||
"base": <PlanInput|null>,
|
||
"meta": { "id", "planId", "planName", "name", "isBase", "parentScenarioId" } }
|
||
```
|
||
`base` ist das Eltern-Szenario (null beim Basisszenario) – daraus rechnet der Client den Diff.
|
||
Dies ist der einzige Endpunkt, der die Berechnung ausführt (neben `export`). → 404 wenn fremd.
|
||
|
||
### `PATCH /api/scenarios/<scenarioId>`
|
||
Akzeptiert eine **Union** von zwei Formen:
|
||
1. Vollständiges Profil: `{ householdType, inflationRateDefault, persons[], name? }` – ersetzt
|
||
die Personen in einer Transaktion.
|
||
2. Teilaktualisierung: `{ name?, initialCash? }` – `initialCash` 0…1'000'000'000, gerundet.
|
||
|
||
→ 200 `{ scenario: { id, name } }`
|
||
|
||
### `DELETE /api/scenarios/<scenarioId>`
|
||
→ 200 `{ ok: true }` · 400 wenn es das **Basisszenario** ist.
|
||
|
||
### `POST /api/scenarios/<scenarioId>/copy`
|
||
`{ name }` – vollständige Kopie; setzt `parentScenarioId` sowie die Herkunfts-Verweise.
|
||
→ 201 `{ scenarioId: "<neue Id>" }`
|
||
|
||
### `GET /api/scenarios/<scenarioId>/export`
|
||
→ `text/csv; charset=utf-8`, `Content-Disposition: attachment`. Liefert immer den **aktuellen**
|
||
Stand, auch wenn im Client eine ältere Version betrachtet wird ([3.8.4](#384-analyse-auf-einer-bestimmten-version)).
|
||
|
||
### `GET /api/scenarios/<scenarioId>/versions`
|
||
Änderungshistorie, neueste zuerst – **ohne** die Snapshots (je zig Kilobyte).
|
||
```json
|
||
{ "currentMajor": 2,
|
||
"versions": [ { "id", "major", "minor", "comment", "isMajor",
|
||
"createdAt", "updatedAt", "author" } ] }
|
||
```
|
||
|
||
### `POST /api/scenarios/<scenarioId>/versions`
|
||
`{ comment }` (Pflicht, ≥ 3 Zeichen) – legt den aktuellen Stand als **Hauptversion** fest und
|
||
setzt `Scenario.currentMajor`. → 201 `{ version: { major, minor } }` · 400 ohne Kommentar.
|
||
|
||
### `GET /api/scenarios/<scenarioId>/versions/<versionId>`
|
||
Ein einzelner Stand samt Berechnung und der Vorwarnung für das Wiederherstellen:
|
||
```json
|
||
{ "version": { "id", "major", "minor", "comment", "isMajor", "createdAt" },
|
||
"plan": <PlanInput>, "computed": <PlanComputed>,
|
||
"impact": { "lostElementIds": [], "lostPhaseIds": [], "affectedChildren": [] } }
|
||
```
|
||
`impact` benennt die Kind-Szenarien, die durch ein Wiederherstellen ihre Diff-Basis verlören.
|
||
|
||
### `POST /api/scenarios/<scenarioId>/versions/<versionId>`
|
||
Setzt das Szenario auf diesen Stand zurück – **IDs bleiben erhalten**, und der wiederhergestellte
|
||
Stand wird selbst als neue Version festgehalten («Wiederhergestellt aus A.B»).
|
||
→ 200 `{ version: { major, minor } }`
|
||
|
||
## 6.4 Phasen
|
||
|
||
### `POST /api/scenarios/<scenarioId>/phases`
|
||
Body optional: `{ name?, durationYears? }`.
|
||
Hängt eine Phase am Ende an, kappt die Dauer, vergibt Default-Name, legt vorbelegte
|
||
`ElementPhaseValue` für alle aktiven Elemente an (alles in einer Transaktion).
|
||
→ 201 `{ phase: { id } }`
|
||
|
||
### `PUT /api/phases/<phaseId>`
|
||
`{ name?, durationYears? }` – `durationYears` 1–80, wird gekappt.
|
||
→ 200 `{ phase: { id } }`
|
||
|
||
### `DELETE /api/phases/<phaseId>`
|
||
Nur die letzte Phase. → 200 `{ ok }` · 400 „Nur die letzte Phase kann gelöscht werden."
|
||
|
||
### `PUT /api/phases/<phaseId>/cash-transition`
|
||
Body = `CashTransitionData` (siehe 5.4.5). Speichert den Cash-Entscheid für den Übergang **nach**
|
||
dieser Phase (einmalige Sonderein-/ausgaben). Schreibt die Spalte `Phase.cashTransition`.
|
||
→ 200 `{ ok }` · 404 wenn die Phase nicht dem Benutzer gehört.
|
||
|
||
## 6.5 Elemente
|
||
|
||
### `POST /api/scenarios/<scenarioId>/elements`
|
||
`{ category, name, ownerRole? }` → 201 `{ element: { id } }`
|
||
- `PERSON_ONLY_CATEGORIES` ohne Person → 400
|
||
- fehlendes `ownerRole` sonst → `HOUSEHOLD`
|
||
- `orderIndex` = Max + 1
|
||
|
||
### `PATCH /api/elements/<elementId>`
|
||
`{ name }` (1–120). → 200 `{ ok }`
|
||
|
||
### `DELETE /api/elements/<elementId>`
|
||
→ 200 `{ ok }`, Cascade auf alle Phasen-/Übergangswerte.
|
||
|
||
### `PUT /api/elements/<elementId>/phase/<phaseId>`
|
||
Body = `PhaseData`. **Upsert** auf `@@unique([elementId, phaseId])`. Prüft zusätzlich, dass die
|
||
Phase zum selben Plan gehört wie das Element. → 200 `{ ok }`
|
||
|
||
### `PUT /api/elements/<elementId>/transition/<fromPhaseId>`
|
||
Body = `TransitionData`. **Upsert** auf `@@unique([elementId, fromPhaseId])`. → 200 `{ ok }`
|
||
|
||
---
|
||
|
||
# 7. Betrieb und Deployment
|
||
|
||
## 7.1 Container
|
||
|
||
**Dockerfile** – vierstufiger Multi-Stage-Build auf `node:20-alpine`:
|
||
1. `base` – Arbeitsverzeichnis `/app`
|
||
2. `deps` – `npm ci` (mit `prisma/` für den `postinstall`-Hook `prisma generate`)
|
||
3. `builder` – `npx prisma generate` + `npm run build`
|
||
4. `runner` – nur Laufzeit-Artefakte; `ENTRYPOINT ./docker-entrypoint.sh`, `CMD npm start`, Port 3000
|
||
|
||
**`docker-entrypoint.sh`** führt vor dem Start `npx prisma migrate deploy` aus – Migrationen
|
||
laufen also automatisch bei jedem Container-Start.
|
||
|
||
## 7.2 docker-compose
|
||
|
||
Zwei Services:
|
||
- **`app`** – hängt an zwei Netzwerken: `agent-net` (extern, Traefik) und `internal` (DB).
|
||
- **`db`** – `postgres:16-alpine`, nur im `internal`-Netz, persistiert auf Volume `fpt_db_data`.
|
||
Die Datenbank ist von aussen **nicht** erreichbar.
|
||
|
||
**Traefik-Labels:**
|
||
```
|
||
traefik.enable=true
|
||
traefik.docker.network=agent-net ← kritisch, siehe unten
|
||
traefik.http.routers.fpt.rule=Host(`fpt.aicds.ch`)
|
||
traefik.http.routers.fpt.entrypoints=websecure
|
||
traefik.http.routers.fpt.tls.certresolver=myresolver
|
||
traefik.http.services.fpt.loadbalancer.server.port=3000
|
||
```
|
||
|
||
Zwei projektspezifische Fallstricke sind im Code dokumentiert bzw. durch die Konventionen gesetzt:
|
||
1. **`traefik.docker.network=agent-net` ist zwingend.** Der Container hängt an zwei Netzwerken;
|
||
ohne diese Angabe wählt Traefik zufällig eines – landet es im internen DB-Netzwerk, ist das
|
||
Backend unerreichbar (Timeout). (Kommentar in `docker-compose.yml`.)
|
||
2. **Der Router-Name muss projektspezifisch sein** (hier `fpt`, nicht `app`), sonst kollidiert er
|
||
mit anderen Projekten auf derselben VM.
|
||
|
||
## 7.3 CI/CD
|
||
|
||
`.gitea/workflows/deploy.yaml`: **Push auf `main` = automatisches Live-Deployment**
|
||
(bewusst kein Review-Gate). Ablauf:
|
||
1. Checkout
|
||
2. `.env` aus den Gitea-Secrets `SESSION_SECRET` und `POSTGRES_PASSWORD` schreiben
|
||
3. Code nach `/opt/aicds/apps/FPT/` kopieren
|
||
4. `docker compose down` → `docker compose up -d --build` → `docker image prune -f`
|
||
|
||
Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain `fpt.aicds.ch`, Gitea unter
|
||
`git.aicds.ch`.
|
||
|
||
---
|
||
|
||
# 8. Qualitätssicherung
|
||
|
||
## 8.1 Teststrategie
|
||
|
||
Getestet wird ausschliesslich der Berechnungskern – bewusst, da dort die Fachlogik und das
|
||
Regressionsrisiko liegen. Ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`,
|
||
Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests.
|
||
|
||
| Datei | Tests | Schwerpunkt |
|
||
|---|---|---|
|
||
| `calculations.test.ts` | 43 | AHV-Rentenformel, Immobilie, Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests" |
|
||
| `sensitivity.test.ts` | 15 | Treiber-Transformationen (Reinheit, Einheiten, Kappung), Verfügbarkeit, Tornado-Sortierung und -Richtung, Erklärung wirkungsloser Treiber |
|
||
| `explain.test.ts` | 12 | Verlaufswerte je Element, Vollständigkeit beider Wasserfall-Zerlegungen, Rechenweg-Protokoll, Gültigkeit der Spezifikations-Verweise |
|
||
| `montecarlo.test.ts` | 18 | Determinismus, Volatilität/Vol-Drag, Böden, Reproduzierbarkeit; Element-Gruppierung, Szenario-Vergleich, Inflation je Szenario, plannedReturnOf; Schwellen-Ablesung (probabilityAtLeast), Monotonie über Schwellen, Fall 2 systematisch unter 50 % |
|
||
| `distribution.test.ts` | 8 | Kapitaltopf und Quoten-Zerlegung gegen die Cash-Brücke; Entwurfswerte anwenden ohne Verlust bestehender Felder |
|
||
| `actuals.test.ts` | 22 | Zuordnung über Kopie-Ketten, Sprung im Ist-Jahr, Weiterrechnen ab dem Ist-Wert, geschlossene Brücken, Fluss-Rückrechnung |
|
||
| `dataview.test.ts` | 7 | beide Sichten in einem Zug, Rückfall ohne Ist-Werte, Abweichungsmessung |
|
||
| `ratefields.test.ts` | 17 | Erkennung geänderter Raten, Reichweite (diese/folgende/alle), Erhalt der übrigen Werte in den Zielphasen |
|
||
| `versioning.test.ts` | 22 | Nummerierung und Zusammenfassung je Sitzung, unveränderte Stände, Benutzertrennung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien |
|
||
| `versioning-coverage.test.ts` | 3 | statischer Wächter: jeder schreibende Endpunkt löst eine Version aus |
|
||
| `livesim.test.ts` | 15 | Element-Regler bewegt genau ein Element; Aufschlüsselung deckt sich mit dem Sammelregler; neutrale Stellung verändert den Plan nicht; Ruinmeldung |
|
||
| `phaseplan.test.ts` | 8 | Ableitung der Lebensabschnitte aus den Pensionierungszeitpunkten (Einzel/Paar/bereits pensioniert); letzter Teil immer offen |
|
||
| `bridges.test.ts` | 10 | Vermögens- und Cash-Brücke gehen über sieben Plankonstellationen ohne Restgrösse auf; Umbuchungen bleiben aus der Vermögensbrücke heraus |
|
||
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
|
||
| `migrations.test.ts` | 3 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-**Datenübernahme** (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen) |
|
||
| **Total** | **212** | |
|
||
|
||
## 8.2 Testfälle
|
||
|
||
| Test | Prüft |
|
||
|---|---|
|
||
| **Amtliche Tabelle 318.117.1** | `ahvMonthlyFullPension` reproduziert 11 Stützstellen der amtlichen Rentenskala exakt (Mindestrente, Formel 1, Wendepunkt 45'360 → 1'915, Formel 2, Maximalrente) |
|
||
| **Kappung / Stetigkeit** | unter 15'120 → 1'260, über 90'720 → 2'520; kein Sprung am Wendepunkt |
|
||
| **AHV volle Karriere** | mdJE 100'000 → Maximalrente 32'760 |
|
||
| **AHV abgestuft / Mindestrente** | mdJE 60'000 → Formel 2; mdJE 10'000 → 1'260 × 13 |
|
||
| **AHV Vorgeschichte dominiert** | 39 Jahre à 40'000 + 5 Jahre à 200'000 → mdJE 58'182 |
|
||
| **AHV Ausfalljahre** | 4 Ausfalljahre → `32'760 × 40/44`; Ausfalljahre im Plan senken nur die Skala, nicht das mdJE |
|
||
| **AHV ohne Prüfung** | ohne erfassten Wert gilt der geplante Durchschnitt (nicht 0) |
|
||
| **AHV bereits pensioniert** | Karriere aus der Phasenzelle der ersten Phase |
|
||
| **AHV Plafonierung** | zwei Maximalrenten im Paar-Plan → gekappt auf `32'760 × 1.5` |
|
||
| **AHV Netto → Brutto** | mdJE = `70'000 × 1.12`, nicht 70'000 |
|
||
| **Immobilie: Hebel** | 1 Mio / 900k Hypothek / 1 % / 10 J. → Endwert 304'622; deutlich mehr als eine Wertsteigerung auf das Eigenkapital ergäbe |
|
||
| **Immobilie: ohne Wertsteigerung** | Verhalten unverändert (1 Mio − 800k = 200'000) |
|
||
| **Immobilie: Zins-Schalter** | `INCLUDED` → kein Cash-Abzug; `ADD` → 10 × 1 % von 900'000 = 90'000 |
|
||
| **Immobilie: Zins sinkt** | Jahr 1: 10'000, Jahr 10: 9'100; schlägt auf die Quote durch |
|
||
| **Immobilie: Verkauf** | Grundstückgewinnsteuer auf `Verkaufspreis − Kaufpreis`, nicht auf den Verkehrswert |
|
||
| **Teilverkauf (42)** | Betrag ins Cash (→ `capitalInflow`), Rest bleibt aktiv; am Endwert gekappt; Halten/Vollverkauf unverändert |
|
||
| **Sonderamortisation (15)** | Einmaltilgung senkt Restschuld, belastet Cash (→ `capitalInvest`), am Restsaldo gekappt |
|
||
| **MC: Determinismus** | Streuung 0 reproduziert exakt das deterministische Ergebnis (Bänder kollabieren) |
|
||
| **MC: Volatilität / Vol-Drag** | σ > 0 spreizt p10<median<p90; Median unter dem deterministischen Wert |
|
||
| **MC: Erfolg / Reproduzierbarkeit** | P(≥ Ziel) fällt mit steigendem Ziel; gleicher Seed → identisches Ergebnis |
|
||
| **MC: Boden / Ruin** | 0%-Boden hält PK/3a ≥ Startwert; sicherer Verzehr → Ruinwahrscheinlichkeit 100 % |
|
||
| **MC: Herkunfts-Kette** | `resolveRootElementId` folgt der Kette bis zum Ursprung; Verweis ins Leere → eigenes Element ist Wurzel; defekte Kette terminiert |
|
||
| **MC: Element-Gruppen** | Kopie und Original ergeben **eine** Gruppe (nicht zwei); Auflösung auch über ein nicht ausgewähltes Zwischen-Szenario; ein nur in einem Szenario neues Element bildet eine eigene Gruppe |
|
||
| **MC: Parameter-Übersetzung** | `paramsForScenario` bildet die Gruppen-Parameter auf die Element-Ids des jeweiligen Szenarios ab |
|
||
| **MC: Szenario-Vergleich** | Zwei Szenarien, die sich nur in der **geplanten** Rendite unterscheiden: identischer Median/P10/Ruin (gleicher Seed, gleiche Struktur), aber **höhere Erfolgswahrscheinlichkeit** beim pessimistisch geplanten – der einzige Unterschied ist der Zielbetrag |
|
||
| **Tornado: Reinheit** | `applyDriver` lässt den Ausgangsplan unverändert |
|
||
| **Tornado: Einheiten** | Inflation absolut gesetzt; Rendite/Lohnentwicklung in pp verschoben; Ausgaben relativ skaliert (Einkommen unberührt) |
|
||
| **Tornado: Lebensdauer** | verschiebt nur die letzte Phase; Kappung bei mindestens 1 Jahr |
|
||
| **Tornado: Verfügbarkeit** | Treiber ohne passende Elemente werden ausgeblendet (z. B. Immobilien-Wertsteigerung ohne Immobilie) |
|
||
| **Tornado: Sortierung/Richtung** | absteigend nach Spannweite; Treiber ohne Bandbreite hat Spannweite 0 und steht zuunterst; höhere Ausgaben → tieferes, höhere Rendite → höheres Endvermögen |
|
||
| **Tornado: wirkungslose Treiber** | jeder Nullbalken trägt eine Begründung; verkaufte Immobilie → Wertsteigerung wirkungslos mit konkretem Hinweis, gehaltene Immobilie → Spannweite > 0 ohne Hinweis |
|
||
| **Brücken: Restgrösse** | `residual === 0` je Phase für Vermögens- **und** Cash-Brücke über sieben Plankonstellationen (Ansparen, Verrentung, Kapitalbezug, Immobilie gehalten/verkauft, Einmalposten, Sofort-Tilgung mit Sonderamortisation) |
|
||
| **Brücken: Kontrollpunkte** | `startWealth`/`endWealth`/`cashStart`/`cashEnd` der Brücken stimmen mit den offiziellen Phasen-Kennzahlen überein |
|
||
| **Brücken: Umbuchungen** | Sparraten und Amortisationen erscheinen nur in der Cash-Brücke; die Vermögensänderung erklärt sich exakt aus Quote + Erträgen + Wertsteigerung + PK-Beiträgen |
|
||
| **Brücken: Verrentung/Verkauf** | verrentetes PK-Kapital erscheint als Vermögensabgang am Übergang; Verkaufsdifferenz und Grundstückgewinnsteuer nur beim Verkauf, nicht beim Halten |
|
||
| **Verlauf: ein Punkt je Jahr** | je aktivem Element genau `durationYears` Punkte pro Phase |
|
||
| **Verlauf: Konvexität** | 200'000 @ 5 % + 10'000 Sparbeitrag: Jahr 1 = 220'000, und die **Jahreszuwächse wachsen** – eine Gerade zwischen den Phasengrenzen hätte konstante Zuwächse |
|
||
| **Verlauf: Immobilie** | Verkehrswert, Restschuld und Eigenkapital werden getrennt geführt; Eigenkapital = Verkehrswert − Restschuld |
|
||
| **Brücke: Vermögen geht auf** | Restposten ≤ 5 CHF über einen Plan mit allen Element-Arten und Übergangs-Entscheiden |
|
||
| **Brücke: Cash geht auf** | dito; Start- und Endwert stimmen mit `cashStart`/`cashEnd` überein |
|
||
| **Brücke: Umbuchung** | 5 × 12'000 Sparbeitrag: Cash −60'000, Vermögen **unverändert** – die Sparrate erscheint nur im Cash-Wasserfall |
|
||
| **Brücke: PK-Beiträge** | 4 × 10'000 PK-Beitrag: Vermögen +40'000, Cash unbelastet |
|
||
| **Brücke: Übergangsposten** | Verrentung, Kapitalbezugssteuer und Einmalposten erscheinen in der Brücke der **Folge**phase |
|
||
| **explain: Standardmässig aus** | ohne Option keine Traces; mit Option identische Ergebniswerte (Endvermögen, Cash, Ruinalter) |
|
||
| **explain: Abdeckung** | Traces auf Element-, Übergangs-, Phasen- und Plan-Ebene vorhanden |
|
||
| **explain: eingesetzte Zahlen** | jeder Schritt trägt die Formel **und** die substituierte Fassung; das Resultat stimmt mit dem Ergebniswert überein |
|
||
| **explain: Spec-Verweise** | jeder `specAnchor` trifft eine existierende Überschrift in `SPEZIFIKATION.md` (gegen die echte Datei geprüft) |
|
||
| **Vermögen je Jahr** | 100k @ 10 % über 3 J. → 110k/121k/133.1k je Jahrespunkt; Endjahr = Phasen-Endvermögen |
|
||
| **Vermögen real** | 100k bei 10 % Inflation → real 90'909 |
|
||
| Test 1 – Ansparen | Einkommen +2 % nominal, Ausgaben real flach: Endvermögen 761'654 nominal / 565'928 real (±1 %), kein negatives Cash |
|
||
| Test 2 – Verzehr/Ruin | Rente nominal fix 60k, Ausgaben real 100k, Vermögen 900k @3 %: `ruinAge === 94` |
|
||
| Test 3 – Cash-Ausgleich | Sparrate 6'364: `cashEnd === 5472`, nie negativ |
|
||
| Test 4 – Liquiditätslücke | Sparrate 10'000: `cashEnd === -5436`, `cashNegative === true` |
|
||
| Ausgaben real → nominal | Jahr 1 nominal = real; Jahr 5 = `100'000 × 1.02^4` |
|
||
| Einkommen flach | Lohnerhöhung 0 % → `incomeStart === incomeEnd === 80'000` |
|
||
| Bezugsrate | 3 × 10'000 Entnahme → `cashEnd === 30'000`, Asset-Endwert 0, `plannedWithdrawRate === 10'000` |
|
||
| Kapitalzufluss/-investition | Verkauf in P1, Reinvestition in P2: `capitalInflow === 10'000`, `capitalInvest === 10'000`, `cashStart === 0`, `startWealthNominal === 10'000` (kein Doppelzählen) |
|
||
| Cash-Anfangswert | `initialCash` 50'000 fliesst in Phase 1 ein |
|
||
| **Tilgung stoppt** | Schuld 25'000, Tilgung 10'000/J., 5 Jahre: Gesamtabfluss 25'000 (nicht 50'000), `cashEnd === 75'000`, Restschuld 0 |
|
||
| **Amortisation stoppt** | Hypothek 15'000, Amortisation 10'000/J., 4 Jahre: `cashEnd === 85'000` (nicht 60'000), Immobilie schuldenfrei |
|
||
| **Vorbezugssteuer** | PK-Vorbezug 100'000 brutto @ 8 %: `capitalInflow === 92'000`, Restkapital 200'000 (brutto entnommen) |
|
||
| **Einmaliger Zufluss** | 100'000 nominal @ 10 % Steuer → `oneOffInflow === 90'000`, Cash-Start Folgephase +90'000; Phase 1 hat keinen Zufluss |
|
||
| **Einmalige Kosten** | 20'000 real, 2 % Inflation, Grenze nach 10 J. → `20'000 × 1.02^10`, entsprechend vom Cash abgezogen |
|
||
| **Zufluss + Kosten / NONE** | `BOTH`: +50'000 −20'000 → Cash-Start 30'000. `NONE` mit erfassten Beträgen → keine Wirkung |
|
||
| **Liquiditätslücke durch Kosten** | Kosten 25'000 bei Cash 10'000 → `cashStart === −15'000`, `cashNegative`, `incomplete` |
|
||
| **Letzte Phase** | Cash-Entscheid der letzten Phase bleibt wirkungslos (kein Übergang mehr) |
|
||
| Fortschreibung | Einkommens-Basiswert P1 → Startwert P2 = `100'000 × 1.02^5`; `cashStart(P2) === cashEnd(P1)` |
|
||
|
||
## 8.3 Ausführung
|
||
|
||
```bash
|
||
npm test # vitest run
|
||
npm run lint # eslint
|
||
npm run build # next build
|
||
```
|
||
|
||
**Verifikationseinschränkung:** Lokal steht keine Datenbank/Docker zur Verfügung. Die
|
||
Verifikation erfolgt über `npm run build` und die isolierten Berechnungstests; End-to-End-Prüfung
|
||
erst gegen das Deployment (`fpt.aicds.ch`).
|
||
|
||
---
|
||
|
||
# 9. Bekannte Einschränkungen und Modellentscheide
|
||
|
||
Dieser Abschnitt hält fest, was im Code steht und beim Weiterentwickeln bekannt sein muss.
|
||
|
||
## 9.1 Cash wird nicht automatisch ausgeglichen
|
||
|
||
Wird das Cash negativ, meldet das Tool eine Liquiditätslücke (rotes Icon, `incomplete = true`),
|
||
greift aber nicht ein – es wird kein Vermögen automatisch verkauft und kein Kredit aufgenommen.
|
||
Negatives Cash geht mit negativem Vorzeichen ins Gesamtvermögen ein.
|
||
|
||
## 9.2 Verwaiste `PERSON_B`-Elemente
|
||
|
||
Die Haushaltsform ist eine **Szenario-Eigenschaft** und lässt sich im Dialog „Plan-Einstellungen"
|
||
auch bei einem bestehenden Szenario nachträglich ändern (nicht pro Phase – innerhalb eines Szenarios
|
||
gilt sie durchgehend). Wechselt ein Szenario dabei von `COUPLE` auf `SINGLE`, schneidet
|
||
`PlanProfileFields` die Personen auf eine zusammen und `PATCH /api/scenarios/<id>` löscht Person B
|
||
aus der Datenbank. Elemente mit `ownerRole = "PERSON_B"` bleiben bestehen. In der Berechnung
|
||
liefert `personByRole` dann `null`:
|
||
|
||
- `AHV`: wird per `if (!owner || …) continue;` übersprungen – keine Rente.
|
||
- `PENSION_FUND` / `PILLAR_3A`: `owner` ist `null`, deshalb fällt `ownerWorking` auf `anyWorking`
|
||
zurück, d. h. das Element folgt dem Erwerbsstatus des Haushalts statt der (nicht mehr
|
||
existierenden) Person.
|
||
|
||
Es gibt keine Bereinigung und keine Warnung im UI. Da die Haushaltsform normalerweise nicht
|
||
nachträglich geändert wird, ist der Fall selten – er ist aber erreichbar.
|
||
|
||
## 9.3 Immobilien: was noch fehlt
|
||
|
||
Seit Version 0.5 kennt das Modul Hypothekarzins und Wertsteigerung. Nicht modelliert sind:
|
||
|
||
- **Nebenkosten und Unterhalt** (Faustregel ~1 % des Werts). Lassen sich heute als normales
|
||
Ausgaben-Element erfassen.
|
||
- **Eigenmietwert** – ohne echte Steuerlogik (Roadmap Nr. 23) nur halb wirksam.
|
||
- **Mieteinnahmen** (Renditeliegenschaften) – anderer Anwendungsfall.
|
||
- **Wertvermehrende Investitionen** und die **Haltedauer-Abstufung** der
|
||
Grundstückgewinnsteuer (kantonal, teils stark degressiv).
|
||
- **Zinsänderungsrisiko**: Der Zinssatz gilt für die ganze Phase. Ein Zinsschock lässt sich
|
||
nur abbilden, indem man an dieser Stelle eine Phasengrenze zieht und den Satz neu setzt.
|
||
|
||
## 9.4 Kein CSRF-Token
|
||
|
||
Zustandsändernde Requests sind allein durch `sameSite=lax` geschützt. Das deckt klassische
|
||
Cross-Site-Formular-POSTs ab, ist aber schwächer als ein expliziter Token.
|
||
|
||
## 9.5 Logout invalidiert das Token nicht serverseitig
|
||
|
||
Das JWT ist zustandslos und bis zu 30 Tage gültig. `logout` löscht nur das Cookie.
|
||
|
||
## 9.6 Spar- und Bezugsraten werden nicht indexiert
|
||
|
||
`annualContribution`, `annualWithdrawal`, `amortization` und `annualRepayment` sind flache
|
||
Nominalbeträge, die über die Phasenjahre **konstant** bleiben. Eine Sparrate von 10'000 bleibt
|
||
20 Jahre lang 10'000 nominal und verliert dabei real an Gewicht. Wer eine mitwachsende Rate
|
||
abbilden will, muss die Phase teilen und den Betrag in der Folgephase erhöhen.
|
||
|
||
## 9.7 Nur ein Zufluss und eine Kostenposition pro Übergang
|
||
|
||
`CashTransitionData` hält genau ein Zufluss- und ein Kostenpaar (Bezeichnung + Betrag).
|
||
„Erbschaft + Autoverkauf + Poolbau + Küche" am selben Übergang lässt sich nur durch
|
||
Zusammenfassen abbilden („Diverses, 45'000") – die Aufschlüsselung geht dabei verloren.
|
||
Bewusster Entscheid zugunsten eines einfachen UI; erweiterbar auf Listen.
|
||
|
||
## 9.8 Einmalige Ereignisse nur an Phasengrenzen
|
||
|
||
Ein Ereignis kann nur an einem Phasenübergang liegen. Ein Poolbau in Jahr 3 einer 10-jährigen
|
||
Phase ist nur abbildbar, wenn dort eine Phasengrenze gezogen wird. Nach der letzten Phase gibt
|
||
es keinen Übergang – ein dort erfasster Betrag bleibt wirkungslos (durch Test abgedeckt).
|
||
|
||
## 9.9 Gemeinsames Einkommen zählt bei Paaren nicht für die AHV
|
||
|
||
Das mdJE ist eine **personenbezogene** Grösse. Einkommen wird deshalb nur einer Person
|
||
zugerechnet, wenn das `INCOME`-Element ihr zugeordnet ist (`PERSON_A`/`PERSON_B`). Bei einem
|
||
**Einzelplan** zählt `HOUSEHOLD` zur Person A – es gibt ja nur eine. Bei einem **Paar-Plan**
|
||
bleibt `HOUSEHOLD`-Einkommen für die AHV unberücksichtigt.
|
||
|
||
Wer in einem Paar-Plan den Lohn als „Gemeinsam" erfasst, sieht deshalb im Prüf-Dialog ein
|
||
geplantes Durchschnittseinkommen von 0 und bekäme die Mindestrente. Der Dialog zeigt den Wert
|
||
prominent an, sodass der Fehler auffällt – aber es gibt keine aktive Warnung. Erwerbseinkommen
|
||
sollte in Paar-Plänen immer personenscharf erfasst werden.
|
||
|
||
Ebenfalls nicht modelliert: das **Einkommenssplitting** verheirateter Paare (während der Ehe
|
||
werden die Einkommen hälftig geteilt) sowie Erziehungs- und Betreuungsgutschriften. Beides würde
|
||
das mdJE real beeinflussen und wäre der nächste Ausbauschritt.
|
||
|
||
## 9.10 Verhaltensänderung: AHV-Rente bestehender Pläne
|
||
|
||
Bis Version 0.3 erhielt jede AHV-Position pauschal die **Maximalrente** (32'760), gekürzt nur um
|
||
Ausfalljahre – unabhängig vom Einkommen. Seit 0.4 folgt sie der Rentenformel. Bestehende Pläne
|
||
zeigen dadurch eine **andere, in der Regel tiefere** AHV-Rente, sobald das geplante
|
||
Durchschnittseinkommen unter 90'720 liegt. Das ist keine Regression, sondern die Korrektur einer
|
||
zu optimistischen Pauschale.
|
||
|
||
Zwei Fälle brauchen Aufmerksamkeit:
|
||
|
||
- **Bereits pensionierte Personen** (bei Planbeginn): Ohne erfasstes Durchschnittseinkommen
|
||
ergibt das mdJE 0 → **Mindestrente**. Vorher war es die Maximalrente. Die Felder stehen in der
|
||
AHV-Phasenzelle der ersten Phase; solange sie leer sind, ist die Rente bewusst konservativ.
|
||
- **Paar-Pläne mit `HOUSEHOLD`-Einkommen**: siehe 9.9.
|
||
|
||
## 9.11 AHV-Rente wird nach der Pensionierung nicht indexiert
|
||
|
||
Die Rente wird zum Pensionierungszeitpunkt in heutigem Geld berechnet und danach **nominal
|
||
eingefroren**. Die echte AHV wird alle zwei Jahre an den Mischindex angepasst. Über 25
|
||
Rentenjahre verliert die modellierte Rente damit real spürbar an Wert – das Modell ist an dieser
|
||
Stelle deutlich konservativ. Bewusster Alt-Entscheid, unabhängig von der Rentenformel; der
|
||
grösste verbliebene Hebel im AHV-Modell.
|
||
|
||
## 9.12 `PILLAR_3A_MAX_ANNUAL` wird nur im UI erzwungen
|
||
|
||
Das Feld ist per `max`-Prop hart geklammert. Das Zod-Schema kennt für `annualContribution` nur
|
||
`≥ 0` – ein direkter API-Aufruf kann die Obergrenze überschreiten.
|
||
|
||
## 9.13 Verhaltensänderung: Nettolohn-Klarstellung und AHV
|
||
|
||
Bis Version 0.4 war nirgends definiert, ob `INCOME` netto oder brutto ist – der Hilfetext sagte
|
||
nur „die Zahl auf dem Lohnausweis", und dort stehen beide. Für den Cash-Fluss war das folgenlos,
|
||
weil sich beide Konventionen aufheben (`brutto − Ausgaben inkl. Abzüge` ≡ `netto − Ausgaben ohne
|
||
Abzüge`). Mit der einkommensabhängigen AHV (0.4) hing daran aber plötzlich eine Rente.
|
||
|
||
Seit 0.5 gilt: **`amount` ist der Nettolohn**, und die AHV rechnet mit `× 1.12` auf brutto hoch.
|
||
Konsequenzen:
|
||
|
||
- **Wer bisher netto erfasst hat** (die dokumentierte Absicht): Die AHV-Rente **steigt** – sie
|
||
war zuvor um bis zu ~1'900/Jahr zu tief, am stärksten bei mittleren Einkommen (bei 80'000
|
||
brutto: 28'974 statt 30'902). Über 90'720 brutto verschwindet der Effekt, weil beide Werte in
|
||
die Maximalrente laufen.
|
||
- **Wer brutto erfasst hat**: Cash-Fluss und AHV sind nun beide zu hoch. Der Einkommensbetrag
|
||
gehört auf netto korrigiert und die Lohnabzüge aus dem Ausgabenbetrag entfernt.
|
||
|
||
## 9.14 Keine Steuerschätzung
|
||
|
||
Bewusst **nicht** umgesetzt: eine automatische Schätzung von Einkommens- und Vermögenssteuer.
|
||
|
||
Die Bemessungsgrundlage ist das *steuerbare* Einkommen, nicht der Nettolohn – also brutto minus
|
||
PK, 3a, Berufsauslagen, Versicherungs- und Kinderabzüge. Darauf kommen drei Ebenen (Bund,
|
||
Kanton, Gemeinde); allein der Gemeindesteuerfuss variiert innerhalb eines Kantons um rund den
|
||
Faktor zwei. Dazu Zivilstand, Kinder, Konfession und für die Vermögenssteuer 26 kantonale
|
||
Tarife mit eigenen Freibeträgen. Das Tool kennt weder Wohnort noch Kinder.
|
||
|
||
Eine Schätzung daraus läge im Einzelfall schnell 30–50 % daneben – bei vielen Haushalten dem
|
||
grössten Ausgabenposten. Eine selbst berechnete Zahl wirkt zudem autoritativ und wird nicht
|
||
hinterfragt. Der Benutzer kennt seine Steuerrechnung dagegen exakt aus der letzten Veranlagung.
|
||
|
||
**Heutiger Weg:** ein normales Ausgaben-Element „Steuern" – dafür braucht es kein neues Feld.
|
||
Echte Steuerlogik ist Roadmap Nr. 23, mit Kanton und Gemeinde als Eingabe.
|
||
|
||
## 9.15 Monte-Carlo misst Risiko um die Annahmen, nicht deren Richtigkeit
|
||
|
||
Die Simulation streut um die eingegebenen Mittelwerte (historische Rendite/Inflation). Sie
|
||
beurteilt **nicht**, ob diese Mittelwerte realistisch sind. Ein zu optimistischer Mittelwert
|
||
liefert eine beruhigende, aber unrealistische Erfolgsquote – die präzise Prozentzahl täuscht
|
||
dann Sicherheit vor. Der Dialog weist das explizit aus; die Mittelwerte sind Pflichtfelder ohne
|
||
Default, damit sie bewusst gesetzt werden.
|
||
|
||
Weitere bewusste Vereinfachungen der Stufe A: Inflation und Renditen werden **unabhängig**
|
||
gezogen (real sind sie negativ korreliert); die Normalverteilungs-Alternative wird gar nicht
|
||
angeboten (fette Ränder fest eingebaut); die Simulationsparameter werden **nicht persistiert**
|
||
(ephemer im Dialog). Ein historischer Backtest (Stufe B) und korrelierte/vollständigere Modelle
|
||
(Stufe C) sind offen.
|
||
|
||
## 9.16 Diff ist live gegen die Vorlage
|
||
|
||
Die Abweichungs-Markierung vergleicht **immer den aktuellen Stand** des Eltern-Szenarios. Ändert
|
||
man die Vorlage nachträglich, verschiebt sich die Markierung in allen Kindern rückwirkend: Setzt
|
||
man in der Vorlage einen Wert auf das, was ein Szenario ohnehin hatte, verschwindet dort die
|
||
gelbe Markierung, ohne dass das Szenario angefasst wurde.
|
||
|
||
Das ist logisch korrekt („weicht ab von der Vorlage"), kann aber überraschen. Die Alternative –
|
||
ein eingefrorener Snapshot beim Kopieren – wäre schnell veraltet und würde Abweichungen anzeigen,
|
||
die keine mehr sind. Bewusster Entscheid zugunsten des Live-Vergleichs.
|
||
|
||
Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kind ab dann als
|
||
**neu** (der Herkunfts-Verweis zeigt ins Leere). Auch das folgt aus dem Live-Vergleich.
|
||
|
||
## 9.17 Kleinere Beobachtungen
|
||
|
||
- `planToCsv(plan, computed)` erhält den `plan`-Parameter, verwendet ihn aber nicht.
|
||
- `Plan.branchFromPhaseId` hat keine Fremdschlüssel-Constraint; die Phase kann gelöscht werden,
|
||
ohne dass das Feld bereinigt wird.
|
||
- Die Element-Umbenennung ist als API vorhanden, im UI aber nicht erreichbar.
|
||
- Die `README.md` ist noch der unveränderte `create-next-app`-Text.
|
||
|
||
(Der frühere `Selection`-Rest in `PlanView` und der ProfileMenu-Lint-Fehler sind seit 0.13
|
||
behoben – `npm run lint` läuft fehlerfrei.)
|
||
|
||
## 9.18 Tornado: was der Chart nicht leistet
|
||
|
||
**Die Balkenlänge hängt von den eingegebenen Bandbreiten ab.** Wer Renditen mit ±2 Prozentpunkten
|
||
und Ausgaben mit ±20 % variiert, misst zu einem Teil die eigene Wahl dieser Bandbreiten. Deshalb
|
||
sind sie Pflichteingabe ohne Default und im Ergebnis sichtbar ([4.13.3](#4133-bandbreiten-sind-pflicht--ohne-default)).
|
||
Aussagekräftig ist die **Reihenfolge**, nicht der absolute Betrag.
|
||
|
||
**One-at-a-time sieht keine Wechselwirkungen.** Schlechte Renditen *und* hohe Ausgaben treffen
|
||
härter als die Summe der Einzelbalken – weil in der Folge Kapital verzehrt wird, das später zur
|
||
Verzinsung fehlt. Für Kombinationen ist die Monte-Carlo-Simulation zuständig.
|
||
|
||
**Das Pensionsalter ist bewusst nicht enthalten.** Die Roadmap nennt es als Top-Hebel, aber
|
||
`retirementAge` lässt sich im aktuellen Datenmodell nicht isoliert variieren, ohne die
|
||
Phasengrenzen mitzuverschieben – und das Ergebnis wäre nicht ungenau, sondern **irreführend**.
|
||
Beispiel: Person 45, Pension mit 65, Phase 1 = 20 Jahre (45→65), Phase 2 = 20 Jahre.
|
||
|
||
- **Pensionsalter auf 62:** Phase 1 beginnt mit 45, also `45 < 62` → die Phase bleibt vollständig
|
||
Erwerbsphase. Die Person arbeitet im Modell weiterhin bis 65, der Pensions-Übergang liegt an
|
||
derselben Grenze. Wirkung auf das Ergebnis: **praktisch null.**
|
||
- **Pensionsalter auf 68:** Phase 2 beginnt mit 65, also `65 < 68` → Phase 2 wird zur
|
||
**Erwerbsphase**, das Einkommen läuft weiter. Zugleich wird `ownerRetiresNext` an der Grenze
|
||
nach Phase 1 falsch (65 ≥ 68 trifft nicht zu), womit der **Pensions-Übergang komplett entfällt**:
|
||
keine PK-Verrentung, kein 3a-Bezug, keine AHV-Rente. Der Balken wäre riesig – er misst aber den
|
||
Ausfall der Vorsorgelogik, nicht „drei Jahre länger arbeiten".
|
||
|
||
Fachlich korrekt wäre nur, `retirementAge` **und** die Phasengrenze gemeinsam zu verschieben
|
||
(Erwerbsphase kürzer, Pensionsphase länger). Das hat eigene Sonderfälle – Paare mit
|
||
unterschiedlichem Pensionsalter, Grenzen abseits des Pensionsereignisses, Verschiebung grösser als
|
||
die Phasendauer – und ist als eigener Arbeitsschritt offen. Der verwandte Treiber **Lebensdauer**
|
||
(Dauer der letzten Phase) ist dagegen sauber abgebildet und deckt einen Teil des Bedürfnisses ab.
|
||
|
||
## 9.19 Simulationsparameter werden nicht gespeichert
|
||
|
||
Weder die Monte-Carlo-Annahmen noch die Tornado-Bandbreiten werden persistiert; beide leben nur
|
||
im geöffneten Dialog. Das ist ein bewusster Entscheid (kein Datenmodell für Annahmen, keine
|
||
Migration), hat aber den Preis, dass eine Analyse bei jedem Öffnen neu parametrisiert werden muss.
|
||
Bei der Monte-Carlo-Simulation über mehrere Szenarien fällt das stärker ins Gewicht als zuvor,
|
||
weil dort mehr Eingaben zusammenkommen.
|
||
|
||
---
|
||
## 9.20 Transparenz legt auch die Vereinfachungen offen
|
||
|
||
Die vollständige Offenlegung der Rechenwege macht sichtbar, was das Modell **nicht** kann: die
|
||
nach der Pensionierung nicht indexierte AHV-Rente ([9.11](#911-ahv-rente-wird-nach-der-pensionierung-nicht-indexiert)),
|
||
den pauschalen Netto-Brutto-Faktor 1.12 ([4.4.5](#445-netto-brutto-umrechnung-für-die-ahv)), die
|
||
nicht indexierten Spar- und Bezugsraten ([9.6](#96-spar--und-bezugsraten-werden-nicht-indexiert))
|
||
und die fehlende Steuerberechnung ([9.14](#914-keine-steuerschätzung)).
|
||
|
||
Das ist der eigentliche Wert der Offenlegung und kein Nebeneffekt. Entscheidend ist aber, dass die
|
||
Erklärtexte die Grenze **mitnennen**, statt sie zu übergehen – dafür ist das Feld `note` je
|
||
Rechenschritt da. Ein fachkundiger Nutzer, der eine Vereinfachung selbst entdeckt, nachdem ihm
|
||
volle Transparenz zugesagt wurde, zieht den härteren Schluss.
|
||
|
||
## 9.21 Wasserfall: die Zuordnung ist eine Interpretation
|
||
|
||
Welcher Posten in welchen Wasserfall gehört, ist eine **fachliche Entscheidung** und nicht aus den
|
||
Zahlen ableitbar. Die hier getroffene – Umbuchungen nur im Cash-Wasserfall, echte Zu- und Abgänge
|
||
nur im Vermögens-Wasserfall ([4.14.2](#4142-die-beiden-wasserfälle)) – ist begründet, aber nicht
|
||
die einzig denkbare. Wer die Sparrate als „gebundenes Geld" verstanden wissen will, würde sie
|
||
anders einordnen.
|
||
|
||
Konkret uneindeutig sind zwei Fälle:
|
||
- **Amortisation** senkt Cash und Hypothek. Im Vermögens-Wasserfall taucht sie nicht auf, obwohl
|
||
sie das *Eigenkapital* erhöht – die Erhöhung ist bereits im unveränderten Vermögenssaldo
|
||
enthalten.
|
||
- **Verrentung** erscheint als Vermögensabgang, obwohl der Gegenwert als Rentenstrom weiterlebt.
|
||
Der Rentenstrom ist im Modell aber kein Bilanzposten, sondern Einkommen; er taucht in den
|
||
Folgejahren über die Quote wieder auf.
|
||
|
||
Der Restposten (`residual`) ist die Kontrollgrösse dafür, dass die gewählte Zerlegung wenigstens
|
||
**vollständig** ist – nicht dafür, dass sie die einzig sinnvolle ist.
|
||
|
||
## 9.22 Verkaufspreis und Verkehrswert bleiben unabhängig
|
||
|
||
Seit Version 0.12 wird der Verkaufspreis mit dem modellierten Verkehrswert vorbelegt und die
|
||
Abweichung ausgewiesen ([3.5.8](#358-verkaufspreis-und-modellierter-verkehrswert)). Die beiden
|
||
Grössen bleiben aber **entkoppelt** – das Tool erzwingt keine Konsistenz.
|
||
|
||
Das ist bewusst so: Ein Verkauf unter dem Verkehrswert ist ein realer Fall (Notverkauf, Verkauf
|
||
an Nachkommen, Liebhaberobjekt ohne Markt). Eine Zwangskopplung würde diese Fälle unmöglich
|
||
machen. Der Preis dafür ist, dass eine unplausible Kombination weiterhin eingebbar bleibt – neu
|
||
aber nicht mehr unbemerkt.
|
||
|
||
Eine Folge bleibt bestehen und ist nicht offensichtlich: **Wird die Immobilie vor Planende
|
||
verkauft, hat die angenommene Wertsteigerung keinen Einfluss mehr auf das Endvermögen.** Der
|
||
Erlös folgt allein dem erfassten Verkaufspreis. Im Tornado führt das zu einem Nullbalken, der
|
||
seit 0.12 erklärt wird ([4.13.5](#4135-wirkungslose-treiber-werden-erklärt)); in der
|
||
Vermögensbrücke erscheint stattdessen die Differenz `Verkaufspreis − Verkehrswert` als eigener
|
||
Posten.
|
||
|
||
## 9.23 Assistent: Teilzustand bei Abbruch
|
||
|
||
Der Plan-Assistent und der Beispielplan senden am Ende eine **Sequenz** bestehender API-Aufrufe
|
||
(Plan → Phase 1 → Elemente → Folgephasen). Bricht die Sequenz mittendrin ab (Netzfehler),
|
||
existiert ein **Teil-Plan**. Der ist normal weiterbearbeitbar und der Fehlerhinweis sagt das
|
||
auch – aber es gibt kein automatisches Rollback. Das wäre nur mit Backend-Unterstützung
|
||
(Transaktion über mehrere Requests oder Batch-Endpunkt) sauber lösbar und ist bewusst nicht
|
||
gebaut: Der seltene Fehlerfall rechtfertigt keinen neuen Endpunkt.
|
||
|
||
## 9.24 Tour ohne Spotlight-Engine
|
||
|
||
Die Tour hebt ihr Ziel per Rahmen-Puls und `scrollIntoView` hervor – bewusst ohne
|
||
Spotlight-Overlay und Positionierungs-Engine (die Karte sitzt fix unten). Bei stark
|
||
verschachtelten Scroll-Situationen kann das Ziel teilweise verdeckt sein. Der Einfachheit
|
||
halber in Kauf genommen; eine echte Coach-Mark-Bibliothek wäre der nächste Schritt, wenn die
|
||
Tour sich bewährt.
|
||
|
||
## 9.25 Die Quote ist kein fester Betrag
|
||
|
||
Die Spar-/Verzehrquote **verändert sich über die Phasenjahre**: Das Einkommen wächst mit der
|
||
Lohnerhöhung, die real erfassten Ausgaben mit der Inflation. Bei 0 % Lohnerhöhung und 1.5 %
|
||
Inflation sinkt eine Quote von 20'000 über fünf Jahre auf rund 15'100 – ohne dass der Nutzer
|
||
etwas geändert hätte.
|
||
|
||
`annualContribution` und die übrigen Raten sind dagegen **flache Jahresbeträge**
|
||
([9.6](#96-spar--und-bezugsraten-werden-nicht-indexiert)). Wer die Quote des **ersten** Jahres
|
||
als Rate verteilt, erzeugt sich damit in den späteren Jahren eine Liquiditätslücke.
|
||
|
||
Der Verteil-Dialog begegnet dem auf drei Arten, statt es zu verstecken:
|
||
- Er zeigt Quote **erstes Jahr**, **letztes Jahr** und **absolut über die Phase**.
|
||
- Er warnt ausdrücklich, wenn die Quote über die Phase sinkt.
|
||
- Die Live-Vorschau rechnet den ganzen Plan neu und meldet eine entstehende Liquiditätslücke
|
||
sofort, statt sie erst nach dem Speichern sichtbar zu machen.
|
||
|
||
Bewusst **nicht** umgesetzt ist eine automatische Deckelung: Es gibt legitime Gründe, mehr zu
|
||
sparen als die laufende Quote hergibt (etwa wenn ein Cash-Polster aus der Vorphase abgebaut
|
||
werden soll). Das Werkzeug informiert, es bevormundet nicht.
|
||
|
||
## 9.26 Fall 2 ist der Nullpunkt – nicht 50 %
|
||
|
||
Naiv erwartet man, dass eine Planung ihren eigenen Endbetrag in **50 %** der Fälle erreicht, sobald
|
||
man um genau die geplanten Werte würfelt. Das ist falsch, und der Fehler ist gross genug, um ein
|
||
Urteil zu verderben.
|
||
|
||
Der Grund ist der **Volatilitäts-Drag** ([4.12.5](#4125-ergebnis)): Der deterministische Pfad
|
||
kapitalisiert das arithmetische Mittel, der Median einer Simulation folgt aber näher dem
|
||
geometrischen. Gemessen an einem Testplan (800k Startkapital, 30 Jahre, geplante Rendite 5.2 %,
|
||
deterministisches Endvermögen 3'457'554):
|
||
|
||
| Streuung σ | Median-Endvermögen | P(Plan-Endbetrag erreicht) |
|
||
|---|---|---|
|
||
| 0 % | 3'457'554 (= deterministisch) | – |
|
||
| 3 % | 3'432'399 | ~48 % |
|
||
| 6 % | 3'322'529 | ~45 % |
|
||
| 15 % | 2'612'222 | **37 %** |
|
||
| 25 % | 1'451'659 | **27 %** |
|
||
|
||
Eine Planung mit hoher Aktienquote landet also bei **27–37 %**, obwohl an ihr nichts falsch ist.
|
||
Ein fixer 50-%-Schnitt würde sie als «zu optimistisch» brandmarken.
|
||
|
||
Deshalb ist der Nullpunkt für das Urteil **Fall 2** – dieselbe Schwelle, dieselbe Streuung, nur der
|
||
Mittelpunkt aus den eigenen Planannahmen statt aus der Historie
|
||
([4.12.7](#4127-zwei-welten-vier-fälle)). Der Vol-Drag steckt dann in **beiden** Zahlen und kürzt
|
||
sich weg; übrig bleibt genau die Frage, die interessiert: *Ist meine Renditeannahme optimistischer
|
||
als die Vergangenheit?* Das Toleranzband von ± 5 Prozentpunkten fängt Simulationsrauschen ab.
|
||
|
||
Fall 2 selbst wird nur klein als Referenz gezeigt: als Zahl ist er wenig aussagekräftig (er misst
|
||
im Wesentlichen die eigene Streuung), als **Vergleichsanker** dagegen unverzichtbar. Der Hinweis,
|
||
dass die Simulation Risiko **um deine Annahmen** misst und nicht deren Richtigkeit
|
||
([9.15](#915-monte-carlo-misst-risiko-um-die-annahmen-nicht-deren-richtigkeit)), bleibt bestehen.
|
||
|
||
|
||
## 9.27 Warum die Regler Standardbereiche haben – und der Tornado nicht
|
||
|
||
Zwei Kapitel dieser Spezifikation scheinen sich zu widersprechen:
|
||
[9.18](#918-tornado-was-der-chart-nicht-leistet) begründet, warum die
|
||
Sensitivitätsanalyse **bewusst keine** Default-Bandbreiten anbietet, während die Live-Simulation
|
||
([4.15](#415-live-simulation-was-wäre-wenn-regler)) für jeden Regler einen vorbelegten Bereich
|
||
mitbringt. Das ist kein Versehen.
|
||
|
||
**Beim Tornado bestimmt die Bandbreite das Ergebnis.** Die Balkenlänge ist die Spannweite
|
||
zwischen dem tiefen und dem hohen Wert – wer «Rendite ±3 pp» gegen «Ausgaben ±5 %» stellt,
|
||
erzeugt eine Rangfolge, die er selbst vorgegeben hat. Ein Default wäre dort eine **frei erfundene
|
||
Aussage**: Das Werkzeug würde behaupten, ein Treiber sei wichtiger als ein anderer, obwohl der
|
||
Unterschied nur aus den voreingestellten Bereichen stammt. Deshalb ist die Bandbreite dort
|
||
Pflichteingabe ohne Vorschlag.
|
||
|
||
**Ein Regler vergleicht nichts.** Er zeigt genau einen Zustand: «bei dieser Rendite kommt dieses
|
||
Endvermögen heraus». Der Bereich bestimmt nur, wie weit sich der Schieber bewegen lässt – er
|
||
verändert das angezeigte Ergebnis an keiner Stelle. Ein Standardbereich erfindet hier also keine
|
||
Aussage; er macht den Regler überhaupt erst bedienbar, denn ohne Ober- und Untergrenze gibt es
|
||
keinen Schieber.
|
||
|
||
Die Bereiche sind trotzdem **an beiden Enden editierbar** (Häkchen «Bereiche anpassen»), und
|
||
neben jedem Regler steht sein **Neutralpunkt** – der Wert, bei dem der Plan unverändert bleibt.
|
||
Bei allen Verschiebungs-Treibern ist das 0, bei der Inflation der Planwert selbst, weil sie als
|
||
einzige absolut und nicht als Differenz eingegeben wird.
|
||
|
||
## 9.28 Was die Versionierung nicht leistet
|
||
|
||
**Das Zeitfenster ist eine Konvention, keine Wahrheit.** Zehn Minuten sind gesetzt, weil FPT
|
||
keinen Speichern-Knopf hat und der Nutzer den Schnitt sonst nie selbst zieht
|
||
([3.8.1](#381-eine-version-je-sitzung--nicht-je-änderung)). Wer nach acht Minuten Pause
|
||
weiterarbeitet, landet in derselben Version; wer nach zwölf Minuten eine Kleinigkeit ändert,
|
||
bekommt eine neue. Beides ist gelegentlich nicht das, was man gemeint hätte. Ein
|
||
Speichern-Knopf wäre die exaktere Lösung, würde aber die Bedienung des ganzen Werkzeugs
|
||
umkrempeln.
|
||
|
||
**Eine Nebenversion hält den Stand am ENDE der Sitzung fest**, nicht jeden Zwischenschritt
|
||
darin. Wer innerhalb einer Sitzung etwas ändert und wieder zurücknimmt, findet den
|
||
Zwischenstand nirgends. Die Historie ist eine Folge von Arbeitsständen, kein Undo.
|
||
|
||
**Es gibt keinen Versionsvergleich.** Zwei Stände lassen sich nur nacheinander ansehen, nicht
|
||
nebeneinander stellen. Der bestehende Diff ([3.2.6](#326-abweichungs-markierung-diff))
|
||
vergleicht Szenarien gegen ihr Eltern-Szenario, nicht Versionen gegeneinander – technisch
|
||
wäre beides verwandt, aber es ist bewusst nicht Teil dieser Stufe.
|
||
|
||
**Die Historie wird nie beschnitten.** Jede Version hält den vollständigen Zustand als JSON
|
||
(Grössenordnung 40 KB bei sechs Phasen und zwölf Elementen). Bei intensiver Nutzung wächst
|
||
das linear; eine Aufräumregel (etwa: Nebenversionen älter als ein Jahr verdichten, Haupt-
|
||
versionen behalten) gibt es noch nicht. Bei den heutigen Datenmengen ist das unkritisch.
|
||
|
||
**«Wer» unterscheidet heute nichts.** Ein Plan gehört genau einem Benutzer, es gibt keine
|
||
Freigabe und keine Rollen – die Spalte zeigt also immer denselben Namen. Sie ist Vorbereitung
|
||
auf den Finanzberater, und die Logik trennt Benutzer bereits sauber (Änderungen verschiedener
|
||
Benutzer werden nie in einer Version zusammengefasst).
|
||
|
||
**Der Plan selbst ist nicht versioniert**, nur seine Szenarien. Wird ein Szenario gelöscht,
|
||
verschwindet seine Historie mit ihm (Cascade). Das ist gewollt: Eine Historie ohne das Objekt,
|
||
das sie beschreibt, wäre nicht wiederherstellbar.
|
||
|
||
## 9.29 Acht Zahlen je Zelle – und wie wir sie vermeiden
|
||
|
||
Mit den effektiven Werten ([3.9](#39-effektive-werte-plan-ist-vergleich)) bekommt die Matrix
|
||
eine zweite Achse. Naiv kombiniert ergibt das je Zelle: nominal **und** real, Plan **und** Ist,
|
||
Phasenbeginn **und** Phasenende – **acht Zahlen**. Das ist keine Tabelle mehr, das ist ein
|
||
Zahlenfeld.
|
||
|
||
Zwei Entscheide halten es lesbar, und sie fallen an den zwei Orten **verschieden** aus.
|
||
|
||
**In der Matrix: Wert und Abweichung statt zweier Rohwerte.** Es gibt nur «Plan» oder
|
||
«Effektiv», kein «beide». Im Ist-Modus steht der Ist-Wert und daneben klein die Differenz zum
|
||
Plan, grün oder rot. Das beantwortet auch die bessere Frage: nicht «wie lauteten die zwei
|
||
Zahlen», sondern «wie weit bin ich weg». Nominal/real bleibt dort bei drei Möglichkeiten – es
|
||
sind Zahlen in einer Zelle, keine Linien in einem Bild.
|
||
|
||
**Nur die Abweichung trägt Farbe.** Würde man die Beträge selbst einfärben, entstünde ein
|
||
Ampelteppich, in dem die eigentliche Aussage untergeht.
|
||
|
||
**In den Grafiken: nominal/real wird zur Einfachauswahl.** Der Vermögensverlauf zeichnete
|
||
bisher je Serie **zwei** Linien (nominal durchgezogen, real gestrichelt). Mit Plan/Ist wären es
|
||
vier, bei zwei Szenarien acht. Das Stilbudget geht deshalb an die **wichtigere** Unterscheidung:
|
||
Plan gestrichelt, Ist durchgezogen – genau die «Plan-Linie vs. Ist-Linie», die die Roadmap
|
||
verlangt. Wer real sehen will, schaltet um, statt eine zweite Linie dazuzubekommen.
|
||
|
||
**Nicht jede Grafik verträgt beides.** «Plan und Ist gleichzeitig» gibt es nur beim
|
||
Vermögensverlauf. Die Vermögensaufteilung zeigt schon Beginn **und** Ende je Phase als
|
||
gestapelte Balken – Plan und Ist daneben vervierfachte sie. Und die Grafik «Einkommen vs.
|
||
Ausgaben» lebt vom Band zwischen zwei Linien; ein zweites Paar darüber macht genau diese
|
||
Aussage unkenntlich. Beide zeigen deshalb nur die gewählte Grundlage.
|
||
|
||
**Serienobergrenze vier.** Szenario mal Version mal Datenquelle wächst schnell; darüber hinaus
|
||
hilft keine Farbpalette mehr.
|
||
|
||
---
|
||
|
||
# 10. Glossar
|
||
|
||
| Begriff | Bedeutung im FPT |
|
||
|---|---|
|
||
| **Plan** | Selbsttragende Planungseinheit: Grundprofil + Phasenkette + Elemente |
|
||
| **Szenario** | Die berechenbare Einheit (seit V6): trägt Grundprofil, Phasenkette und Elemente. Jeder Plan hat genau ein Basisszenario; weitere entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als Baum darunter |
|
||
| **Logisches Element** | Dasselbe finanzielle Element über Szenariogrenzen hinweg, erkannt über die Herkunfts-Kette `sourceElementId` – Grundlage der einmaligen Parametereingabe im Szenario-Vergleich ([4.12.6](#4126-mehrere-szenarien-im-vergleich)) |
|
||
| **Spannweite (Tornado)** | Differenz zwischen dem Ergebnis beim tiefen und beim hohen Wert eines Treibers; bestimmt Balkenlänge und Rangfolge |
|
||
| **Umbuchung** | Bewegung, die Geld zwischen Cash und einem Vermögenswert verschiebt, ohne das Vermögen zu verändern (Sparrate, Amortisation, Zusatzinvestition). Erscheint nur im Cash-Wasserfall |
|
||
| **Vermögens-/Cash-Brücke** | Zerlegung der Vermögens- bzw. Cash-Veränderung einer Phase in ihre Summanden (`WealthBridge` / `CashBridge`) |
|
||
| **Restposten** | Differenz zwischen gerechnetem Endwert und der Summe der Brücken-Summanden; reine Rundung, Kontrollgrösse für die Vollständigkeit |
|
||
| **Rechenweg (Trace)** | Protokoll der Rechenschritte mit Formel, eingesetzten Zahlen, Ergebnis und geltender Vereinfachung; entsteht innerhalb von `computePlan` |
|
||
| **Grundprofil** | Haushaltsform, Personen (Alter, Pensionsalter, Name), Inflationsannahme |
|
||
| **Lebensphase** | Zeitabschnitt mit fester Dauer; darf keine Pensionierung überspannen |
|
||
| **Phasentyp** | `ERWERB` / `PENSION` / `MIXED`; abgeleitet, nie gespeichert |
|
||
| **Finanzielles Element** | Plan-weite Entität einer der 8 Kategorien, über alle Phasen identisch |
|
||
| **Übergang** | Grenze zwischen zwei Phasen; Ort der einmaligen Entscheide |
|
||
| **Einmalige Sonderein-/ausgabe** | Ereignis am Übergang (Erbschaft, Poolbau), das direkt aufs Cash wirkt und nicht in die Quote eingeht |
|
||
| **Pensions-Übergang** | Übergang, bei dem der Besitzer des Elements pensioniert wird |
|
||
| **Carry / Fortschreibung** | Live-Übertragung des Endwerts einer Phase in die nächste |
|
||
| **Cash** | Systemseitiges Ausgleichskonto; darf negativ werden (Liquiditätslücke) |
|
||
| **Quote** | Einkommen − nominale Ausgaben eines Jahres; negativ = **Verzehr** |
|
||
| **Geplante Sparrate** | 3a-Beiträge + Sparbeiträge + Amortisationen + Tilgungen |
|
||
| **Geplante Verzehrrate** | Summe der Bezugsraten aus Sonstigem Vermögen |
|
||
| **Kapitalzufluss** | Verkaufserlöse + PK-/3a-Bezüge aus dem Übergang in die Phase |
|
||
| **Kapitalinvestition** | Zusatzinvestitionen + Sofort-Tilgungen |
|
||
| **Nominal** | Betrag in Franken des jeweiligen Jahres |
|
||
| **Real** | Kaufkraftbereinigt auf den Planbeginn (`nominal / Deflator`) |
|
||
| **Deflator** | Kumulierte Inflation seit Planbeginn |
|
||
| **Ausfalljahr** | Jahr ohne AHV-Beiträge; kürzt die Rente um 1/44 |
|
||
| **mdJE** | Massgebendes durchschnittliches Jahreseinkommen – Mittel der Beitragsjahre, bestimmt die Rentenhöhe |
|
||
| **R0** | Mindestbetrag der vollen AHV-Monatsrente (1'260); alle Schwellen sind Vielfache davon |
|
||
| **Wendepunkt** | mdJE = 36 × R0 = 45'360; dort wechselt die Rentenformel von Teil 1 auf Teil 2 |
|
||
| **Plafonierung** | Deckelung der Ehepaar-AHV auf 150 % der Einzel-Maximalrente |
|
||
| **Umwandlungssatz** | Prozentsatz zur Verrentung des PK-Kapitals |
|
||
| **Ruin(alter)** | Alter von Person A, in dem das Gesamtvermögen erstmals unter 0 fällt |
|
||
| **Nachlass** | Endvermögen der letzten Phase (nominal) |
|
||
|
||
---
|
||
|
||
*Ende der Spezifikation v0.16*
|