Files
FPT/src/lib/calculations.test.ts
T
admGitAICDS e901d23970
Deploy App / deploy (push) Successful in 59s
AHV einkommensabhaengig + Warnhinweis bei Aenderungen in frueheren Phasen
Roadmap Nr. 3: Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) ueber
das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente.

- Formel aus den amtlichen Randbedingungen hergeleitet und gegen die Tabelle
  318.117.1 verifiziert: 51/51 Zeilen exakt. Schwellen sind Vielfache von R0=1'260
  (12/36/72 x R0 = 15'120 / 45'360 / 90'720). Stuetzstellen als Golden Tests.
- Alles REAL gerechnet: die AHV wertet vergangene Einkommen auf UND indexiert die
  Schwellen -- real hebt sich das auf. Nominal wuerde die Rente systematisch zu hoch
  ausfallen (Beispiel: faelschlich Maximalrente, ~41'600 ueber 25 Rentenjahre).
- Pruefung der Beitragskarriere am Pensions-Uebergang; Zusatzfelder fuer die Jahre
  vor Planbeginn nur, wenn der Plan nicht bis Alter 21 zurueckreicht.
- Sonderfall "bei Planbeginn bereits pensioniert": Felder in der Phasenzelle.
- Ohne Pruefung gilt der geplante Durchschnitt (nicht 0) -- sonst waere die Rente
  still viel zu tief.
- 13. Altersrente: Jahresbetrag = Monatsrente x 13.

Roadmap Nr. 4: Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen
existieren -- Werte schreiben sich fort und wirken bis ans Planende durch.

Verhaltensaenderung fuer bestehende Plaene: siehe SPEZIFIKATION 9.10.
Zwoelf Regressionstests (18 -> 30). Spezifikation auf v0.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:02:42 +02:00

534 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import { ahvMonthlyFullPension, computePlan } from "@/lib/calculations";
import type { CashTransitionData, ElementCategory, PhaseData, TransitionData } from "@/lib/elements";
import type { PlanInput } from "@/lib/types";
// --- kleine Bau-Helfer ---
let idc = 0;
const nid = () => `id${idc++}`;
function el(
category: ElementCategory,
ownerRole: string | null,
phaseValues: Record<string, PhaseData>,
transitionValues: Record<string, TransitionData> = {}
) {
return { id: nid(), category, name: category, ownerRole: ownerRole as never, orderIndex: idc, phaseValues, transitionValues };
}
function plan(opts: {
age: number;
retirementAge: number;
inflation?: number;
initialCash?: number;
phases: { id: string; durationYears: number; cashTransition?: CashTransitionData }[];
elements: ReturnType<typeof el>[];
household?: "SINGLE" | "COUPLE";
}): PlanInput {
return {
id: "plan",
name: "T",
householdType: opts.household ?? "SINGLE",
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,
cashTransition: p.cashTransition ?? {},
})),
elements: opts.elements,
};
}
const within = (actual: number, expected: number, pct: number) => Math.abs(actual - expected) <= Math.abs(expected) * pct;
// Stuetzstellen aus der AMTLICHEN Tabelle 318.117.1 "Monatliche Vollrenten, Skala 44"
// (BSV, gueltig ab 1.1.2025/2026): mdJE -> monatliche Vollrente. Deckt Mindestrente,
// Formel 1, den Wendepunkt (45'360), Formel 2 und die Maximalrente ab.
const AHV_AMTLICHE_TABELLE: [number, number][] = [
[15120, 1260], // Mindestrente (12 x R0)
[16632, 1293],
[22680, 1424],
[30240, 1588],
[43848, 1882],
[45360, 1915], // Wendepunkt (36 x R0)
[46872, 1935],
[60480, 2117],
[75600, 2318],
[89208, 2500],
[90720, 2520], // Maximalrente (72 x R0)
];
describe("AHV-Rentenformel (Skala 44)", () => {
it("reproduziert die amtliche Tabelle 318.117.1 exakt", () => {
for (const [mdJE, erwartet] of AHV_AMTLICHE_TABELLE) {
expect(Math.round(ahvMonthlyFullPension(mdJE)), `mdJE ${mdJE}`).toBe(erwartet);
}
});
it("kappt ausserhalb der Schwellen", () => {
expect(ahvMonthlyFullPension(0)).toBe(1260); // unter 12 x R0 -> Mindestrente
expect(ahvMonthlyFullPension(10000)).toBe(1260);
expect(ahvMonthlyFullPension(150000)).toBe(2520); // ueber 72 x R0 -> Maximalrente
});
it("ist am Wendepunkt stetig (kein Sprung zwischen Formel 1 und 2)", () => {
const links = ahvMonthlyFullPension(45360 - 0.01);
const rechts = ahvMonthlyFullPension(45360 + 0.01);
expect(Math.abs(links - rechts)).toBeLessThan(0.01);
});
});
// V5-Modell: Einkommen = nominale Basis + nominale Lohnerhoehung; Ausgaben = REALE Basis +
// reale Mehrausgaben, nominal = real x (plan-weite Inflation).
describe("AHV einkommensabhaengig", () => {
// 60-jaehrig, Pension mit 65: 39 Beitragsjahre vor Planbeginn (ab 21), 5 im Plan.
function ahvPlan(opts: {
age?: number;
income: number;
avgIncomeBefore?: number;
gapYearsBefore?: number;
gapYearsInPlan?: number;
reviewed?: boolean;
}) {
const age = opts.age ?? 60;
return plan({
age,
retirementAge: 65,
inflation: 0, // real = nominal, damit die Erwartungswerte von Hand pruefbar bleiben
phases: [
{ id: "p1", durationYears: 65 - age },
{ id: "p2", durationYears: 10 },
],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: opts.income, teuerungsausgleich: 0 }, p2: {} }),
el(
"AHV",
"PERSON_A",
{ p1: { gapYears: opts.gapYearsInPlan ?? 0 }, p2: {} },
{
p1: {
reviewed: opts.reviewed ?? true,
avgIncomeBefore: opts.avgIncomeBefore ?? 0,
gapYearsBefore: opts.gapYearsBefore ?? 0,
},
}
),
],
});
}
const renteIn = (p: PlanInput, phaseIdx: number) =>
computePlan(p).phases[phaseIdx].elements.find((e) => e.category === "AHV")!.startValue;
it("volle Karriere auf Maximalniveau -> Maximalrente 32'760", () => {
const p = ahvPlan({ income: 100000, avgIncomeBefore: 100000 });
expect(renteIn(p, 1)).toBe(32760); // 2'520 x 13
});
it("mdJE unter der Schwelle -> abgestufte Rente (Formel 2)", () => {
// mdJE = 60'000 -> 1'260 x (1.04 + 0.16 x 60'000/15'120) = 2'118.1/Monat x 13 = 27'535
const p = ahvPlan({ income: 60000, avgIncomeBefore: 60000 });
const erwartet = Math.round(ahvMonthlyFullPension(60000) * 13);
expect(renteIn(p, 1)).toBe(erwartet);
expect(renteIn(p, 1)).toBeLessThan(32760);
});
it("tiefes Einkommen -> Mindestrente 1'260 x 13", () => {
const p = ahvPlan({ income: 10000, avgIncomeBefore: 10000 });
expect(renteIn(p, 1)).toBe(1260 * 13);
});
it("Einkommen vor Planbeginn dominiert bei kurzer Restlaufzeit", () => {
// 39 Jahre vor Planbeginn zu 40'000, nur 5 Jahre im Plan zu 200'000.
const p = ahvPlan({ income: 200000, avgIncomeBefore: 40000 });
const mdJE = (40000 * 39 + 200000 * 5) / 44; // = 58'181.8
expect(renteIn(p, 1)).toBe(Math.round(ahvMonthlyFullPension(mdJE) * 13));
});
it("Ausfalljahre kuerzen die Rente ueber die Skala 44", () => {
const ohne = ahvPlan({ income: 100000, avgIncomeBefore: 100000 });
const mit = ahvPlan({ income: 100000, avgIncomeBefore: 100000, gapYearsBefore: 4 });
expect(renteIn(mit, 1)).toBe(Math.round(32760 * (40 / 44))); // 4 Ausfalljahre = 4/44 weniger
expect(renteIn(mit, 1)).toBeLessThan(renteIn(ohne, 1));
});
it("Ausfalljahre im Plan senken die Beitragsjahre, nicht das Durchschnittseinkommen", () => {
// Gleiches Einkommen, aber 2 Ausfalljahre im Plan -> nur die Skala sinkt.
const p = ahvPlan({ income: 100000, avgIncomeBefore: 100000, gapYearsInPlan: 2 });
expect(renteIn(p, 1)).toBe(Math.round(32760 * (42 / 44)));
});
it("ohne Pruefung gilt der geplante Durchschnitt auch fuer die Jahre vor Planbeginn", () => {
// Kein avgIncomeBefore erfasst -> darf NICHT als 0 gerechnet werden (sonst mdJE ~45%).
const p = plan({
age: 60,
retirementAge: 65,
inflation: 0,
phases: [
{ id: "p1", durationYears: 5 },
{ id: "p2", durationYears: 5 },
],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 80000, teuerungsausgleich: 0 }, p2: {} }),
el("AHV", "PERSON_A", { p1: {}, p2: {} }), // keine Uebergangsdaten
],
});
const rente = computePlan(p).phases[1].elements.find((e) => e.category === "AHV")!.startValue;
expect(rente).toBe(Math.round(ahvMonthlyFullPension(80000) * 13)); // mdJE = 80'000, nicht 9'091
});
it("bereits bei Planbeginn pensioniert: Karriere kommt aus der Phasenzelle", () => {
const p = plan({
age: 66,
retirementAge: 65,
inflation: 0,
phases: [{ id: "p1", durationYears: 10 }],
elements: [el("AHV", "PERSON_A", { p1: { avgIncomeBefore: 60000, gapYearsBefore: 0 } })],
});
const rente = computePlan(p).phases[0].elements.find((e) => e.category === "AHV")!.startValue;
expect(rente).toBe(Math.round(ahvMonthlyFullPension(60000) * 13));
});
it("Ehepaar-Plafonierung greift weiterhin (150% der Maximalrente)", () => {
const p: PlanInput = {
id: "plan", name: "T", householdType: "COUPLE", inflationRateDefault: 0, initialCash: 0,
persons: [
{ id: "A", role: "PERSON_A", name: null, age: 66, retirementAge: 65 },
{ id: "B", role: "PERSON_B", name: null, age: 66, retirementAge: 65 },
],
phases: [{ id: "p1", sequenceNumber: 1, name: "p1", durationYears: 5, cashTransition: {} }],
elements: [
el("AHV", "PERSON_A", { p1: { avgIncomeBefore: 100000 } }),
el("AHV", "PERSON_B", { p1: { avgIncomeBefore: 100000 } }),
],
};
const ph = computePlan(p).phases[0];
const summe = ph.elements.filter((e) => e.category === "AHV").reduce((s, e) => s + e.startValue, 0);
expect(summe).toBe(Math.round(32760 * 1.5)); // plafoniert, nicht 2 x 32'760
});
});
describe("V5 Golden Tests", () => {
it("Test 1 Ansparen (Einkommen +2% nominal, Ausgaben real flach -> nominal +2%)", () => {
const p = plan({
age: 40,
retirementAge: 60,
phases: [{ id: "p1", durationYears: 15 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 120000, teuerungsausgleich: 2 } }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 200000, expectedReturn: 5, annualContribution: 0 } }),
],
});
const ph = computePlan(p).phases[0];
expect(within(ph.endWealthNominal, 761654, 0.01)).toBe(true);
expect(within(ph.endWealthReal, 565928, 0.01)).toBe(true);
expect(ph.cashNegative).toBe(false);
});
it("Test 2 Verzehr/Ruin: Ausgaben nominal +2%, Rente fix -> Ruin Alter 94", () => {
const p = plan({
age: 65,
retirementAge: 65,
phases: [{ id: "p1", durationYears: 35 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 60000, teuerungsausgleich: 0 } }), // Rente nominal fix
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 900000, expectedReturn: 3, annualContribution: 0 } }),
],
});
expect(computePlan(p).ruinAge).toBe(94);
});
it("Test 3 Cash-Ausgleich, Rate 6'364, Cash nie negativ", () => {
const p = plan({
age: 40,
retirementAge: 60,
phases: [{ id: "p1", durationYears: 3 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 0 } }),
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 6364 } }),
],
});
const ph = computePlan(p).phases[0];
expect(ph.cashEnd).toBe(5472);
expect(ph.cashNegative).toBe(false);
});
it("Test 4 Cash-Ausgleich, Rate 10'000, Cash wird negativ", () => {
const p = plan({
age: 40,
retirementAge: 60,
phases: [{ id: "p1", durationYears: 3 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 90000, teuerungsausgleich: 0 } }),
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 10000 } }),
],
});
const ph = computePlan(p).phases[0];
expect(ph.cashEnd).toBe(-5436);
expect(ph.cashNegative).toBe(true);
});
it("Ausgaben real -> nominal: reale Basis 100'000, 2% Inflation, 5 J.", () => {
const p = plan({
age: 40,
retirementAge: 60,
phases: [{ id: "p1", durationYears: 5 }],
elements: [el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } })],
});
const ph = computePlan(p).phases[0];
expect(ph.expenseStart).toBe(100000); // Jahr 1 nominal = real
expect(ph.expenseEnd).toBe(Math.round(100000 * Math.pow(1.02, 4))); // 108'243
});
it("Einkommen nominal flach bei Lohnerhoehung 0%", () => {
const p = plan({
age: 40,
retirementAge: 60,
phases: [{ id: "p1", durationYears: 10 }],
elements: [el("INCOME", "PERSON_A", { p1: { amount: 80000, teuerungsausgleich: 0 } })],
});
const ph = computePlan(p).phases[0];
expect(ph.incomeStart).toBe(80000);
expect(ph.incomeEnd).toBe(80000);
});
it("Bezugsrate aus Sonstigem Vermoegen fliesst ins Cash (Verzehrrate)", () => {
const p = plan({
age: 65,
retirementAge: 65,
inflation: 0,
phases: [{ id: "p1", durationYears: 3 }],
elements: [el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 30000, expectedReturn: 0, annualContribution: 0, annualWithdrawal: 10000 } })],
});
const ph = computePlan(p).phases[0];
expect(ph.plannedWithdrawRate).toBe(10000);
expect(ph.cashEnd).toBe(30000); // 3 x 10'000 Entnahme -> Cash
const asset = ph.elements.find((e) => e.category === "OTHER_ASSET")!;
expect(asset.endValue).toBe(0);
});
it("Kapitalzufluss/-investitionen: Verkauf in Phase 1, Reinvestition in Phase 2", () => {
const p = plan({
age: 40,
retirementAge: 70,
inflation: 0,
phases: [
{ id: "p1", durationYears: 1 },
{ id: "p2", durationYears: 1 },
],
elements: [
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 10000, expectedReturn: 0, annualContribution: 0 }, p2: {} }, { p1: { decision: "SELL" } }),
el("OTHER_ASSET", "HOUSEHOLD", { p1: { startValue: 0, expectedReturn: 0, annualContribution: 0 }, p2: { additionalInvestment: 10000, expectedReturn: 0 } }),
],
});
const r = computePlan(p);
const p2 = r.phases[1];
expect(p2.capitalInflow).toBe(10000); // Verkaufserloes aus dem Uebergang
expect(p2.capitalInvest).toBe(10000); // Zusatzinvestition
expect(p2.cashStart).toBe(0); // Startwert bereits nach Abzug der Investition
expect(p2.cashEnd).toBe(0);
// Startvermoegen Phase 2: Cash(0) + Vermoegen B(10'000) = 10'000 (kein Doppelzaehlen).
expect(p2.startWealthNominal).toBe(10000);
});
it("Cash-Anfangswert fliesst in die erste Phase ein", () => {
const p = plan({
age: 40,
retirementAge: 60,
inflation: 0,
initialCash: 50000,
phases: [{ id: "p1", durationYears: 3 }],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 100000, teuerungsausgleich: 0 } }),
],
});
const ph = computePlan(p).phases[0];
expect(ph.cashStart).toBe(50000);
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("Einmaliger Zufluss am Uebergang: nominal erfasst, Steuer abgezogen, direkt ins Cash", () => {
const p = plan({
age: 40,
retirementAge: 70,
inflation: 0,
initialCash: 1000,
phases: [
{ id: "p1", durationYears: 2, cashTransition: { mode: "INFLOW", inflowLabel: "Erbschaft", inflowAmount: 100000, inflowTaxRate: 10 } },
{ id: "p2", durationYears: 1 },
],
elements: [],
});
const r = computePlan(p);
expect(r.phases[1].oneOffInflow).toBe(90000); // 100'000 abzueglich 10% Steuer
expect(r.phases[1].oneOffInflowLabel).toBe("Erbschaft");
expect(r.phases[1].cashStart).toBe(91000); // 1'000 + 90'000
expect(r.phases[0].oneOffInflow).toBe(0); // Phase 1 hat keinen eingehenden Uebergang
});
it("Einmalige Kosten am Uebergang: real erfasst, mit Inflation aufgewertet", () => {
// Kosten 20'000 real, 2% Inflation, Grenze nach 10 Jahren -> 20'000 x 1.02^10 = 24'380.
const p = plan({
age: 40,
retirementAge: 70,
inflation: 2,
initialCash: 100000,
phases: [
{ id: "p1", durationYears: 10, cashTransition: { mode: "OUTFLOW", outflowLabel: "Poolbau", outflowAmount: 20000 } },
{ id: "p2", durationYears: 1 },
],
elements: [],
});
const r = computePlan(p);
const erwartet = Math.round(20000 * Math.pow(1.02, 10));
expect(r.phases[1].oneOffOutflow).toBe(erwartet);
expect(r.phases[1].oneOffOutflowLabel).toBe("Poolbau");
expect(r.phases[1].cashStart).toBe(100000 - erwartet);
});
it("Zufluss und Kosten zusammen (BOTH); Modus NONE bleibt wirkungslos", () => {
const beide = plan({
age: 40, retirementAge: 70, inflation: 0, initialCash: 0,
phases: [
{ id: "p1", durationYears: 1, cashTransition: { mode: "BOTH", inflowAmount: 50000, outflowAmount: 20000 } },
{ id: "p2", durationYears: 1 },
],
elements: [],
});
const r1 = computePlan(beide);
expect(r1.phases[1].cashStart).toBe(30000); // +50'000 -20'000
// Betraege sind erfasst, aber der Entscheid lautet "1:1 uebernehmen" -> keine Wirkung.
const keine = plan({
age: 40, retirementAge: 70, inflation: 0, initialCash: 0,
phases: [
{ id: "p1", durationYears: 1, cashTransition: { mode: "NONE", inflowAmount: 50000, outflowAmount: 20000 } },
{ id: "p2", durationYears: 1 },
],
elements: [],
});
expect(computePlan(keine).phases[1].cashStart).toBe(0);
});
it("Einmalige Kosten koennen eine Liquiditaetsluecke ausloesen", () => {
const p = plan({
age: 40, retirementAge: 70, inflation: 0, initialCash: 10000,
phases: [
{ id: "p1", durationYears: 1, cashTransition: { mode: "OUTFLOW", outflowAmount: 25000 } },
{ id: "p2", durationYears: 1 },
],
elements: [],
});
const p2 = computePlan(p).phases[1];
expect(p2.cashStart).toBe(-15000);
expect(p2.cashNegative).toBe(true);
expect(p2.incomplete).toBe(true);
});
it("Cash-Entscheid der LETZTEN Phase bleibt wirkungslos (kein Uebergang mehr)", () => {
const p = plan({
age: 40, retirementAge: 70, inflation: 0, initialCash: 5000,
phases: [{ id: "p1", durationYears: 1, cashTransition: { mode: "INFLOW", inflowAmount: 999999 } }],
elements: [],
});
const r = computePlan(p);
expect(r.phases[0].cashEnd).toBe(5000);
expect(r.nachlass).toBe(5000);
});
it("Fortschreibung: nominaler Einkommens-Basiswert Phase 1 -> Startwert Phase 2; Cash laeuft fort", () => {
const p = plan({
age: 40,
retirementAge: 70,
phases: [
{ id: "p1", durationYears: 5 },
{ id: "p2", durationYears: 5 },
],
elements: [
el("INCOME", "PERSON_A", { p1: { amount: 100000, teuerungsausgleich: 2 }, p2: {} }),
el("EXPENSE", "HOUSEHOLD", { p1: { amount: 80000, teuerungsausgleich: 0 }, p2: {} }),
],
});
const r = computePlan(p);
const expectedBasis = Math.round(100000 * Math.pow(1.02, 5));
const incomeP2 = r.phases[1].elements.find((e) => e.category === "INCOME")!;
expect(incomeP2.startValue).toBe(expectedBasis);
expect(r.phases[1].cashStart).toBe(r.phases[0].cashEnd);
});
});