Einmalige Sonderein-/ausgaben am Cash-Uebergang (Roadmap Nr. 1)
Deploy App / deploy (push) Successful in 1m51s
Deploy App / deploy (push) Successful in 1m51s
Der Cash-Uebergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 uebernehmen / einmaliger Zufluss / einmalige Kosten / beides. Die Betraege gehen direkt aufs Cash-Konto und bleiben aus der Spar-/Verzehrquote heraus. - Zufluss NOMINAL erfasst, real angezeigt (wie Einkommen), optionaler Steuersatz (Default 0 %). Kosten REAL erfasst, nominal angezeigt (wie Ausgaben). Umrechnung ueber den Bestands-Deflator an der Phasengrenze. - Entscheid startet unbeantwortet und zaehlt im "offen"-Badge mit; eine neue Phase erzeugt damit automatisch einen offenen Cash-Entscheid am neuen Uebergang. - Eigene Kopf-Kennzahlen statt Vermischung mit Kapitalzufluss/-investitionen: eine Erbschaft ist kein Verkaufserloes, ein Poolbau keine Investition. - Cash ist kein FinancialElement -> der Entscheid haengt als JSON an der Von-Phase (neue Spalte Phase.cashTransition + Migration). Szenario-Kopie nimmt ihn mit. Fuenf Regressionstests ergaenzt (13 -> 18). Spezifikation auf v0.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { computePlan } from "@/lib/calculations";
|
||||
import type { ElementCategory, PhaseData, TransitionData } from "@/lib/elements";
|
||||
import type { CashTransitionData, ElementCategory, PhaseData, TransitionData } from "@/lib/elements";
|
||||
import type { PlanInput } from "@/lib/types";
|
||||
|
||||
// --- kleine Bau-Helfer ---
|
||||
@@ -21,7 +21,7 @@ function plan(opts: {
|
||||
retirementAge: number;
|
||||
inflation?: number;
|
||||
initialCash?: number;
|
||||
phases: { id: string; durationYears: number }[];
|
||||
phases: { id: string; durationYears: number; cashTransition?: CashTransitionData }[];
|
||||
elements: ReturnType<typeof el>[];
|
||||
household?: "SINGLE" | "COUPLE";
|
||||
}): PlanInput {
|
||||
@@ -32,7 +32,13 @@ 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 })),
|
||||
phases: opts.phases.map((p, i) => ({
|
||||
id: p.id,
|
||||
sequenceNumber: i + 1,
|
||||
name: p.id,
|
||||
durationYears: p.durationYears,
|
||||
cashTransition: p.cashTransition ?? {},
|
||||
})),
|
||||
elements: opts.elements,
|
||||
};
|
||||
}
|
||||
@@ -251,6 +257,95 @@ describe("V5 Golden Tests", () => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user