Netto/Brutto-Klarstellung fuer die AHV + erweitertes Immobilien-Modul
Deploy App / deploy (push) Successful in 59s

Roadmap Nr. 9 (reduziert): Einkommen ist neu explizit als NETTOLOHN definiert
(Label + Hilfetext). Bisher stand nirgends, ob netto oder brutto gemeint ist -- fuer
den Cash-Fluss egal (beide Konventionen heben sich auf), aber seit der
einkommensabhaengigen AHV haengt eine Rente daran. Die AHV bemisst sich am
Bruttolohn, deshalb rechnet das Tool intern mit AHV_GROSS_FROM_NET_FACTOR = 1.12
hoch. Ohne das war die Rente um bis zu ~1'900/Jahr zu tief (Details: SPEZ 9.13).
Der Faktor ist hergeleitet und dokumentiert (AHV/IV/EO 5.3% + ALV 1.1% + NBU ~1% +
PK ~2-5% auf den koordinierten Lohn) -- fix vertretbar, weil das mdJE selbst ein
Karriere-Durchschnitt ist. Keine Aufschluesselung, kein sichtbares Feld (kommt mit
Roadmap Nr. 41 als erklaerte Konstante).

Roadmap Nr. 8: Immobilie neu mit Hypothekarzins (% der Restschuld, Zinsbetrag sinkt
mit der Amortisation, read-only "Beginn -> Ende") und Wertsteigerung.

WICHTIG: Die Wertsteigerung wirkt auf die LIEGENSCHAFT, nicht auf das Eigenkapital.
1% von 1 Mio sind 10'000/Jahr, also 10% eines Eigenkapitals von 100'000 -- das ist
der Hebel. Auf dem EK gerechnet waeren es 1'000 (Beispiel: 304'622 statt 210'462).
Kaufpreis und Verkehrswert laufen deshalb getrennt; die Grundstueckgewinnsteuer
bemisst sich weiterhin am urspruenglichen Kaufpreis.

Doppelzaehlung: Schalter interestHandling auf der Immobilie, Default INCLUDED --
bestehende Plaene haben die Zinsen in den Ausgaben und aendern sich nicht.

