// Verteilung von verfügbarem Kapital und laufender Spar-/Verzehrquote. // // Reine Orchestrierung: Beide Werkzeuge schreiben ausschliesslich Felder, die es längst gibt // (additionalInvestment, extraAmortization, immediateRepayment, annualContribution, // annualWithdrawal, amortization, annualRepayment). An der Berechnung ändert sich nichts -- // die Vorschau entsteht, indem der Plan mit den Entwurfswerten kopiert und erneut durch // computePlan geschickt wird. Dadurch ist die angezeigte Wirkung per Konstruktion exakt die // spätere, inklusive aller Kappungen (z. B. Bezugsrate am Bestand, Amortisation an der // Restschuld). import type { PhaseComputed } from "@/lib/calculations"; import type { PhaseData, TransitionData } from "@/lib/elements"; import type { PlanInput } from "@/lib/types"; // --- Verfügbares Kapital ------------------------------------------------------------------ // // Der "Topf" ist das Kapital, das beim Übergang IN diese Phase zur Verfügung steht -- also // bevor etwas davon investiert oder in Schulden gesteckt wurde. Alle Bestandteile stammen aus // der Cash-Brücke (seit 0.11), es braucht keine zusätzliche Berechnung: // // Topf = Cash-Ende der Vorphase + Kapitalzufluss + einmaliger Zufluss − einmalige Kosten // = cashStart + Investitionen + Sofort-Tilgungen // // Die zweite Form ist die, die hier verwendet wird: Sie zerlegt den Topf direkt in // "schon verteilt" und "Rest auf Cash". export interface CapitalPot { total: number; // gesamter verteilbarer Betrag fromPreviousCash: number; // Cash-Endbestand der Vorphase capitalInflow: number; // Verkäufe, PK-/3a-Bezüge oneOffInflow: number; // Erbschaft o. ä. (netto nach Steuer) oneOffOutflow: number; // einmalige Kosten (mindern den Topf) allocatedInvestments: number; // Zusatzeinlagen in PK/3a/Vermögen allocatedRepayments: number; // Sonderamortisation + Sofort-Tilgung rest: number; // bleibt auf dem Cash-Konto (= cashStart) } export function capitalPot(phase: PhaseComputed): CapitalPot { const c = phase.cashBridge; return { total: c.cashStart + c.investments + c.immediateRepay, fromPreviousCash: c.openingCash, capitalInflow: c.capitalInflow, oneOffInflow: c.oneOffInflow, oneOffOutflow: c.oneOffOutflow, allocatedInvestments: c.investments, allocatedRepayments: c.immediateRepay, rest: c.cashStart, }; } // --- Spar-/Verzehrquote ------------------------------------------------------------------- // // Die Quote ist KEIN fester Betrag: Einkommen wächst mit der Lohnerhöhung, Ausgaben mit der // Inflation. Deshalb werden drei Grössen ausgewiesen -- erstes Jahr, letztes Jahr und die // Summe über die ganze Phase ("absolute Quote"). Die Summe ist die Grösse, gegen die sich // eine flache Jahresrate sinnvoll verteilen lässt. // // Hinweis zur Einheit: `total` summiert nominale Franken verschiedener Jahre -- exakt so, wie // das Cash-Konto im Modell funktioniert. Für die Frage "wie viel Cash steht über die Phase // zur Verfügung" ist das die richtige Grösse, als Kaufkraft-Aussage taugt sie nicht. export interface QuotaSummary { start: number; // Quote im ersten Phasenjahr end: number; // Quote im letzten Phasenjahr total: number; // Summe über alle Phasenjahre allocatedOut: number; // Sparbeiträge + Amortisationen + Tilgungen, über die Phase summiert allocatedIn: number; // Bezugsraten, über die Phase summiert netToCash: number; // was unter dem Strich aufs Cash geht (= cashEnd − cashStart) isConsumption: boolean; } export function quotaSummary(phase: PhaseComputed): QuotaSummary { const c = phase.cashBridge; const allocatedOut = c.savingRates + c.debtRates; return { start: phase.quotaStart, end: phase.quotaEnd, total: c.quotaTotal, allocatedOut, allocatedIn: c.withdrawals, netToCash: c.quotaTotal - allocatedOut + c.withdrawals, isConsumption: c.quotaTotal < 0, }; } // --- Entwurfswerte auf einen Plan anwenden (für die Live-Vorschau) ------------------------ // Beide Funktionen sind rein und lassen den Ausgangsplan unberührt. export interface PhasePatch { elementId: string; field: keyof PhaseData; value: number; } export function applyPhasePatches(plan: PlanInput, phaseId: string, patches: PhasePatch[]): PlanInput { if (patches.length === 0) return plan; const byElement = new Map(); for (const p of patches) { const list = byElement.get(p.elementId) ?? []; list.push(p); byElement.set(p.elementId, list); } return { ...plan, elements: plan.elements.map((e) => { const list = byElement.get(e.id); if (!list) return e; const merged: PhaseData = { ...(e.phaseValues[phaseId] ?? {}) }; for (const p of list) (merged as Record)[p.field as string] = p.value; return { ...e, phaseValues: { ...e.phaseValues, [phaseId]: merged } }; }), }; } export interface TransitionPatch { elementId: string; field: keyof TransitionData; value: number; } export function applyTransitionPatches( plan: PlanInput, fromPhaseId: string, patches: TransitionPatch[] ): PlanInput { if (patches.length === 0) return plan; const byElement = new Map(); for (const p of patches) { const list = byElement.get(p.elementId) ?? []; list.push(p); byElement.set(p.elementId, list); } return { ...plan, elements: plan.elements.map((e) => { const list = byElement.get(e.id); if (!list) return e; // Bestehende Entscheide (decision, salePrice, payoutMode …) bleiben erhalten -- hier // werden nur die Betragsfelder überschrieben. const merged: TransitionData = { ...(e.transitionValues[fromPhaseId] ?? {}) }; for (const p of list) (merged as Record)[p.field as string] = p.value; return { ...e, transitionValues: { ...e.transitionValues, [fromPhaseId]: merged } }; }), }; }