Fix: Uebergangs-Defaults (Steuer/Umwandlung) wurden in der Berechnung als 0 gerechnet
Deploy App / deploy (push) Successful in 54s

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:18:03 +02:00
parent b45af8f264
commit 9506f43b67
3 changed files with 27 additions and 13 deletions
+10 -5
View File
@@ -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") && (
<NumberField label="Kapitalbezugssteuer (%)" step={0.5} value={num(td.capitalTaxRate, 8)} onChange={(v) => setT({ capitalTaxRate: v })} />
<NumberField label="Kapitalbezugssteuer (%)" step={0.5} value={num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)} onChange={(v) => setT({ capitalTaxRate: v })} />
)}
{mode === "COMBI" && (
<MoneyField
@@ -363,7 +368,7 @@ export function ElementTransitionFields({
label="Kapitalbezugssteuer (%)"
help="Die Saeule 3a wird bei Pensionierung vollstaendig bezogen."
step={0.5}
value={num(td.capitalTaxRate, 8)}
value={num(td.capitalTaxRate, DEFAULT_CAPITAL_TAX_RATE)}
onChange={(v) => setT({ capitalTaxRate: v })}
/>
);
@@ -385,7 +390,7 @@ export function ElementTransitionFields({
{decision === "SELL" && (
<>
<MoneyField label="Verkaufspreis (CHF)" value={num(td.salePrice)} onChange={(v) => setT({ salePrice: v })} />
<NumberField label="Grundstueckgewinnsteuer (%)" step={1} value={num(td.saleTaxRate, 20)} onChange={(v) => setT({ saleTaxRate: v })} />
<NumberField label="Grundstueckgewinnsteuer (%)" step={1} value={num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE)} onChange={(v) => setT({ saleTaxRate: v })} />
</>
)}
</>
+9 -6
View File
@@ -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";
}
+8 -2
View File
@@ -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