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
+67 -1
View File
@@ -32,7 +32,7 @@ function plan(opts: {
inflationRateDefault: opts.inflation ?? 2,
initialCash: opts.initialCash ?? 0,
persons: [{ id: "A", role: "PERSON_A", name: null, age: opts.age, retirementAge: opts.retirementAge }],
phases: opts.phases.map((p, i) => ({ id: p.id, sequenceNumber: i + 1, name: p.id, durationYears: p.durationYears, inflationRate: null })),
phases: opts.phases.map((p, i) => ({ id: p.id, sequenceNumber: i + 1, name: p.id, durationYears: p.durationYears })),
elements: opts.elements,
};
}
@@ -185,6 +185,72 @@ describe("V5 Golden Tests", () => {
expect(ph.cashEnd).toBe(50000);
});
it("Tilgung stoppt, sobald die Schuld getilgt ist (belastet Cash und Sparrate nicht weiter)", () => {
// Schuld 25'000, Tilgung 10'000/Jahr, 5 Jahre: getilgt im Jahr 3 (10'000 + 10'000 + 5'000).
// Gesamtabfluss = 25'000, NICHT 50'000. Einkommen = Ausgaben, damit nur die Tilgung wirkt.
const p = plan({
age: 40,
retirementAge: 70,
inflation: 0,
initialCash: 100000,
phases: [{ id: "p1", durationYears: 5 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 50000, teuerungsausgleich: 0 } }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 50000, teuerungsausgleich: 0 } }),
el("OTHER_DEBT", "HOUSEHOLD", { p1: { startValue: 25000, annualRepayment: 10000 } }),
],
});
const ph = computePlan(p).phases[0];
expect(ph.cashEnd).toBe(75000); // 100'000 - 25'000
expect(ph.plannedSaveRate).toBe(10000); // erstes Jahr volle Rate
const debt = ph.elements.find((e) => e.category === "OTHER_DEBT")!;
expect(debt.endValue).toBe(0);
});
it("Amortisation stoppt, sobald die Hypothek abbezahlt ist", () => {
// Hypothek 15'000, Amortisation 10'000/Jahr, 4 Jahre: abbezahlt im Jahr 2 (10'000 + 5'000).
const p = plan({
age: 40,
retirementAge: 70,
inflation: 0,
initialCash: 100000,
phases: [{ id: "p1", durationYears: 4 }],
elements: [
el("REAL_ESTATE", "HOUSEHOLD", { p1: { purchasePrice: 500000, mortgage: 15000, amortization: 10000 } }),
],
});
const ph = computePlan(p).phases[0];
expect(ph.cashEnd).toBe(85000); // 100'000 - 15'000 (nicht - 40'000)
const re = ph.elements.find((e) => e.category === "REAL_ESTATE")!;
expect(re.endValue).toBe(500000); // schuldenfrei
});
it("Vorbezug PK vor Pensionierung: Kapitalbezugssteuer wird abgezogen", () => {
// Bezug 100'000 brutto bei 8% Steuer -> 92'000 netto ins Cash der Folgephase.
const p = plan({
age: 40,
retirementAge: 70,
inflation: 0,
phases: [
{ id: "p1", durationYears: 1 },
{ id: "p2", durationYears: 1 },
],
elements: [
el(
"PENSION_FUND",
"PERSON_A",
{ p1: { currentValue: 300000, expectedReturn: 0, annualContribution: 0 }, p2: {} },
{ p1: { withdrawalMode: "AMOUNT", withdrawal: 100000, capitalTaxRate: 8 } }
),
],
});
const r = computePlan(p);
expect(r.phases[1].capitalInflow).toBe(92000); // netto nach 8% Steuer
expect(r.phases[1].cashStart).toBe(92000);
const pk = r.phases[1].elements.find((e) => e.category === "PENSION_FUND")!;
expect(pk.startValue).toBe(200000); // brutto 100'000 dem Kapital entnommen
});
it("Fortschreibung: nominaler Einkommens-Basiswert Phase 1 -> Startwert Phase 2; Cash laeuft fort", () => {
const p = plan({
age: 40,