Netto/Brutto-Klarstellung fuer die AHV + erweitertes Immobilien-Modul
Deploy App / deploy (push) Successful in 59s
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:
+175
-37
@@ -4,10 +4,10 @@
|
|||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||||||
| **Version** | 0.4 |
|
| **Version** | 0.5 |
|
||||||
| **Datum** | 2026-07-17 |
|
| **Datum** | 2026-07-17 |
|
||||||
| **Status** | Lebendes Dokument |
|
| **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` (v1–v5) im Ordner `Info Dateien` – diese sind ab Version 0.1 dieses Dokuments obsolet |
|
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1–v5) im Ordner `Info Dateien` – diese sind ab Version 0.1 dieses Dokuments obsolet |
|
||||||
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
|
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
| Version | Datum | Autor | Änderung |
|
| 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.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.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). |
|
| 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 124–340.
|
|||||||
|
|
||||||
#### INCOME (Einkommen)
|
#### 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 |
|
| 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` |
|
| ≈ real (heutige Kaufkraft) | – | Read-only Info: `amount / deflatorStart` |
|
||||||
| Nominale Lohnerhöhung (%/Jahr) | `teuerungsausgleich` | Default 0 %. 0 % = nominal gleichbleibend, real sinkend. |
|
| 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)
|
#### EXPENSE (Ausgaben)
|
||||||
|
|
||||||
Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."
|
Hinweistext: „Ausgaben werden REAL erfasst (in heutiger Kaufkraft)."
|
||||||
@@ -433,10 +442,28 @@ Wie PK, aber:
|
|||||||
|
|
||||||
#### REAL_ESTATE (Immobilie)
|
#### REAL_ESTATE (Immobilie)
|
||||||
|
|
||||||
| Zustand | Felder |
|
| Feld | JSON | Semantik |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| Phase 1 / Neukauf | **Kaufpreis** (`purchasePrice`), **Hypothek** (`mortgage`), **Amortisation CHF/Jahr** (`amortization`) |
|
| Kaufpreis | `purchasePrice` | nur Phase 1 / Neukauf; ab Phase 2 read-only fortgeschrieben |
|
||||||
| ab Phase 2, fortgeschrieben | **Startwert Netto (fortgeschrieben)** (read-only = Kaufpreis − Resthypothek), **Amortisation** |
|
| 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)
|
#### OTHER_ASSET (Sonstiges Vermögen)
|
||||||
|
|
||||||
@@ -866,7 +893,7 @@ Pro Person wird über die Phasen hinweg akkumuliert (`AhvCareer`):
|
|||||||
|
|
||||||
| Feld | Bedeutung |
|
| 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) |
|
| `planYears` | Beitragsjahre im Plan = Σ (Phasendauer − Ausfalljahre der Phase) |
|
||||||
| `yearsBeforePlan` | `max(0, Alter bei Planbeginn − 21)` |
|
| `yearsBeforePlan` | `max(0, Alter bei Planbeginn − 21)` |
|
||||||
| `gapYearsInPlan` | Summe der Ausfalljahre im Plan |
|
| `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
|
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)).
|
(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.4–1.6 %, teils vom Arbeitgeber getragen |
|
||||||
|
| PK | ~2–5 % | BVG-Altersgutschrift 7/10/15/18 % auf den **koordinierten** Lohn (Brutto − 26'460, max. 90'720), Arbeitnehmer ≥ die Hälfte |
|
||||||
|
|
||||||
|
Total ~9–13 % je nach Alter und Lohn → Faktor `1/(1−q)` = **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
|
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)
|
### 4.6.5 REAL_ESTATE (Immobilie)
|
||||||
|
|
||||||
```
|
```
|
||||||
purchase = round(phaseData.purchasePrice) // in JEDER Phase aus den Phasendaten
|
purchase = hasCarry ? carry.propertyPurchase : round(phaseData.purchasePrice) // Kaufpreis
|
||||||
mortgageStart = hasCarry ? carry.mortgage : round(phaseData.mortgage)
|
valueStart = hasCarry ? carry.propertyValue : round(phaseData.purchasePrice) // Verkehrswert
|
||||||
amort = round(phaseData.amortization) // wird JÄHRLICH am Restsaldo gekappt
|
mortgage = hasCarry ? carry.mortgage : round(phaseData.mortgage)
|
||||||
equity = purchase − mortgageStart → startValue, wealthStart
|
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
|
**Kaufpreis und Verkehrswert laufen getrennt.** Der Verkehrswert wächst mit `valueGrowth`, der
|
||||||
Linearformel. Sobald sie 0 erreicht, entfällt die Amortisationsrate.
|
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):
|
Falls **nicht** fortgeschrieben und **nicht** Phase 1 (= Neukauf in einer späteren Phase):
|
||||||
`investmentsFromCash += max(0, equity)` – das Eigenkapital wird aus dem Cash finanziert.
|
`investmentsFromCash += max(0, equity)` – das Eigenkapital wird aus dem Cash finanziert.
|
||||||
@@ -1085,10 +1165,12 @@ Dann für `t = 1 .. duration`:
|
|||||||
// 1. Einkommen (nominal)
|
// 1. Einkommen (nominal)
|
||||||
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t−1))
|
incomeFlow = renteTotal + Σ (inc.basis × (1 + inc.idx/100)^(t−1))
|
||||||
|
|
||||||
// 2. Ausgaben (real → nominal)
|
// 2. Ausgaben (real → nominal) + Hypothekarzins
|
||||||
inflFactor = cumInflStart × (1 + infl/100)^(t−1)
|
inflFactor = cumInflStart × (1 + infl/100)^(t−1)
|
||||||
expenseReal = Σ (exp.basis × (1 + exp.idx/100)^(t−1))
|
expenseRealBase = Σ (exp.basis × (1 + exp.idx/100)^(t−1))
|
||||||
expenseNominal = expenseReal × inflFactor
|
interestNominal = Σ (re.mortgage × re.interestRate/100) // nur wo interestHandling = ADD
|
||||||
|
expenseNominal = expenseRealBase × inflFactor + interestNominal
|
||||||
|
expenseReal = expenseRealBase + interestNominal / inflFactor
|
||||||
|
|
||||||
// 3. Quote
|
// 3. Quote
|
||||||
quote = incomeFlow − expenseNominal
|
quote = incomeFlow − expenseNominal
|
||||||
@@ -1108,6 +1190,7 @@ für jede Immobilie re:
|
|||||||
pay = min(re.amort, re.mortgage) // nie mehr als die Restschuld
|
pay = min(re.amort, re.mortgage) // nie mehr als die Restschuld
|
||||||
re.mortgage −= pay
|
re.mortgage −= pay
|
||||||
debtRates += pay
|
debtRates += pay
|
||||||
|
re.value ×= (1 + re.growth/100) // Wertsteigerung auf die LIEGENSCHAFT
|
||||||
für jede Schuld d:
|
für jede Schuld d:
|
||||||
pay = min(d.repay, d.owed)
|
pay = min(d.repay, d.owed)
|
||||||
d.owed −= pay
|
d.owed −= pay
|
||||||
@@ -1119,7 +1202,7 @@ cash += quote − fixedRatesTotal − debtRates + cashFromWithdraw
|
|||||||
falls cash < 0 → cashNegative = true
|
falls cash < 0 → cashNegative = true
|
||||||
|
|
||||||
// 8. Ruin prüfen (Gesamtvermögen zum Jahresende)
|
// 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
|
falls ruinAge === null und total < 0 → ruinAge = age(Person A) + yearsBefore + t
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1146,7 +1229,7 @@ Einkommen: startValue = basis
|
|||||||
Ausgaben: startValue = basis × cumInflStart
|
Ausgaben: startValue = basis × cumInflStart
|
||||||
endValue = basis × (1 + idx/100)^(duration−1) × flowDeflatorEnd
|
endValue = basis × (1 + idx/100)^(duration−1) × flowDeflatorEnd
|
||||||
Assets: endValue = a.value (nach der Jahresschleife)
|
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")
|
Schulden: endValue = −d.owed (0, falls getilgt; + Notiz „Wird getilgt")
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1209,19 +1292,25 @@ Sonst → `carry.value = ec.endValue`.
|
|||||||
|
|
||||||
### 4.9.4 REAL_ESTATE
|
### 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"`:
|
`decision = "SELL"`:
|
||||||
```
|
```
|
||||||
gain = max(0, salePrice − purchase)
|
gain = max(0, salePrice − re.purchase) // URSPRÜNGLICHER Kaufpreis, nicht der Verkehrswert
|
||||||
tax = gain × (saleTaxRate / 100)
|
tax = gain × (saleTaxRate / 100)
|
||||||
txInflow += round(salePrice − restMortgage − tax)
|
txInflow += round(salePrice − re.mortgage − tax)
|
||||||
carry.status = "SOLD"
|
carry.status = "SOLD"
|
||||||
```
|
```
|
||||||
Der Nettoerlös ist also Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer.
|
Der Nettoerlös ist Verkaufspreis minus Hypothekenablösung minus Grundstückgewinnsteuer. Ein
|
||||||
Ein Verlustverkauf erzeugt keine Steuer (`gain` bei 0 geklammert).
|
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
|
### 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_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_CONTRIBUTION_START_AGE` | 21 | Beitragspflicht ab 1. Januar nach dem 20. Geburtstag |
|
||||||
| `AHV_MAX_ANNUAL_SINGLE` | 32'760 | **abgeleitet**: `2 × R0 × 13` |
|
| `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_COUPLE_CAP_FACTOR` | 1.5 | Ehepaar-Plafonierung: 150 % der Einzel-Maximalrente |
|
||||||
| `AHV_FULL_CONTRIBUTION_YEARS` | 44 | Volle Beitragsdauer (Rentenskala 44) |
|
| `AHV_FULL_CONTRIBUTION_YEARS` | 44 | Volle Beitragsdauer (Rentenskala 44) |
|
||||||
| `PILLAR_3A_MAX_ANNUAL` | 7'258 | Max. 3a-Beitrag/Jahr für PK-Versicherte (2026) |
|
| `PILLAR_3A_MAX_ANNUAL` | 7'258 | Max. 3a-Beitrag/Jahr für PK-Versicherte (2026) |
|
||||||
@@ -1503,6 +1593,9 @@ Referenz: `prisma/schema.prisma` Zeilen 4–6, `src/lib/elements.ts` Zeilen 48
|
|||||||
| `purchasePrice` | REAL_ESTATE | ≥ 0 |
|
| `purchasePrice` | REAL_ESTATE | ≥ 0 |
|
||||||
| `mortgage` | REAL_ESTATE | ≥ 0 |
|
| `mortgage` | REAL_ESTATE | ≥ 0 |
|
||||||
| `amortization` | REAL_ESTATE | ≥ 0 |
|
| `amortization` | REAL_ESTATE | ≥ 0 |
|
||||||
|
| `interestRate` | REAL_ESTATE – Hypothekarzins %/Jahr | 0–20 |
|
||||||
|
| `interestHandling` | REAL_ESTATE – Doppelzählungs-Schalter | `INCLUDED` (Default) \| `ADD` |
|
||||||
|
| `valueGrowth` | REAL_ESTATE – Wertsteigerung %/Jahr auf die Liegenschaft | −20 bis 20 |
|
||||||
| `annualRepayment` | OTHER_DEBT | ≥ 0 |
|
| `annualRepayment` | OTHER_DEBT | ≥ 0 |
|
||||||
|
|
||||||
### 5.4.4 JSON-Payload `TransitionData`
|
### 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
|
## 8.1 Teststrategie
|
||||||
|
|
||||||
Getestet wird ausschliesslich der Berechnungskern – bewusst, da dort die Fachlogik und das
|
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,
|
Regressionsrisiko liegen. `src/lib/calculations.test.ts` enthält 36 Tests (AHV-Rentenformel,
|
||||||
AHV einkommensabhängig, „V5 Golden Tests"),
|
Immobilie, AHV einkommensabhängig, „V5 Golden Tests"),
|
||||||
ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`).
|
ausgeführt mit Vitest in der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`).
|
||||||
Es gibt **keine** Komponenten-, API- oder E2E-Tests.
|
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 ohne Prüfung** | ohne erfassten Wert gilt der geplante Durchschnitt (nicht 0) |
|
||||||
| **AHV bereits pensioniert** | Karriere aus der Phasenzelle der ersten Phase |
|
| **AHV bereits pensioniert** | Karriere aus der Phasenzelle der ersten Phase |
|
||||||
| **AHV Plafonierung** | zwei Maximalrenten im Paar-Plan → gekappt auf `32'760 × 1.5` |
|
| **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 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 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 |
|
| 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
|
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.
|
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
|
Seit Version 0.5 kennt das Modul Hypothekarzins und Wertsteigerung. Nicht modelliert sind:
|
||||||
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
|
- **Nebenkosten und Unterhalt** (Faustregel ~1 % des Werts). Lassen sich heute als normales
|
||||||
manuell erhöht – was dann allerdings auch die Berechnung der Grundstückgewinnsteuer beim Verkauf
|
Ausgaben-Element erfassen.
|
||||||
beeinflusst (`gain = salePrice − purchase` liest den `purchasePrice` der Verkaufsphase).
|
- **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
|
## 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
|
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.
|
`≥ 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 30–50 % 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.
|
- `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
|
- Der Typ `Selection` in `PlanView` hat nur eine Variante (`{ type: "phase" }`) – ein Rest der
|
||||||
|
|||||||
@@ -93,8 +93,15 @@ function buildCarryData(category: string, prev: PhaseData): PhaseData {
|
|||||||
case "OTHER_ASSET":
|
case "OTHER_ASSET":
|
||||||
return { annualContribution: num(prev.annualContribution), expectedReturn: num(prev.expectedReturn) };
|
return { annualContribution: num(prev.annualContribution), expectedReturn: num(prev.expectedReturn) };
|
||||||
case "REAL_ESTATE":
|
case "REAL_ESTATE":
|
||||||
// purchasePrice + amortization bleiben; die Resthypothek wird live fortgeschrieben.
|
// purchasePrice + amortization bleiben; Resthypothek und Verkehrswert werden live
|
||||||
return { purchasePrice: num(prev.purchasePrice), amortization: num(prev.amortization) };
|
// fortgeschrieben. Zinssatz, Zins-Behandlung und Wertsteigerung gelten weiter.
|
||||||
|
return {
|
||||||
|
purchasePrice: num(prev.purchasePrice),
|
||||||
|
amortization: num(prev.amortization),
|
||||||
|
interestRate: num(prev.interestRate),
|
||||||
|
interestHandling: prev.interestHandling ?? "INCLUDED",
|
||||||
|
valueGrowth: num(prev.valueGrowth),
|
||||||
|
};
|
||||||
case "OTHER_DEBT":
|
case "OTHER_DEBT":
|
||||||
return { annualRepayment: num(prev.annualRepayment) };
|
return { annualRepayment: num(prev.annualRepayment) };
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface CellContext {
|
|||||||
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
carriedEndValue: number; // Endwert des Elements in der (Vor-)Phase, fuer Bezugs-Maxima
|
||||||
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
carried: boolean; // Phase >= 2: Basiswert wird aus der Vorphase fortgeschrieben
|
||||||
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
derivedStart: number; // fortgeschriebener Basiswert (read-only Anzeige)
|
||||||
|
derivedMortgage: number; // nur Immobilie: fortgeschriebene Resthypothek zu Phasenbeginn
|
||||||
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
deflatorStart: number; // Kaufkraft-Deflator zu Phasenbeginn (real <-> nominal, erstes Jahr)
|
||||||
// Warnhinweis: Anzahl Phasen NACH dieser (Aenderungen schreiben sich dorthin fort).
|
// Warnhinweis: Anzahl Phasen NACH dieser (Aenderungen schreiben sich dorthin fort).
|
||||||
laterPhaseCount: number;
|
laterPhaseCount: number;
|
||||||
@@ -148,7 +149,8 @@ export function AhvReviewFields({
|
|||||||
}) {
|
}) {
|
||||||
const brauchtVorgeschichte = career.yearsBeforePlan > 0;
|
const brauchtVorgeschichte = career.yearsBeforePlan > 0;
|
||||||
// Vorbelegung mit dem geplanten Durchschnitt: besser als 0, wenn der Nutzer nichts weiss.
|
// Vorbelegung mit dem geplanten Durchschnitt: besser als 0, wenn der Nutzer nichts weiss.
|
||||||
const avgBefore = typeof td.avgIncomeBefore === "number" ? td.avgIncomeBefore : Math.round(career.plannedAvgIncome);
|
const avgBefore =
|
||||||
|
typeof td.avgIncomeBefore === "number" ? td.avgIncomeBefore : Math.round(career.plannedAvgGrossIncome);
|
||||||
const gapBefore = Math.max(0, Math.round(num(td.gapYearsBefore)));
|
const gapBefore = Math.max(0, Math.round(num(td.gapYearsBefore)));
|
||||||
const mdJE = ahvMdje(career, brauchtVorgeschichte ? avgBefore : 0, gapBefore);
|
const mdJE = ahvMdje(career, brauchtVorgeschichte ? avgBefore : 0, gapBefore);
|
||||||
const rente = ahvAnnualPension(mdJE, career.gapYearsInPlan + gapBefore);
|
const rente = ahvAnnualPension(mdJE, career.gapYearsInPlan + gapBefore);
|
||||||
@@ -157,14 +159,16 @@ export function AhvReviewFields({
|
|||||||
<>
|
<>
|
||||||
<p className="col-span-full rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
<p className="col-span-full rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||||
Die AHV-Rente haengt vom <strong className="text-fg">massgebenden durchschnittlichen Jahreseinkommen</strong>{" "}
|
Die AHV-Rente haengt vom <strong className="text-fg">massgebenden durchschnittlichen Jahreseinkommen</strong>{" "}
|
||||||
ueber die ganze Beitragsdauer (ab Alter 21) ab. Alle Betraege sind REAL (heutige Kaufkraft) –
|
ueber die ganze Beitragsdauer (ab Alter 21) ab. Massgebend ist der <strong className="text-fg">Bruttolohn</strong>;
|
||||||
die AHV wertet vergangene Einkommen auf und indexiert die Schwellen, was sich real weitgehend aufhebt.
|
das Tool rechnet die im Plan erfassten Nettoeinkommen dafuer automatisch hoch. Alle Betraege sind
|
||||||
|
REAL (heutige Kaufkraft) – die AHV wertet vergangene Einkommen auf und indexiert die Schwellen,
|
||||||
|
was sich real weitgehend aufhebt.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<DerivedField
|
<DerivedField
|
||||||
label="Geplantes Durchschnittseinkommen (aus dem Plan)"
|
label="Geplantes Bruttoeinkommen (aus dem Plan)"
|
||||||
value={Math.round(career.plannedAvgIncome)}
|
value={Math.round(career.plannedAvgGrossIncome)}
|
||||||
help={`Reales Mittel ueber ${career.planYears} Beitragsjahre der Erwerbsphasen dieses Plans. Nur Einkommen, die dieser Person zugeordnet sind.`}
|
help={`Reales Mittel ueber ${career.planYears} Beitragsjahre der Erwerbsphasen dieses Plans, hochgerechnet vom Netto- auf den Bruttolohn. Nur Einkommen, die dieser Person zugeordnet sind.`}
|
||||||
/>
|
/>
|
||||||
<DerivedField
|
<DerivedField
|
||||||
label="Beitragsjahre im Plan"
|
label="Beitragsjahre im Plan"
|
||||||
@@ -179,8 +183,8 @@ export function AhvReviewFields({
|
|||||||
davor kennt das Tool nicht – bitte ergaenzen.
|
davor kennt das Tool nicht – bitte ergaenzen.
|
||||||
</p>
|
</p>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Durchschnittseinkommen vor Planbeginn (real)"
|
label="Durchschnittliches Bruttoeinkommen vor Planbeginn (real)"
|
||||||
help="In HEUTIGER Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung uebernehmen – dort ist die Aufwertung bereits enthalten. Die Rohbetraege im Kontoauszug sind historisch-nominal und daher zu tief."
|
help="BRUTTO und in HEUTIGER Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung uebernehmen – dort ist die Aufwertung bereits enthalten. Die Rohbetraege im Kontoauszug sind historisch-nominal und daher zu tief."
|
||||||
value={avgBefore}
|
value={avgBefore}
|
||||||
onChange={(v) => setT({ avgIncomeBefore: v })}
|
onChange={(v) => setT({ avgIncomeBefore: v })}
|
||||||
/>
|
/>
|
||||||
@@ -416,11 +420,11 @@ export function ElementPhaseFields({
|
|||||||
<>
|
<>
|
||||||
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
<p className="col-span-2 rounded-lg bg-surface-2 px-3 py-2 text-xs text-muted">
|
||||||
{isIncome
|
{isIncome
|
||||||
? "Einkommen wird NOMINAL erfasst (die Zahl auf dem Lohnausweis). Der reale Wert (heutige Kaufkraft) wird nur zur Info angezeigt."
|
? "Bitte das NETTO-Einkommen erfassen: der Betrag, der nach allen Lohnabzuegen (AHV/ALV, Pensionskasse, NBU) tatsaechlich aufs Konto kommt – nicht der Bruttolohn. Erfasst wird NOMINAL; der reale Wert (heutige Kaufkraft) erscheint nur zur Info. Fuer die AHV-Rente rechnet das Tool intern auf den Bruttolohn zurueck."
|
||||||
: "Ausgaben werden REAL erfasst (in heutiger Kaufkraft). Die Inflation (plan-weit) rechnet daraus automatisch die nominalen Ausgaben – nur zur Info."}
|
: "Ausgaben werden REAL erfasst (in heutiger Kaufkraft). Die Inflation (plan-weit) rechnet daraus automatisch die nominalen Ausgaben – nur zur Info."}
|
||||||
</p>
|
</p>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label={isIncome ? "Jahreseinkommen NOMINAL (erstes Jahr)" : "Jahresausgaben REAL (erstes Jahr, heutige Kaufkraft)"}
|
label={isIncome ? "Jahreseinkommen NETTO, nominal (erstes Jahr)" : "Jahresausgaben REAL (erstes Jahr, heutige Kaufkraft)"}
|
||||||
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
|
help={carried ? "Vorbelegt mit dem fortgeschriebenen Wert der Vorphase; hier bewusst aenderbar." : undefined}
|
||||||
value={baseValue}
|
value={baseValue}
|
||||||
onChange={(v) => setP({ amount: v })}
|
onChange={(v) => setP({ amount: v })}
|
||||||
@@ -457,8 +461,8 @@ export function ElementPhaseFields({
|
|||||||
bitte hier erfassen (REAL, heutige Kaufkraft).
|
bitte hier erfassen (REAL, heutige Kaufkraft).
|
||||||
</p>
|
</p>
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Durchschnittseinkommen der Beitragsjahre (real)"
|
label="Durchschnittliches Bruttoeinkommen der Beitragsjahre (real)"
|
||||||
help="Am besten aus der AHV-Rentenvorausberechnung bzw. der Rentenverfuegung. Ab 90'720 gibt es die Maximalrente."
|
help="BRUTTO und in heutiger Kaufkraft. Am besten aus der AHV-Rentenvorausberechnung bzw. der Rentenverfuegung. Ab 90'720 gibt es die Maximalrente."
|
||||||
value={num(pd.avgIncomeBefore)}
|
value={num(pd.avgIncomeBefore)}
|
||||||
onChange={(v) => setP({ avgIncomeBefore: v })}
|
onChange={(v) => setP({ avgIncomeBefore: v })}
|
||||||
/>
|
/>
|
||||||
@@ -559,34 +563,75 @@ export function ElementPhaseFields({
|
|||||||
<NumberField label="Erwartete Rendite (%/Jahr)" step={0.1} value={num(pd.expectedReturn)} onChange={(v) => setP({ expectedReturn: v })} />
|
<NumberField label="Erwartete Rendite (%/Jahr)" step={0.1} value={num(pd.expectedReturn)} onChange={(v) => setP({ expectedReturn: v })} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
case "REAL_ESTATE":
|
case "REAL_ESTATE": {
|
||||||
if (carried) {
|
// Zinsbetrag zu Phasenbeginn und -ende: die Restschuld sinkt mit der Amortisation,
|
||||||
return (
|
// der Zinsbetrag also mit. Am Nullpunkt gekappt (analog zur Berechnung).
|
||||||
<>
|
const hypStart = carried ? context.derivedMortgage : num(pd.mortgage);
|
||||||
<DerivedField label="Startwert Netto (fortgeschrieben)" value={context.derivedStart} help="Kaufpreis minus fortgeschriebene Resthypothek." />
|
const hypEnde = Math.max(0, hypStart - num(pd.amortization) * context.durationYears);
|
||||||
<MoneyField
|
const zinsStart = Math.round((hypStart * num(pd.interestRate)) / 100);
|
||||||
label="Amortisation (CHF/Jahr)"
|
const zinsEnde = Math.round((hypEnde * num(pd.interestRate)) / 100);
|
||||||
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote."
|
const handling = pd.interestHandling ?? "INCLUDED";
|
||||||
value={num(pd.amortization)}
|
|
||||||
|
|
||||||
onChange={(v) => setP({ amortization: v })}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MoneyField label="Kaufpreis (CHF)" value={num(pd.purchasePrice)} onChange={(v) => setP({ purchasePrice: v })} />
|
{carried ? (
|
||||||
<MoneyField label="Hypothek (CHF)" value={num(pd.mortgage)} onChange={(v) => setP({ mortgage: v })} />
|
<>
|
||||||
|
<DerivedField
|
||||||
|
label="Startwert Netto (fortgeschrieben)"
|
||||||
|
value={context.derivedStart}
|
||||||
|
help="Verkehrswert (inkl. bisheriger Wertsteigerung) minus fortgeschriebene Resthypothek."
|
||||||
|
/>
|
||||||
|
<DerivedField label="Resthypothek (fortgeschrieben)" value={hypStart} help="Aus der Vorphase fortgeschrieben." />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MoneyField label="Kaufpreis (CHF)" value={num(pd.purchasePrice)} onChange={(v) => setP({ purchasePrice: v })} />
|
||||||
|
<MoneyField label="Hypothek (CHF)" value={num(pd.mortgage)} onChange={(v) => setP({ mortgage: v })} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<MoneyField
|
<MoneyField
|
||||||
label="Amortisation (CHF/Jahr)"
|
label="Amortisation (CHF/Jahr)"
|
||||||
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote."
|
help="Jaehrliche Reduktion der Hypothek. Zaehlt gegen die Sparquote und endet, sobald die Hypothek abbezahlt ist."
|
||||||
value={num(pd.amortization)}
|
value={num(pd.amortization)}
|
||||||
|
|
||||||
onChange={(v) => setP({ amortization: v })}
|
onChange={(v) => setP({ amortization: v })}
|
||||||
/>
|
/>
|
||||||
|
<NumberField
|
||||||
|
label="Hypothekarzins (%/Jahr)"
|
||||||
|
help="Zinssatz auf der Restschuld. Der Zinsbetrag sinkt automatisch mit der Amortisation."
|
||||||
|
step={0.1}
|
||||||
|
value={num(pd.interestRate)}
|
||||||
|
onChange={(v) => setP({ interestRate: v })}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<FieldLabel
|
||||||
|
label="Hypothekarzins-Betrag (Beginn → Ende)"
|
||||||
|
help="Restschuld x Zinssatz, im ersten und im letzten Jahr dieser Phase. Wird automatisch berechnet."
|
||||||
|
/>
|
||||||
|
<div className="w-full rounded-lg border border-dashed border-border bg-surface-2 px-2.5 py-1.5 text-sm text-muted">
|
||||||
|
{formatChf(zinsStart)} <span className="text-faint">→</span> {formatChf(zinsEnde)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NumberField
|
||||||
|
label="Geschaetzte Wertsteigerung (%/Jahr)"
|
||||||
|
help="Wirkt auf den Wert der LIEGENSCHAFT, nicht auf das Eigenkapital. 1% von 1 Mio sind 10'000 pro Jahr – bei 100'000 Eigenkapital also 10% darauf (Hebel)."
|
||||||
|
step={0.1}
|
||||||
|
value={num(pd.valueGrowth)}
|
||||||
|
onChange={(v) => setP({ valueGrowth: v })}
|
||||||
|
/>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<SelectField
|
||||||
|
label="Sind die Hypothekarzinsen bereits in den Ausgaben enthalten?"
|
||||||
|
help="Damit die Zinsen nicht doppelt zaehlen. Standard: bereits enthalten – so verhalten sich bestehende Plaene unveraendert."
|
||||||
|
value={handling}
|
||||||
|
onChange={(v: "INCLUDED" | "ADD") => setP({ interestHandling: v })}
|
||||||
|
options={[
|
||||||
|
{ value: "INCLUDED", label: "Ja – bereits im Ausgaben-Element beruecksichtigt" },
|
||||||
|
{ value: "ADD", label: "Nein – bitte zu den Ausgaben dazuzaehlen" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case "OTHER_ASSET":
|
case "OTHER_ASSET":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export function PlanView({
|
|||||||
carriedEndValue: ce?.endValue ?? 0,
|
carriedEndValue: ce?.endValue ?? 0,
|
||||||
carried: ce?.carried ?? false,
|
carried: ce?.carried ?? false,
|
||||||
derivedStart: ce?.baseValue ?? 0,
|
derivedStart: ce?.baseValue ?? 0,
|
||||||
|
derivedMortgage: ce?.mortgageStart ?? 0,
|
||||||
deflatorStart: phase.cumulativeInflationStart,
|
deflatorStart: phase.cumulativeInflationStart,
|
||||||
laterPhaseCount: laterPhaseCount(phase),
|
laterPhaseCount: laterPhaseCount(phase),
|
||||||
ahvCareer: careerFor(element),
|
ahvCareer: careerFor(element),
|
||||||
@@ -211,6 +212,7 @@ export function PlanView({
|
|||||||
carriedEndValue: ce?.endValue ?? 0,
|
carriedEndValue: ce?.endValue ?? 0,
|
||||||
carried: ce?.carried ?? false,
|
carried: ce?.carried ?? false,
|
||||||
derivedStart: 0,
|
derivedStart: 0,
|
||||||
|
derivedMortgage: 0,
|
||||||
deflatorStart: fromPhase.cumulativeInflationStart,
|
deflatorStart: fromPhase.cumulativeInflationStart,
|
||||||
laterPhaseCount: laterPhaseCount(fromPhase),
|
laterPhaseCount: laterPhaseCount(fromPhase),
|
||||||
ahvCareer: careerFor(element),
|
ahvCareer: careerFor(element),
|
||||||
@@ -935,6 +937,7 @@ function AddElementDialog({
|
|||||||
carriedEndValue: 0,
|
carriedEndValue: 0,
|
||||||
carried: false,
|
carried: false,
|
||||||
derivedStart: 0,
|
derivedStart: 0,
|
||||||
|
derivedMortgage: 0,
|
||||||
deflatorStart: firstPhase.cumulativeInflationStart,
|
deflatorStart: firstPhase.cumulativeInflationStart,
|
||||||
laterPhaseCount: 0, // beim Anlegen bewusst kein Warnhinweis
|
laterPhaseCount: 0, // beim Anlegen bewusst kein Warnhinweis
|
||||||
ahvCareer: null,
|
ahvCareer: null,
|
||||||
|
|||||||
@@ -84,6 +84,83 @@ describe("AHV-Rentenformel (Skala 44)", () => {
|
|||||||
|
|
||||||
// V5-Modell: Einkommen = nominale Basis + nominale Lohnerhoehung; Ausgaben = REALE Basis +
|
// V5-Modell: Einkommen = nominale Basis + nominale Lohnerhoehung; Ausgaben = REALE Basis +
|
||||||
// reale Mehrausgaben, nominal = real x (plan-weite Inflation).
|
// reale Mehrausgaben, nominal = real x (plan-weite Inflation).
|
||||||
|
// Netto -> Brutto fuer die AHV (AHV_GROSS_FROM_NET_FACTOR).
|
||||||
|
const BRUTTO = 1.12;
|
||||||
|
|
||||||
|
describe("Immobilie: Zins und Wertsteigerung", () => {
|
||||||
|
function immoPlan(pd: PhaseData, jahre = 10) {
|
||||||
|
return plan({
|
||||||
|
age: 40,
|
||||||
|
retirementAge: 70,
|
||||||
|
inflation: 0,
|
||||||
|
initialCash: 500000,
|
||||||
|
phases: [{ id: "p1", durationYears: jahre }],
|
||||||
|
elements: [el("REAL_ESTATE", "HOUSEHOLD", { p1: pd })],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const immo = (p: PlanInput, i = 0) =>
|
||||||
|
computePlan(p).phases[i].elements.find((e) => e.category === "REAL_ESTATE")!;
|
||||||
|
|
||||||
|
it("Wertsteigerung wirkt auf die LIEGENSCHAFT, nicht auf das Eigenkapital (Hebel)", () => {
|
||||||
|
// Kaufpreis 1 Mio, Hypothek 900k -> EK 100k. 1%/J. auf die Liegenschaft, Amort. 10k/J.
|
||||||
|
const p = immoPlan({ purchasePrice: 1000000, mortgage: 900000, amortization: 10000, valueGrowth: 1 });
|
||||||
|
const wertEnde = 1000000 * Math.pow(1.01, 10);
|
||||||
|
const hypEnde = 900000 - 10000 * 10;
|
||||||
|
expect(immo(p).startValue).toBe(100000);
|
||||||
|
expect(immo(p).endValue).toBe(Math.round(wertEnde - hypEnde)); // ~304'622
|
||||||
|
// Der Hebel: 1% auf 1 Mio sind rund 10% des Eigenkapitals -- nicht 1%.
|
||||||
|
expect(immo(p).endValue).toBeGreaterThan(Math.round(100000 * Math.pow(1.01, 10) + 100000));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne Wertsteigerung bleibt es beim bisherigen Verhalten", () => {
|
||||||
|
const p = immoPlan({ purchasePrice: 1000000, mortgage: 900000, amortization: 10000 });
|
||||||
|
expect(immo(p).endValue).toBe(200000); // 1 Mio - 800k Resthypothek
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Hypothekarzins belastet das Cash nur bei 'ADD'", () => {
|
||||||
|
const ohne = immoPlan({ purchasePrice: 1000000, mortgage: 900000, amortization: 0, interestRate: 1 });
|
||||||
|
expect(computePlan(ohne).phases[0].cashEnd).toBe(500000); // Default INCLUDED -> kein Abzug
|
||||||
|
|
||||||
|
const mit = immoPlan({
|
||||||
|
purchasePrice: 1000000, mortgage: 900000, amortization: 0, interestRate: 1, interestHandling: "ADD",
|
||||||
|
});
|
||||||
|
// 10 Jahre x 1% von 900'000 = 90'000
|
||||||
|
expect(computePlan(mit).phases[0].cashEnd).toBe(500000 - 90000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Zinsbetrag sinkt mit der Amortisation und zaehlt in die Quote", () => {
|
||||||
|
const p = immoPlan({
|
||||||
|
purchasePrice: 1000000, mortgage: 1000000, amortization: 10000, interestRate: 1, interestHandling: "ADD",
|
||||||
|
});
|
||||||
|
const ph = computePlan(p).phases[0];
|
||||||
|
// Jahr 1: 1% von 1'000'000 = 10'000. Jahr 10: 1% von (1'000'000 - 9 x 10'000) = 9'100.
|
||||||
|
expect(ph.expenseStart).toBe(10000);
|
||||||
|
expect(ph.expenseEnd).toBe(9100);
|
||||||
|
expect(ph.quotaStart).toBe(-10000); // Zins schlaegt auf die Quote durch
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Verkauf: Grundstueckgewinnsteuer bemisst sich am urspruenglichen Kaufpreis", () => {
|
||||||
|
const p = plan({
|
||||||
|
age: 40, retirementAge: 70, inflation: 0, initialCash: 0,
|
||||||
|
phases: [
|
||||||
|
{ id: "p1", durationYears: 10 },
|
||||||
|
{ id: "p2", durationYears: 1 },
|
||||||
|
],
|
||||||
|
elements: [
|
||||||
|
el(
|
||||||
|
"REAL_ESTATE", "HOUSEHOLD",
|
||||||
|
{ p1: { purchasePrice: 1000000, mortgage: 900000, amortization: 10000, valueGrowth: 1 }, p2: {} },
|
||||||
|
{ p1: { decision: "SELL", salePrice: 1200000, saleTaxRate: 20 } }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const r = computePlan(p);
|
||||||
|
// Gewinn = 1'200'000 - 1'000'000 (Kaufpreis!) = 200'000 -> Steuer 40'000.
|
||||||
|
// Erloes = 1'200'000 - 800'000 Resthypothek - 40'000 = 360'000.
|
||||||
|
expect(r.phases[1].capitalInflow).toBe(360000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("AHV einkommensabhaengig", () => {
|
describe("AHV einkommensabhaengig", () => {
|
||||||
// 60-jaehrig, Pension mit 65: 39 Beitragsjahre vor Planbeginn (ab 21), 5 im Plan.
|
// 60-jaehrig, Pension mit 65: 39 Beitragsjahre vor Planbeginn (ab 21), 5 im Plan.
|
||||||
function ahvPlan(opts: {
|
function ahvPlan(opts: {
|
||||||
@@ -130,22 +207,44 @@ describe("AHV einkommensabhaengig", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("mdJE unter der Schwelle -> abgestufte Rente (Formel 2)", () => {
|
it("mdJE unter der Schwelle -> abgestufte Rente (Formel 2)", () => {
|
||||||
// mdJE = 60'000 -> 1'260 x (1.04 + 0.16 x 60'000/15'120) = 2'118.1/Monat x 13 = 27'535
|
// Plan-Einkommen ist NETTO -> fuer die AHV auf brutto hochgerechnet; das Feld
|
||||||
|
// "vor Planbeginn" ist bereits brutto. 39 Jahre davor, 5 im Plan.
|
||||||
const p = ahvPlan({ income: 60000, avgIncomeBefore: 60000 });
|
const p = ahvPlan({ income: 60000, avgIncomeBefore: 60000 });
|
||||||
const erwartet = Math.round(ahvMonthlyFullPension(60000) * 13);
|
const mdJE = (60000 * 39 + 60000 * BRUTTO * 5) / 44;
|
||||||
expect(renteIn(p, 1)).toBe(erwartet);
|
expect(renteIn(p, 1)).toBe(Math.round(ahvMonthlyFullPension(mdJE) * 13));
|
||||||
expect(renteIn(p, 1)).toBeLessThan(32760);
|
expect(renteIn(p, 1)).toBeLessThan(32760);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Nettolohn wird fuer die AHV auf den Bruttolohn hochgerechnet", () => {
|
||||||
|
// Ohne Vorgeschichte (Alter 21 bei Planbeginn) haengt das mdJE nur am Plan-Einkommen.
|
||||||
|
const p = plan({
|
||||||
|
age: 21,
|
||||||
|
retirementAge: 65,
|
||||||
|
inflation: 0,
|
||||||
|
phases: [
|
||||||
|
{ id: "p1", durationYears: 44 },
|
||||||
|
{ id: "p2", durationYears: 5 },
|
||||||
|
],
|
||||||
|
elements: [
|
||||||
|
el("INCOME", "PERSON_A", { p1: { amount: 70000, teuerungsausgleich: 0 }, p2: {} }),
|
||||||
|
el("AHV", "PERSON_A", { p1: {}, p2: {} }, { p1: { reviewed: true } }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const rente = computePlan(p).phases[1].elements.find((e) => e.category === "AHV")!.startValue;
|
||||||
|
// mdJE = 70'000 x 1.12 = 78'400 (brutto), NICHT 70'000.
|
||||||
|
expect(rente).toBe(Math.round(ahvMonthlyFullPension(70000 * BRUTTO) * 13));
|
||||||
|
expect(rente).toBeGreaterThan(Math.round(ahvMonthlyFullPension(70000) * 13));
|
||||||
|
});
|
||||||
|
|
||||||
it("tiefes Einkommen -> Mindestrente 1'260 x 13", () => {
|
it("tiefes Einkommen -> Mindestrente 1'260 x 13", () => {
|
||||||
const p = ahvPlan({ income: 10000, avgIncomeBefore: 10000 });
|
const p = ahvPlan({ income: 10000, avgIncomeBefore: 10000 });
|
||||||
expect(renteIn(p, 1)).toBe(1260 * 13);
|
expect(renteIn(p, 1)).toBe(1260 * 13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Einkommen vor Planbeginn dominiert bei kurzer Restlaufzeit", () => {
|
it("Einkommen vor Planbeginn dominiert bei kurzer Restlaufzeit", () => {
|
||||||
// 39 Jahre vor Planbeginn zu 40'000, nur 5 Jahre im Plan zu 200'000.
|
// 39 Jahre vor Planbeginn zu 40'000 brutto, nur 5 Jahre im Plan zu 200'000 netto.
|
||||||
const p = ahvPlan({ income: 200000, avgIncomeBefore: 40000 });
|
const p = ahvPlan({ income: 200000, avgIncomeBefore: 40000 });
|
||||||
const mdJE = (40000 * 39 + 200000 * 5) / 44; // = 58'181.8
|
const mdJE = (40000 * 39 + 200000 * BRUTTO * 5) / 44;
|
||||||
expect(renteIn(p, 1)).toBe(Math.round(ahvMonthlyFullPension(mdJE) * 13));
|
expect(renteIn(p, 1)).toBe(Math.round(ahvMonthlyFullPension(mdJE) * 13));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,7 +277,8 @@ describe("AHV einkommensabhaengig", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const rente = computePlan(p).phases[1].elements.find((e) => e.category === "AHV")!.startValue;
|
const rente = computePlan(p).phases[1].elements.find((e) => e.category === "AHV")!.startValue;
|
||||||
expect(rente).toBe(Math.round(ahvMonthlyFullPension(80000) * 13)); // mdJE = 80'000, nicht 9'091
|
// mdJE = geplantes Brutto (80'000 x 1.12), nicht ~9'091 (das waere der Fallback auf 0).
|
||||||
|
expect(rente).toBe(Math.round(ahvMonthlyFullPension(80000 * BRUTTO) * 13));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("bereits bei Planbeginn pensioniert: Karriere kommt aus der Phasenzelle", () => {
|
it("bereits bei Planbeginn pensioniert: Karriere kommt aus der Phasenzelle", () => {
|
||||||
|
|||||||
+87
-27
@@ -2,6 +2,7 @@ import {
|
|||||||
AHV_CONTRIBUTION_START_AGE,
|
AHV_CONTRIBUTION_START_AGE,
|
||||||
AHV_COUPLE_CAP_FACTOR,
|
AHV_COUPLE_CAP_FACTOR,
|
||||||
AHV_FULL_CONTRIBUTION_YEARS,
|
AHV_FULL_CONTRIBUTION_YEARS,
|
||||||
|
AHV_GROSS_FROM_NET_FACTOR,
|
||||||
AHV_MAX_ANNUAL_SINGLE,
|
AHV_MAX_ANNUAL_SINGLE,
|
||||||
AHV_MIN_MONTHLY_FULL,
|
AHV_MIN_MONTHLY_FULL,
|
||||||
AHV_PENSION_MONTHS,
|
AHV_PENSION_MONTHS,
|
||||||
@@ -34,6 +35,7 @@ export interface ElementPhaseComputed {
|
|||||||
locked: boolean;
|
locked: boolean;
|
||||||
carried: boolean; // Phase >= 2: Start-/Basiswert wird aus der Vorphase fortgeschrieben
|
carried: boolean; // Phase >= 2: Start-/Basiswert wird aus der Vorphase fortgeschrieben
|
||||||
baseValue: number; // fortgeschriebener Basiswert (read-only Anzeige ab Phase 2; ohne Zusatzeinlage)
|
baseValue: number; // fortgeschriebener Basiswert (read-only Anzeige ab Phase 2; ohne Zusatzeinlage)
|
||||||
|
mortgageStart: number; // nur REAL_ESTATE: Resthypothek zu Phasenbeginn (0 sonst)
|
||||||
startValue: number; // Wert/Flow zu Phasenbeginn (Aktiven +, Schulden -, Einkommen/Ausgaben = Flow Jahr 1)
|
startValue: number; // Wert/Flow zu Phasenbeginn (Aktiven +, Schulden -, Einkommen/Ausgaben = Flow Jahr 1)
|
||||||
endValue: number; // Wert/Flow am Phasenende (letztes Jahr)
|
endValue: number; // Wert/Flow am Phasenende (letztes Jahr)
|
||||||
summary: string;
|
summary: string;
|
||||||
@@ -119,10 +121,12 @@ export function ahvMonthlyFullPension(mdJE: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Beitragskarriere einer Person fuer die AHV -- akkumuliert ueber die Erwerbsphasen des Plans.
|
// Beitragskarriere einer Person fuer die AHV -- akkumuliert ueber die Erwerbsphasen des Plans.
|
||||||
|
// ACHTUNG: Alle Einkommen sind BRUTTO. Das Tool erfasst netto (so stimmt der Cash-Fluss), die
|
||||||
|
// AHV bemisst sich aber am Bruttolohn -- die Umrechnung passiert beim Aufbau der Karriere.
|
||||||
export interface AhvCareer {
|
export interface AhvCareer {
|
||||||
personId: string;
|
personId: string;
|
||||||
role: PersonRole;
|
role: PersonRole;
|
||||||
plannedAvgIncome: number; // reales Durchschnittseinkommen der Beitragsjahre IM Plan
|
plannedAvgGrossIncome: number; // reales BRUTTO-Durchschnittseinkommen der Beitragsjahre im Plan
|
||||||
planYears: number; // Beitragsjahre im Plan (Dauer abzueglich Ausfalljahre)
|
planYears: number; // Beitragsjahre im Plan (Dauer abzueglich Ausfalljahre)
|
||||||
yearsBeforePlan: number; // Jahre zwischen Alter 21 und Planbeginn
|
yearsBeforePlan: number; // Jahre zwischen Alter 21 und Planbeginn
|
||||||
gapYearsInPlan: number;
|
gapYearsInPlan: number;
|
||||||
@@ -132,11 +136,12 @@ export interface AhvCareer {
|
|||||||
// Einkommen ueber ALLE Beitragsjahre (vor Planbeginn + im Plan). REAL gerechnet, weil die
|
// Einkommen ueber ALLE Beitragsjahre (vor Planbeginn + im Plan). REAL gerechnet, weil die
|
||||||
// AHV vergangene Einkommen aufwertet UND die Schwellen indexiert -- beides hebt sich in
|
// AHV vergangene Einkommen aufwertet UND die Schwellen indexiert -- beides hebt sich in
|
||||||
// realer Betrachtung weitgehend auf.
|
// realer Betrachtung weitgehend auf.
|
||||||
export function ahvMdje(career: AhvCareer, avgIncomeBefore: number, gapYearsBefore: number): number {
|
// `avgGrossIncomeBefore` ist -- wie die Karriere -- ein BRUTTO-Wert.
|
||||||
|
export function ahvMdje(career: AhvCareer, avgGrossIncomeBefore: number, gapYearsBefore: number): number {
|
||||||
const yearsBefore = Math.max(0, career.yearsBeforePlan - Math.max(0, gapYearsBefore));
|
const yearsBefore = Math.max(0, career.yearsBeforePlan - Math.max(0, gapYearsBefore));
|
||||||
const totalYears = yearsBefore + career.planYears;
|
const totalYears = yearsBefore + career.planYears;
|
||||||
if (totalYears <= 0) return 0;
|
if (totalYears <= 0) return 0;
|
||||||
return (avgIncomeBefore * yearsBefore + career.plannedAvgIncome * career.planYears) / totalYears;
|
return (avgGrossIncomeBefore * yearsBefore + career.plannedAvgGrossIncome * career.planYears) / totalYears;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jaehrliche AHV-Rente: Vollrente zum mdJE, mal 13 Zahlungen, gekuerzt um die Ausfalljahre
|
// Jaehrliche AHV-Rente: Vollrente zum mdJE, mal 13 Zahlungen, gekuerzt um die Ausfalljahre
|
||||||
@@ -171,6 +176,8 @@ interface Carry {
|
|||||||
status: ElementStatus;
|
status: ElementStatus;
|
||||||
value: number; // Aktiven-Saldo (PK/3a/Sonstiges Vermoegen) am Ende der Vorphase
|
value: number; // Aktiven-Saldo (PK/3a/Sonstiges Vermoegen) am Ende der Vorphase
|
||||||
mortgage: number; // Immobilie: Resthypothek
|
mortgage: number; // Immobilie: Resthypothek
|
||||||
|
propertyValue: number; // Immobilie: Verkehrswert am Ende der Vorphase (inkl. Wertsteigerung)
|
||||||
|
propertyPurchase: number; // Immobilie: urspruenglicher Kaufpreis (Basis der Grundstueckgewinnsteuer)
|
||||||
owed: number; // Schulden: Restschuld (positiv)
|
owed: number; // Schulden: Restschuld (positiv)
|
||||||
pkPensionAnnual: number; // PK: jaehrliche Rente nach Verrentung
|
pkPensionAnnual: number; // PK: jaehrliche Rente nach Verrentung
|
||||||
flowBasis: number; // Einkommen/Ausgaben: indexierter Basiswert der naechsten Phase
|
flowBasis: number; // Einkommen/Ausgaben: indexierter Basiswert der naechsten Phase
|
||||||
@@ -178,7 +185,17 @@ interface Carry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyCarry(): Carry {
|
function emptyCarry(): Carry {
|
||||||
return { status: "ACTIVE", value: 0, mortgage: 0, owed: 0, pkPensionAnnual: 0, flowBasis: 0, hasCarry: false };
|
return {
|
||||||
|
status: "ACTIVE",
|
||||||
|
value: 0,
|
||||||
|
mortgage: 0,
|
||||||
|
propertyValue: 0,
|
||||||
|
propertyPurchase: 0,
|
||||||
|
owed: 0,
|
||||||
|
pkPensionAnnual: 0,
|
||||||
|
flowBasis: 0,
|
||||||
|
hasCarry: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(v: number): string {
|
function fmt(v: number): string {
|
||||||
@@ -309,8 +326,18 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
let renteTotal = 0; // AHV + PK-Renten (nominal fix)
|
let renteTotal = 0; // AHV + PK-Renten (nominal fix)
|
||||||
const assets: { value: number; rate: number; r: number; withdrawal: number; ec: ElementPhaseComputed }[] = [];
|
const assets: { value: number; rate: number; r: number; withdrawal: number; ec: ElementPhaseComputed }[] = [];
|
||||||
// mortgage/owed sind LAUFENDE Salden: sie werden in der Jahresschleife abgebaut und am
|
// mortgage/owed sind LAUFENDE Salden: sie werden in der Jahresschleife abgebaut und am
|
||||||
// Nullpunkt gestoppt (keine Rate mehr, sobald abbezahlt).
|
// Nullpunkt gestoppt (keine Rate mehr, sobald abbezahlt). `value` ist der Verkehrswert der
|
||||||
const realEstates: { purchase: number; mortgage: number; amort: number; ec: ElementPhaseComputed }[] = [];
|
// Liegenschaft (waechst mit valueGrowth), `purchase` der urspruengliche Kaufpreis.
|
||||||
|
const realEstates: {
|
||||||
|
value: number;
|
||||||
|
purchase: number;
|
||||||
|
mortgage: number;
|
||||||
|
amort: number;
|
||||||
|
growth: number;
|
||||||
|
interestRate: number;
|
||||||
|
addInterest: boolean;
|
||||||
|
ec: ElementPhaseComputed;
|
||||||
|
}[] = [];
|
||||||
const debts: { owed: number; repay: number; ec: ElementPhaseComputed }[] = [];
|
const debts: { owed: number; repay: number; ec: ElementPhaseComputed }[] = [];
|
||||||
let fixedRatesTotal = 0; // Sparraten mit konstantem Jahresbetrag: 3a + Sonstiges Vermoegen
|
let fixedRatesTotal = 0; // Sparraten mit konstantem Jahresbetrag: 3a + Sonstiges Vermoegen
|
||||||
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermoegen
|
let plannedWithdrawTotal = 0; // Bezugsraten (fliessen ins Cash): Sonstiges Vermoegen
|
||||||
@@ -333,6 +360,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
locked: carry.status !== "ACTIVE",
|
locked: carry.status !== "ACTIVE",
|
||||||
carried: carry.hasCarry,
|
carried: carry.hasCarry,
|
||||||
baseValue: 0,
|
baseValue: 0,
|
||||||
|
mortgageStart: 0,
|
||||||
startValue: 0,
|
startValue: 0,
|
||||||
endValue: 0,
|
endValue: 0,
|
||||||
summary: "",
|
summary: "",
|
||||||
@@ -369,14 +397,15 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
|
|
||||||
// AHV: reales Erwerbseinkommen der Person mitfuehren. Nur Einkommen, die einer
|
// AHV: reales Erwerbseinkommen der Person mitfuehren. Nur Einkommen, die einer
|
||||||
// Person zugeordnet sind -- bei einem Einzelplan zaehlt "Gemeinsam" zur Person A.
|
// Person zugeordnet sind -- bei einem Einzelplan zaehlt "Gemeinsam" zur Person A.
|
||||||
|
// Das Feld ist NETTO erfasst; die AHV bemisst sich am Bruttolohn -> hochrechnen.
|
||||||
if (e.category === "INCOME" && ownerWorking) {
|
if (e.category === "INCOME" && ownerWorking) {
|
||||||
const attributed =
|
const attributed =
|
||||||
owner ?? (plan.householdType === "SINGLE" && e.ownerRole === "HOUSEHOLD" ? personA : null);
|
owner ?? (plan.householdType === "SINGLE" && e.ownerRole === "HOUSEHOLD" ? personA : null);
|
||||||
if (attributed) {
|
if (attributed) {
|
||||||
const avgReal = avgRealFlow(basis, idx, infl, duration, cumInflStart);
|
const avgRealGross = avgRealFlow(basis, idx, infl, duration, cumInflStart) * AHV_GROSS_FROM_NET_FACTOR;
|
||||||
phaseRealIncomeByPerson.set(
|
phaseRealIncomeByPerson.set(
|
||||||
attributed.id,
|
attributed.id,
|
||||||
(phaseRealIncomeByPerson.get(attributed.id) ?? 0) + avgReal
|
(phaseRealIncomeByPerson.get(attributed.id) ?? 0) + avgRealGross
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,15 +483,30 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "REAL_ESTATE": {
|
case "REAL_ESTATE": {
|
||||||
const purchase = Math.round(num(pd.purchasePrice));
|
// Urspruenglicher Kaufpreis (Basis der Grundstueckgewinnsteuer) und Verkehrswert
|
||||||
|
// (waechst mit der Wertsteigerung) laufen getrennt.
|
||||||
|
const purchase = carry.hasCarry ? carry.propertyPurchase : Math.round(num(pd.purchasePrice));
|
||||||
|
const valueStart = carry.hasCarry ? carry.propertyValue : Math.round(num(pd.purchasePrice));
|
||||||
const mortgageStart = carry.hasCarry ? carry.mortgage : Math.round(num(pd.mortgage));
|
const mortgageStart = carry.hasCarry ? carry.mortgage : Math.round(num(pd.mortgage));
|
||||||
const amort = Math.round(num(pd.amortization));
|
const amort = Math.round(num(pd.amortization));
|
||||||
const equity = purchase - mortgageStart;
|
const equity = valueStart - mortgageStart;
|
||||||
if (!carry.hasCarry && !isFirstPhase) investmentsFromCash += Math.max(0, equity);
|
if (!carry.hasCarry && !isFirstPhase) investmentsFromCash += Math.max(0, equity);
|
||||||
ec.baseValue = equity;
|
ec.baseValue = equity;
|
||||||
|
ec.mortgageStart = mortgageStart;
|
||||||
ec.startValue = equity;
|
ec.startValue = equity;
|
||||||
wealthStart += equity;
|
wealthStart += equity;
|
||||||
realEstates.push({ purchase, mortgage: mortgageStart, amort, ec });
|
realEstates.push({
|
||||||
|
value: valueStart,
|
||||||
|
purchase,
|
||||||
|
mortgage: mortgageStart,
|
||||||
|
amort,
|
||||||
|
growth: num(pd.valueGrowth),
|
||||||
|
interestRate: num(pd.interestRate),
|
||||||
|
// Default INCLUDED: bestehende Plaene haben die Zinsen in den Ausgaben -> nicht
|
||||||
|
// nochmals abziehen. Nur bei bewusstem "ADD" rechnet das Tool sie dazu.
|
||||||
|
addInterest: pd.interestHandling === "ADD",
|
||||||
|
ec,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "OTHER_DEBT": {
|
case "OTHER_DEBT": {
|
||||||
@@ -508,9 +552,20 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1);
|
for (const inc of incomes) incomeFlow += inc.basis * Math.pow(1 + inc.idx / 100, t - 1);
|
||||||
// Ausgaben: real (Basis x (1+reale Mehrausgabe)^(t-1)); nominal = real x kumul. Inflation.
|
// Ausgaben: real (Basis x (1+reale Mehrausgabe)^(t-1)); nominal = real x kumul. Inflation.
|
||||||
const inflFactor = cumInflStart * Math.pow(1 + infl / 100, t - 1);
|
const inflFactor = cumInflStart * Math.pow(1 + infl / 100, t - 1);
|
||||||
let expenseReal = 0;
|
let expenseRealBase = 0;
|
||||||
for (const exp of expenses) expenseReal += exp.basis * Math.pow(1 + exp.idx / 100, t - 1);
|
for (const exp of expenses) expenseRealBase += exp.basis * Math.pow(1 + exp.idx / 100, t - 1);
|
||||||
const expenseNominal = expenseReal * inflFactor;
|
|
||||||
|
// Hypothekarzins: NOMINAL aus der Restschuld zu Jahresbeginn -- nicht mit der Inflation
|
||||||
|
// hochrechnen. Zaehlt zu den Ausgaben (und damit in die Quote), sofern nicht bereits
|
||||||
|
// im Ausgaben-Element enthalten.
|
||||||
|
let interestNominal = 0;
|
||||||
|
for (const re of realEstates) {
|
||||||
|
if (!re.addInterest) continue;
|
||||||
|
interestNominal += re.mortgage * (re.interestRate / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expenseNominal = expenseRealBase * inflFactor + interestNominal;
|
||||||
|
const expenseReal = expenseRealBase + interestNominal / (inflFactor || 1);
|
||||||
const quote = incomeFlow - expenseNominal;
|
const quote = incomeFlow - expenseNominal;
|
||||||
|
|
||||||
yearly.push({
|
yearly.push({
|
||||||
@@ -548,6 +603,9 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
const pay = Math.min(re.amort, re.mortgage);
|
const pay = Math.min(re.amort, re.mortgage);
|
||||||
re.mortgage -= pay;
|
re.mortgage -= pay;
|
||||||
debtRates += pay;
|
debtRates += pay;
|
||||||
|
// Wertsteigerung wirkt auf die LIEGENSCHAFT, nicht auf das Eigenkapital -- das ist der
|
||||||
|
// Hebel: 1 % von 1 Mio sind 10'000, also 10 % eines Eigenkapitals von 100'000.
|
||||||
|
re.value *= 1 + re.growth / 100;
|
||||||
}
|
}
|
||||||
for (const d of debts) {
|
for (const d of debts) {
|
||||||
const pay = Math.min(d.repay, d.owed);
|
const pay = Math.min(d.repay, d.owed);
|
||||||
@@ -562,7 +620,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
// Gesamtvermoegen zum Jahresende t (fuer Ruin-Erkennung).
|
// Gesamtvermoegen zum Jahresende t (fuer Ruin-Erkennung).
|
||||||
let total = cash;
|
let total = cash;
|
||||||
for (const a of assets) total += a.value;
|
for (const a of assets) total += a.value;
|
||||||
for (const re of realEstates) total += re.purchase - re.mortgage;
|
for (const re of realEstates) total += re.value - re.mortgage;
|
||||||
for (const d of debts) total += -d.owed;
|
for (const d of debts) total += -d.owed;
|
||||||
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
|
if (ruinAge === null && total < 0) ruinAge = personA.age + yearsBefore + t;
|
||||||
}
|
}
|
||||||
@@ -588,7 +646,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
wealthEnd += a.ec.endValue;
|
wealthEnd += a.ec.endValue;
|
||||||
}
|
}
|
||||||
for (const re of realEstates) {
|
for (const re of realEstates) {
|
||||||
re.ec.endValue = re.purchase - re.mortgage;
|
re.ec.endValue = Math.round(re.value - re.mortgage);
|
||||||
re.ec.summary = fmt(re.ec.endValue);
|
re.ec.summary = fmt(re.ec.endValue);
|
||||||
wealthEnd += re.ec.endValue;
|
wealthEnd += re.ec.endValue;
|
||||||
}
|
}
|
||||||
@@ -670,7 +728,6 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
for (const e of orderedElements) {
|
for (const e of orderedElements) {
|
||||||
const carry = carries.get(e.id)!;
|
const carry = carries.get(e.id)!;
|
||||||
const ec = ecById.get(e.id)!;
|
const ec = ecById.get(e.id)!;
|
||||||
const pd = e.phaseValues[phase.id] ?? {};
|
|
||||||
const td = e.transitionValues[phase.id] ?? {};
|
const td = e.transitionValues[phase.id] ?? {};
|
||||||
const owner = e.ownerRole && e.ownerRole !== "HOUSEHOLD" ? personByRole(persons, e.ownerRole) : null;
|
const owner = e.ownerRole && e.ownerRole !== "HOUSEHOLD" ? personByRole(persons, e.ownerRole) : null;
|
||||||
const ownerRetiresNext =
|
const ownerRetiresNext =
|
||||||
@@ -690,8 +747,8 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
ahvBeforeByPerson.set(owner.id, {
|
ahvBeforeByPerson.set(owner.id, {
|
||||||
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schaetzung fuer die Jahre
|
// Ohne erfassten Wert gilt der geplante Durchschnitt als Schaetzung fuer die Jahre
|
||||||
// vor Planbeginn -- exakt der Wert, den der Pruef-Dialog vorbelegt. Ein Fallback auf
|
// vor Planbeginn -- exakt der Wert, den der Pruef-Dialog vorbelegt. Ein Fallback auf
|
||||||
// 0 wuerde die Rente still und massiv zu tief rechnen.
|
// 0 wuerde die Rente still und massiv zu tief rechnen. Beide Werte sind BRUTTO.
|
||||||
avg: num(td.avgIncomeBefore, career.plannedAvgIncome),
|
avg: num(td.avgIncomeBefore, career.plannedAvgGrossIncome),
|
||||||
gap: Math.max(0, Math.round(num(td.gapYearsBefore))),
|
gap: Math.max(0, Math.round(num(td.gapYearsBefore))),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -752,17 +809,20 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "REAL_ESTATE": {
|
case "REAL_ESTATE": {
|
||||||
// ec.endValue = Kaufpreis - Resthypothek am Phasenende -> Resthypothek zurueckrechnen.
|
const re = realEstates.find((r) => r.ec.elementId === e.id);
|
||||||
const purchase = Math.round(num(pd.purchasePrice));
|
if (!re) break;
|
||||||
const restMortgage = purchase - ec.endValue;
|
|
||||||
if (td.decision === "SELL") {
|
if (td.decision === "SELL") {
|
||||||
const salePrice = Math.round(num(td.salePrice));
|
const salePrice = Math.round(num(td.salePrice));
|
||||||
const gain = Math.max(0, salePrice - purchase);
|
// Grundstueckgewinnsteuer bemisst sich am urspruenglichen Kaufpreis, nicht am
|
||||||
|
// zwischenzeitlich gestiegenen Verkehrswert.
|
||||||
|
const gain = Math.max(0, salePrice - re.purchase);
|
||||||
const tax = gain * (num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE) / 100);
|
const tax = gain * (num(td.saleTaxRate, DEFAULT_PROPERTY_GAINS_TAX_RATE) / 100);
|
||||||
txInflow += Math.round(salePrice - restMortgage - tax);
|
txInflow += Math.round(salePrice - re.mortgage - tax);
|
||||||
carry.status = "SOLD";
|
carry.status = "SOLD";
|
||||||
} else {
|
} else {
|
||||||
carry.mortgage = restMortgage;
|
carry.mortgage = re.mortgage;
|
||||||
|
carry.propertyValue = re.value;
|
||||||
|
carry.propertyPurchase = re.purchase;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -813,11 +873,11 @@ function buildCareer(
|
|||||||
gapAccum: Map<string, number>
|
gapAccum: Map<string, number>
|
||||||
): AhvCareer {
|
): AhvCareer {
|
||||||
const planYears = yearsAccum.get(owner.id) ?? 0;
|
const planYears = yearsAccum.get(owner.id) ?? 0;
|
||||||
const incomeSum = incomeAccum.get(owner.id) ?? 0;
|
const incomeSum = incomeAccum.get(owner.id) ?? 0; // bereits brutto (siehe Element-Setup)
|
||||||
return {
|
return {
|
||||||
personId: owner.id,
|
personId: owner.id,
|
||||||
role: owner.role,
|
role: owner.role,
|
||||||
plannedAvgIncome: planYears > 0 ? incomeSum / planYears : 0,
|
plannedAvgGrossIncome: planYears > 0 ? incomeSum / planYears : 0,
|
||||||
planYears,
|
planYears,
|
||||||
yearsBeforePlan: ahvYearsBeforePlan(owner.age),
|
yearsBeforePlan: ahvYearsBeforePlan(owner.age),
|
||||||
gapYearsInPlan: gapAccum.get(owner.id) ?? 0,
|
gapYearsInPlan: gapAccum.get(owner.id) ?? 0,
|
||||||
|
|||||||
@@ -18,6 +18,25 @@ export const AHV_CONTRIBUTION_START_AGE = 21;
|
|||||||
// Abgeleitet, damit eine Anpassung von R0 nicht an zwei Stellen nachgezogen werden muss.
|
// Abgeleitet, damit eine Anpassung von R0 nicht an zwei Stellen nachgezogen werden muss.
|
||||||
export const AHV_MAX_ANNUAL_SINGLE = 2 * AHV_MIN_MONTHLY_FULL * AHV_PENSION_MONTHS;
|
export const AHV_MAX_ANNUAL_SINGLE = 2 * AHV_MIN_MONTHLY_FULL * AHV_PENSION_MONTHS;
|
||||||
|
|
||||||
|
// Umrechnung Netto- -> Bruttolohn fuer die AHV. Das Tool erfasst das Einkommen NETTO (so
|
||||||
|
// denkt der Nutzer, und so stimmt der Cash-Fluss), die AHV bemisst sich aber am BRUTTOlohn.
|
||||||
|
//
|
||||||
|
// Herleitung (Arbeitnehmer-Abzuege in % des Bruttolohns):
|
||||||
|
// AHV/IV/EO 5.30 % (10.6 % total, haelftig geteilt)
|
||||||
|
// ALV 1.10 % ( 2.2 % total, haelftig geteilt, bis 148'200)
|
||||||
|
// NBU ~1.00 % (variiert 0.4-1.6 %, teils vom Arbeitgeber getragen)
|
||||||
|
// PK ~2-5 % (BVG-Altersgutschrift 7/10/15/18 % auf den KOORDINIERTEN Lohn
|
||||||
|
// [Brutto - 26'460, max. 90'720], Arbeitnehmer >= die Haelfte)
|
||||||
|
// -> total ~9-13 % je nach Alter und Lohn; Faktor 1/(1-q) = 1.10 bis 1.16, Mittel ~1.12.
|
||||||
|
//
|
||||||
|
// Ein fixer Faktor ist hier vertretbar, weil das mdJE selbst ein Durchschnitt ueber die ganze
|
||||||
|
// Beitragskarriere (44 Jahre) ist: der altersabhaengige PK-Satz mittelt sich dabei heraus.
|
||||||
|
// Die Restunschaerfe (~3 %) ist deutlich kleiner als der Fehler, den die Umrechnung behebt (~11 %).
|
||||||
|
//
|
||||||
|
// Quellen: BSV "Betraege gueltig ab 1.1.2026" (Koordinationsabzug 26'460, obere Limite 90'720,
|
||||||
|
// keine Aenderung gegenueber 2025); AHV-Merkblaetter 2.01 (AHV/IV/EO) und 2.08 (ALV).
|
||||||
|
export const AHV_GROSS_FROM_NET_FACTOR = 1.12;
|
||||||
|
|
||||||
// Ehepaar-Plafonierung: die Summe beider Einzelrenten ist auf 150% der Einzel-
|
// Ehepaar-Plafonierung: die Summe beider Einzelrenten ist auf 150% der Einzel-
|
||||||
// Maximalrente begrenzt. Bei Ueberschreitung werden beide Renten proportional gekuerzt.
|
// Maximalrente begrenzt. Bei Ueberschreitung werden beide Renten proportional gekuerzt.
|
||||||
export const AHV_COUPLE_CAP_FACTOR = 1.5;
|
export const AHV_COUPLE_CAP_FACTOR = 1.5;
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ export interface PhaseData {
|
|||||||
purchasePrice?: number;
|
purchasePrice?: number;
|
||||||
mortgage?: number;
|
mortgage?: number;
|
||||||
amortization?: number;
|
amortization?: number;
|
||||||
|
// Hypothekarzins in % der Restschuld. Der Zinsbetrag sinkt dadurch mit der Amortisation.
|
||||||
|
interestRate?: number;
|
||||||
|
// Steuert die Doppelzaehlung: Sind die Zinsen im Ausgaben-Element bereits enthalten
|
||||||
|
// (INCLUDED, Default -- Verhalten bisheriger Plaene) oder soll das Tool sie dazurechnen (ADD)?
|
||||||
|
interestHandling?: "INCLUDED" | "ADD";
|
||||||
|
// Geschaetzte jaehrliche Wertveraenderung der LIEGENSCHAFT (nicht des Eigenkapitals).
|
||||||
|
valueGrowth?: number;
|
||||||
// OTHER_DEBT
|
// OTHER_DEBT
|
||||||
annualRepayment?: number;
|
annualRepayment?: number;
|
||||||
}
|
}
|
||||||
@@ -157,6 +164,9 @@ export const phaseDataSchema = z
|
|||||||
purchasePrice: nonNeg.optional(),
|
purchasePrice: nonNeg.optional(),
|
||||||
mortgage: nonNeg.optional(),
|
mortgage: nonNeg.optional(),
|
||||||
amortization: nonNeg.optional(),
|
amortization: nonNeg.optional(),
|
||||||
|
interestRate: z.number().min(0).max(20).optional(),
|
||||||
|
interestHandling: z.enum(["INCLUDED", "ADD"]).optional(),
|
||||||
|
valueGrowth: z.number().min(-20).max(20).optional(),
|
||||||
annualRepayment: nonNeg.optional(),
|
annualRepayment: nonNeg.optional(),
|
||||||
})
|
})
|
||||||
.strip();
|
.strip();
|
||||||
|
|||||||
Reference in New Issue
Block a user