Pensionsalter anpassen (Roadmap 44)
Deploy App / deploy (push) Successful in 1m1s

Das Pensionsalter laesst sich neu veraendern: Nicht das Alter wird gesetzt,
sondern die Phasengrenze verschoben -- Vorphase laenger, Folgephase kuerzer,
Gesamtdauer gleich. Faellt eine Phase dabei weg, werden die beiden Uebergaenge
nach Bestaetigung zusammengelegt.

- neues reines Modul lib/retirement.ts + POST /api/scenarios/<id>/retirement
- Pensionsalter als Tornado-Treiber und Live-Simulations-Regler
- AHV-Referenzalter 65: Rente ab 65 unabhaengig vom Pensionsalter; vor 65
  Beitrag als Nichterwerbstaetige(r) (neues Feld ahvContribution).
  AHV wird dafuer jahresweise statt phasenweise gerechnet.
- Punkt B: personenzugeordnetes Einkommen faellt bei Pensionierung auf 0
- Punkt A: Wiederkehr-Parameter werden live aus der Vorphase geerbt,
  sichtbar als "Aus Vorphase uebernehmen"
- Punkt C: Kapitalzufluss am Pensions-Uebergang per Quote auf Amortisation,
  Anlage und Cash verteilbar
- Fix: carry.flowBasis wurde vor der Jahresschleife berechnet, effektive
  Werte kamen deshalb nie in der Folgephase an

SPEZIFIKATION 0.26 (neue Kapitel 3.12, 4.4.7, 4.16). 221 -> 261 Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:24:40 +02:00
parent c429624635
commit d6855ef1a0
20 changed files with 2057 additions and 184 deletions
+53
View File
@@ -58,6 +58,9 @@ export interface PhaseData {
teuerungsausgleich?: number;
// AHV
gapYears?: number;
// Nur AHV, nur in Phasen NACH einer Fruehpensionierung: jaehrlicher Beitrag als
// Nichterwerbstaetige(r) bis zum Referenzalter. Faellt als Kosten an (Kap. 4.4.6).
ahvContribution?: number;
// AHV, nur wenn die Person bei Planbeginn BEREITS pensioniert ist (dann gibt es keinen
// Pensions-Übergang, an dem die Karriere geprüft werden könnte): Beitragskarriere.
// avgIncomeBefore ist REAL (heutige Kaufkraft).
@@ -135,6 +138,14 @@ export interface CashTransitionData {
// Einmalige Kosten (z. B. Poolbau): REAL erfasst (heutige Kaufkraft).
outflowLabel?: string;
outflowAmount?: number;
// Verwendung des Kapitalzuflusses am Pensions-Übergang (Roadmap Nr. 44, Punkt C).
// Bewusst in PROZENT und nicht in Franken: Verschiebt man das Pensionsalter, ändert sich
// das bezogene Kapital -- eine Franken-Angabe müsste man dann von Hand nachziehen, eine
// Quote skaliert mit. Der nicht zugeteilte Rest bleibt Cash.
capitalUseAmortizationPct?: number;
capitalUseInvestPct?: number;
// Ziel der Anlage-Quote; ohne Angabe das erste aktive «Sonstiges Vermögen».
capitalUseTargetElementId?: string;
}
// --- Zod-Schemas (nachsichtig: unbekannte Felder werden verworfen) ---
@@ -149,6 +160,9 @@ export const cashTransitionSchema = z
inflowTaxRate: z.number().min(0).max(100).optional(),
outflowLabel: z.string().max(120).optional(),
outflowAmount: nonNeg.optional(),
capitalUseAmortizationPct: z.number().min(0).max(100).optional(),
capitalUseInvestPct: z.number().min(0).max(100).optional(),
capitalUseTargetElementId: z.string().max(60).optional(),
})
.strip();
@@ -157,6 +171,7 @@ export const phaseDataSchema = z
amount: nonNeg.optional(),
teuerungsausgleich: z.number().min(-20).max(50).optional(),
gapYears: z.number().int().min(0).optional(),
ahvContribution: nonNeg.optional(),
avgIncomeBefore: nonNeg.optional(),
gapYearsBefore: z.number().int().min(0).max(50).optional(),
currentValue: nonNeg.optional(),
@@ -198,3 +213,41 @@ export const transitionDataSchema = z
export function num(value: number | undefined | null, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
// --- Vererbung der Wiederkehr-Parameter (Roadmap Nr. 44, Punkt A) -----------------------
//
// Raten, Beitraege, Amortisation, Wertsteigerung und Zinssatz gelten weiter, bis man sie
// bewusst aendert: Fehlt der Wert in einer Phase, gilt der aus der Vorphase. Genau diese
// Regel wendet auch `computePlan` an -- hier steht sie fuer das UI, damit die Anzeige
// "Aus Vorphase uebernehmen: <Wert>" nicht raten muss.
export const INHERITABLE_KEYS = [
"teuerungsausgleich",
"expectedReturn",
"annualContribution",
"annualWithdrawal",
"amortization",
"valueGrowth",
"interestRate",
"annualRepayment",
] as const;
export type InheritableKey = (typeof INHERITABLE_KEYS)[number];
// Wert, der in `phaseId` gelten wuerde, wenn das Feld dort leer bleibt. `orderedPhaseIds`
// muss in Phasenreihenfolge vorliegen.
export function inheritedPhaseValues(
phaseValues: Record<string, PhaseData>,
orderedPhaseIds: string[],
phaseId: string
): Record<string, number> {
const out: Record<string, number> = {};
for (const id of orderedPhaseIds) {
if (id === phaseId) break;
const pd = phaseValues[id] ?? {};
for (const key of INHERITABLE_KEYS) {
const v = pd[key];
if (typeof v === "number") out[key] = v;
}
}
return out;
}