Einmalige Sonderein-/ausgaben am Cash-Uebergang (Roadmap Nr. 1)
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:
2026-07-17 08:14:44 +02:00
parent 87e6a5f564
commit 9f5bd754eb
12 changed files with 665 additions and 30 deletions
+44 -1
View File
@@ -61,6 +61,13 @@ export interface PhaseComputed {
plannedWithdrawRate: number; // geplante Verzehrrate: Bezugsraten aus Sonstigem Vermoegen
capitalInflow: number; // Kapitalzufluss: PK-/3a-Bezuege + Verkaeufe (aus dem Uebergang in diese Phase)
capitalInvest: number; // Kapitalinvestitionen: Zusatz-/Neuinvestitionen + sofortige Tilgungen
// Einmalige Sonderein-/ausgaben aus dem Uebergang in DIESE Phase (nominal, netto nach Steuer).
// Bewusst getrennt von capitalInflow/capitalInvest: eine Erbschaft ist kein Verkaufserloes,
// ein Poolbau keine Kapitalinvestition.
oneOffInflow: number;
oneOffInflowLabel: string | null;
oneOffOutflow: number;
oneOffOutflowLabel: string | null;
cashStart: number;
cashEnd: number;
cashNegative: boolean; // Cash faellt in dieser Phase (irgendwann) unter 0 -> Liquiditaetsluecke
@@ -147,6 +154,10 @@ export function computePlan(plan: PlanInput): PlanComputed {
// Aus dem Uebergang der Vorphase in DIESE Phase fliessende Groessen (Kopf-Kennzahlen).
let incomingInflow = 0; // Brutto-Zufluss: Verkaeufe + PK-/3a-Bezuege
let incomingImmediateRepay = 0; // sofortige Schuldentilgungen (Abfluss)
let incomingOneOffInflow = 0; // einmaliger Sonderzufluss (netto nach Steuer)
let incomingOneOffInflowLabel: string | null = null;
let incomingOneOffOutflow = 0; // einmalige Sonderkosten (nominal)
let incomingOneOffOutflowLabel: string | null = null;
let ruinAge: number | null = null;
for (let i = 0; i < phases.length; i++) {
@@ -500,6 +511,10 @@ export function computePlan(plan: PlanInput): PlanComputed {
plannedWithdrawRate: plannedWithdrawTotal,
capitalInflow: Math.round(incomingInflow),
capitalInvest: Math.round(investmentsFromCash + incomingImmediateRepay),
oneOffInflow: Math.round(incomingOneOffInflow),
oneOffInflowLabel: incomingOneOffInflowLabel,
oneOffOutflow: Math.round(incomingOneOffOutflow),
oneOffOutflowLabel: incomingOneOffOutflowLabel,
cashStart: Math.round(cashStart),
cashEnd,
cashNegative,
@@ -516,6 +531,30 @@ export function computePlan(plan: PlanInput): PlanComputed {
// --- Uebergang: Carry aktualisieren, Cash der Folgephase bilden ---
let txInflow = 0;
let txImmediateRepay = 0;
// Einmalige Sonderein-/ausgaben auf dem Cash-Konto. Nur sinnvoll, wenn eine Folgephase
// existiert -- nach der letzten Phase gibt es keinen Uebergang. Der Wechselkurs zwischen
// real und nominal ist an dieser Grenze `cumulativeInflation` (Bestands-Deflator am
// Phasenende), denn Cash ist ein Bestand.
let txOneOffInflow = 0;
let txOneOffOutflow = 0;
let txOneOffInflowLabel: string | null = null;
let txOneOffOutflowLabel: string | null = null;
if (nextPhase) {
const ct = phase.cashTransition ?? {};
const mode = ct.mode ?? "NONE";
if (mode === "INFLOW" || mode === "BOTH") {
// Zufluss ist NOMINAL erfasst (wie Einkommen); Steuer mindert den Netto-Zufluss.
const gross = Math.round(num(ct.inflowAmount));
txOneOffInflow = Math.round(gross * (1 - num(ct.inflowTaxRate, 0) / 100));
txOneOffInflowLabel = ct.inflowLabel?.trim() || null;
}
if (mode === "OUTFLOW" || mode === "BOTH") {
// Kosten sind REAL erfasst (wie Ausgaben) -> mit der kumulierten Inflation aufwerten.
txOneOffOutflow = Math.round(num(ct.outflowAmount) * cumulativeInflation);
txOneOffOutflowLabel = ct.outflowLabel?.trim() || null;
}
}
for (const e of orderedElements) {
const carry = carries.get(e.id)!;
const ec = ecById.get(e.id)!;
@@ -616,9 +655,13 @@ export function computePlan(plan: PlanInput): PlanComputed {
carry.hasCarry = true;
}
cashCarryIn = cashEnd + txInflow - txImmediateRepay;
cashCarryIn = cashEnd + txInflow + txOneOffInflow - txImmediateRepay - txOneOffOutflow;
incomingInflow = txInflow;
incomingImmediateRepay = txImmediateRepay;
incomingOneOffInflow = txOneOffInflow;
incomingOneOffInflowLabel = txOneOffInflowLabel;
incomingOneOffOutflow = txOneOffOutflow;
incomingOneOffOutflowLabel = txOneOffOutflowLabel;
yearsBefore += duration;
}