Keine Steuerschaetzung (Begruendung: SPEZ 9.14). Sechs Regressionstests (30 -> 36).
Spezifikation auf v0.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:12:19 +02:00
parent e901d23970
commit a97de5b1ed
8 changed files with 485 additions and 103 deletions
+175 -37
View File
@@ -4,10 +4,10 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.4 |
| **Version** | 0.5 |
| **Datum** | 2026-07-17 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `9f5bd75` inkl. einkommensabhängiger AHV und Fortschreibungs-Warnhinweis (Branch `main`) |
| **Codestand** | Arbeitsstand nach `e901d23` inkl. Netto/Brutto-Klarstellung und erweitertem Immobilien-Modul (Branch `main`) |
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1v5) im Ordner `Info Dateien` diese sind ab Version 0.1 dieses Dokuments obsolet |
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
@@ -17,6 +17,7 @@
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.5 | 2026-07-17 | Claude (Opus 4.8) | **Netto/Brutto geklärt** (Roadmap Nr. 9, reduziert) und **Immobilien-Modul erweitert** (Roadmap Nr. 8). Einkommen ist neu explizit als **Nettolohn** definiert (Label und Hilfetext); für die AHV rechnet das Tool intern mit `AHV_GROSS_FROM_NET_FACTOR = 1.12` auf den Bruttolohn hoch die AHV bemisst sich am Brutto, die bisherige Netto-Basis unterschätzte die Rente um bis zu ~1'900/Jahr. Immobilie neu mit **Hypothekarzins** (% der Restschuld, sinkt mit der Amortisation, mit Doppelzählungs-Schalter) und **Wertsteigerung** (auf die **Liegenschaft**, nicht auf das Eigenkapital Hebeleffekt). Grundstückgewinnsteuer bemisst sich neu explizit am ursprünglichen Kaufpreis. Keine Aufschlüsselung bei Einkommen oder Ausgaben, keine Steuerschätzung (Begründung: 9.14). Neues Kapitel 4.4.5; 4.6.5 und 4.9.4 überarbeitet; Abschnitt 9 um zwei Punkte ergänzt. Sechs Regressionstests (30 → 36). **Verhaltensänderung:** siehe 9.13. |
| 0.4 | 2026-07-17 | Claude (Opus 4.8) | **AHV-Rente einkommensabhängig** (Roadmap Nr. 3) und **Fortschreibungs-Warnhinweis** (Roadmap Nr. 4). Die AHV-Rente folgt neu der amtlichen Rentenformel (Skala 44) über das massgebende durchschnittliche Jahreseinkommen statt pauschal der Maximalrente; verifiziert gegen die amtliche Tabelle 318.117.1 (51/51 Zeilen). Prüfung der Beitragskarriere am Pensions-Übergang, mit Zusatzfeldern für die Jahre vor Planbeginn (ab Alter 21). Alles **real** gerechnet. Neue Konstanten `AHV_MIN_MONTHLY_FULL`, `AHV_PENSION_MONTHS`, `AHV_CONTRIBUTION_START_AGE`; `AHV_MAX_ANNUAL_SINGLE` neu abgeleitet. Warnhinweis in Phasenzellen und Phasen-Detail, wenn Folgephasen existieren. Neue Kapitel 3.5.6, 4.4; Abschnitt 9 um zwei Punkte ergänzt. Zwölf Regressionstests (18 → 30). **Verhaltensänderung:** siehe 9.10. |
| 0.3 | 2026-07-16 | Claude (Opus 4.8) | **Einmalige Sonderein-/ausgaben** umgesetzt (Roadmap Nr. 1). Der Cash-Übergang zwischen zwei Phasen ist neu ein eigener Entscheid: 1:1 übernehmen · einmaliger Zufluss · einmalige Kosten · beides. Beträge gehen direkt aufs Cash-Konto. Zufluss wird nominal erfasst (real angezeigt) mit optionalem Steuersatz (Default 0 %), Kosten werden real erfasst (nominal angezeigt); beide bleiben aus der Sparquote heraus. Der Entscheid startet unbeantwortet und zählt im „offen"-Badge mit. Neue Spalte `Phase.cashTransition` (JSON) + Migration, neue Route `PUT /api/phases/<id>/cash-transition`, eigene Kennzahlen im Phasenkopf. Fünf Regressionstests ergänzt (13 → 18). Neue Kapitel 3.5.5, 4.9.6; Abschnitt 9 um zwei Grenzen ergänzt. |
| 0.2 | 2026-07-16 | Claude (Opus 4.8) | Drei Fixes umgesetzt und dokumentiert: **(1)** `Phase.inflationRate` ersatzlos entfernt (DB-Migration, API, Typen, UI) die Inflation liegt seit V5 plan-weit; das Feld war wirkungslos. **(2)** Amortisation und Tilgung stoppen neu, sobald Hypothek bzw. Schuld abbezahlt sind sie belasten danach weder Cash noch Sparquote (Kap. 4.6.5, 4.6.7, 4.7, 4.8). **(3)** Kapitalbezugssteuer greift neu auch bei PK-/3a-Vorbezügen vor der Pensionierung, inkl. Steuerfeld und Netto-Vorschau im UI (Kap. 3.5.2, 4.9.1, 4.9.2). Kennzahl `plannedSaveRate` ist neu die Rate des **ersten** Phasenjahres. Drei Regressionstests ergänzt (10 → 13). Abschnitt 9 neu nummeriert (erledigte Punkte entfernt, „Sparraten werden nicht indexiert" ergänzt). |
@@ -385,14 +386,22 @@ Referenz: `src/components/ElementDetail.tsx` Zeilen 124340.
#### INCOME (Einkommen)
Hinweistext: „Einkommen wird NOMINAL erfasst (die Zahl auf dem Lohnausweis)."
**Einkommen ist der NETTOLOHN** der Betrag, der nach allen Lohnabzügen (AHV/ALV,
Pensionskasse, NBU) tatsächlich aufs Konto kommt. Das ist die für den Cash-Fluss richtige
Grösse und die, in der man denkt.
| Feld | JSON | Semantik |
|---|---|---|
| Jahreseinkommen NOMINAL (erstes Jahr) | `amount` | Basiswert Jahr 1. Ab Phase 2 vorbelegt mit dem fortgeschriebenen Wert, bewusst änderbar (Teilzeit, Beförderung). |
| Jahreseinkommen NETTO, nominal (erstes Jahr) | `amount` | Basiswert Jahr 1. Ab Phase 2 vorbelegt mit dem fortgeschriebenen Wert, bewusst änderbar (Teilzeit, Beförderung). |
| ≈ real (heutige Kaufkraft) | | Read-only Info: `amount / deflatorStart` |
| Nominale Lohnerhöhung (%/Jahr) | `teuerungsausgleich` | Default 0 %. 0 % = nominal gleichbleibend, real sinkend. |
Für die **AHV** rechnet das Tool intern auf den Bruttolohn zurück siehe
[4.4.5](#445-netto-brutto-umrechnung-für-die-ahv). Warum die Definition überhaupt nötig war:
Für den Cash-Fluss sind beide Konventionen gleichwertig (`brutto Ausgaben inkl. Abzüge`
`netto Ausgaben ohne Abzüge`), weshalb die Ambiguität lange folgenlos blieb. Mit der
einkommensabhängigen AHV hängt daran aber eine Rente.
#### EXPENSE (Ausgaben)
Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."
@@ -433,10 +442,28 @@ Wie PK, aber:
#### REAL_ESTATE (Immobilie)
| Zustand | Felder |
|---|---|
| Phase 1 / Neukauf | **Kaufpreis** (`purchasePrice`), **Hypothek** (`mortgage`), **Amortisation CHF/Jahr** (`amortization`) |
| ab Phase 2, fortgeschrieben | **Startwert Netto (fortgeschrieben)** (read-only = Kaufpreis Resthypothek), **Amortisation** |
| Feld | JSON | Semantik |
|---|---|---|
| Kaufpreis | `purchasePrice` | nur Phase 1 / Neukauf; ab Phase 2 read-only fortgeschrieben |
| Hypothek | `mortgage` | dito |
| Startwert Netto (fortgeschrieben) | | read-only ab Phase 2: **Verkehrswert** Resthypothek |
| Resthypothek (fortgeschrieben) | | read-only ab Phase 2 |
| Amortisation (CHF/Jahr) | `amortization` | endet, sobald die Hypothek abbezahlt ist |
| **Hypothekarzins (%/Jahr)** | `interestRate` | Zinssatz auf der **Restschuld** |
| **Hypothekarzins-Betrag (Beginn → Ende)** | | read-only: Zinsbetrag im ersten und im letzten Jahr der Phase |
| **Geschätzte Wertsteigerung (%/Jahr)** | `valueGrowth` | wirkt auf die **Liegenschaft** |
| **Sind die Zinsen bereits in den Ausgaben enthalten?** | `interestHandling` | `INCLUDED` (Default) / `ADD` |
Der Zinsbetrag sinkt automatisch mit der Amortisation das kann kein manueller
Ausgabenposten. Beispiel: Hypothek 1'000'000, Amortisation 10'000/Jahr, Zins 1 %, 10 Jahre →
Anzeige `10'000 → 9'100` (Jahr 1 auf 1'000'000, Jahr 10 auf 910'000).
Der Schalter `interestHandling` verhindert die Doppelzählung: Bestehende Pläne haben die
Zinsen im Ausgabenbetrag, deshalb ist `INCLUDED` der Default und das Tool zieht **nichts** ab.
Erst `ADD` rechnet die Zinsen dazu dann gehören sie aus dem Ausgabenbetrag entfernt. Der
Schalter sitzt bewusst auf der **Immobilie** und nicht auf dem Ausgaben-Element: Bei mehreren
Ausgaben-Elementen wäre sonst unklar, welches die Zinsen trägt (und zwei auf „Ja" würden
doppelt zählen).
#### OTHER_ASSET (Sonstiges Vermögen)
@@ -866,7 +893,7 @@ Pro Person wird über die Phasen hinweg akkumuliert (`AhvCareer`):
| Feld | Bedeutung |
|---|---|
| `plannedAvgIncome` | reales Durchschnittseinkommen der Beitragsjahre **im Plan** |
| `plannedAvgGrossIncome` | reales **Brutto**-Durchschnittseinkommen der Beitragsjahre **im Plan** (siehe 4.4.5) |
| `planYears` | Beitragsjahre im Plan = Σ (Phasendauer Ausfalljahre der Phase) |
| `yearsBeforePlan` | `max(0, Alter bei Planbeginn 21)` |
| `gapYearsInPlan` | Summe der Ausfalljahre im Plan |
@@ -919,7 +946,45 @@ Einkommen wird einer Person nur zugerechnet, wenn das `INCOME`-Element ihr zugeo
einem **Einzelplan** zählt „Gemeinsam" (`HOUSEHOLD`) zur Person A; bei einem **Paar-Plan** nicht
(siehe [9.9](#99-gemeinsames-einkommen-zählt-bei-paaren-nicht-für-die-ahv)).
### 4.4.5 Jahresrente, Skala und Plafonierung
### 4.4.5 Netto-Brutto-Umrechnung für die AHV
Das Tool erfasst das Einkommen **netto**, die AHV bemisst sich am **Bruttolohn**. Beim Aufbau
der Karriere wird deshalb hochgerechnet:
```
plannedAvgGrossIncome = avgRealFlow(...) × AHV_GROSS_FROM_NET_FACTOR // = 1.12
```
Sämtliche Werte in `AhvCareer`, im Prüf-Dialog und im mdJE sind damit **brutto** auch das
Feld „Durchschnittliches Bruttoeinkommen vor Planbeginn", das der Benutzer aus der
Rentenvorausberechnung übernimmt (die ohnehin brutto-basiert ist). Eine Einheit im ganzen
Ablauf, keine Umrechnung an der Feldgrenze.
**Herleitung des Faktors** (Arbeitnehmer-Abzüge in % des Bruttolohns):
| Abzug | Satz | Bemerkung |
|---|---|---|
| AHV/IV/EO | 5.30 % | 10.6 % total, hälftig geteilt |
| ALV | 1.10 % | 2.2 % total, hälftig geteilt, bis 148'200 |
| NBU | ~1.00 % | variiert 0.41.6 %, teils vom Arbeitgeber getragen |
| PK | ~25 % | BVG-Altersgutschrift 7/10/15/18 % auf den **koordinierten** Lohn (Brutto 26'460, max. 90'720), Arbeitnehmer ≥ die Hälfte |
Total ~913 % je nach Alter und Lohn → Faktor `1/(1q)` = **1.10 bis 1.16**, Mittel **1.12**.
**Warum ein fixer Faktor genügt:** Das mdJE ist selbst ein Durchschnitt über die ganze
Beitragskarriere (44 Jahre). Der altersabhängige PK-Satz mittelt sich dabei heraus ein
Karriere-Faktor gegen einen Karriere-Durchschnitt ist konzeptionell stimmig, nicht bloss eine
Näherung. Die Restunschärfe (~3 %) ist deutlich kleiner als der Fehler, den die Umrechnung
behebt (~11 %, siehe 9.13).
**Grenzen:** Überobligatorische PK-Pläne, vom Arbeitgeber getragene NBU, Selbstständige und
Löhne über 148'200 (ALV sinkt auf 0.5 %) weichen ab. Der Faktor ist heute eine nicht
sichtbare Konstante; mit Roadmap Nr. 41 wird er in der Formel-Erklärung ausgewiesen.
Quellen: BSV „Beträge gültig ab 1.1.2026" (Koordinationsabzug 26'460, obere Limite 90'720,
„keine Änderung gegenüber 2025"); AHV-Merkblätter 2.01 (AHV/IV/EO) und 2.08 (ALV).
### 4.4.6 Jahresrente, Skala und Plafonierung
```
factor = max(0, (44 Ausfalljahre total) / 44) // Rentenskala 44
@@ -1030,14 +1095,29 @@ Identisch zu PK, mit zwei Unterschieden:
### 4.6.5 REAL_ESTATE (Immobilie)
```
purchase = round(phaseData.purchasePrice) // in JEDER Phase aus den Phasendaten
mortgageStart = hasCarry ? carry.mortgage : round(phaseData.mortgage)
amort = round(phaseData.amortization) // wird JÄHRLICH am Restsaldo gekappt
equity = purchase mortgageStart → startValue, wealthStart
purchase = hasCarry ? carry.propertyPurchase : round(phaseData.purchasePrice) // Kaufpreis
valueStart = hasCarry ? carry.propertyValue : round(phaseData.purchasePrice) // Verkehrswert
mortgage = hasCarry ? carry.mortgage : round(phaseData.mortgage)
amort = round(phaseData.amortization) // wird JÄHRLICH am Restsaldo gekappt
equity = valueStart mortgage → startValue, wealthStart
```
Die Hypothek wird als **laufender Saldo** in der Jahresschleife geführt (siehe 4.7), nicht per
Linearformel. Sobald sie 0 erreicht, entfällt die Amortisationsrate.
**Kaufpreis und Verkehrswert laufen getrennt.** Der Verkehrswert wächst mit `valueGrowth`, der
Kaufpreis bleibt der ursprüngliche er ist die Basis der Grundstückgewinnsteuer beim Verkauf
(4.9.4). Beide werden über die Phasen fortgeschrieben (`carry.propertyValue`,
`carry.propertyPurchase`); die Hypothek läuft als Saldo in der Jahresschleife (4.7).
**Die Wertsteigerung wirkt auf die Liegenschaft, nicht auf das Eigenkapital.** Das ist der
Hebel, der Wohneigentum finanziell auszeichnet: 1 % von 1'000'000 sind 10'000 im Jahr bei
einem Eigenkapital von 100'000 also 10 % darauf. Würde man die Wertsteigerung auf das
Eigenkapital rechnen, ergäbe dieselbe Annahme nur 1'000 im Jahr:
> Kaufpreis 1'000'000 · Hypothek 900'000 · EK 100'000 · 1 %/J. · Amortisation 10'000/J. · 10 J.
> → korrekt **304'622**; auf das Eigenkapital gerechnet nur **210'462**. Differenz **94'160**,
> und sie wächst mit dem Belehnungsgrad.
Der angezeigte Elementwert bleibt das **Eigenkapital** (`Verkehrswert Restschuld`); nur die
Basis der Verzinsung ist die Liegenschaft.
Falls **nicht** fortgeschrieben und **nicht** Phase 1 (= Neukauf in einer späteren Phase):
`investmentsFromCash += max(0, equity)` das Eigenkapital wird aus dem Cash finanziert.
@@ -1085,10 +1165,12 @@ Dann für `t = 1 .. duration`:
// 1. Einkommen (nominal)
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t1))
// 2. Ausgaben (real → nominal)
inflFactor = cumInflStart × (1 + infl/100)^(t1)
expenseReal = Σ (exp.basis × (1 + exp.idx/100)^(t1))
expenseNominal = expenseReal × inflFactor
// 2. Ausgaben (real → nominal) + Hypothekarzins
inflFactor = cumInflStart × (1 + infl/100)^(t1)
expenseRealBase = Σ (exp.basis × (1 + exp.idx/100)^(t1))
interestNominal = Σ (re.mortgage × re.interestRate/100) // nur wo interestHandling = ADD
expenseNominal = expenseRealBase × inflFactor + interestNominal
expenseReal = expenseRealBase + interestNominal / inflFactor
// 3. Quote
quote = incomeFlow expenseNominal
@@ -1108,6 +1190,7 @@ für jede Immobilie re:
pay = min(re.amort, re.mortgage) // nie mehr als die Restschuld
re.mortgage = pay
debtRates += pay
re.value ×= (1 + re.growth/100) // Wertsteigerung auf die LIEGENSCHAFT
für jede Schuld d:
pay = min(d.repay, d.owed)
d.owed = pay
@@ -1119,7 +1202,7 @@ cash += quote fixedRatesTotal debtRates + cashFromWithdraw
falls cash < 0 → cashNegative = true
// 8. Ruin prüfen (Gesamtvermögen zum Jahresende)
total = cash + Σ asset.value + Σ (re.purchase re.mortgage) + Σ (d.owed)
total = cash + Σ asset.value + Σ (re.value re.mortgage) + Σ (d.owed)
falls ruinAge === null und total < 0 → ruinAge = age(Person A) + yearsBefore + t
```
@@ -1146,7 +1229,7 @@ Einkommen: startValue = basis
Ausgaben: startValue = basis × cumInflStart
endValue = basis × (1 + idx/100)^(duration1) × flowDeflatorEnd
Assets: endValue = a.value (nach der Jahresschleife)
Immobilie: endValue = purchase re.mortgage (laufender Saldo nach der Jahresschleife)
Immobilie: endValue = re.value re.mortgage (Verkehrswert inkl. Wertsteigerung, minus Restschuld)
Schulden: endValue = d.owed (0, falls getilgt; + Notiz „Wird getilgt")
```
@@ -1209,19 +1292,25 @@ Sonst → `carry.value = ec.endValue`.
### 4.9.4 REAL_ESTATE
Die Resthypothek wird zurückgerechnet: `restMortgage = purchase ec.endValue`.
Gelesen werden die laufenden Werte aus der Jahresschleife (`re.value`, `re.mortgage`,
`re.purchase`).
`decision = "SELL"`:
```
gain = max(0, salePrice purchase)
gain = max(0, salePrice re.purchase) // URSPRÜNGLICHER Kaufpreis, nicht der Verkehrswert
tax = gain × (saleTaxRate / 100)
txInflow += round(salePrice restMortgage tax)
txInflow += round(salePrice re.mortgage tax)
carry.status = "SOLD"
```
Der Nettoerlös ist also Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer.
Ein Verlustverkauf erzeugt keine Steuer (`gain` bei 0 geklammert).
Der Nettoerlös ist Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer. Ein
Verlustverkauf erzeugt keine Steuer (`gain` bei 0 geklammert).
Sonst → `carry.mortgage = restMortgage`.
**Die Steuer bemisst sich am ursprünglichen Kaufpreis**, nicht am zwischenzeitlich gestiegenen
Verkehrswert deshalb führt das Modell beide getrennt (4.6.5). Nicht modelliert sind
wertvermehrende Investitionen und die Haltedauer-Abstufung (Roadmap Nr. 23).
Sonst → `carry.mortgage`, `carry.propertyValue` und `carry.propertyPurchase` werden
fortgeschrieben.
### 4.9.5 OTHER_DEBT
@@ -1293,6 +1382,7 @@ Zentral in `src/lib/constants.ts` geführt, weil sie sich periodisch durch Bunde
| `AHV_PENSION_MONTHS` | 13 | Rentenzahlungen pro Jahr 13. Altersrente ab 1.1.2026 |
| `AHV_CONTRIBUTION_START_AGE` | 21 | Beitragspflicht ab 1. Januar nach dem 20. Geburtstag |
| `AHV_MAX_ANNUAL_SINGLE` | 32'760 | **abgeleitet**: `2 × R0 × 13` |
| `AHV_GROSS_FROM_NET_FACTOR` | 1.12 | Netto → Brutto für die AHV; Herleitung siehe [4.4.5](#445-netto-brutto-umrechnung-für-die-ahv) |
| `AHV_COUPLE_CAP_FACTOR` | 1.5 | Ehepaar-Plafonierung: 150 % der Einzel-Maximalrente |
| `AHV_FULL_CONTRIBUTION_YEARS` | 44 | Volle Beitragsdauer (Rentenskala 44) |
| `PILLAR_3A_MAX_ANNUAL` | 7'258 | Max. 3a-Beitrag/Jahr für PK-Versicherte (2026) |
@@ -1503,6 +1593,9 @@ Referenz: `prisma/schema.prisma` Zeilen 46, `src/lib/elements.ts` Zeilen 48
| `purchasePrice` | REAL_ESTATE | ≥ 0 |
| `mortgage` | REAL_ESTATE | ≥ 0 |
| `amortization` | REAL_ESTATE | ≥ 0 |
| `interestRate` | REAL_ESTATE Hypothekarzins %/Jahr | 020 |
| `interestHandling` | REAL_ESTATE Doppelzählungs-Schalter | `INCLUDED` (Default) \| `ADD` |
| `valueGrowth` | REAL_ESTATE Wertsteigerung %/Jahr auf die Liegenschaft | 20 bis 20 |
| `annualRepayment` | OTHER_DEBT | ≥ 0 |
### 5.4.4 JSON-Payload `TransitionData`
@@ -1800,8 +1893,8 @@ Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain `fpt.aicds.ch`, Gi
## 8.1 Teststrategie
Getestet wird ausschliesslich der Berechnungskern bewusst, da dort die Fachlogik und das
Regressionsrisiko liegen. `src/lib/calculations.test.ts` enthält 30 Tests (AHV-Rentenformel,
AHV einkommensabhängig, „V5 Golden Tests"),
Regressionsrisiko liegen. `src/lib/calculations.test.ts` enthält 36 Tests (AHV-Rentenformel,
Immobilie, AHV einkommensabhängig, „V5 Golden Tests"),
ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`).
Es gibt **keine** Komponenten-, API- oder E2E-Tests.
@@ -1818,6 +1911,12 @@ Es gibt **keine** Komponenten-, API- oder E2E-Tests.
| **AHV ohne Prüfung** | ohne erfassten Wert gilt der geplante Durchschnitt (nicht 0) |
| **AHV bereits pensioniert** | Karriere aus der Phasenzelle der ersten Phase |
| **AHV Plafonierung** | zwei Maximalrenten im Paar-Plan → gekappt auf `32'760 × 1.5` |
| **AHV Netto → Brutto** | mdJE = `70'000 × 1.12`, nicht 70'000 |
| **Immobilie: Hebel** | 1 Mio / 900k Hypothek / 1 % / 10 J. → Endwert 304'622; deutlich mehr als eine Wertsteigerung auf das Eigenkapital ergäbe |
| **Immobilie: ohne Wertsteigerung** | Verhalten unverändert (1 Mio 800k = 200'000) |
| **Immobilie: Zins-Schalter** | `INCLUDED` → kein Cash-Abzug; `ADD` → 10 × 1 % von 900'000 = 90'000 |
| **Immobilie: Zins sinkt** | Jahr 1: 10'000, Jahr 10: 9'100; schlägt auf die Quote durch |
| **Immobilie: Verkauf** | Grundstückgewinnsteuer auf `Verkaufspreis Kaufpreis`, nicht auf den Verkehrswert |
| Test 1 Ansparen | Einkommen +2 % nominal, Ausgaben real flach: Endvermögen 761'654 nominal / 565'928 real (±1 %), kein negatives Cash |
| Test 2 Verzehr/Ruin | Rente nominal fix 60k, Ausgaben real 100k, Vermögen 900k @3 %: `ruinAge === 94` |
| Test 3 Cash-Ausgleich | Sparrate 6'364: `cashEnd === 5472`, nie negativ |
@@ -1878,13 +1977,18 @@ liefert `personByRole` dann `null`:
Es gibt keine Bereinigung und keine Warnung im UI. Da die Haushaltsform normalerweise nicht
nachträglich geändert wird, ist der Fall selten er ist aber erreichbar.
## 9.3 Immobilien ohne Wertentwicklung
## 9.3 Immobilien: was noch fehlt
Der Immobilienwert ist in jeder Phase der aus den Phasendaten gelesene `purchasePrice`. Der
Kaufpreis wird beim Anlegen einer Folgephase mitkopiert (`buildCarryData`). Eine Wertsteigerung
lässt sich nur indirekt abbilden, indem man den `purchasePrice` in einer späteren Phase
manuell erhöht was dann allerdings auch die Berechnung der Grundstückgewinnsteuer beim Verkauf
beeinflusst (`gain = salePrice purchase` liest den `purchasePrice` der Verkaufsphase).
Seit Version 0.5 kennt das Modul Hypothekarzins und Wertsteigerung. Nicht modelliert sind:
- **Nebenkosten und Unterhalt** (Faustregel ~1 % des Werts). Lassen sich heute als normales
Ausgaben-Element erfassen.
- **Eigenmietwert** ohne echte Steuerlogik (Roadmap Nr. 23) nur halb wirksam.
- **Mieteinnahmen** (Renditeliegenschaften) anderer Anwendungsfall.
- **Wertvermehrende Investitionen** und die **Haltedauer-Abstufung** der
Grundstückgewinnsteuer (kantonal, teils stark degressiv).
- **Zinsänderungsrisiko**: Der Zinssatz gilt für die ganze Phase. Ein Zinsschock lässt sich
nur abbilden, indem man an dieser Stelle eine Phasengrenze zieht und den Satz neu setzt.
## 9.4 Kein CSRF-Token
@@ -1959,7 +2063,41 @@ grösste verbliebene Hebel im AHV-Modell.
Das Feld ist per `max`-Prop hart geklammert. Das Zod-Schema kennt für `annualContribution` nur
`≥ 0` ein direkter API-Aufruf kann die Obergrenze überschreiten.
## 9.13 Kleinere Beobachtungen
## 9.13 Verhaltensänderung: Nettolohn-Klarstellung und AHV
Bis Version 0.4 war nirgends definiert, ob `INCOME` netto oder brutto ist der Hilfetext sagte
nur „die Zahl auf dem Lohnausweis", und dort stehen beide. Für den Cash-Fluss war das folgenlos,
weil sich beide Konventionen aufheben (`brutto Ausgaben inkl. Abzüge``netto Ausgaben ohne
Abzüge`). Mit der einkommensabhängigen AHV (0.4) hing daran aber plötzlich eine Rente.
Seit 0.5 gilt: **`amount` ist der Nettolohn**, und die AHV rechnet mit `× 1.12` auf brutto hoch.
Konsequenzen:
- **Wer bisher netto erfasst hat** (die dokumentierte Absicht): Die AHV-Rente **steigt** sie
war zuvor um bis zu ~1'900/Jahr zu tief, am stärksten bei mittleren Einkommen (bei 80'000
brutto: 28'974 statt 30'902). Über 90'720 brutto verschwindet der Effekt, weil beide Werte in
die Maximalrente laufen.
- **Wer brutto erfasst hat**: Cash-Fluss und AHV sind nun beide zu hoch. Der Einkommensbetrag
gehört auf netto korrigiert und die Lohnabzüge aus dem Ausgabenbetrag entfernt.
## 9.14 Keine Steuerschätzung
Bewusst **nicht** umgesetzt: eine automatische Schätzung von Einkommens- und Vermögenssteuer.
Die Bemessungsgrundlage ist das *steuerbare* Einkommen, nicht der Nettolohn also brutto minus
PK, 3a, Berufsauslagen, Versicherungs- und Kinderabzüge. Darauf kommen drei Ebenen (Bund,
Kanton, Gemeinde); allein der Gemeindesteuerfuss variiert innerhalb eines Kantons um rund den
Faktor zwei. Dazu Zivilstand, Kinder, Konfession und für die Vermögenssteuer 26 kantonale
Tarife mit eigenen Freibeträgen. Das Tool kennt weder Wohnort noch Kinder.
Eine Schätzung daraus läge im Einzelfall schnell 3050 % daneben bei vielen Haushalten dem
grössten Ausgabenposten. Eine selbst berechnete Zahl wirkt zudem autoritativ und wird nicht
hinterfragt. Der Benutzer kennt seine Steuerrechnung dagegen exakt aus der letzten Veranlagung.
**Heutiger Weg:** ein normales Ausgaben-Element „Steuern" dafür braucht es kein neues Feld.
Echte Steuerlogik ist Roadmap Nr. 23, mit Kanton und Gemeinde als Eingabe.
## 9.15 Kleinere Beobachtungen
- `planToCsv(plan, computed)` erhält den `plan`-Parameter, verwendet ihn aber nicht.
- Der Typ `Selection` in `PlanView` hat nur eine Variante (`{ type: "phase" }`) ein Rest der