Drei Berechnungs-Fixes: Phaseninflation, Tilgungsraten, Vorbezugssteuer
- Phase.inflationRate ersatzlos entfernt (inkl. Migration). Die Inflation liegt seit V5 plan-weit; das Feld wurde von computePlan nie gelesen und war wirkungslos. - Amortisation und Tilgung stoppen, sobald Hypothek bzw. Schuld abbezahlt sind: Hypothek/Restschuld sind neu laufende Salden, die Rate ist pro Jahr am Restsaldo gekappt. Belastet danach weder Cash noch Sparquote. - Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezuegen vor der Pensionierung. Der Bezug wird brutto dem Kapital entnommen, netto ins Cash gebucht. plannedSaveRate ist neu die Rate des ersten Phasenjahres. Drei Regressionstests ergaenzt (10 -> 13). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,6 @@ export interface CellContext {
|
||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||
phaseInflation: number; // Plan-Inflationsrate (Info)
|
||||
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
||||
}
|
||||
|
||||
@@ -49,7 +48,9 @@ function DerivedField({ label, value, help }: { label: string; value: number; he
|
||||
);
|
||||
}
|
||||
|
||||
// PK/3a-Bezugs-Entscheid im normalen Uebergang: Kein Bezug / Bezug (+ Betrag).
|
||||
// PK/3a-Bezugs-Entscheid im normalen Uebergang (Vorbezug): Kein Bezug / Bezug (+ Betrag +
|
||||
// Kapitalbezugssteuer). Der Bezugsbetrag wird brutto dem Kapital entnommen; ins Cash fliesst
|
||||
// der Betrag nach Abzug der Steuer.
|
||||
function WithdrawalDecision({
|
||||
td,
|
||||
setT,
|
||||
@@ -62,6 +63,8 @@ function WithdrawalDecision({
|
||||
label: string;
|
||||
}) {
|
||||
const mode = td.withdrawalMode ?? "NONE";
|
||||
const gross = num(td.withdrawal);
|
||||
const taxRate = num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE);
|
||||
return (
|
||||
<>
|
||||
<SelectField
|
||||
@@ -74,13 +77,27 @@ function WithdrawalDecision({
|
||||
]}
|
||||
/>
|
||||
{mode === "AMOUNT" && (
|
||||
<MoneyField
|
||||
label={label}
|
||||
help={`Maximal ${formatChf(max)} (Endwert der Vorphase).`}
|
||||
value={num(td.withdrawal)}
|
||||
max={max}
|
||||
onChange={(v) => setT({ withdrawal: v })}
|
||||
/>
|
||||
<>
|
||||
<MoneyField
|
||||
label={label}
|
||||
help={`Bruttobetrag, dem Kapital entnommen. Maximal ${formatChf(max)} (Endwert der Vorphase).`}
|
||||
value={gross}
|
||||
max={max}
|
||||
onChange={(v) => setT({ withdrawal: v })}
|
||||
/>
|
||||
<NumberField
|
||||
label="Kapitalbezugssteuer (%)"
|
||||
help="Ein Vorbezug ist wie der Bezug bei Pensionierung kapitalbezugssteuerpflichtig."
|
||||
step={0.5}
|
||||
value={taxRate}
|
||||
onChange={(v) => setT({ capitalTaxRate: v })}
|
||||
/>
|
||||
<DerivedField
|
||||
label="Auszahlung netto (ins Cash)"
|
||||
value={Math.round(gross * (1 - taxRate / 100))}
|
||||
help="Bruttobezug abzueglich Kapitalbezugssteuer. Wird automatisch berechnet."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ export function PlanProfileFields({
|
||||
|
||||
<NumberField
|
||||
label="Erwartete Inflationsrate (%)"
|
||||
help="Langfristige Annahme zur jaehrlichen Teuerung. Kann pro Lebensphase individuell ueberschrieben werden."
|
||||
help="Langfristige Annahme zur jaehrlichen Teuerung. Gilt plan-weit fuer alle Lebensphasen."
|
||||
value={draft.inflationRateDefault}
|
||||
step={0.1}
|
||||
onChange={(v) => onChange({ ...draft, inflationRateDefault: v })}
|
||||
|
||||
@@ -154,10 +154,6 @@ export function PlanView({
|
||||
return !!before?.working && !!after && !after.working;
|
||||
}
|
||||
|
||||
function phaseInflationFor(phaseId: string): number {
|
||||
return plan.phases.find((p) => p.id === phaseId)?.inflationRate ?? plan.inflationRateDefault;
|
||||
}
|
||||
|
||||
// Baut den Kontext fuer eine Phasenzelle.
|
||||
function buildPhaseContext(phase: PhaseComputed, element: ElementInput): CellContext {
|
||||
const ce = computedElement(phase.id, element.id);
|
||||
@@ -175,7 +171,6 @@ export function PlanView({
|
||||
carriedEndValue: ce?.endValue ?? 0,
|
||||
carried: ce?.carried ?? false,
|
||||
derivedStart: ce?.baseValue ?? 0,
|
||||
phaseInflation: phaseInflationFor(phase.id),
|
||||
deflatorStart: phase.cumulativeInflationStart,
|
||||
};
|
||||
}
|
||||
@@ -192,7 +187,6 @@ export function PlanView({
|
||||
carriedEndValue: ce?.endValue ?? 0,
|
||||
carried: ce?.carried ?? false,
|
||||
derivedStart: 0,
|
||||
phaseInflation: phaseInflationFor(fromPhase.id),
|
||||
deflatorStart: fromPhase.cumulativeInflationStart,
|
||||
};
|
||||
}
|
||||
@@ -241,7 +235,7 @@ export function PlanView({
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex);
|
||||
}
|
||||
|
||||
async function handleAddPhase(payload: { name?: string; durationYears?: number; inflationRate?: number | null }) {
|
||||
async function handleAddPhase(payload: { name?: string; durationYears?: number }) {
|
||||
await api.post(`/api/plans/${plan.id}/phases`, payload);
|
||||
setShowAddPhase(false);
|
||||
onChanged();
|
||||
@@ -855,7 +849,6 @@ function AddElementDialog({
|
||||
carriedEndValue: 0,
|
||||
carried: false,
|
||||
derivedStart: 0,
|
||||
phaseInflation: plan.inflationRateDefault,
|
||||
deflatorStart: firstPhase.cumulativeInflationStart,
|
||||
};
|
||||
|
||||
@@ -954,7 +947,7 @@ function AddPhaseDialog({
|
||||
}: {
|
||||
maxDurationYears: number | null;
|
||||
onClose: () => void;
|
||||
onCreate: (payload: { name?: string; durationYears?: number; inflationRate?: number | null }) => void;
|
||||
onCreate: (payload: { name?: string; durationYears?: number }) => void;
|
||||
}) {
|
||||
const cap = maxDurationYears;
|
||||
const [name, setName] = useState("");
|
||||
|
||||
Reference in New Issue
Block a user