From 9506f43b67a53475ab7aea643fe01181bdd998b1 Mon Sep 17 00:00:00 2001 From: kelle Date: Tue, 14 Jul 2026 15:18:03 +0200 Subject: [PATCH] Fix: Uebergangs-Defaults (Steuer/Umwandlung) wurden in der Berechnung als 0 gerechnet Kapitalbezugssteuer, Grundstueckgewinnsteuer und PK-Umwandlungssatz wurden im UI nur optisch vorbelegt (num(td.x, 8/20/6)), die Berechnung las aber num(td.x) mit Fallback 0. Ein nicht angetupftes Feld fuehrte so zu 0% Steuer bzw. 0 Rente. Defaults sind jetzt zentrale Konstanten (constants.ts) und werden in Berechnung UND UI als Fallback genutzt. Betrifft: PK-Kapital/-Kombi/-Rente, 3a-Bezug, Immobilienverkauf. Co-Authored-By: Claude Opus 4.8 --- src/components/ElementDetail.tsx | 15 ++++++++++----- src/lib/calculations.ts | 15 +++++++++------ src/lib/constants.ts | 10 ++++++++-- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/ElementDetail.tsx b/src/components/ElementDetail.tsx index 78342b5..830a766 100644 --- a/src/components/ElementDetail.tsx +++ b/src/components/ElementDetail.tsx @@ -6,7 +6,12 @@ import { FieldLabel, MoneyField, NumberField, SelectField } from "@/components/F import { formatChf } from "@/lib/format"; import { api } from "@/lib/api-client"; import { CATEGORY_LABELS, num } from "@/lib/elements"; -import { PILLAR_3A_MAX_ANNUAL } from "@/lib/constants"; +import { + DEFAULT_CAPITAL_TAX_RATE, + DEFAULT_PK_CONVERSION_RATE, + DEFAULT_PROPERTY_GAINS_TAX_RATE, + PILLAR_3A_MAX_ANNUAL, +} from "@/lib/constants"; import type { ElementCategory, PhaseData, TransitionData } from "@/lib/elements"; export interface CellContext { @@ -336,12 +341,12 @@ export function ElementTransitionFields({ label="Umwandlungssatz (%)" help="Jaehrliche Rente = verrentetes Kapital x Umwandlungssatz." step={0.1} - value={num(td.conversionRate, 6)} + value={num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)} onChange={(v) => setT({ conversionRate: v })} /> )} {(mode === "CAPITAL" || mode === "COMBI") && ( - setT({ capitalTaxRate: v })} /> + setT({ capitalTaxRate: v })} /> )} {mode === "COMBI" && ( setT({ capitalTaxRate: v })} /> ); @@ -385,7 +390,7 @@ export function ElementTransitionFields({ {decision === "SELL" && ( <> setT({ salePrice: v })} /> - setT({ saleTaxRate: v })} /> + setT({ saleTaxRate: v })} /> )} diff --git a/src/lib/calculations.ts b/src/lib/calculations.ts index 160a302..2ea96f3 100644 --- a/src/lib/calculations.ts +++ b/src/lib/calculations.ts @@ -2,6 +2,9 @@ import { AHV_COUPLE_CAP_FACTOR, AHV_FULL_CONTRIBUTION_YEARS, AHV_MAX_ANNUAL_SINGLE, + DEFAULT_CAPITAL_TAX_RATE, + DEFAULT_PK_CONVERSION_RATE, + DEFAULT_PROPERTY_GAINS_TAX_RATE, } from "@/lib/constants"; import { num } from "@/lib/elements"; import type { ElementCategory } from "@/lib/elements"; @@ -459,18 +462,18 @@ export function computePlan(plan: PlanInput): PlanComputed { const value = ec.endValue; const mode = td.payoutMode ?? "PENSION"; if (mode === "CAPITAL") { - const net = Math.round(value * (1 - num(td.capitalTaxRate) / 100)); + const net = Math.round(value * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100)); outgoing += net; carry.value = 0; carry.pkPensionAnnual = 0; } else if (mode === "PENSION") { - carry.pkPensionAnnual = Math.round((value * num(td.conversionRate)) / 100); + carry.pkPensionAnnual = Math.round((value * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100); carry.value = 0; } else { const capital = Math.min(value, Math.round(num(td.capitalAmount))); - const net = Math.round(capital * (1 - num(td.capitalTaxRate) / 100)); + const net = Math.round(capital * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100)); outgoing += net; - carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate)) / 100); + carry.pkPensionAnnual = Math.round(((value - capital) * num(td.conversionRate, DEFAULT_PK_CONVERSION_RATE)) / 100); carry.value = 0; } } else { @@ -482,7 +485,7 @@ export function computePlan(plan: PlanInput): PlanComputed { } case "PILLAR_3A": { if (ownerRetiresNext) { - const net = Math.round(ec.endValue * (1 - num(td.capitalTaxRate) / 100)); + const net = Math.round(ec.endValue * (1 - num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE) / 100)); outgoing += net; carry.value = 0; } else { @@ -506,7 +509,7 @@ export function computePlan(plan: PlanInput): PlanComputed { const purchase = Math.round(num(e.phaseValues[phase.id]?.purchasePrice)); const salePrice = Math.round(num(td.salePrice)); const gain = Math.max(0, salePrice - purchase); - const tax = gain * (num(td.saleTaxRate) / 100); + const tax = gain * (num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE) / 100); outgoing += Math.round(salePrice - carry.mortgage - tax); carry.status = "SOLD"; } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 4656b2d..a1c0726 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -13,6 +13,12 @@ export const AHV_COUPLE_CAP_FACTOR = 1.5; // Beitragsjahr (Ausfalljahr) wird die Rente um 1/44 gekuerzt. export const AHV_FULL_CONTRIBUTION_YEARS = 44; -// Maximaler jaehrlicher Saeule-3a-Beitrag fuer PK-Versicherte (2026). Wird in -// 100er-Schritten erfasst (nicht 1'000er wie andere Betraege). +// Maximaler jaehrlicher Saeule-3a-Beitrag fuer PK-Versicherte (2026). export const PILLAR_3A_MAX_ANNUAL = 7258; + +// Default-Annahmen fuer Uebergangs-Ereignisse (pro Ereignis editierbar). Werden SOWOHL als +// UI-Vorschlag als auch in der Berechnung als Fallback verwendet -- damit ein nicht +// angetupfter Wert nicht faelschlich als 0 gerechnet wird. +export const DEFAULT_PK_CONVERSION_RATE = 6; // % (jaehrliche Rente = Kapital x Satz) +export const DEFAULT_CAPITAL_TAX_RATE = 8; // % Kapitalbezugssteuer (PK-/3a-Kapitalbezug) +export const DEFAULT_PROPERTY_GAINS_TAX_RATE = 20; // % Grundstueckgewinnsteuer