Kapitalbezug: Personenzuordnung, Herkunft und Filter fuer bezogene Guthaben
Deploy App / deploy (push) Successful in 1m9s
Deploy App / deploy (push) Successful in 1m9s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-6
@@ -92,6 +92,12 @@ export interface ElementPhaseComputed {
|
||||
// Betrag, der aus einem Kapitalbezug (PK/3a) des vorigen Übergangs in dieses Element
|
||||
// umgeleitet wurde (Punkt C). Nur bei OTHER_ASSET und nur ab Phase 2 > 0.
|
||||
capitalFromTransfer?: number;
|
||||
capitalFromTransferSources?: { name: string; amount: number }[];
|
||||
// Nimmt dieses Element in DIESER Phase überhaupt noch Geld auf? PK und Säule 3a nicht mehr,
|
||||
// sobald der Besitzer pensioniert ist -- die Rechnung ignoriert dort Beiträge und
|
||||
// Zusatzeinlagen. Die Verteil-Dialoge dürfen solche Zeilen deshalb gar nicht erst anbieten
|
||||
// (sonst tippt man Beträge ein, die wirkungslos bleiben). Fehlt das Feld: nimmt auf.
|
||||
acceptsCapital?: boolean;
|
||||
yearly: ElementYearPoint[]; // Verlauf innerhalb dieser Phase
|
||||
trace?: Trace; // Rechenweg der Phasenwerte (nur mit explain)
|
||||
transitionTrace?: Trace; // Rechenweg des Übergangs NACH dieser Phase (nur mit explain)
|
||||
@@ -301,6 +307,10 @@ interface Carry {
|
||||
// Zusatzeinlage behandelt -- dadurch erscheint er im Element, in der Cash-Brücke und im
|
||||
// Verteil-Dialog, statt unsichtbar im Bestand zu verschwinden.
|
||||
capitalIn: number;
|
||||
// Woher der Betrag stammt (PK bzw. 3a, je Person) -- damit im UI nachvollziehbar bleibt,
|
||||
// welcher Bezug wohin geflossen ist. Bei zwei Personen fliessen sonst mehrere Beträge
|
||||
// ununterscheidbar in dasselbe Element.
|
||||
capitalInSources: { name: string; amount: number }[];
|
||||
// Punkt A (Roadmap Nr. 44): zuletzt verwendete Wiederkehr-Parameter (Raten, Beiträge,
|
||||
// Amortisation). Fehlt der Wert in einer Phase, gilt der aus der Vorphase.
|
||||
rates: Record<string, number>;
|
||||
@@ -318,6 +328,7 @@ function emptyCarry(): Carry {
|
||||
pkPensionAnnual: 0,
|
||||
flowBasis: 0,
|
||||
capitalIn: 0,
|
||||
capitalInSources: [],
|
||||
rates: {},
|
||||
hasCarry: false,
|
||||
};
|
||||
@@ -330,6 +341,18 @@ function fmt(v: number): string {
|
||||
}
|
||||
|
||||
// Generisch, damit der Aufrufer den vollen Personen-Typ (inkl. age) behält.
|
||||
// Sprechende Herkunft eines Kapitalbezugs: Elementname plus Person. Ohne die Person sind zwei
|
||||
// gleichnamige Guthaben (z. B. beide «Säule 3a») im UI nicht auseinanderzuhalten.
|
||||
function sourceLabelOf(
|
||||
e: { name: string; ownerRole: string | null },
|
||||
persons: { role: PersonRole; name: string | null }[]
|
||||
): string {
|
||||
if (e.ownerRole !== "PERSON_A" && e.ownerRole !== "PERSON_B") return e.name;
|
||||
const p = persons.find((x) => x.role === e.ownerRole);
|
||||
const who = p?.name?.trim() || (e.ownerRole === "PERSON_A" ? "Person A" : "Person B");
|
||||
return `${e.name} (${who})`;
|
||||
}
|
||||
|
||||
function personByRole<T extends { role: PersonRole }>(persons: T[], role: string): T | null {
|
||||
return persons.find((p) => p.role === role) ?? null;
|
||||
}
|
||||
@@ -670,9 +693,11 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
ec.startValue = carry.pkPensionAnnual;
|
||||
ec.endValue = carry.pkPensionAnnual;
|
||||
ec.summary = `Rente ${fmt(carry.pkPensionAnnual)}`;
|
||||
ec.acceptsCapital = false;
|
||||
} else if (!ownerWorking) {
|
||||
ec.note = "Vollständig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
ec.acceptsCapital = false;
|
||||
} else {
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
@@ -690,6 +715,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
if (!ownerWorking) {
|
||||
ec.note = "Vollständig bezogen";
|
||||
ec.summary = "Bezogen";
|
||||
ec.acceptsCapital = false;
|
||||
} else {
|
||||
const base = carry.hasCarry ? carry.value : Math.round(num(pd.currentValue));
|
||||
const topUp = carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0;
|
||||
@@ -711,7 +737,10 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const fromTransfer = carry.hasCarry ? carry.capitalIn : 0;
|
||||
const topUp = (carry.hasCarry ? Math.round(num(pd.additionalInvestment)) : 0) + fromTransfer;
|
||||
ec.capitalFromTransfer = fromTransfer;
|
||||
carry.capitalIn = 0; // verbraucht -- sonst flösse er in jeder Folgephase erneut
|
||||
ec.capitalFromTransferSources = carry.hasCarry ? carry.capitalInSources : [];
|
||||
// Verbraucht -- sonst flösse derselbe Betrag in jeder Folgephase erneut.
|
||||
carry.capitalIn = 0;
|
||||
carry.capitalInSources = [];
|
||||
const start = base + topUp;
|
||||
const rate = Math.round(inherited("annualContribution"));
|
||||
const withdrawal = Math.round(inherited("annualWithdrawal"));
|
||||
@@ -1427,7 +1456,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
let txImmediateRepay = 0;
|
||||
// Kapitalbezüge, deren Verwendung je Element geregelt ist (Punkt C). Gesammelt WÄHREND
|
||||
// der Übergangs-Schleife, angewendet danach -- die Zielelemente werden erst dort bekannt.
|
||||
const capitalUses: { net: number; td: TransitionData }[] = [];
|
||||
const capitalUses: { net: number; td: TransitionData; sourceName: string }[] = [];
|
||||
// Echte Vermögensänderungen an dieser Grenze (für die Brücke der Folgephase).
|
||||
// Verkäufe, Bezüge und Tilgungen sind für sich Umbuchungen -- vermögenswirksam sind
|
||||
// nur die Steuer, die Verrentung (Kapital verlässt die Bilanz) und die Differenz
|
||||
@@ -1507,7 +1536,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const net = Math.round(value * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += value - net;
|
||||
capitalUses.push({ net, td });
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
carry.value = 0;
|
||||
carry.pkPensionAnnual = 0;
|
||||
} else if (mode === "PENSION") {
|
||||
@@ -1519,7 +1548,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const net = Math.round(capital * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += capital - net;
|
||||
capitalUses.push({ net, td });
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100);
|
||||
txPensionConversion += value - capital;
|
||||
carry.value = 0;
|
||||
@@ -1540,7 +1569,7 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
const net = Math.round(ec.endValue * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100));
|
||||
txInflow += net;
|
||||
txTax += ec.endValue - net;
|
||||
capitalUses.push({ net, td });
|
||||
capitalUses.push({ net, td, sourceName: sourceLabelOf(e, persons) });
|
||||
carry.value = 0;
|
||||
} else {
|
||||
const withdrawal = Math.min(ec.endValue, Math.round(num(td.withdrawal)));
|
||||
@@ -1720,7 +1749,11 @@ export function computePlan(plan: PlanInput, sample?: PlanSample, options?: Comp
|
||||
orderedElements.find((e) => e.category === "OTHER_ASSET" && carries.get(e.id)!.status === "ACTIVE");
|
||||
// Der Betrag wandert NICHT direkt in den Bestand, sondern über `capitalIn` in die
|
||||
// Zusatzeinlage der Folgephase -- dadurch wird er im UI überall sichtbar.
|
||||
if (target) carries.get(target.id)!.capitalIn += investBudget;
|
||||
if (target) {
|
||||
const c = carries.get(target.id)!;
|
||||
c.capitalIn += investBudget;
|
||||
c.capitalInSources.push({ name: use.sourceName, amount: investBudget });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user