Pensionierung als Eigenschaft der Person: Treiber, Tests, Spezifikation 0.35
Deploy App / deploy (push) Successful in 1m57s
Deploy App / deploy (push) Successful in 1m57s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+228
-30
@@ -4,10 +4,10 @@
|
||||
| | |
|
||||
|---|---|
|
||||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||||
| **Version** | 0.34 |
|
||||
| **Version** | 0.35 |
|
||||
| **Datum** | 2026-07-25 |
|
||||
| **Status** | Lebendes Dokument |
|
||||
| **Codestand** | Arbeitsstand nach `0953880` inkl. Modul-Review 4 (Nachbesserungen) (Branch `main`) |
|
||||
| **Codestand** | Arbeitsstand nach `1865db5` inkl. Pensionierung als Eigenschaft der Person (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` |
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---|---|---|---|
|
||||
| 0.35 | 2026-07-26 | Claude (Opus 5) | **Die Pensionierung ist eine Eigenschaft der PERSON, nicht der Zeitachse** (neues Kapitel 3.13). Der grösste Eingriff seit V7. Bisher hing jeder Bezugs-Entscheid an `transitionValues[phaseId]` – am Schlüssel Element × Phasen-ID. Daraus folgte fast alles, was an der Pensionsplanung störte: Entscheide, die inhaltlich **eine** Frage sind, lagen in drei weit auseinander liegenden Matrix-Zellen; das Alter zu ändern war ein struktureller Eingriff, bei dem Entscheide über `mergeTransition` verlustbehaftet von Grenze zu Grenze gerettet werden mussten; ein Szenario nur für ein anderes Pensionsalter hiess, alles neu zu entscheiden; und der Ziel-Solver (Roadmap Nr. 21) hätte nichts zum Anfassen gehabt. **Neu liegt der Entscheid am ELEMENT** (`FinancialElement.retirementDecision`, ohne Phasenbezug) und überlebt damit jede Verschiebung der Zeitachse. (1) **Neuer Pensionierungs-Bildschirm** gleichrangig neben der Matrix, mit der **Rentenlücke** als Leitzahl – keine neue Rechnung, sondern die Verzehrquote im ersten voll pensionierten Jahr; sie fehlte bisher nur als Begriff. Gerechnet im Rechenkern (`PlanComputed.retirement`), damit Bildschirm und PDF-Bericht nicht auseinanderlaufen. Die Matrix-Zellen am Pensions-Übergang bleiben bedienbar und nutzen **dieselbe Komponente** (`RetirementFields`) – zwei Ansichten auf ein Objekt, kein Duplikat. (2) **AHV-Vorbezug und -Aufschub** werden gerechnet (Kap. 4.4.7 neu geschrieben): Kürzung 6,8 %/Jahr, Zuschlag +5,2/10,8/17,1/24,0/31,5 % nach 1–5 Jahren, Teilbezug 20–80 %. Bis 0.34 startete die Rente **immer** mit 65 – wer mit 62 aufhörte, bekam die ungekürzte Rente drei Jahre später, wer bis 68 arbeitete, verschenkte den Zuschlag. Dabei wurde eine fachliche Trennung eingeführt, die es vorher gar nicht gab: **Rentenbeginn und Beitragspflicht sind zwei verschiedene Alter.** Wer mit 62 aufhört und ab 63 vorbezieht, bezieht ab 63 **und** zahlt bis 65 weiter als Nichterwerbstätige(r). (3) **Pensionskasse: ein Regler statt eines Modus.** `payoutMode` (`PENSION`/`CAPITAL`/`COMBI`) und der absolute `capitalAmount` entfallen zugunsten von `capitalSharePct` (0–100 %). Als Quote, weil sich das Guthaben mit dem Pensionsalter ändert – ein fixer Betrag bedeutete beim Verschieben still ein anderes Verhältnis. (4) **Säule 3a: wählbares Bezugsalter** (60–70) statt starr am Pensions-Übergang. Ein Konto lässt sich nur ganz auflösen, und alle Bezüge desselben Jahres werden steuerlich zusammengezählt – gestaffelt wird deshalb über Konten und Jahre. Gezogen wird an der ersten Phasengrenze bei oder nach dem Wunschalter. (5) **Planungshorizont** (`Person.planningHorizonAge`): Bisher ergab sich das Planende stillschweigend aus der Summe der Phasendauern – zwei Szenarien konnten unbemerkt verschieden weit rechnen und waren nicht vergleichbar. Neue Funktion `planHorizonChange`, neuer Endpunkt `POST /api/scenarios/<id>/horizon`. (6) **Ampel mit drei Zuständen** (Kap. 3.5.3 neu): `unbeantwortet` · `auf Vorgabe` · `bestätigt`. Mit durchgängigen Vorgaben – bewusst, damit niemand am Anfang Fragen beantworten muss, die er erst am Ende beantworten kann – entstand ein Zustand, den das Modell nicht kannte: Das System **hat** eine Antwort, nur nicht die des Benutzers. Eine Vorgabe wie «volle Rente statt Kapitalbezug» als beantwortet zu zählen hiesse, sie unbemerkt durchgehen zu lassen. Sie zählt deshalb mit, aber getrennt benannt: «2 offene Entscheide · 3 Vorgaben ungeprüft», bestätigt wird je Säule. (7) **Drei neue Treiber** in Tornado und Live-Simulation: PK-Kapitalanteil, AHV-Vorbezug/Aufschub (in Monaten, neue Einheit `delta_months`) – und das bestehende Pensionsalter wird endlich **korrekt**, weil der AHV-Beginn jetzt mitzieht. (8) Nebenbei zwei Vereinfachungen: Die Beitragskarriere vor Planbeginn lag an **zwei** Orten (Übergangszelle bzw. Phasenzelle für bereits Pensionierte) mit zwei Codepfaden – jetzt an einem. Und Szenario-Kopie, Versionierung und Diff tragen den Entscheid mit; ohne das wäre die Kopie genau für den Zweck unbrauchbar, für den man sie am häufigsten anlegt. **Keine Datenmigration** (Testdaten wurden vorgängig gelöscht); alte Werte in `transitionValues` werden ignoriert, betroffene Elemente erscheinen als «Vorgabe ungeprüft». Neues Modul `retirement-decision.ts`, neue Komponenten `RetirementPanel` und `RetirementFields`. 23 Tests ergänzt (292 → 315). |
|
||||
| 0.34 | 2026-07-25 | Claude (Opus 5) | **Modul-Review 4, Nachbesserungen: die Übergangs-Entscheide bis ans Ende durchgezogen.** (1) **Zuordnung überall dort, wo Elemente über ihren Namen angeboten werden.** Zwei Personen nennen ihre Guthaben typischerweise gleich («Säule 3a», «ETF»); ohne die Person wählt man im Dropdown blind. Betroffen waren das **Ziel der Anlage-Quote** beim Kapitalbezug (dort mit hoher Folgewirkung: Ein Fehlgriff leitet das Alterskapital in das Depot der falschen Person) und die Zeilen im Dialog **«Kapital verteilen»**. Die Klartext-Zuordnung liegt neu als `ownerLabel` in `src/lib/elements.ts` und wird von allen drei Stellen genutzt. (2) **Herkunft des umgeleiteten Alterskapitals wird ausgewiesen.** Fliessen PK **und** 3a in dasselbe Vermögens-Element, stand dort bisher nur eine Summe – ob wirklich beide angekommen sind, liess sich nicht prüfen. `Carry` und `ElementPhaseComputed` führen neu `capitalInSources` bzw. `capitalFromTransferSources` mit: Betrag **je Quelle**, benannt mit Element **und** Person. Sichtbar am Ziel-Element und im Dialog «Kapital verteilen». Das Feld heisst neu **«Zusatzinvestition aus Kapitalbezug»** (vorher «Davon aus Kapitalbezug (PK/3a)» – irreführend, weil es kein Anteil an der manuell erfassten Zusatzinvestition ist, sondern ein zweiter, davon unabhängiger Betrag). (3) **Der Dialog «Kapital verteilen» zeigt das bereits Zugeteilte.** Vorher stand dort eine **0**, obwohl die Quote geflossen war – das Feld führt nur den manuell erfassten Teil. Neu erscheint darüber eine read-only Zeile mit dem aus dem Bezugs-Entscheid stammenden Betrag samt Aufschlüsselung, darunter das editierbare Feld und die Summe beider. (4) **Bezogene Vorsorge-Guthaben werden in beiden Verteil-Dialogen nicht mehr angeboten.** Nach der Pensionierung ignoriert die Rechnung Beiträge und Zusatzeinlagen in PK und Säule 3a – die Dialoge boten sie trotzdem an, inklusive eines aus der Vorphase geerbten 3a-Beitrags, der dort als aktive Rate erschien. Der Filter prüfte nur den `status` (`ACTIVE`), und der bleibt nach dem Bezug bestehen. Neu setzt die Rechnung selbst das Kennzeichen `acceptsCapital: false`; die Dialoge lesen es, statt die Regel ein zweites Mal nachzubauen. 4 Tests ergänzt (288 → 292). |
|
||||
| 0.33 | 2026-07-25 | Claude (Opus 5) | **Modul-Review 4 (Matrix: Phasen und Elemente).** (1) **Kapitalverwendung neu am Vorsorge-Element** (Punkt C aus Roadmap Nr. 44): Die Prozent-Aufteilung des bezogenen Alterskapitals hing am **Cash-Übergang** – dem falschen Ort, denn mit zwei Guthaben (PK und 3a) liess sie sich dort gar nicht getrennt beantworten. Sie steht jetzt beim **Bezugs-Entscheid** der Pensionskasse (nur bei Kapitalbezug) bzw. der **Säule 3a**. Beide Dialoge führen neu **brutto → Steuersatz → netto** und darunter die Verteilung. Der zugeteilte Betrag fliesst über den regulären Weg (`Carry.capitalIn` → Zusatzeinlage der Folgephase) und ist damit **überall sichtbar**: am Ziel-Element, in der Cash-Brücke als Investition und im «Kapital verteilen»-Dialog. Vorher erhöhte er still den Bestand, weshalb Element und Dialog eine **0** zeigten. Die **Säule 3a** ist am Pensions-Übergang neu ein **offener Entscheid** (Steuersatz und Verwendung); vorher galt sie als automatisch beantwortet. (2) **Phasendauer: die Folgephase gleicht aus** (Kap. 3.3.2). Bis 0.32 prüfte die Kappung nur die **bearbeitete** Phase – wurde Phase 1 von 10 auf 12 Jahre verlängert, überspannte danach Phase 2 die Pensionierung, und die tragende Invariante aus Roadmap Nr. 44 kippte. Neu trägt die Folgephase die Differenz (Gesamtdauer bleibt gleich, wie beim Verschieben des Pensionsalters); passt sie nicht, wird blockiert; vorher erscheint eine Rückfrage. Neue reine Funktion `planDurationChange`. (3) **Element und Phase direkt bedienbar:** In der Matrix tragen Element-Zeile und Phasenkopf neu **Stift** (umbenennen, beim Element inkl. **Zuordnung**) und **Papierkorb**; das Expand-Symbol ist **immer** sichtbar statt nur bei Mouseover. `PATCH /api/elements/<id>` nimmt dafür neu auch `ownerRole` (bleibt für AHV/PK/3a personengebunden). (4) **Hilfetexte** werden über ein **Portal** gezeichnet – in scrollenden Dialogen schnitt der Container sie vorher ab; sie klappen nach oben, wenn unten kein Platz ist. (5) **Verteil-Dialoge:** Zeilen zeigen die **Zuordnung** (Person A/B/Gemeinsam) und sind nach **«vom Cash»/«ins Cash»** gruppiert; die Vorbelegung nutzt neu den **effektiven** Wert inklusive Vererbung aus der Vorphase – ein geerbter 3a-Beitrag erschien vorher als 0. (6) **Matrix:** alle Phasenspalten **gleich breit**, bei vielen Phasen wird horizontal gescrollt; **«Alle auf-/zuklappen»**; eine zugeklappte Kategorie zeigt je Phase die **Summe** ihrer Elemente. (7) **Phasen-Detailansicht** nutzt die neue Aufteilungs-Grafik (Fläche + Ring) statt der alten Balken. (8) **Übersicht:** «Leer starten» steht neu auch im leeren Zustand zur Wahl. (9) Nebenbei: dritte vom Umlaut-Sweep verstümmelte Hex-Farbe (`#7c3äd`) repariert, das Phasen-Panel nutzt den eigenen Bestätigungs-Dialog statt `window.confirm`. 10 Tests ergänzt (278 → 288). |
|
||||
| 0.32 | 2026-07-25 | Claude (Opus 5) | **Modul-Review 3, Nachbesserungen – darunter ein gravierender Rechenfehler bei den effektiven Werten.** (1) **Immobilien-Bugfix (Kap. 3.9):** Der Ist-Wizard belegte den Immobilienwert mit dem **Eigenkapital** vor (`ElementYearPoint.value`), während Erfassung und Rechenkern den **Verkehrswert** erwarten. Der Rechenkern setzte den vorbelegten Wert als Verkehrswert ein, liess die Hypothek aber stehen – das Eigenkapital brach im Ist-Jahr schlagartig ein, typischerweise ins Negative. Sichtbar wurde das als **negative Gesamt-Abweichung, obwohl nur ein Lohn erhöht** wurde, und als «wegbrechendes» Wohneigentum in der Vermögensaufteilung. Neu wird `propertyValue` vorbelegt; das Feld ist als «Verkehrswert + Restschuld» beschriftet. Drei Regressionstests. (2) **Ist-Datensätze bearbeitbar:** Ein Klick auf die Zeile (oder «Bearbeiten») öffnet den erfassten Satz erneut; neuer Endpunkt `PUT /api/plans/<id>/actuals/<setId>`. Beim Bearbeiten überschreiben die Planwerte die erfassten Zahlen nicht mehr. (3) **Ring-Klick in der Vermögensaufteilung repariert:** Recharts 3 reicht im Klick-Parameter **kein `activePayload`** mehr durch (nur noch `activeIndex`) – der Handler feuerte nie, der Ring zeigte immer das Planende. (4) **Seitenleiste sauber dreistufig:** Ebene 1 Pläne, Ebene 2 die vier Bereiche (Szenarien, Effektive Werte, Analysen, Berichte) mit **bündigen Symbolen**, Ebene 3 nur die Szenarien – verschachtelt nach Herkunft. (5) Die **Szenario-Liste** zeigt neben der Version deren **Kommentar**. |
|
||||
@@ -789,25 +790,46 @@ 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:
|
||||
Seit 0.35 gibt es **drei** Zustände statt zwei. Der dritte ist der interessanteste.
|
||||
|
||||
| Zustand | Bedeutung |
|
||||
|---|---|
|
||||
| **unbeantwortet** | Das System weiss nichts. Verkauf/Halten, Tilgung, Cash-Übergang. |
|
||||
| **auf Vorgabe** | Das System **hat** eine Antwort – nur nicht die des Benutzers. Gilt für die Pensionierungs-Entscheide (AHV/PK/3a), die seit 0.35 durchgängige Vorgaben haben. |
|
||||
| **bestätigt** | Der Benutzer hat hingeschaut (`retirementDecision.confirmed`), je Säule. |
|
||||
|
||||
Warum der dritte Zustand nötig wurde: Die Vorgaben sind Absicht – ohne sie müsste man am
|
||||
Anfang Fragen beantworten, die man erst am Ende beantworten kann, und der Plan wäre bis dahin
|
||||
nicht rechenbar ([3.13.2](#3132-kein-leeres-formular)). Eine Vorgabe aber als *beantwortet* zu
|
||||
zählen hiesse, eine stillschweigend gesetzte Annahme wie «volle Rente statt Kapitalbezug»
|
||||
durchgehen zu lassen, obwohl sie das Ergebnis massiv verändert. Sie zählt deshalb **mit** –
|
||||
aber in einem eigenen Topf, damit sie sich nicht wie ein vergessenes Eingabefeld liest:
|
||||
|
||||
> **2 offene Entscheide · 3 Vorgaben ungeprüft**
|
||||
|
||||
`openTransitionCount` und `totalOpenDecisions` liefern deshalb ein Paar `DecisionCounts`
|
||||
(`{ open, unconfirmed }`) statt einer Zahl. `decisionsText` formuliert es. Ein Entscheid gilt
|
||||
weiterhin als beantwortet (`isTransitionAnswered`), wenn das jeweilige Feld 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 |
|
||||
| `PENSION_FUND`, `PILLAR_3A`, `AHV` | am **Pensions-Übergang**: über `confirmed` (siehe oben); 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 „?".
|
||||
Der Übergangs-Spaltenkopf zeigt „N offen" (Akzentfarbe), „N Vorgaben" (gedeckt, weil es kein
|
||||
Handlungsdefizit ist) oder „geprüft" (grün, Häkchen). Offene Zellen sind hervorgehoben und
|
||||
zeigen „?".
|
||||
|
||||
**Auswirkung auf den PDF-Bericht:** Die Kennzahl «Offene Entscheide» weist beide Töpfe
|
||||
zusammen aus. Berichte von vor 0.35 sind deshalb nicht direkt vergleichbar.
|
||||
|
||||
**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.
|
||||
Referenz: `src/lib/decisions.ts` (`openTransitionCount`, `totalOpenDecisions`, `decisionsText`).
|
||||
|
||||
### 3.5.4 Geführter Übergang (Review-Dialog)
|
||||
|
||||
@@ -2014,6 +2036,127 @@ frühere Filter auf `status === "ACTIVE"` griff nicht: Der Status bleibt nach de
|
||||
Referenz: `src/lib/retirement.ts`, `src/components/RetirementAdjuster.tsx`,
|
||||
`src/components/FormField.tsx` (`InheritableField`).
|
||||
|
||||
## 3.13 Pensionierung
|
||||
|
||||
### 3.13.1 Warum sie eine Eigenschaft der Person ist
|
||||
|
||||
Bis 0.34 war die Pensionierung als Eigenschaft der **Zeitachse** modelliert: ein Phasenübergang,
|
||||
an dem verstreut in drei Matrix-Zellen (AHV, PK, 3a) je ein Entscheid hing – gespeichert unter
|
||||
`transitionValues[phaseId]`, also am Schlüssel Element × Phasen-ID. Daraus folgte fast alles,
|
||||
was an der Pensionsplanung störte:
|
||||
|
||||
- Entscheide, die inhaltlich **eine** Frage sind, lagen räumlich weit auseinander.
|
||||
- Das Alter zu ändern war ein **struktureller** Eingriff. Beim Zusammenlegen zweier Übergänge
|
||||
mussten Entscheide über `mergeTransition` gerettet werden – verlustbehaftet.
|
||||
- Ein Szenario nur für ein anderes Pensionsalter hiess: alle Entscheide erneut treffen. Genau
|
||||
dafür legt man aber Szenarien an.
|
||||
- Ziel-Solver (Roadmap Nr. 21) und Live-Simulation hatten **nichts zum Anfassen**.
|
||||
|
||||
Der Denkfehler: Die Pensionierung ist keine Eigenschaft der Zeitachse. **Sie ist eine
|
||||
Eigenschaft der Person** – die Zeitachse ist die Folge davon.
|
||||
|
||||
Seit 0.35 liegt der Entscheid deshalb am **Element** (`FinancialElement.retirementDecision`,
|
||||
ohne Phasenbezug) und gilt für die Pensionierung des Besitzers, wo immer die gerade liegt.
|
||||
Ohne Phasen-ID im Schlüssel überlebt er **jede** Zeitachsen-Änderung: Alter verschieben,
|
||||
Phase zusammenlegen, Szenario kopieren.
|
||||
|
||||
Was **nicht** dort hineingehört: Vorbezüge (PK/3a vor der Pensionierung), Verkäufe, Tilgungen,
|
||||
der Cash-Übergang. Das sind echte Ereignisse an einer bestimmten Grenze und bleiben in
|
||||
`transitionValues`.
|
||||
|
||||
### 3.13.2 Kein leeres Formular
|
||||
|
||||
Ein Widerspruch steckt in der Sache: Der **Zeitpunkt** muss früh feststehen (er definiert die
|
||||
Phasengrenze), die **Bezugsentscheide** lassen sich aber erst beurteilen, wenn bis dahin geplant
|
||||
ist. Ein Formular, das am Anfang leer dasteht, verlangt also Antworten, die noch niemand geben
|
||||
kann – und der Plan wäre bis dahin nicht einmal rechenbar.
|
||||
|
||||
Auflösung: **ein vollständiger Vorschlag, den man korrigiert.** `withRetirementDefaults` füllt
|
||||
jeden Entscheid mit einer Vorgabe, die für die meisten Fälle richtig ist:
|
||||
|
||||
| Säule | Vorgabe | warum diese |
|
||||
|---|---|---|
|
||||
| AHV | Bezug ab Referenzalter | der gesetzliche Normalfall |
|
||||
| Pensionskasse | `capitalSharePct = 0` (volle Rente) | die Rente ist die Regel; ein Kapitalbezug ist der begründungspflichtige Fall |
|
||||
| Säule 3a | `withdrawalAge` = Pensionsalter, gekappt auf 60–70 | wer mit 58 aufhört, kann die 3a trotzdem erst mit 60 beziehen |
|
||||
|
||||
Die Rentenlücke steht damit ab der ersten Sekunde da und wird mit jeder geplanten Phase genauer.
|
||||
Dass eine Vorgabe nicht dasselbe ist wie ein Entscheid, hält die **dreistufige Ampel** fest
|
||||
([3.5.3](#353-ampel-logik-offene-entscheide)).
|
||||
|
||||
### 3.13.3 Der Bildschirm
|
||||
|
||||
Eigene Ansicht, gleichrangig **neben** der Matrix (Umschalter darüber), plus eine Kurzfassung in
|
||||
der Matrix-Ansicht, die dorthin führt. Aufbau in der Reihenfolge, in der man tatsächlich denkt:
|
||||
*Wann höre ich auf? → Was kommt dann rein? → Was habe ich auf einen Schlag? → Reicht das?
|
||||
→ Was mache ich mit dem Haufen?*
|
||||
|
||||
Die Leitzahl ist die **Rentenlücke**. Sie ist keine neue Rechnung, sondern die Verzehrquote im
|
||||
ersten Jahr, in dem niemand mehr arbeitet – Renteneinkommen minus Ausgaben. Genau das rechnet
|
||||
die Jahresschleife ohnehin; es fehlte nur der Begriff. Gerechnet wird sie im **Rechenkern**
|
||||
(`PlanComputed.retirement`), nicht im UI: Eine Nebenrechnung in der Komponente hätte dieselbe
|
||||
Driftgefahr wie bei den Rechenwegen ([4.14.3](#4143-rechenweg-protokoll)) und fiele im
|
||||
PDF-Bericht anders aus.
|
||||
|
||||
Daneben steht die zweite Zahl, **wie lange das Vermögen trägt** (`ruinAge`, bzw. «über die ganze
|
||||
Planung»).
|
||||
|
||||
**Die Matrix-Zellen bleiben bedienbar.** AHV, PK und 3a am Pensions-Übergang zeigen exakt
|
||||
dieselbe Komponente (`RetirementFields`) – zwei Ansichten auf dasselbe Objekt, kein Duplikat.
|
||||
Wären es zwei Implementierungen, liefen sie auseinander.
|
||||
|
||||
### 3.13.4 Der Feldsatz
|
||||
|
||||
| Säule | Felder |
|
||||
|---|---|
|
||||
| **AHV** | `ahvDraw` (`EARLY`/`REFERENCE`/`DEFERRED`), `ahvMonths`, `ahvSharePct` (Teilbezug 20–80 %); dazu abgesetzt als *Grundlage der Schätzung*: `avgIncomeBefore`, `gapYearsBefore` |
|
||||
| **Pensionskasse** | `capitalSharePct` (0–100 %), `conversionRate`, `capitalTaxRate`, `recentBuyIn` (Hinweis-Flag), Kapitalverwendung `capitalUse*` |
|
||||
| **Säule 3a** | `withdrawalAge` (60–70), `capitalTaxRate`, Kapitalverwendung `capitalUse*` |
|
||||
| **alle** | `confirmed` |
|
||||
|
||||
Kürzung, Zuschlag und die Aufteilung Kapital/Rente sind **gerechnet, nicht erfasst** – ihre
|
||||
Formeln stehen in [4.4.7](#447-referenzalter-die-rente-beginnt-mit-65-nicht-mit-der-pensionierung)
|
||||
und [4.9.1](#491-pension_fund).
|
||||
|
||||
Die Beitragskarriere vor Planbeginn lag bis 0.34 an **zwei** Orten – in der Übergangszelle und,
|
||||
für bei Planbeginn bereits Pensionierte, in der Phasenzelle der ersten Phase – mit zwei
|
||||
Codepfaden für dieselbe Frage. Jetzt an einem.
|
||||
|
||||
### 3.13.5 Planungshorizont
|
||||
|
||||
`Person.planningHorizonAge` macht das Planende **explizit**. Bisher ergab es sich stillschweigend
|
||||
aus der Summe der Phasendauern: Zwei Szenarien konnten dadurch unbemerkt verschieden weit
|
||||
rechnen und waren nicht vergleichbar – obwohl das ihr Zweck ist.
|
||||
|
||||
Die Mechanik ist dieselbe wie beim Pensionsalter: **Zahl stellen, Struktur folgt.** Die letzte
|
||||
Lebensphase wird so angepasst, dass der Plan bis zum Horizont läuft (`planHorizonChange`,
|
||||
`POST /api/scenarios/<id>/horizon`). Fiele sie dabei unter ein Jahr, wird blockiert. Bei zwei
|
||||
Personen läuft der Plan bis zum **spätesten** Horizont.
|
||||
|
||||
Bewusst neutral formuliert («Planungshorizont», nicht «Sterbealter»). Der Default liegt über der
|
||||
Lebenserwartung: Eine zu kurze Planung sieht tragfähig aus, obwohl das Geld nur nicht lange
|
||||
genug reichen muss.
|
||||
|
||||
### 3.13.6 Was bewusst nicht abgebildet wird
|
||||
|
||||
- **Teilpensionierung in Schritten.** Gesetzlich seit AHV 21 in bis zu drei Schritten möglich
|
||||
(Art. 13a BVG). Voll modelliert hiesse, dass eine Person teilweise erwerbstätig ist – das
|
||||
bricht die Invariante, dass jede Pensionierung auf einer Phasengrenze liegt
|
||||
([4.16.1](#4161-die-tragende-invariante)). Wer stufenweise aufhört, bildet das heute über
|
||||
einen Teilzeit-Lohn und eine Kapitalquote ab.
|
||||
- **Progressive Kapitalbezugssteuer.** Alle Bezüge desselben Jahres werden zusammengezählt, bei
|
||||
Ehepaaren auch die des Partners. Das Tool rechnet mit einem Pauschalsatz und weist auf die
|
||||
Staffelung hin (Roadmap Nr. 14, siehe [9.14](#914-keine-steuerschätzung)).
|
||||
- **Die 3-Jahres-Sperrfrist nach einem PK-Einkauf** (Art. 79b Abs. 3 BVG). Das Tool kennt keine
|
||||
Einkäufe und kann die Frist deshalb nicht prüfen – statt einer Automatik gibt es die Frage
|
||||
«in den letzten drei Jahren eingekauft?» und einen Warnhinweis.
|
||||
- **Reglementarische Grenzen** (Mindestalter 58 vs. 60, Kapitalquote 25 % vs. 100 %,
|
||||
Aufschubmöglichkeit) werden als Hinweis gezeigt, nicht als Sperre: Ein Planungstool, das den
|
||||
Fall verbietet, den die eigene Kasse erlaubt, wäre falsch.
|
||||
|
||||
Referenz: `src/lib/retirement-decision.ts`, `src/components/RetirementPanel.tsx`,
|
||||
`src/components/RetirementFields.tsx`, `src/lib/retirement.ts` (`planHorizonChange`).
|
||||
|
||||
---
|
||||
|
||||
# 4. Berechnungsmodell
|
||||
@@ -2241,27 +2384,51 @@ Daraus folgen drei Fälle:
|
||||
| Pensionierung **vor 65** | Die Pensionsphase beginnt vor 65. Bis dahin ist die Person **beitragspflichtig als Nichterwerbstätige(r)**; der Beitrag (`ahvContribution`) ist eine laufende Ausgabe. Mit 65 fällt er weg und die Rente setzt ein. |
|
||||
|
||||
Beide Wechsel können **innerhalb derselben Lebensphase** stattfinden. Die AHV wird deshalb
|
||||
**jahresweise** ausgewertet statt als Phasenkonstante:
|
||||
**jahresweise** ausgewertet statt als Phasenkonstante.
|
||||
|
||||
### Vorbezug und Aufschub (seit 0.35)
|
||||
|
||||
Bis 0.34 begann die Rente **immer** mit 65. Das war schlicht falsch: Wer mit 62 aufhörte, bekam
|
||||
die ungekürzte Rente erst drei Jahre später; wer bis 68 arbeitete, verschenkte den Zuschlag.
|
||||
Der Bezugszeitpunkt ist seither Teil des Pensionierungs-Entscheids
|
||||
([3.13](#313-pensionierung)) und wird gerechnet:
|
||||
|
||||
| | Regel | Grenzen |
|
||||
|---|---|---|
|
||||
| **Vorbezug** | **−6,8 % pro Jahr**, lebenslang, monatlich anteilig | höchstens 36 Monate (ab 62) |
|
||||
| **Aufschub** | **+5,2 / 10,8 / 17,1 / 24,0 / 31,5 %** nach 1–5 Jahren; dazwischen linear interpoliert | 12 bis 60 Monate |
|
||||
| **Teilbezug** | Kürzung bzw. Zuschlag wirken nur auf den bezogenen Anteil | 20–80 % |
|
||||
|
||||
Der Faktor greift **nach** der Ehepaar-Plafonierung: Der Plafond gilt für die ordentlichen
|
||||
Renten, die individuelle Kürzung setzt darauf auf.
|
||||
|
||||
**Rentenbeginn und Beitragspflicht sind zwei verschiedene Alter** – eine Trennung, die es vor
|
||||
0.35 gar nicht gab. Der Vorbezug zieht nur den *Rentenbeginn* vor; die Beitragspflicht als
|
||||
Nichterwerbstätige(r) endet unabhängig davon erst mit dem **Referenzalter**. Wer mit 62 aufhört
|
||||
und ab 63 vorbezieht, bezieht ab 63 **und** zahlt bis 65 weiter:
|
||||
|
||||
```
|
||||
für jedes Jahr t der Phase:
|
||||
alterImJahr = alterZuPhasenbeginn + t − 1
|
||||
alterImJahr ≥ 65 → Rente fliesst (Einkommen)
|
||||
sonst, wenn pensioniert → Beitrag fällt an (Ausgabe, wirkt auf die Verzehrquote)
|
||||
sonst → nichts
|
||||
alterImJahr ≥ individuellesRentenalter → Rente fliesst (Einkommen)
|
||||
nicht erwerbstätig ∧ alterImJahr < 65 → Beitrag fällt an (Ausgabe)
|
||||
```
|
||||
|
||||
Der Verlaufspunkt des AHV-Elements zeigt den Beitrag als **negativen** Wert – so ist in der
|
||||
Grafik zu sehen, dass die AHV in diesen Jahren Geld kostet, statt welches zu bringen.
|
||||
Beide Bedingungen können im selben Jahr gelten. Der Verlaufspunkt des AHV-Elements zeigt die
|
||||
Differenz; ein reiner Beitrag erscheint **negativ**, damit in der Grafik sichtbar ist, dass die
|
||||
AHV in diesen Jahren Geld kostet, statt welches zu bringen.
|
||||
|
||||
**Ein Aufschub der Rente wird nicht abgebildet.** Wer über 65 hinaus arbeitet, könnte den Bezug
|
||||
aufschieben und erhielte dafür einen Zuschlag. Das Tool lässt die Rente stattdessen fliessen –
|
||||
die vorsichtigere Annahme, und eine, die keinen zusätzlichen Entscheid verlangt.
|
||||
**Für das Jahresraster wird das Rentenalter gerundet.** Die Monatsgenauigkeit steckt im
|
||||
*Faktor*, nicht im Auszahlungszeitpunkt – FPT rechnet durchgehend jahresweise.
|
||||
|
||||
**Nicht abgebildet:** die seit 1.1.2025 tieferen, einkommensabhängigen Kürzungssätze für Frauen
|
||||
der Übergangsgeneration (Jahrgänge 1961–1969). Sie laufen aus, und ihre Nachbildung erforderte
|
||||
eine zweite, jahrgangsabhängige Rentenformel.
|
||||
|
||||
**Der Beitrag als Nichterwerbstätige(r)** bemisst sich am Vermögen und am Renteneinkommen, nicht
|
||||
am Lohn. Die Bandbreite ist entsprechend enorm: vom Mindestbeitrag von rund **530 CHF/Jahr** bis
|
||||
zum Höchstbeitrag von rund **26'500 CHF/Jahr**. Deshalb gibt es hier **keinen Default** – ein
|
||||
stiller Vorschlag würde nicht hinterfragt (vgl. [9.15](#915-defaults-für-annahmen-sind-gefährlich)).
|
||||
stiller Vorschlag würde nicht hinterfragt (vgl. [9.15](#915-monte-carlo-misst-risiko-um-die-annahmen-nicht-deren-richtigkeit)).
|
||||
Der Hilfetext nennt die Bandbreite ausdrücklich, damit auch jemand ohne Vorwissen ein Gefühl für
|
||||
die Grössenordnung bekommt.
|
||||
|
||||
@@ -2536,13 +2703,28 @@ ownerRetiresNext = owner existiert
|
||||
|
||||
### 4.9.1 PENSION_FUND
|
||||
|
||||
**Pensions-Übergang** (`ownerRetiresNext`), `value = ec.endValue`, Default-Modus `PENSION`:
|
||||
**Pensions-Übergang** (`ownerRetiresNext`), `value = ec.endValue`. Gelesen wird der
|
||||
**Pensionierungs-Entscheid des Elements** ([3.13](#313-pensionierung)), nicht die Übergangszelle:
|
||||
|
||||
| `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` |
|
||||
```
|
||||
sharePct = clamp(retirementDecision.capitalSharePct ?? 0, 0, 100)
|
||||
capital = round(value × sharePct / 100)
|
||||
rest = value − capital
|
||||
|
||||
falls capital > 0:
|
||||
netto = round(capital × (1 − capitalTaxRate/100))
|
||||
txInflow += netto
|
||||
txTax += capital − netto
|
||||
-> geht in die Kapitalverwendung (Punkt C, Kap. 3.12.5)
|
||||
|
||||
carry.pkPensionAnnual = rest > 0 ? round(rest × conversionRate / 100) : 0
|
||||
carry.value = 0
|
||||
```
|
||||
|
||||
Eine **Quote** statt des früheren Modus (`PENSION`/`CAPITAL`/`COMBI`) plus Frankenbetrag:
|
||||
0 % ist die volle Rente, 100 % der volle Kapitalbezug, alles dazwischen die Kombination.
|
||||
Der Grund ist derselbe wie bei Punkt C – verschiebt man das Pensionsalter, ändert sich das
|
||||
Guthaben. Ein fixer Betrag bedeutete dann still ein anderes Verhältnis, eine Quote skaliert mit.
|
||||
|
||||
**Normaler Übergang (Vorbezug)** – brutto entnommen, netto ins Cash:
|
||||
```
|
||||
@@ -2553,10 +2735,26 @@ 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.
|
||||
**Bezug am gewählten Alter** (seit 0.35): Der Bezug hängt nicht mehr starr am Pensions-Übergang,
|
||||
sondern am `withdrawalAge` des Pensionierungs-Entscheids (60–70, Vorgabe = Pensionsalter).
|
||||
Gezogen wird an der **ersten Phasengrenze bei oder nach** diesem Alter; liegt der Wunsch hinter
|
||||
dem Planende, greift die letzte Grenze, damit kein Guthaben unbezogen liegen bleibt.
|
||||
|
||||
```
|
||||
netto = round(ec.endValue × (1 − capitalTaxRate/100))
|
||||
txInflow += netto
|
||||
carry.value = 0
|
||||
```
|
||||
|
||||
Warum das Alter und nicht ein Ja/Nein: Ein 3a-Konto lässt sich bei der Pensionierung nur
|
||||
**ganz** auflösen, und alle Kapitalbezüge desselben Jahres werden steuerlich zusammengezählt.
|
||||
Gestaffelt wird deshalb über mehrere Konten mit unterschiedlichen Bezugsjahren – dieses Feld
|
||||
ist die einzige Stellschraube dafür. Innerhalb einer Phase kennt das Modell kein
|
||||
Einzelereignis; wer exakt staffeln will, setzt eine Phasengrenze.
|
||||
|
||||
**Normaler Übergang (Vorbezug)**: unverändert in der Übergangszelle – Bruttoentnahme,
|
||||
Netto-Zufluss nach Kapitalbezugssteuer. Ein Vorbezug ist ein Ereignis an einer bestimmten
|
||||
Grenze und gehört deshalb nicht zum Pensionierungs-Entscheid.
|
||||
|
||||
### 4.9.3 OTHER_ASSET
|
||||
|
||||
@@ -4031,7 +4229,7 @@ Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests
|
||||
| `migrations.test.ts` | 3 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-**Datenübernahme** (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen) |
|
||||
| `rate-limit.test.ts` | 6 | Fixed-Window: erlaubt bis Limit, blockt danach, startet nach Fensterablauf neu, trennt je Schlüssel; Client-IP aus X-Forwarded-For / X-Real-IP |
|
||||
| `csv.test.ts` | 7 | BOM, alle vier Blöcke, jedes Element als Zeile, Beginn-/Ende-/Übergangsspalten, Entscheid im Klartext, ein Eintrag je Planjahr, Maskierung von `;` und `"` |
|
||||
| **Total** | **292** | |
|
||||
| **Total** | **315** | |
|
||||
|
||||
## 8.2 Testfälle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user