4791dccf93
Deploy App / deploy (push) Successful in 57s
Roadmap Nr. 43 (Detailansichten) und Nr. 41 (Berechnungslogiken offenlegen).
Systemparameter-Ansicht:
- SYSTEM_PARAMETERS in constants.ts: Wert, Bedeutung, Herleitung, Quelle,
Stand -- aus derselben Datei, aus der gerechnet wird
Detailansichten (nur lesen, per Expand-Icon):
- Element: Verlauf ueber ALLE Planjahre (neu ElementPhaseComputed.yearly),
bei Immobilien Verkehrswert/Restschuld/Eigenkapital getrennt
- Phase: Vermoegensaufteilung + zwei Wasserfaelle
Zwei getrennte Wasserfaelle (WealthBridge / CashBridge):
- Sparraten, Amortisationen und Investitionen sind UMBUCHUNGEN und erscheinen
nur im Cash-Wasserfall -- als Vermoegensabgang gezeichnet wuerden sie einen
Verlust vortaeuschen, den es nicht gibt
- PK-Beitraege dagegen sind ein echter Vermoegenszugang (belasten kein Cash),
Verrentung ein echter Abgang (Kapital verlaesst die Bilanz)
- residual als Kontrollgroesse fuer die Vollstaendigkeit der Zerlegung
Rechenweg-Protokoll, vollstaendige Abdeckung:
- computePlan(plan, sample?, { explain }) protokolliert die Schritte, die es
ohnehin ausfuehrt -- die Erklaerung IST die Rechnung, statt einer zweiten
Formel-Implementierung im UI, die still abdriften koennte
- standardmaessig aus (Monte Carlo bleibt unberuehrt)
- Arithmetik nicht umgestellt: Zwischengroessen werden als Differenz
abgeleitet, damit die 43 Golden Tests bitgleich bleiben
- jeder Trace verlinkt in die SPEZIFIKATION; ein Test prueft gegen die echte
Datei, dass alle Verweise eine existierende Ueberschrift treffen
SPEZIFIKATION auf 0.11: neue Kapitel 3.6.7, 3.6.8, 4.14, 9.20, 9.21.
12 Tests ergaenzt (80 -> 92). Keine DB-Aenderung.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2881 lines
157 KiB
Markdown
2881 lines
157 KiB
Markdown
# FPT – Financial Planning Tool
|
||
## Funktionale und Technische Spezifikation
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||
| **Version** | 0.11 |
|
||
| **Datum** | 2026-07-18 |
|
||
| **Status** | Lebendes Dokument |
|
||
| **Codestand** | Arbeitsstand nach `1836cad` inkl. Detailansichten, Wasserfall-Zerlegungen und vollständiger Rechenweg-Offenlegung (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.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
|
||
|
||
Dialog mit Name plus dem vollständigen 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.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 geloescht 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.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 | `1'234'567 (890'123)` – nominal, real in Klammern |
|
||
| Real | `890'123` |
|
||
|
||
„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
|
||
|
||
Jeder Phasenkopf zeigt kompakt:
|
||
|
||
| 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>` |
|
||
| Einkommen | Jahr 1 → letztes Jahr |
|
||
| Ausgaben | Jahr 1 → letztes Jahr (nominal) |
|
||
| **Quote** bzw. **Verzehr** | Einkommen − Ausgaben; Label wechselt auf „Verzehr", wenn Jahr 1 negativ |
|
||
| Geplante Sparrate | 3a + Sparbeitrag + Amortisation + Tilgung, im **ersten** Phasenjahr |
|
||
| Geplante Verzehrrate | Summe der Bezugsraten |
|
||
| Kapitalzufluss | nur wenn > 0: Verkäufe + PK-/3a-Bezüge aus dem Übergang **in** diese Phase |
|
||
| Kapitalinvestitionen | nur wenn > 0: Zusatzinvestitionen + Sofort-Tilgungen |
|
||
| **Einmaliger Zufluss** | nur wenn > 0: Bezeichnung + Betrag (grün), aus dem Übergang in diese Phase |
|
||
| **Einmalige Kosten** | nur wenn > 0: Bezeichnung + Betrag (rot) |
|
||
| Vermögen | Start → Ende (inkl. Cash) |
|
||
|
||
Die Einmalposten stehen bewusst **getrennt** von Kapitalzufluss/-investitionen: Eine Erbschaft
|
||
ist kein Verkaufserlös und ein Poolbau keine Kapitalinvestition – eine Vermischung würde die
|
||
Kennzahl falsch beschriften.
|
||
|
||
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). 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.7 Bedienoberfläche
|
||
|
||
### 3.7.1 Layout
|
||
|
||
- **Sidebar** (Desktop ab `lg` fix, mobil als Overlay): Logo, „Übersicht", Planliste mit
|
||
Phasenanzahl, „+"-Button für neuen Plan.
|
||
- **Header**: Menü-Button (mobil), Plantitel, Profilmenü.
|
||
- **Hauptbereich**: Übersicht (Plan-Kacheln) oder Planansicht (Umschalter, Zeitachse,
|
||
Grundprofil-Leiste, Aktionsbuttons, Ruin-Banner, Matrix, Detail-Panel, Dashboard).
|
||
|
||
### 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` Zeilen 46–74.
|
||
|
||
---
|
||
|
||
# 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.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.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 entsteht nur durch die Rundung der einzelnen
|
||
Posten auf ganze Franken und liegt im einstelligen Bereich; ein grösserer Wert wäre ein Hinweis
|
||
auf eine unvollständige Zerlegung. Zwei Tests prüfen ihn über einen Plan, der alle Element-Arten
|
||
und Übergangs-Entscheide enthält.
|
||
|
||
### 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`.
|
||
|
||
---
|
||
|
||
# 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/ 12 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/ 17 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`. Rein, läuft im Browser. |
|
||
| `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 |
|
||
|
||
**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 |
|
||
| `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 |
|
||
|
||
### 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.
|
||
|
||
## 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`.
|
||
|
||
## 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 geloescht 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` | 14 | Treiber-Transformationen (Reinheit, Einheiten, Kappung), Verfügbarkeit, Tornado-Sortierung und -Richtung |
|
||
| `explain.test.ts` | 12 | Verlaufswerte je Element, Vollständigkeit beider Wasserfall-Zerlegungen, Rechenweg-Protokoll, Gültigkeit der Spezifikations-Verweise |
|
||
| `montecarlo.test.ts` | 13 | Determinismus, Volatilität/Vol-Drag, Böden, Reproduzierbarkeit; Element-Gruppierung und Szenario-Vergleich |
|
||
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
|
||
| `migrations.test.ts` | 1 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein |
|
||
| **Total** | **92** | |
|
||
|
||
## 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 |
|
||
| **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.
|
||
- Der Typ `Selection` in `PlanView` hat nur eine Variante (`{ type: "phase" }`) – ein Rest der
|
||
früheren Struktur mit mehreren Auswahlarten.
|
||
- `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.
|
||
- `npm run lint` meldet einen bestehenden Fehler in `ProfileMenu.tsx` Zeile 15
|
||
(`react-hooks/set-state-in-effect`); an vergleichbaren Stellen ist die Regel andernorts
|
||
bewusst per `eslint-disable` deaktiviert.
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
# 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.11*
|