Fix: Cash-Vorbelegung im Ist-Wizard zeigte den Phasen-Endwert
Deploy App / deploy (push) Successful in 1m4s

Der Wizard las cashBridge.cashEnd -- also den Stand am Phasenende statt
am gewaehlten Stichtag. In einer Phase 2026-2036 erschien fuer 2031 der
Wert von 2036.

Ursache: computePlan wies den Cash-Bestand nur je PHASE aus. YearPoint
traegt neu ein Feld `cash` (Stand am Jahresende), analog zu
wealthNominal; der Wizard liest daraus.

Die Vorbelegung der Elemente war nie betroffen -- die stammte schon
immer aus dem Jahresverlauf.

Zwei Regressionstests (208 -> 210). Spezifikation 0.22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:53:59 +02:00
parent e117e5a4e5
commit 44c6c81f5d
4 changed files with 72 additions and 7 deletions
+3 -4
View File
@@ -178,11 +178,10 @@ export function ActualsDialog({
if (!base) return 0;
const planYear = toPlanYear(year, base.plan.startYear);
if (planYear === null) return 0;
// Der Stand am Ende GENAU DIESES Jahres -- nicht am Ende der Phase. In einer Phase über
// zehn Jahre, in der sich das Cash-Konto füllt, sind das zwei völlig verschiedene Zahlen.
const computed = computePlan(base.plan);
const ph = computed.phases.find(
(p) => planYear <= computed.phases.slice(0, p.sequenceNumber).reduce((s, x) => s + x.durationYears, 0)
);
return Math.round(ph?.cashBridge.cashEnd ?? 0);
return computed.yearly.find((y) => y.year === planYear)?.cash ?? 0;
}, [base, year]);
function startWizard() {
+58
View File
@@ -220,6 +220,64 @@ describe("Berechnung mit Ist-Werten", () => {
});
});
describe("Cash-Bestand je Jahr", () => {
// Genau der gemeldete Fall: EINE lange Phase, in der sich das Cash-Konto kontinuierlich
// füllt. Die Vorbelegung im Wizard muss den Stand des GEWÄHLTEN Jahres zeigen -- nicht den
// Stand am Phasenende. Zuvor las der Dialog `cashBridge.cashEnd` und zeigte in der
// Phasenmitte den Endwert an.
function savingPlan(): PlanInput {
return {
id: "s",
name: "T",
householdType: "SINGLE",
inflationRateDefault: 0,
initialCash: 100,
startYear: 2026,
persons: [{ id: "A", role: "PERSON_A", name: null, age: 40, retirementAge: 70 }],
phases: [{ id: "p1", sequenceNumber: 1, name: "Erwerb", durationYears: 10, cashTransition: {} }],
elements: [
{
id: "inc",
category: "INCOME",
name: "Lohn",
ownerRole: "HOUSEHOLD",
orderIndex: 1,
phaseValues: { p1: { amount: 100000 } },
transitionValues: {},
sourceElementId: null,
},
{
id: "exp",
category: "EXPENSE",
name: "Leben",
ownerRole: "HOUSEHOLD",
orderIndex: 2,
phaseValues: { p1: { amount: 90000 } },
transitionValues: {},
sourceElementId: null,
},
],
} as unknown as PlanInput;
}
it("weist den Cash-Stand für JEDES Jahr aus, nicht nur je Phase", () => {
const computed = computePlan(savingPlan());
// 10'000 Überschuss pro Jahr auf 100 Startguthaben.
expect(computed.yearly.find((y) => y.year === 1)!.cash).toBe(10100);
expect(computed.yearly.find((y) => y.year === 5)!.cash).toBe(50100);
expect(computed.yearly.find((y) => y.year === 10)!.cash).toBe(100100);
});
it("liegt in der Phasenmitte deutlich unter dem Phasen-Endwert", () => {
// Die eigentliche Regression: Mitte und Ende dürfen nicht dieselbe Zahl sein.
const computed = computePlan(savingPlan());
const mid = computed.yearly.find((y) => y.year === 5)!.cash;
const phaseEnd = computed.phases[0].cashBridge.cashEnd;
expect(phaseEnd).toBe(computed.yearly.find((y) => y.year === 10)!.cash);
expect(mid).toBeLessThan(phaseEnd);
});
});
describe("rebaseFlow", () => {
it("trifft im Ist-Jahr genau den erfassten Betrag", () => {
// Basis so, dass basis * (1+idx)^(t-1) === Ist-Wert.
+7
View File
@@ -198,6 +198,11 @@ export interface YearPoint {
// Vermögensverlauf über ALLE Jahre statt nur über die Phasengrenzen.
wealthNominal: number;
wealthReal: number;
// Cash-Bestand am Jahresende. Die Brücken führen Cash nur je PHASE (Anfang/Ende) -- für
// eine Aussage zu einem einzelnen Jahr (Vorbelegung der effektiven Werte, Kap. 3.9.2)
// reicht das nicht: In einer Phase über zehn Jahre ist der Phasen-Endwert etwas ganz
// anderes als der Stand in der Mitte.
cash: number;
}
export interface PlanComputed {
@@ -751,6 +756,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
expenseReal: Math.round(expenseReal),
wealthNominal: 0,
wealthReal: 0,
cash: 0,
};
yearly.push(yearPoint);
@@ -907,6 +913,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
for (const d of debts) total += -d.owed;
yearPoint.wealthNominal = Math.round(total);
yearPoint.wealthReal = Math.round(total / (cumInfl[yearsBefore + t] || 1));
yearPoint.cash = Math.round(cash);
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
}