Fix: Cash-Vorbelegung im Ist-Wizard zeigte den Phasen-Endwert
Deploy App / deploy (push) Successful in 1m4s
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:
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user