Pensionierung: Entscheid ans Element, AHV-Vorbezug/Aufschub, PK-Kapitalquote
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+237
-80
@@ -10,11 +10,21 @@ import {
|
||||
DEFAULT_CAPITAL_TAX_RATE,
|
||||
DEFAULT_PK_CONVERSION_RATE,
|
||||
DEFAULT_PROPERTY_GAINS_TAX_RATE,
|
||||
PILLAR_3A_MAX_WITHDRAWAL_AGE,
|
||||
PILLAR_3A_MIN_WITHDRAWAL_AGE,
|
||||
} from "@/lib/constants";
|
||||
import { num } from "@/lib/elements";
|
||||
import { actualsForYear, rebaseFlow, type ResolvedActuals } from "@/lib/actuals";
|
||||
import type { ElementCategory, TransitionData } from "@/lib/elements";
|
||||
import type { PersonRole, PlanInput } from "@/lib/types";
|
||||
import {
|
||||
ahvDrawLabel,
|
||||
ahvFactor,
|
||||
ahvShiftMonths,
|
||||
ahvStartAge,
|
||||
withRetirementDefaults,
|
||||
type RetirementDecision,
|
||||
} from "@/lib/retirement-decision";
|
||||
import type { ElementCategory } from "@/lib/elements";
|
||||
import type { ElementInput, PersonRole, PlanInput } from "@/lib/types";
|
||||
|
||||
export type PhaseType = "ERWERB" | "PENSION" | "MIXED";
|
||||
export type ElementStatus = "ACTIVE" | "SOLD" | "SETTLED";
|
||||
@@ -215,10 +225,37 @@ export interface YearPoint {
|
||||
cash: number;
|
||||
}
|
||||
|
||||
// Kennzahlen des Pensionierungs-Bildschirms. Bewusst im Rechenkern und nicht im UI: Es sind
|
||||
// die beiden Zahlen, an denen die ganze Planung haengt -- eine Nebenrechnung in der
|
||||
// Komponente wuerde im PDF-Bericht anders ausfallen.
|
||||
export interface RetirementPersonSummary {
|
||||
personId: string;
|
||||
role: PersonRole;
|
||||
retirementAge: number;
|
||||
planningHorizonAge: number | null;
|
||||
ahvAnnual: number; // inkl. Kuerzung bei Vorbezug bzw. Zuschlag bei Aufschub
|
||||
ahvFromAge: number; // individuelles Rentenalter, nicht zwingend das Referenzalter
|
||||
ahvDraw: string; // lesbare Kurzfassung ("Referenzalter", "Vorbezug 2 J. (-13.6 %)")
|
||||
pkPensionAnnual: number;
|
||||
capitalAtRetirement: number; // PK-Kapital + Saeule 3a, NETTO nach Kapitalbezugssteuer
|
||||
}
|
||||
|
||||
export interface RetirementOverview {
|
||||
// Planjahr, ab dem NIEMAND mehr erwerbstaetig ist. null, solange jemand arbeitet.
|
||||
firstRetirementYear: number | null;
|
||||
pensionIncome: number | null;
|
||||
expenses: number | null;
|
||||
// Renteneinkommen minus Ausgaben im ersten voll pensionierten Jahr. Negativ = Luecke, die
|
||||
// aus dem Vermoegen zu decken ist.
|
||||
gapAnnual: number | null;
|
||||
perPerson: RetirementPersonSummary[];
|
||||
}
|
||||
|
||||
export interface PlanComputed {
|
||||
phases: PhaseComputed[];
|
||||
yearly: YearPoint[];
|
||||
nachlass: number;
|
||||
retirement: RetirementOverview;
|
||||
ruinAge: number | null; // Alter (Person A), in dem das Gesamtvermögen (inkl. Cash) erstmals < 0 fällt
|
||||
ahvCareer: AhvCareer[]; // Beitragskarriere je Person (für die AHV-Prüfung am Übergang)
|
||||
traces?: Trace[]; // plan-weite Rechenwege: Deflatoren, AHV-Karriere, Ruinalter (nur mit explain)
|
||||
@@ -405,11 +442,32 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
// AHV-Beitragskarriere je Person: reales Einkommen x Beitragsjahre, und Beitragsjahre.
|
||||
const ahvIncomeAccum = new Map<string, number>();
|
||||
const ahvYearsAccum = new Map<string, number>();
|
||||
// Karriere VOR Planbeginn -- aus der Prüfung am Pensions-Übergang bzw. (für bereits
|
||||
// Pensionierte) aus der Phasenzelle der ersten Phase.
|
||||
const ahvBeforeByPerson = new Map<string, { avg: number; gap: number }>();
|
||||
// Karriere VOR Planbeginn und der Bezugs-Entscheid, je Person. Beides steht seit 0.34 im
|
||||
// Pensionierungs-Entscheid des AHV-Elements -- vorher lag es an ZWEI Orten (Übergangszelle
|
||||
// bzw., für bei Planbeginn bereits Pensionierte, Phasenzelle der ersten Phase), was zwei
|
||||
// Codepfade für dieselbe Frage bedeutete.
|
||||
// avg === null heisst NICHT ERFASST -- dann gilt der geplante Durchschnitt als Schaetzung
|
||||
// (siehe unten). Ein Fallback auf 0 wuerde die Rente still und massiv zu tief rechnen.
|
||||
const ahvBeforeByPerson = new Map<string, { avg: number | null; gap: number }>();
|
||||
const ahvDrawByPerson = new Map<string, RetirementDecision>();
|
||||
for (const e of plan.elements) {
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
const owner = personByRole(persons, e.ownerRole);
|
||||
if (!owner) continue;
|
||||
const rd = withRetirementDefaults("AHV", retirementAge.get(owner.id) ?? AHV_REFERENCE_AGE, e.retirementDecision);
|
||||
ahvDrawByPerson.set(owner.id, rd);
|
||||
ahvBeforeByPerson.set(owner.id, {
|
||||
avg: typeof rd.avgIncomeBefore === "number" ? rd.avgIncomeBefore : null,
|
||||
gap: Math.max(0, Math.round(num(rd.gapYearsBefore))),
|
||||
});
|
||||
}
|
||||
const carries = new Map<string, Carry>();
|
||||
for (const e of plan.elements) carries.set(e.id, emptyCarry());
|
||||
// Fuer die Pensionierungs-Uebersicht: die zuletzt gerechnete AHV-Rente je Person und das
|
||||
// netto bezogene Alterskapital (PK + 3a). Beides faellt in der Phasen- bzw. Uebergangs-
|
||||
// schleife ohnehin an -- gesammelt wird es hier, damit es am Ende zur Verfuegung steht.
|
||||
const ahvFinalByPerson = new Map<string, number>();
|
||||
const capitalNetByPerson = new Map<string, number>();
|
||||
|
||||
const result: PhaseComputed[] = [];
|
||||
const yearly: YearPoint[] = [];
|
||||
@@ -471,20 +529,6 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
gapYearsByPerson.set(owner.id, (gapYearsByPerson.get(owner.id) ?? 0) + gy);
|
||||
}
|
||||
|
||||
// Bereits bei Planbeginn pensioniert: es gibt keinen Pensions-Übergang, an dem die
|
||||
// Beitragskarriere geprüft werden könnte -- die Werte liegen dann in der Phasenzelle.
|
||||
for (const e of plan.elements) {
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
const owner = personByRole(persons, e.ownerRole);
|
||||
if (!owner || workingByPerson.get(owner.id)) continue;
|
||||
if (ahvBeforeByPerson.has(owner.id)) continue; // aus dem Übergang bereits gesetzt
|
||||
const pd = e.phaseValues[phase.id] ?? {};
|
||||
ahvBeforeByPerson.set(owner.id, {
|
||||
avg: num(pd.avgIncomeBefore),
|
||||
gap: Math.max(0, Math.round(num(pd.gapYearsBefore))),
|
||||
});
|
||||
}
|
||||
|
||||
// AHV-Renten: Vollrente zum mdJE, gekürzt um die Ausfalljahre.
|
||||
//
|
||||
// Seit Roadmap Nr. 44 wird die Rente für JEDE Person mit AHV-Element gerechnet, nicht nur
|
||||
@@ -496,9 +540,12 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (e.category !== "AHV" || !e.ownerRole) continue;
|
||||
const owner = personByRole(persons, e.ownerRole);
|
||||
if (!owner) continue;
|
||||
const before = ahvBeforeByPerson.get(owner.id) ?? { avg: 0, gap: 0 };
|
||||
const before = ahvBeforeByPerson.get(owner.id) ?? { avg: null, gap: 0 };
|
||||
const career = buildCareer(owner, ahvIncomeAccum, ahvYearsAccum, gapYearsByPerson);
|
||||
const mdJE = ahvMdje(career, before.avg, before.gap);
|
||||
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schaetzung fuer die Jahre vor
|
||||
// Planbeginn -- exakt der Wert, den das UI vorbelegt. Beide Groessen sind BRUTTO.
|
||||
const avgBefore = before.avg ?? career.plannedAvgGrossIncome;
|
||||
const mdJE = ahvMdje(career, avgBefore, before.gap);
|
||||
const totalGap = (gapYearsByPerson.get(owner.id) ?? 0) + before.gap;
|
||||
ahvUncapped.set(owner.id, ahvAnnualPension(mdJE, totalGap));
|
||||
}
|
||||
@@ -508,6 +555,13 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const cap = AHV_MAX_ANNUAL_SINGLE * AHV_COUPLE_CAP_FACTOR;
|
||||
if (sum > cap && sum > 0) for (const [pid, v] of ahvUncapped) ahvFinal.set(pid, Math.round(v * (cap / sum)));
|
||||
}
|
||||
// Vorbezug kürzt, Aufschub erhöht -- und zwar NACH dem Ehepaar-Plafond: Der Plafond gilt
|
||||
// für die ordentlichen Renten, die individuelle Kürzung setzt darauf auf.
|
||||
for (const [pid, v] of [...ahvFinal]) {
|
||||
const f = ahvFactor(ahvDrawByPerson.get(pid) ?? {});
|
||||
if (f !== 1) ahvFinal.set(pid, Math.round(v * f));
|
||||
}
|
||||
for (const [pid, v] of ahvFinal) ahvFinalByPerson.set(pid, v);
|
||||
|
||||
// --- Element-Laufzeitzustände aufbauen ---
|
||||
const orderedElements = [...plan.elements].sort((a, b) => a.orderIndex - b.orderIndex);
|
||||
@@ -522,6 +576,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const ahvItems: {
|
||||
ownerId: string;
|
||||
ownerStartAge: number;
|
||||
// Alter, ab dem die Rente DIESER Person fliesst. Bis 0.33 war das fix das
|
||||
// Referenzalter; mit Vorbezug/Aufschub ist es individuell. Für das Jahresraster
|
||||
// gerundet -- die Monatsgenauigkeit steckt im Faktor, nicht im Auszahlungszeitpunkt.
|
||||
renteFromAge: number;
|
||||
rente: number;
|
||||
beitrag: number;
|
||||
working: boolean;
|
||||
@@ -661,29 +719,46 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (owner) {
|
||||
const rente = ahvFinal.get(owner.id) ?? 0;
|
||||
const beitrag = Math.round(num(pd.ahvContribution));
|
||||
const rd = ahvDrawByPerson.get(owner.id) ?? {};
|
||||
// Rentenbeginn individuell (Vorbezug/Aufschub), Beitragspflicht dagegen IMMER bis
|
||||
// zum Referenzalter: Wer die Rente vorbezieht, ist damit nicht von den Beiträgen
|
||||
// als Nichterwerbstätige(r) befreit -- die beiden Alter sind zu trennen.
|
||||
const renteFromAge = Math.round(ahvStartAge(rd));
|
||||
const ageStart = owner.age + yearsBefore; // Alter im ersten Jahr der Phase
|
||||
const ageEnd = ageStart + duration - 1; // Alter im letzten Jahr der Phase
|
||||
ahvItems.push({ ownerId: owner.id, ownerStartAge: ageStart, rente, beitrag, working: ownerWorking, ec });
|
||||
ahvItems.push({
|
||||
ownerId: owner.id,
|
||||
ownerStartAge: ageStart,
|
||||
renteFromAge,
|
||||
rente,
|
||||
beitrag,
|
||||
working: ownerWorking,
|
||||
ec,
|
||||
});
|
||||
|
||||
const reachesRef = ageEnd >= AHV_REFERENCE_AGE;
|
||||
const startsRetired = ageStart >= AHV_REFERENCE_AGE;
|
||||
ec.startValue = startsRetired ? rente : 0;
|
||||
ec.endValue = reachesRef ? rente : 0;
|
||||
const reachesRente = ageEnd >= renteFromAge;
|
||||
const startsWithRente = ageStart >= renteFromAge;
|
||||
const owesContribution = !ownerWorking && ageStart < AHV_REFERENCE_AGE;
|
||||
ec.startValue = startsWithRente ? rente : 0;
|
||||
ec.endValue = reachesRente ? rente : 0;
|
||||
|
||||
if (startsRetired) {
|
||||
if (startsWithRente) {
|
||||
ec.summary = `Rente ${fmt(rente)}`;
|
||||
} else if (reachesRef && !ownerWorking) {
|
||||
} else if (reachesRente && owesContribution) {
|
||||
ec.summary = `Beitrag ${fmt(beitrag)} → Rente ${fmt(rente)}`;
|
||||
ec.note = `Rente ab Alter ${AHV_REFERENCE_AGE}; bis dahin Beitrag als Nichterwerbstätige(r).`;
|
||||
} else if (reachesRef) {
|
||||
ec.summary = `Rente ab ${AHV_REFERENCE_AGE} ${fmt(rente)}`;
|
||||
} else if (!ownerWorking) {
|
||||
ec.note = `Rente ab Alter ${renteFromAge}; bis Alter ${AHV_REFERENCE_AGE} Beitrag als Nichterwerbstätige(r).`;
|
||||
} else if (reachesRente) {
|
||||
ec.summary = `Rente ab ${renteFromAge} ${fmt(rente)}`;
|
||||
} else if (owesContribution) {
|
||||
ec.summary = `Beitrag ${fmt(beitrag)}`;
|
||||
ec.note = `Frühpensioniert: beitragspflichtig bis Alter ${AHV_REFERENCE_AGE}.`;
|
||||
} else {
|
||||
const gap = Math.max(0, Math.round(num(pd.gapYears)));
|
||||
ec.summary = gap > 0 ? `${gap} Ausfalljahre` : "Keine Ausfalljahre";
|
||||
}
|
||||
if (ahvShiftMonths(rd) !== 0) {
|
||||
ec.note = [ec.note, `AHV: ${ahvDrawLabel(rd)}.`].filter(Boolean).join(" ");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -847,15 +922,17 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
interestNominal += re.mortgage * (re.interestRate / 100);
|
||||
}
|
||||
|
||||
// AHV jahresweise: Die Rente fliesst ab dem Referenzalter -- auch wenn die Person noch
|
||||
// arbeitet. Vorher zahlt eine bereits pensionierte Person Beiträge, die wie eine
|
||||
// Ausgabe auf die Quote schlagen (Kap. 4.4.6).
|
||||
// AHV jahresweise: Die Rente fliesst ab dem individuellen Rentenalter -- auch wenn die
|
||||
// Person noch arbeitet. Unabhängig davon zahlt eine nicht (mehr) erwerbstätige Person
|
||||
// bis zum REFERENZALTER Beiträge, die wie eine Ausgabe auf die Quote schlagen
|
||||
// (Kap. 4.4.6). Beide Bedingungen können gleichzeitig gelten: Wer mit 62 aufhört und
|
||||
// die Rente ab 63 vorbezieht, bezieht ab 63 und zahlt bis 65.
|
||||
let ahvIncome = 0;
|
||||
let ahvCost = 0;
|
||||
for (const a of ahvItems) {
|
||||
const ageThisYear = a.ownerStartAge + t - 1; // Alter zu Jahresbeginn
|
||||
if (ageThisYear >= AHV_REFERENCE_AGE) ahvIncome += a.rente;
|
||||
else if (!a.working) ahvCost += a.beitrag;
|
||||
if (ageThisYear >= a.renteFromAge) ahvIncome += a.rente;
|
||||
if (!a.working && ageThisYear < AHV_REFERENCE_AGE) ahvCost += a.beitrag;
|
||||
}
|
||||
incomeFlow += ahvIncome;
|
||||
|
||||
@@ -1020,8 +1097,9 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
// also als Belastung sichtbar), ab dem Referenzalter die Rente.
|
||||
for (const a of ahvItems) {
|
||||
const ageThisYear = a.ownerStartAge + t - 1; // Alter zu Jahresbeginn
|
||||
const value = ageThisYear >= AHV_REFERENCE_AGE ? a.rente : a.working ? 0 : -a.beitrag;
|
||||
if (a.ec.yearly.length < t) a.ec.yearly.push({ year: yr, age, value });
|
||||
const rente = ageThisYear >= a.renteFromAge ? a.rente : 0;
|
||||
const beitrag = !a.working && ageThisYear < AHV_REFERENCE_AGE ? a.beitrag : 0;
|
||||
if (a.ec.yearly.length < t) a.ec.yearly.push({ year: yr, age, value: rente - beitrag });
|
||||
}
|
||||
// Verrentete PK läuft nominal fix durch die Phase.
|
||||
for (const ec of ecById.values()) {
|
||||
@@ -1229,9 +1307,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (e.category === "AHV") {
|
||||
const owner = e.ownerRole ? personByRole(persons, e.ownerRole) : null;
|
||||
const gap = owner ? gapYearsByPerson.get(owner.id) ?? 0 : 0;
|
||||
const before = owner ? ahvBeforeByPerson.get(owner.id) ?? { avg: 0, gap: 0 } : { avg: 0, gap: 0 };
|
||||
const before = owner ? ahvBeforeByPerson.get(owner.id) ?? { avg: null, gap: 0 } : { avg: null, gap: 0 };
|
||||
const career = owner ? buildCareer(owner, ahvIncomeAccum, ahvYearsAccum, gapYearsByPerson) : null;
|
||||
const mdJE = career ? ahvMdje(career, before.avg, before.gap) : 0;
|
||||
const avgBefore = before.avg ?? career?.plannedAvgGrossIncome ?? 0;
|
||||
const mdJE = career ? ahvMdje(career, avgBefore, before.gap) : 0;
|
||||
ec.trace = {
|
||||
title: `AHV-Rente «${ec.name}»`,
|
||||
specAnchor: "44-ahv-rente",
|
||||
@@ -1456,7 +1535,32 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let txImmediateRepay = 0;
|
||||
// Kapitalbezüge, deren Verwendung je Element geregelt ist (Punkt C). Gesammelt WÄHREND
|
||||
// der Übergangs-Schleife, angewendet danach -- die Zielelemente werden erst dort bekannt.
|
||||
const capitalUses: { net: number; td: TransitionData; sourceName: string }[] = [];
|
||||
const capitalUses: { net: number; td: RetirementDecision; sourceName: string }[] = [];
|
||||
|
||||
// Zieht dieses 3a-Konto an DIESER Phasengrenze? Ein Konto lässt sich nur ganz auflösen,
|
||||
// und innerhalb einer Phase kennt das Modell kein Einzelereignis -- gezogen wird deshalb
|
||||
// an der ERSTEN Grenze bei oder nach dem Wunschalter. Liegt der Wunsch hinter dem
|
||||
// Planende, greift die letzte Grenze, damit das Guthaben nicht unbezogen liegen bleibt.
|
||||
const drawsPillar3aHere = (
|
||||
el: ElementInput,
|
||||
ownerPerson: { id: string; age: number } | null,
|
||||
ageAtBoundary: number
|
||||
): boolean => {
|
||||
// 3a ist personengebunden -- ohne Besitzer gibt es kein Bezugsalter und keinen Bezug.
|
||||
if (!ownerPerson) return false;
|
||||
const rd = withRetirementDefaults(
|
||||
"PILLAR_3A",
|
||||
retirementAge.get(ownerPerson.id) ?? AHV_REFERENCE_AGE,
|
||||
el.retirementDecision
|
||||
);
|
||||
const wish = Math.max(
|
||||
PILLAR_3A_MIN_WITHDRAWAL_AGE,
|
||||
Math.min(PILLAR_3A_MAX_WITHDRAWAL_AGE, Math.round(num(rd.withdrawalAge, ageAtBoundary)))
|
||||
);
|
||||
const prevBoundaryAge = ownerPerson.age + yearsBefore;
|
||||
if (prevBoundaryAge >= wish) return false; // in einer früheren Phase bereits gezogen
|
||||
return ageAtBoundary >= wish || !nextPhase;
|
||||
};
|
||||
// Echte Vermögensänderungen an dieser Grenze (für die Brücke der Folgephase).
|
||||
// Verkäufe, Bezüge und Tilgungen sind für sich Umbuchungen -- vermögenswirksam sind
|
||||
// nur die Steuer, die Verrentung (Kapital verlässt die Bilanz) und die Differenz
|
||||
@@ -1496,6 +1600,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const ownerRetiresNext =
|
||||
!!owner && !!nextPhase && workingByPerson.get(owner.id) === true &&
|
||||
retiresInPhase(owner.id, persons, retirementAge, yearsBefore + duration);
|
||||
// Pensionsalter des Besitzers und sein Alter AN dieser Phasengrenze -- Bezugspunkt für
|
||||
// die Vorgaben des Pensionierungs-Entscheids und für das 3a-Bezugsalter.
|
||||
const ownerRetirementAge = owner ? retirementAge.get(owner.id) ?? AHV_REFERENCE_AGE : AHV_REFERENCE_AGE;
|
||||
const ownerAgeAtBoundary = owner ? owner.age + yearsBefore + duration : 0;
|
||||
|
||||
// Einkommen/Ausgaben: Basiswert wurde bereits im Element-Setup fortgeschrieben.
|
||||
if (e.category === "INCOME" || e.category === "EXPENSE") {
|
||||
@@ -1503,18 +1611,9 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
continue;
|
||||
}
|
||||
|
||||
// AHV: beim Pensions-Übergang die geprüften Karriere-Werte vor Planbeginn übernehmen.
|
||||
// AHV: Der Bezugs-Entscheid hängt seit 0.34 am Element, nicht am Übergang -- hier ist
|
||||
// deshalb nichts mehr zu tun.
|
||||
if (e.category === "AHV") {
|
||||
if (ownerRetiresNext && owner) {
|
||||
const career = buildCareer(owner, ahvIncomeAccum, ahvYearsAccum, gapYearsByPerson);
|
||||
ahvBeforeByPerson.set(owner.id, {
|
||||
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schätzung für die Jahre
|
||||
// vor Planbeginn -- exakt der Wert, den der Prüf-Dialog vorbelegt. Ein Fallback auf
|
||||
// 0 würde die Rente still und massiv zu tief rechnen. Beide Werte sind BRUTTO.
|
||||
avg: num(td.avgIncomeBefore, career.plannedAvgGrossIncome),
|
||||
gap: Math.max(0, Math.round(num(td.gapYearsBefore))),
|
||||
});
|
||||
}
|
||||
carry.hasCarry = true;
|
||||
continue;
|
||||
}
|
||||
@@ -1530,29 +1629,26 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
switch (e.category) {
|
||||
case "PENSION_FUND": {
|
||||
if (ownerRetiresNext) {
|
||||
// EINE Quote statt PENSION/CAPITAL/COMBI plus Frankenbetrag: 0 % = volle Rente,
|
||||
// 100 % = volles Kapital, alles dazwischen ist die Kombination. Als Quote, weil
|
||||
// sich das Guthaben mit dem Pensionsalter ändert -- ein fixer Betrag würde beim
|
||||
// Verschieben des Alters still ein anderes Verhältnis bedeuten.
|
||||
const value = ec.endValue;
|
||||
const mode = td.payoutMode ?? "PENSION";
|
||||
if (mode === "CAPITAL") {
|
||||
const net = Math.round(value * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += value - net;
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
carry.value = 0;
|
||||
carry.pkPensionAnnual = 0;
|
||||
} else if (mode === "PENSION") {
|
||||
carry.pkPensionAnnual = Math.round((value * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100);
|
||||
txPensionConversion += value;
|
||||
carry.value = 0;
|
||||
} else {
|
||||
const capital = Math.min(value, Math.round(num(td.capitalAmount)));
|
||||
const net = Math.round(capital * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
const rd = withRetirementDefaults("PENSION_FUND", ownerRetirementAge, e.retirementDecision);
|
||||
const sharePct = Math.max(0, Math.min(100, num(rd.capitalSharePct)));
|
||||
const capital = Math.round((value * sharePct) / 100);
|
||||
const rest = value - capital;
|
||||
if (capital > 0) {
|
||||
const net = Math.round(capital * (1 - num(rd.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += capital - net;
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100);
|
||||
txPensionConversion += value - capital;
|
||||
carry.value = 0;
|
||||
capitalUses.push({ net, td: rd, sourceName: sourceLabelOf(e, persons) });
|
||||
if (owner) capitalNetByPerson.set(owner.id, (capitalNetByPerson.get(owner.id) ?? 0) + net);
|
||||
}
|
||||
carry.pkPensionAnnual =
|
||||
rest > 0 ? Math.round((rest * num(rd.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100) : 0;
|
||||
txPensionConversion += rest;
|
||||
carry.value = 0;
|
||||
} else {
|
||||
// Vorbezug (z. B. Wohneigentum/Selbstständigkeit): ebenfalls kapitalbezugssteuerpflichtig.
|
||||
// Das Kapital wird brutto entnommen, netto (nach Steuer) fliesst es ins Cash.
|
||||
@@ -1565,11 +1661,17 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
break;
|
||||
}
|
||||
case "PILLAR_3A": {
|
||||
if (ownerRetiresNext) {
|
||||
const net = Math.round(ec.endValue * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
// Der Bezug hängt seit 0.34 am gewählten Alter, nicht mehr starr am Pensions-
|
||||
// Übergang: Ein 3a-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.
|
||||
if (drawsPillar3aHere(e, owner, ownerAgeAtBoundary)) {
|
||||
const rd = withRetirementDefaults("PILLAR_3A", ownerRetirementAge, e.retirementDecision);
|
||||
const net = Math.round(ec.endValue * (1 - num(rd.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += ec.endValue - net;
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
capitalUses.push({ net, td: rd, sourceName: sourceLabelOf(e, persons) });
|
||||
if (owner) capitalNetByPerson.set(owner.id, (capitalNetByPerson.get(owner.id) ?? 0) + net);
|
||||
carry.value = 0;
|
||||
} else {
|
||||
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
|
||||
@@ -1794,6 +1896,58 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const nachlass = result.length > 0 ? result[result.length - 1].endWealthNominal : 0;
|
||||
const ahvCareer = persons.map((p) => buildCareer(p, ahvIncomeAccum, ahvYearsAccum, gapYearsByPerson));
|
||||
|
||||
// --- Pensionierung: die Kennzahlen des gleichnamigen Bildschirms -------------------------
|
||||
//
|
||||
// Bewusst HIER und nicht im UI: Rentenlücke und Reichweite sind die beiden Zahlen, an denen
|
||||
// die ganze Planung hängt. Eine Nebenrechnung in der Komponente hätte dieselbe Driftgefahr
|
||||
// wie bei den Rechenwegen (Kap. 4.14.3) -- und würde im PDF-Bericht anders ausfallen.
|
||||
//
|
||||
// Die Rentenlücke ist KEINE neue Grösse: Sie ist die Verzehrquote im ersten Jahr, in dem
|
||||
// niemand mehr arbeitet -- Renteneinkommen minus Ausgaben. Genau das rechnet die
|
||||
// Jahresschleife ohnehin, es fehlte nur der Name dafür.
|
||||
const retirement = ((): RetirementOverview => {
|
||||
// Startjahr je Phase (1-basiert) -- PhaseComputed fuehrt nur die Dauer.
|
||||
const startYearOf = new Map<string, number>();
|
||||
let acc = 1;
|
||||
for (const r of result) {
|
||||
startYearOf.set(r.id, acc);
|
||||
acc += r.durationYears;
|
||||
}
|
||||
const firstRetiredPhase = result.find((p) => p.persons.every((x) => !x.working));
|
||||
const perPerson = persons.map((p) => {
|
||||
// Die erste Phase, in der DIESE Person nicht mehr arbeitet -- dort stehen ihre Renten.
|
||||
const ph = result.find((r) => r.persons.find((x) => x.role === p.role && !x.working));
|
||||
const own = (cat: ElementCategory) =>
|
||||
ph?.elements.find((e) => e.category === cat && e.ownerRole === p.role);
|
||||
const rd = ahvDrawByPerson.get(p.id) ?? {};
|
||||
return {
|
||||
personId: p.id,
|
||||
role: p.role,
|
||||
retirementAge: retirementAge.get(p.id) ?? AHV_REFERENCE_AGE,
|
||||
planningHorizonAge: p.planningHorizonAge ?? null,
|
||||
ahvAnnual: Math.round(ahvFinalByPerson.get(p.id) ?? 0),
|
||||
ahvFromAge: Math.round(ahvStartAge(rd)),
|
||||
ahvDraw: ahvDrawLabel(rd),
|
||||
pkPensionAnnual: Math.round(own("PENSION_FUND")?.startValue ?? 0),
|
||||
// Einmalig verfügbares Alterskapital: netto, nach Kapitalbezugssteuer.
|
||||
capitalAtRetirement: Math.round(capitalNetByPerson.get(p.id) ?? 0),
|
||||
};
|
||||
});
|
||||
if (!firstRetiredPhase) {
|
||||
return { firstRetirementYear: null, pensionIncome: null, expenses: null, gapAnnual: null, perPerson };
|
||||
}
|
||||
// Erstes Jahr dieser Phase: Einkommen und Ausgaben stehen im Jahrespunkt.
|
||||
const firstYear = startYearOf.get(firstRetiredPhase.id) ?? 1;
|
||||
const yp = yearly.find((y) => y.year === firstYear);
|
||||
return {
|
||||
firstRetirementYear: firstYear,
|
||||
pensionIncome: yp ? Math.round(yp.income) : null,
|
||||
expenses: yp ? Math.round(yp.expenseNominal) : null,
|
||||
gapAnnual: yp ? Math.round(yp.income - yp.expenseNominal) : null,
|
||||
perPerson,
|
||||
};
|
||||
})();
|
||||
|
||||
// --- Plan-weite Rechenwege ---
|
||||
const planTraces: Trace[] = [];
|
||||
if (explain) {
|
||||
@@ -1810,8 +1964,9 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
});
|
||||
|
||||
for (const career of ahvCareer) {
|
||||
const before = ahvBeforeByPerson.get(career.personId) ?? { avg: 0, gap: 0 };
|
||||
const mdJE = ahvMdje(career, before.avg, before.gap);
|
||||
const before = ahvBeforeByPerson.get(career.personId) ?? { avg: null, gap: 0 };
|
||||
const avgBefore = before.avg ?? career.plannedAvgGrossIncome;
|
||||
const mdJE = ahvMdje(career, avgBefore, before.gap);
|
||||
planTraces.push({
|
||||
title: `AHV-Beitragskarriere – ${career.role === "PERSON_A" ? "Person A" : "Person B"}`,
|
||||
specAnchor: "442-beitragskarriere-und-mdje",
|
||||
@@ -1820,8 +1975,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
st("Durchschnittliches Bruttöinkommen im Plan (real)", Math.round(career.plannedAvgGrossIncome), `Nettolohn × ${AHV_GROSS_FROM_NET_FACTOR}`, undefined,
|
||||
"Das Tool erfasst netto, die AHV bemisst sich am Brutto. Der Faktor 1.12 ist ein Karriere-Durchschnitt und keine exakte Einzelfall-Umrechnung."),
|
||||
st("Beitragsjahre vor Planbeginn", Math.max(0, career.yearsBeforePlan - before.gap), "Alter bei Planbeginn − 21 − Ausfalljahre davor", undefined, undefined, "Jahre"),
|
||||
st("Durchschnittseinkommen vor Planbeginn (real, brutto)", Math.round(before.avg), undefined, undefined,
|
||||
before.avg === 0 && career.yearsBeforePlan > 0 ? "Noch nicht erfasst – am Pensions-Übergang zu prüfen." : "Aus der AHV-Rentenvorausberechnung übernommen."),
|
||||
st("Durchschnittseinkommen vor Planbeginn (real, brutto)", Math.round(avgBefore), undefined, undefined,
|
||||
before.avg === null && career.yearsBeforePlan > 0
|
||||
? "Nicht erfasst – geschätzt aus dem geplanten Durchschnitt. Im Pensionierungs-Bildschirm zu prüfen."
|
||||
: "Aus der AHV-Rentenvorausberechnung übernommen."),
|
||||
st("Massgebendes durchschnittliches Jahreseinkommen", Math.round(mdJE),
|
||||
"(Einkommen davor × Jahre davor + Einkommen im Plan × Jahre im Plan) / Total Jahre", undefined,
|
||||
"REAL gerechnet: die echte AHV wertet vergangene Einkommen auf UND indexiert die Schwellen – beides hebt sich real weitgehend auf."),
|
||||
@@ -1841,7 +1998,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
});
|
||||
}
|
||||
|
||||
return { phases: result, yearly, nachlass, ruinAge, ahvCareer, traces: explain ? planTraces : undefined };
|
||||
return { phases: result, yearly, nachlass, retirement, ruinAge, ahvCareer, traces: explain ? planTraces : undefined };
|
||||
}
|
||||
|
||||
// Durchschnittliches REALES Jahreseinkommen über eine Phase. Nominal wächst der Flow mit
|
||||
|
||||
Reference in New Issue
Block a user