Drei Berechnungs-Fixes: Phaseninflation, Tilgungsraten, Vorbezugssteuer

- Phase.inflationRate ersatzlos entfernt (inkl. Migration). Die Inflation liegt
  seit V5 plan-weit; das Feld wurde von computePlan nie gelesen und war wirkungslos.
- Amortisation und Tilgung stoppen, sobald Hypothek bzw. Schuld abbezahlt sind:
  Hypothek/Restschuld sind neu laufende Salden, die Rate ist pro Jahr am Restsaldo
  gekappt. Belastet danach weder Cash noch Sparquote.
- Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezuegen vor der Pensionierung.
  Der Bezug wird brutto dem Kapital entnommen, netto ins Cash gebucht.

plannedSaveRate ist neu die Rate des ersten Phasenjahres. Drei Regressionstests
ergaenzt (10 -> 13).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 22:22:34 +02:00
parent f768e01d61
commit dfebbeb397
12 changed files with 141 additions and 49 deletions
+41 -21
View File
@@ -54,7 +54,10 @@ export interface PhaseComputed {
quotaStart: number;
quotaEnd: number;
isConsumption: boolean;
plannedSaveRate: number; // geplante Sparrate: 3a + Sonstiges-Vermoegen-Sparbeitrag + Amort. + Tilgung
// Geplante Sparrate im ERSTEN Phasenjahr: 3a + Sonstiges-Vermoegen-Sparbeitrag + Amort. +
// Tilgung. Amortisation/Tilgung entfallen, sobald Hypothek/Schuld abbezahlt sind -- die Rate
// kann in spaeteren Phasenjahren also tiefer liegen.
plannedSaveRate: number;
plannedWithdrawRate: number; // geplante Verzehrrate: Bezugsraten aus Sonstigem Vermoegen
capitalInflow: number; // Kapitalzufluss: PK-/3a-Bezuege + Verkaeufe (aus dem Uebergang in diese Phase)
capitalInvest: number; // Kapitalinvestitionen: Zusatz-/Neuinvestitionen + sofortige Tilgungen
@@ -208,9 +211,11 @@ export function computePlan(plan: PlanInput): PlanComputed {
const expenses: { basis: number; idx: number; ec: ElementPhaseComputed }[] = [];
let renteTotal = 0; // AHV + PK-Renten (nominal fix)
const assets: { value: number; rate: number; r: number; withdrawal: number; ec: ElementPhaseComputed }[] = [];
const realEstates: { purchase: number; mortgageStart: number; amort: number; ec: ElementPhaseComputed }[] = [];
const debts: { owedStart: number; repay: number; ec: ElementPhaseComputed }[] = [];
let plannedRatesTotal = 0; // Sparraten (verlassen das Cash): 3a + Sonstiges Vermoegen + Amort. + Tilgung
// mortgage/owed sind LAUFENDE Salden: sie werden in der Jahresschleife abgebaut und am
// Nullpunkt gestoppt (keine Rate mehr, sobald abbezahlt).
const realEstates: { purchase: number; mortgage: number; amort: number; ec: ElementPhaseComputed }[] = [];
const debts: { owed: number; repay: number; ec: ElementPhaseComputed }[] = [];
let fixedRatesTotal = 0; // Sparraten mit konstantem Jahresbetrag: 3a + Sonstiges Vermoegen
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermoegen
let investmentsFromCash = 0; // Neuinvestitionen/Aufstockungen (ab Phase 2, aus Cash)
let wealthStart = 0;
@@ -312,7 +317,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
const start = base + topUp;
const rate = Math.round(num(pd.annualContribution));
plannedRatesTotal += rate;
fixedRatesTotal += rate;
if (!isFirstPhase) investmentsFromCash += topUp;
ec.baseValue = base;
ec.startValue = start;
@@ -327,7 +332,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
const start = base + topUp;
const rate = Math.round(num(pd.annualContribution));
const withdrawal = Math.round(num(pd.annualWithdrawal));
plannedRatesTotal += rate;
fixedRatesTotal += rate;
plannedWithdrawTotal += withdrawal;
if (!isFirstPhase) investmentsFromCash += topUp;
ec.baseValue = base;
@@ -340,23 +345,21 @@ export function computePlan(plan: PlanInput): PlanComputed {
const purchase = Math.round(num(pd.purchasePrice));
const mortgageStart = carry.hasCarry ? carry.mortgage : Math.round(num(pd.mortgage));
const amort = Math.round(num(pd.amortization));
plannedRatesTotal += amort;
const equity = purchase - mortgageStart;
if (!carry.hasCarry && !isFirstPhase) investmentsFromCash += Math.max(0, equity);
ec.baseValue = equity;
ec.startValue = equity;
wealthStart += equity;
realEstates.push({ purchase, mortgageStart, amort, ec });
realEstates.push({ purchase, mortgage: mortgageStart, amort, ec });
break;
}
case "OTHER_DEBT": {
const owedStart = carry.hasCarry ? carry.owed : Math.round(num(pd.startValue));
const repay = Math.round(num(pd.annualRepayment));
plannedRatesTotal += repay;
ec.baseValue = -owedStart;
ec.startValue = -owedStart;
wealthStart += -owedStart;
debts.push({ owedStart, repay, ec });
debts.push({ owed: owedStart, repay, ec });
break;
}
}
@@ -374,6 +377,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
let expenseEnd = 0;
let quotaStart = 0;
let quotaEnd = 0;
let plannedSaveRate = 0; // Kopf-Kennzahl: die tatsaechliche Sparrate im ersten Phasenjahr
for (let t = 1; t <= duration; t++) {
// Einkommen: nominal (Basis x (1+Lohnerhoehung)^(t-1)) + Renten (nominal fix).
@@ -413,14 +417,30 @@ export function computePlan(plan: PlanInput): PlanComputed {
a.value = grown - w;
cashFromWithdraw += w;
}
cash += quote - plannedRatesTotal + cashFromWithdraw;
// Amortisation/Tilgung: nur so lange und so viel, wie noch Restschuld besteht. Ist die
// Hypothek/Schuld abbezahlt, entfaellt die Rate -- sie belastet weder Cash noch Sparquote.
let debtRates = 0;
for (const re of realEstates) {
const pay = Math.min(re.amort, re.mortgage);
re.mortgage -= pay;
debtRates += pay;
}
for (const d of debts) {
const pay = Math.min(d.repay, d.owed);
d.owed -= pay;
debtRates += pay;
}
if (t === 1) plannedSaveRate = fixedRatesTotal + debtRates;
cash += quote - fixedRatesTotal - debtRates + cashFromWithdraw;
if (cash < 0) cashNegative = true;
// Gesamtvermoegen zum Jahresende t (fuer Ruin-Erkennung).
let total = cash;
for (const a of assets) total += a.value;
for (const re of realEstates) total += re.purchase - Math.max(0, re.mortgageStart - re.amort * t);
for (const d of debts) total += -Math.max(0, d.owedStart - d.repay * t);
for (const re of realEstates) total += re.purchase - re.mortgage;
for (const d of debts) total += -d.owed;
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
}
@@ -445,17 +465,15 @@ export function computePlan(plan: PlanInput): PlanComputed {
wealthEnd += a.ec.endValue;
}
for (const re of realEstates) {
const mortgageEnd = Math.max(0, re.mortgageStart - re.amort * duration);
re.ec.endValue = re.purchase - mortgageEnd;
re.ec.endValue = re.purchase - re.mortgage;
re.ec.summary = fmt(re.ec.endValue);
wealthEnd += re.ec.endValue;
}
for (const d of debts) {
const owedEnd = Math.max(0, d.owedStart - d.repay * duration);
d.ec.endValue = -owedEnd;
d.ec.endValue = d.owed > 0 ? -d.owed : 0;
d.ec.summary = fmt(d.ec.endValue);
wealthEnd += d.ec.endValue;
if (owedEnd === 0) d.ec.note = "Wird getilgt";
if (d.owed === 0) d.ec.note = "Wird getilgt";
}
const cashEnd = Math.round(cash);
@@ -478,7 +496,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
quotaStart: Math.round(quotaStart),
quotaEnd: Math.round(quotaEnd),
isConsumption: quotaStart < 0,
plannedSaveRate: plannedRatesTotal,
plannedSaveRate,
plannedWithdrawRate: plannedWithdrawTotal,
capitalInflow: Math.round(incomingInflow),
capitalInvest: Math.round(investmentsFromCash + incomingImmediateRepay),
@@ -538,9 +556,11 @@ export function computePlan(plan: PlanInput): PlanComputed {
carry.value = 0;
}
} else {
// Vorbezug (z. B. Wohneigentum/Selbststaendigkeit): ebenfalls kapitalbezugssteuerpflichtig.
// Das Kapital wird brutto entnommen, netto (nach Steuer) fliesst es ins Cash.
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
carry.value = ec.endValue - withdrawal;
txInflow += withdrawal;
txInflow += Math.round(withdrawal * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
}
break;
}
@@ -551,7 +571,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
} else {
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
carry.value = ec.endValue - withdrawal;
txInflow += withdrawal;
txInflow += Math.round(withdrawal * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
}
break;
}