Szenario-Hierarchie: Plan als Behaelter, Diff-Markierung (V6)
Deploy App / deploy (push) Successful in 1m43s
Deploy App / deploy (push) Successful in 1m43s
Groesste Umstrukturierung bisher. Der PLAN ist neu ein schlanker Behaelter ohne Finanzdaten; die berechenbare Einheit ist das SZENARIO. Modell: - Jeder Plan bekommt beim Anlegen automatisch ein Basisszenario (isBase). - Das Grundprofil liegt am Szenario, nicht am Plan -- nur so sind Szenarien mit abweichendem PENSIONSALTER moeglich (Fruehpensionierung), das in Person steckt. - Neue Szenarien sind vollstaendige Kopien eines BELIEBIGEN Szenarios und haengen als Baum darunter (parentScenarioId); die Seitenleiste rueckt sie ein. - Kopierte Phasen/Elemente tragen Herkunfts-Verweise (sourcePhaseId, sourceElementId). Ueber den Namen zu matchen waere fragil gewesen. Abweichungs-Markierung (Diff gegen das Eltern-Szenario, live): - geaendert = gelb, neu = gruen + Badge, entfernt = graue Geisterzeile. - Markiert: Phasen-/Uebergangszellen, Element-Zeilen, Phasenkoepfe, Cash-Anfangswert, Cash-Uebergaenge, Grundprofil. Zaehler ueber der Matrix. - Eigene Theme-Tokens fuer Hell/Dunkel/Warm -- ein fester Gelbwert waere im Dunkelschema unbrauchbar. Charts vergleichen neu die Geschwister-Szenarien statt fremder Plaene. Datenmodell/Migration: - Neue Tabelle Plan; bisheriger Plan -> Scenario (IDs erhalten, damit alle Kind-Fremdschluessel gueltig bleiben); planId -> scenarioId in Person/Phase/ FinancialElement. Bestehende Szenarien werden per rekursivem CTE demselben Behaelter zugeordnet, auch mehrfach verschachtelte. - Migration VOR dem Deploy gegen echtes PostgreSQL verifiziert (PGlite, in-process), inkl. verschachtelter Szenarien und Cascade. Der Test ist als migrations.test.ts committet und sichert kuenftige Migrationen ab. API neu unter /api/scenarios/*. 10 neue Tests (48 -> 58). Spezifikation auf v0.8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+165
-63
@@ -4,10 +4,10 @@
|
||||
| | |
|
||||
|---|---|
|
||||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||||
| **Version** | 0.7 |
|
||||
| **Datum** | 2026-07-17 |
|
||||
| **Version** | 0.8 |
|
||||
| **Datum** | 2026-07-18 |
|
||||
| **Status** | Lebendes Dokument |
|
||||
| **Codestand** | Arbeitsstand nach `5bf35bd` inkl. Monte-Carlo-Simulation (Stufe A) (Branch `main`) |
|
||||
| **Codestand** | Arbeitsstand nach `71137f7` inkl. Szenario-Hierarchie (V6) (Branch `main`) |
|
||||
| **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` |
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---|---|---|---|
|
||||
| 0.8 | 2026-07-18 | Claude (Opus 4.8) | **Szenario-Hierarchie (V6)** – grösste Umstrukturierung bisher. Der **Plan** ist neu ein schlanker Behälter ohne Finanzdaten; die berechenbare Einheit ist das **Szenario**, das Grundprofil (inkl. **Pensionsalter** → Frühpensionierungs-Szenarien), Phasen und Elemente trägt. Jeder Plan erhält beim Anlegen automatisch ein **Basisszenario**; weitere Szenarien entstehen als vollständige Kopie eines beliebigen Szenarios und hängen als **Baum** darunter (Sidebar zeigt die Verschachtelung). Kopierte Phasen/Elemente tragen Herkunfts-Verweise (`sourcePhaseId`, `sourceElementId`) – darauf beruht die **Abweichungs-Markierung**: geändert = gelb, neu = grün, entfernt = graue Geisterzeile (eigene Theme-Tokens für alle drei Farbschemata). Charts vergleichen neu die Geschwister-Szenarien. Datenmodell: neue Tabelle `Plan`, bisheriger `Plan` → `Scenario` (IDs erhalten), `planId` → `scenarioId` in Person/Phase/FinancialElement. API neu unter `/api/scenarios/*`. Neue Kapitel 2.1, 3.2, 4.13; 9 Diff-Tests + Migrations-Test (48 → 58). **Migration mit echtem Postgres (PGlite) verifiziert**, inkl. verschachtelter Szenarien und Cascade. |
|
||||
| 0.7 | 2026-07-17 | Claude (Opus 4.8) | **Monte-Carlo-Simulation** (Roadmap Nr. 19, Stufe A). Button in der Planansicht → Dialog mit Erklärung, Eingaben und Ergebnis. Statt einer festen Rendite/Inflation werden tausende Zufallspfade gerechnet; ausgewiesen werden **Ruinwahrscheinlichkeit**, **Erfolgswahrscheinlichkeit** (P(Endvermögen ≥ Zielbetrag)) und ein **Fächer** (10 %/Median/90 %) plus die deterministische Linie. Läuft komplett im Browser (`computePlan` ist rein). Modell: fettschwänzige Verteilung (Student-t, ν=5), gemeinsamer Marktschock (ρ=0.7), Böden 0 % für PK/3a und −100 % sonst. Pro renditetragendem Element und für die Inflation je: historischer Ø (Pflicht), Streuungsstufe, σ. Neue Datei `montecarlo.ts` + optionaler `sample`-Parameter in `computePlan` (Inflation neu als kumulatives Array; deterministisch identisch). Neues Kapitel 4.12; 7 MC-Tests + Refactor-Absicherung (41 → 48). Keine Verhaltensänderung, keine DB-Änderung. |
|
||||
| 0.6 | 2026-07-17 | Claude (Opus 4.8) | **Teilverkauf** von Sonstigem Vermögen (Roadmap Nr. 42) und **Sonderamortisation** der Hypothek (Roadmap Nr. 15). `OTHER_ASSET` am Übergang neu: Halten / Verkaufen / **Teilverkauf** – ein Betrag fliesst ins Cash (erscheint als „Kapitalzufluss" im Phasenkopf), der Rest bleibt investiert. `REAL_ESTATE` im Halten-Fall neu mit **Einmaltilgung** aus dem Cash (analog zur Sofort-Tilgung bei Schulden; erscheint als „Kapitalinvestition"). Damit lässt sich die indirekte Amortisation via 3a mechanisch nachbilden. Die eigentliche Roadmap Nr. 15 (Steuerwirkung der indirekten Amortisation) bleibt mit dem Steuer-Bündel 13/14/23 zurückgestellt – siehe 9.14. Kapitel 4.9.3/4.9.4 ergänzt. Fünf Regressionstests (36 → 41). Keine Verhaltensänderung für bestehende Pläne. |
|
||||
| 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. |
|
||||
@@ -86,26 +87,39 @@ Referenz: `prisma/schema.prisma` Zeilen 8–10, `src/lib/types.ts` Zeilen 38–4
|
||||
|
||||
# 2. Fachliche Grundkonzepte
|
||||
|
||||
## 2.1 Die vier Ebenen
|
||||
## 2.1 Die Ebenen
|
||||
|
||||
```
|
||||
User
|
||||
└── Plan (Grundprofil: Haushaltsform, Personen, Inflation, Cash-Startwert)
|
||||
├── Person[] (1 bei SINGLE, 2 bei COUPLE)
|
||||
├── Phase[] (geordnete Kette: sequenceNumber 1..n, je mit Dauer in Jahren)
|
||||
└── FinancialElement[] (plan-weit, kategorisiert, optional personenzugeordnet)
|
||||
├── ElementPhaseValue[] (Werte je Phase, JSON)
|
||||
└── ElementTransitionValue[] (Entscheide je Übergang, JSON)
|
||||
└── Plan (Behälter: nur Name — KEINE Finanzdaten)
|
||||
└── Scenario[] (die berechenbare Einheit)
|
||||
│ Grundprofil: Haushaltsform, Personen, Inflation, Cash-Startwert
|
||||
│ isBase = genau eines je Plan · parentScenarioId = Baum + Vergleichsbasis
|
||||
├── Person[] (1 bei SINGLE, 2 bei COUPLE — je Szenario eigen!)
|
||||
├── Phase[] (Kette 1..n; sourcePhaseId = Gegenstück in der Vorlage)
|
||||
└── FinancialElement[] (szenario-weit; sourceElementId = Gegenstück in der Vorlage)
|
||||
├── ElementPhaseValue[] (Werte je Phase, JSON)
|
||||
└── ElementTransitionValue[] (Entscheide je Übergang, JSON)
|
||||
```
|
||||
|
||||
Das zentrale Designprinzip (Element-Rework 07/2026, Migration `20260713100000_element_model_rework`):
|
||||
**Ein finanzielles Element ist über alle Lebensphasen hinweg dieselbe Entität.** Eine
|
||||
Pensionskasse "PK Arbeitgeber" existiert einmal pro Plan; sie hat pro Phase einen Werte-Satz und
|
||||
pro Phasenübergang einen Entscheid-Satz. Dadurch bleibt die Identität eines Vermögensgegenstands
|
||||
über die Zeit erhalten – Voraussetzung für die Fortschreibung (Carry) und für die
|
||||
Vermögensaufteilungs-Grafik.
|
||||
**Zwei Designprinzipien tragen dieses Modell:**
|
||||
|
||||
Referenz: `prisma/schema.prisma` Zeilen 1–10, 114–151.
|
||||
**(1) Ein finanzielles Element ist über alle Lebensphasen hinweg dieselbe Entität.** Eine
|
||||
Pensionskasse „PK Arbeitgeber" existiert einmal pro Szenario; sie hat pro Phase einen Werte-Satz
|
||||
und pro Phasenübergang einen Entscheid-Satz. Dadurch bleibt die Identität eines
|
||||
Vermögensgegenstands über die Zeit erhalten – Voraussetzung für die Fortschreibung (Carry) und
|
||||
die Vermögensaufteilungs-Grafik.
|
||||
|
||||
**(2) Das Grundprofil liegt am Szenario, nicht am Plan** (V6). Nur so lassen sich die
|
||||
wertvollsten Szenario-Fragen abbilden – allen voran ein abweichendes **Pensionsalter**
|
||||
(„Was, wenn ich mit 62 statt 65 aufhöre?"), das in `Person` steckt. Wäre das Profil geteilt,
|
||||
wären Frühpensionierungs-Szenarien unmöglich.
|
||||
|
||||
Der **Plan** ist damit ein reiner Behälter: Er bündelt Szenarien und trägt den Eigentümer
|
||||
(`userId`). Ownership von Szenario/Phase/Element läuft über die Kette
|
||||
`Scenario → Plan → User`.
|
||||
|
||||
Referenz: `prisma/schema.prisma`.
|
||||
|
||||
## 2.2 Die Matrix als Leitmetapher
|
||||
|
||||
@@ -263,21 +277,53 @@ Referenz: `prisma/schema.prisma` Zeile 82.
|
||||
|
||||
### 3.2.5 Szenarien
|
||||
|
||||
Ein Szenario ist eine **Deep-Copy eines Plans bis zu einer gewählten Verzweigungsphase (inklusive)**.
|
||||
Beim Anlegen eines Plans entsteht **automatisch das Basisszenario** (`isBase = true`,
|
||||
Name „Basisszenario"). Der beim Anlegen erfasste Profilteil (Haushaltsform, Personen,
|
||||
Inflation) landet dort, der Name am Plan.
|
||||
|
||||
Kopiert werden:
|
||||
- Grundprofil (Haushaltsform, Inflation, `initialCash`) und alle Personen
|
||||
- alle Phasen mit `sequenceNumber <= branchPhase.sequenceNumber`
|
||||
- **alle** Elemente (unabhängig von der Verzweigungsphase)
|
||||
- Phasenwerte nur für kopierte Phasen; Übergangswerte nur für Übergänge aus kopierten Phasen
|
||||
Ein weiteres Szenario ist eine **vollständige Kopie eines beliebigen bestehenden Szenarios**
|
||||
(nicht nur des Basisszenarios). Kopiert werden Grundprofil, alle Personen, alle Phasen, alle
|
||||
Elemente sowie sämtliche Phasen- und Übergangswerte.
|
||||
|
||||
Gesetzt werden `parentPlanId` (Ursprungsplan) und `branchFromPhaseId` (die **neue** ID der
|
||||
letzten kopierten Phase). Der Benutzer landet direkt im neuen Szenario, das anschliessend
|
||||
unabhängig weiterentwickelt wird.
|
||||
Gesetzt werden dabei:
|
||||
- `parentScenarioId` = das kopierte Szenario → ergibt den **Baum** in der Seitenleiste **und**
|
||||
die **Vergleichsbasis** für die Abweichungs-Markierung
|
||||
- `sourcePhaseId` / `sourceElementId` je kopierter Phase bzw. Element → die **Identität**, über
|
||||
die der Diff „dieselbe Zelle" wiederfindet
|
||||
|
||||
In der Planliste erscheint ein Szenario mit dem Zusatz „· Szenario".
|
||||
Da jede Kopie wieder kopierbar ist, entstehen **Sub-Szenarien** beliebiger Tiefe; die
|
||||
Seitenleiste rückt sie entsprechend ein.
|
||||
|
||||
Referenz: `src/app/api/plans/[planId]/scenario/route.ts`.
|
||||
**Löschen:** Ein Szenario lässt sich löschen, das **Basisszenario nicht** (dafür den ganzen Plan
|
||||
löschen). Das Löschen eines Plans entfernt per Cascade alle seine Szenarien.
|
||||
|
||||
Referenz: `src/app/api/scenarios/[scenarioId]/copy/route.ts`.
|
||||
|
||||
### 3.2.6 Abweichungs-Markierung (Diff)
|
||||
|
||||
Jedes Szenario ausser dem Basisszenario wird **live gegen sein Eltern-Szenario** verglichen.
|
||||
Abweichende Stellen sind in der Matrix farblich markiert:
|
||||
|
||||
| Zustand | Darstellung |
|
||||
|---|---|
|
||||
| **geändert** | gelb hinterlegt (`--diff` / `--diff-soft`) |
|
||||
| **neu** (in der Vorlage nicht vorhanden) | grün hinterlegt + Badge „neu" (`--diff-added`) |
|
||||
| **entfernt** (in der Vorlage vorhanden, hier gelöscht) | graue **Geisterzeile**, durchgestrichen (`--diff-removed`) |
|
||||
|
||||
Markiert werden: Phasenzellen, Übergangszellen, Element-Zeilen (Name/Zuordnung), Phasenköpfe
|
||||
(Name/Dauer), der Cash-Anfangswert, Cash-Übergänge und das Grundprofil-Banner. Über der Matrix
|
||||
steht die Gesamtzahl der Abweichungen.
|
||||
|
||||
Die Farben sind **semantische Tokens** und für Hell, Dunkel und Warm getrennt abgestimmt – ein
|
||||
fester Gelbwert würde im Dunkelschema unbrauchbar aussehen.
|
||||
|
||||
**Zwei bewusste Eigenschaften:**
|
||||
- Der Vergleich läuft gegen das **direkte Eltern-Szenario**, nicht immer gegen die Basis. Bei
|
||||
einem Sub-Szenario ist das genau „was habe ich gegenüber der Vorlage geändert".
|
||||
- Der Diff ist **live**: Ändert man die Vorlage, verschiebt sich die Markierung im Kind
|
||||
rückwirkend (siehe [9.16](#916-diff-ist-live-gegen-die-vorlage)).
|
||||
|
||||
Referenz: `src/lib/diff.ts`.
|
||||
|
||||
## 3.3 Lebensphasen
|
||||
|
||||
@@ -1618,25 +1664,34 @@ PlanComputed ← an den Client geliefert
|
||||
| Feld | Typ | Constraints |
|
||||
|---|---|---|
|
||||
| `id` | String | PK, `cuid()` |
|
||||
| `planId` | String | FK → Plan, **Cascade** |
|
||||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||||
| `role` | `PersonRole` | `PERSON_A` \| `PERSON_B` |
|
||||
| `name` | String? | optional |
|
||||
| `age` | Int | aktuelles Alter |
|
||||
| `retirementAge` | Int | plan-eigenes Pensionsalter |
|
||||
| | | `@@unique([planId, role])` |
|
||||
| `retirementAge` | Int | **szenario-eigenes** Pensionsalter |
|
||||
| | | `@@unique([scenarioId, role])` |
|
||||
|
||||
**Plan**
|
||||
**Plan** (Behälter – trägt keine Finanzdaten)
|
||||
|
||||
| Feld | Typ | Constraints |
|
||||
|---|---|---|
|
||||
| `id` | String | PK, `cuid()` |
|
||||
| `userId` | String | FK → User, **Cascade** |
|
||||
| `name` | String | |
|
||||
| `createdAt` / `updatedAt` | DateTime | |
|
||||
|
||||
**Scenario** (die berechenbare Einheit)
|
||||
|
||||
| Feld | Typ | Constraints |
|
||||
|---|---|---|
|
||||
| `id` | String | PK, `cuid()` |
|
||||
| `planId` | String | FK → Plan, **Cascade** |
|
||||
| `name` | String | |
|
||||
| `isBase` | Boolean | Default false; genau eines je Plan ist `true` |
|
||||
| `parentScenarioId` | String? | FK → Scenario (Self-Relation „ScenarioTree"), **SetNull** – Baum **und** Vergleichsbasis |
|
||||
| `householdType` | `HouseholdType` | `SINGLE` \| `COUPLE` |
|
||||
| `inflationRateDefault` | Float | plan-weite Inflation in % |
|
||||
| `inflationRateDefault` | Float | szenario-weite Inflation in % |
|
||||
| `initialCash` | Float | Default 0 |
|
||||
| `parentPlanId` | String? | FK → Plan (Self-Relation „PlanScenarios"), **SetNull** |
|
||||
| `branchFromPhaseId` | String? | ID der letzten kopierten Phase (**keine** FK-Constraint) |
|
||||
| `createdAt` / `updatedAt` | DateTime | |
|
||||
|
||||
**Phase**
|
||||
@@ -1644,13 +1699,14 @@ PlanComputed ← an den Client geliefert
|
||||
| Feld | Typ | Constraints |
|
||||
|---|---|---|
|
||||
| `id` | String | PK, `cuid()` |
|
||||
| `planId` | String | FK → Plan, **Cascade** |
|
||||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||||
| `sequenceNumber` | Int | 1-basiert, lückenlos |
|
||||
| `name` | String | |
|
||||
| `durationYears` | Int | 1–80 |
|
||||
| `cashTransition` | Json? | Cash-Entscheid beim Übergang **nach** dieser Phase (siehe 5.4.5) |
|
||||
| `sourcePhaseId` | String? | Gegenstück in der Vorlage (**lose** Referenz, kein FK) – Diff-Grundlage |
|
||||
| `createdAt` / `updatedAt` | DateTime | |
|
||||
| | | `@@unique([planId, sequenceNumber])` |
|
||||
| | | `@@unique([scenarioId, sequenceNumber])` |
|
||||
|
||||
`cashTransition` liegt an der Phase und nicht in `ElementTransitionValue`, weil Cash kein
|
||||
`FinancialElement` ist und damit keine `elementId` besitzt. Die Verschlüsselung folgt derselben
|
||||
@@ -1661,13 +1717,18 @@ Logik wie dort: Der Übergang gehört der **Von**-Phase.
|
||||
| Feld | Typ | Constraints |
|
||||
|---|---|---|
|
||||
| `id` | String | PK, `cuid()` |
|
||||
| `planId` | String | FK → Plan, **Cascade** |
|
||||
| `scenarioId` | String | FK → Scenario, **Cascade** |
|
||||
| `category` | `ElementCategory` | 8 Werte |
|
||||
| `name` | String | |
|
||||
| `ownerRole` | `OwnerRole?` | `PERSON_A` \| `PERSON_B` \| `HOUSEHOLD` |
|
||||
| `orderIndex` | Int | Default 0 |
|
||||
| `sourceElementId` | String? | Gegenstück in der Vorlage (**lose** Referenz, kein FK) – Diff-Grundlage |
|
||||
| `createdAt` | DateTime | |
|
||||
|
||||
Die Herkunfts-Verweise sind bewusst **lose** (kein Fremdschlüssel): Wird das Gegenstück in der
|
||||
Vorlage gelöscht, soll die Kopie bestehen bleiben und im Diff einfach als „neu" gelten – ein
|
||||
Cascade wäre hier falsch.
|
||||
|
||||
**ElementPhaseValue**
|
||||
|
||||
| Feld | Typ | Constraints |
|
||||
@@ -1779,6 +1840,18 @@ sondern zu leeren Werten.
|
||||
| `20260715120000_plan_initial_cash` | `Plan.initialCash` |
|
||||
| `20260716210000_drop_phase_inflation_rate` | `Phase.inflationRate` entfernt (Inflation ist plan-weit) |
|
||||
| `20260716230000_phase_cash_transition` | `Phase.cashTransition` (JSONB) für einmalige Sonderein-/ausgaben |
|
||||
| `20260718090000_plan_scenario_hierarchy` | **V6**: `Plan` → `Scenario` (IDs erhalten), neuer Behälter `Plan`, `planId` → `scenarioId`, Herkunfts-Verweise |
|
||||
|
||||
**Zur V6-Migration:** Sie benennt die bisherige `Plan`-Tabelle in `Scenario` um – dadurch
|
||||
bleiben alle IDs und damit sämtliche Kind-Fremdschlüssel gültig. Für jedes bisherige
|
||||
Wurzel-Element entsteht ein neuer Behälter (`'plan_' || id`, deterministisch ableitbar, daher
|
||||
ohne Hilfstabelle); der bisherige Plan-Name wandert dorthin, das Szenario heisst „Basisszenario".
|
||||
Bestehende Szenarien werden per rekursivem CTE demselben Behälter zugeordnet – auch mehrfach
|
||||
verschachtelte. Ein Sicherheitsnetz fängt verwaiste Szenarien ab und macht sie eigenständig.
|
||||
|
||||
Die Migration wurde **vor dem Deploy gegen echtes PostgreSQL verifiziert** (PGlite, in-process):
|
||||
alle Vorgänger-Migrationen einspielen, realistische Daten inkl. verschachtelter Szenarien
|
||||
anlegen, migrieren, Ergebnis und Cascade prüfen.
|
||||
|
||||
## 5.5 Frontend-Architektur
|
||||
|
||||
@@ -1893,39 +1966,54 @@ Liste der eigenen Pläne, sortiert nach `createdAt` aufsteigend.
|
||||
```
|
||||
Validierung: `name` 1–120; `inflationRateDefault` −20…50; `persons` 1–2 Einträge;
|
||||
`age` 0–120; `retirementAge` 30–100; `name` je Person ≤ 60. Zusätzlich Konsistenzregel
|
||||
SINGLE=1 / COUPLE=2 Personen. → 201 `{ plan: { id } }`
|
||||
SINGLE=1 / COUPLE=2 Personen. Legt Plan **und Basisszenario** an.
|
||||
→ 201 `{ plan: { id }, scenario: { id } }`
|
||||
|
||||
### `GET /api/plans/<planId>`
|
||||
Liefert **Eingabe und Berechnung** in einem Zug:
|
||||
`GET /api/plans` liefert die Pläne inkl. Szenario-Kopfdaten:
|
||||
```json
|
||||
{ "plan": <PlanInput>, "computed": <PlanComputed> }
|
||||
{ "plans": [ { "id", "name", "createdAt",
|
||||
"scenarios": [ { "id", "planId", "name", "isBase", "parentScenarioId" } ] } ] }
|
||||
```
|
||||
Dies ist der einzige Endpunkt, der die Berechnung ausführt (neben `export`). → 404 wenn fremd.
|
||||
|
||||
### `PATCH /api/plans/<planId>`
|
||||
`{ name }` – der Plan trägt nur noch den Namen. → 200 `{ plan: { id, name } }`
|
||||
|
||||
### `DELETE /api/plans/<planId>`
|
||||
→ 200 `{ ok: true }`, Cascade über alle Szenarien.
|
||||
|
||||
## 6.3 Szenarien
|
||||
|
||||
### `GET /api/scenarios/<scenarioId>`
|
||||
Liefert Eingabe, Berechnung **und die Vergleichsbasis** in einem Zug:
|
||||
```json
|
||||
{ "plan": <PlanInput>, "computed": <PlanComputed>,
|
||||
"base": <PlanInput|null>,
|
||||
"meta": { "id", "planId", "planName", "name", "isBase", "parentScenarioId" } }
|
||||
```
|
||||
`base` ist das Eltern-Szenario (null beim Basisszenario) – daraus rechnet der Client den Diff.
|
||||
Dies ist der einzige Endpunkt, der die Berechnung ausführt (neben `export`). → 404 wenn fremd.
|
||||
|
||||
### `PATCH /api/scenarios/<scenarioId>`
|
||||
Akzeptiert eine **Union** von zwei Formen:
|
||||
1. Vollständiges Profil: `{ householdType, inflationRateDefault, persons[], name? }` – ersetzt
|
||||
die Personen in einer Transaktion.
|
||||
2. Teilaktualisierung: `{ name?, initialCash? }` – `initialCash` 0…1'000'000'000, gerundet.
|
||||
|
||||
Die Unterscheidung erfolgt über das Vorhandensein von `householdType`.
|
||||
→ 200 `{ plan: { id, name } }`
|
||||
→ 200 `{ scenario: { id, name } }`
|
||||
|
||||
### `DELETE /api/plans/<planId>`
|
||||
→ 200 `{ ok: true }`, Cascade-Löschung.
|
||||
### `DELETE /api/scenarios/<scenarioId>`
|
||||
→ 200 `{ ok: true }` · 400 wenn es das **Basisszenario** ist.
|
||||
|
||||
### `POST /api/plans/<planId>/scenario`
|
||||
```json
|
||||
{ "name": "Frühpensionierung", "branchFromPhaseId": "<phaseId>" }
|
||||
```
|
||||
→ 201 `{ planId: "<neue Id>" }`
|
||||
### `POST /api/scenarios/<scenarioId>/copy`
|
||||
`{ name }` – vollständige Kopie; setzt `parentScenarioId` sowie die Herkunfts-Verweise.
|
||||
→ 201 `{ scenarioId: "<neue Id>" }`
|
||||
|
||||
### `GET /api/plans/<planId>/export`
|
||||
### `GET /api/scenarios/<scenarioId>/export`
|
||||
→ `text/csv; charset=utf-8`, `Content-Disposition: attachment`.
|
||||
|
||||
## 6.3 Phasen
|
||||
## 6.4 Phasen
|
||||
|
||||
### `POST /api/plans/<planId>/phases`
|
||||
### `POST /api/scenarios/<scenarioId>/phases`
|
||||
Body optional: `{ name?, durationYears? }`.
|
||||
Hängt eine Phase am Ende an, kappt die Dauer, vergibt Default-Name, legt vorbelegte
|
||||
`ElementPhaseValue` für alle aktiven Elemente an (alles in einer Transaktion).
|
||||
@@ -1943,9 +2031,9 @@ Body = `CashTransitionData` (siehe 5.4.5). Speichert den Cash-Entscheid für den
|
||||
dieser Phase (einmalige Sonderein-/ausgaben). Schreibt die Spalte `Phase.cashTransition`.
|
||||
→ 200 `{ ok }` · 404 wenn die Phase nicht dem Benutzer gehört.
|
||||
|
||||
## 6.4 Elemente
|
||||
## 6.5 Elemente
|
||||
|
||||
### `POST /api/plans/<planId>/elements`
|
||||
### `POST /api/scenarios/<scenarioId>/elements`
|
||||
`{ category, name, ownerRole? }` → 201 `{ element: { id } }`
|
||||
- `PERSON_ONLY_CATEGORIES` ohne Person → 400
|
||||
- fehlendes `ownerRole` sonst → `HOUSEHOLD`
|
||||
@@ -2024,7 +2112,7 @@ Zielumgebung: Hetzner CX23, Traefik als Reverse Proxy, Domain `fpt.aicds.ch`, Gi
|
||||
Getestet wird ausschliesslich der Berechnungskern – bewusst, da dort die Fachlogik und das
|
||||
Regressionsrisiko liegen. `src/lib/calculations.test.ts` (41 Tests: AHV-Rentenformel, Immobilie,
|
||||
Teilverkauf, Sonderamortisation, AHV einkommensabhängig, „V5 Golden Tests") und
|
||||
`src/lib/montecarlo.test.ts` (7 Tests) ergeben zusammen **48 Tests**, ausgeführt mit Vitest in
|
||||
`src/lib/montecarlo.test.ts` (7 Tests), `src/lib/diff.test.ts` (9 Tests) und `src/lib/migrations.test.ts` (1 Test, spielt alle Migrationen gegen echtes PostgreSQL ein) ergeben zusammen **58 Tests**, ausgeführt mit Vitest in
|
||||
der Node-Umgebung (`vitest.config.ts`, Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-,
|
||||
API- oder E2E-Tests.
|
||||
|
||||
@@ -2098,10 +2186,10 @@ Negatives Cash geht mit negativem Vorzeichen ins Gesamtvermögen ein.
|
||||
|
||||
## 9.2 Verwaiste `PERSON_B`-Elemente
|
||||
|
||||
Die Haushaltsform ist eine **Plan-Eigenschaft** und lässt sich im Dialog „Plan-Einstellungen"
|
||||
auch bei einem bestehenden Plan nachträglich ändern (nicht pro Phase – innerhalb eines Plans
|
||||
gilt sie durchgehend). Wechselt ein Plan dabei von `COUPLE` auf `SINGLE`, schneidet
|
||||
`PlanProfileFields` die Personen auf eine zusammen und `PATCH /api/plans/<id>` löscht Person B
|
||||
Die Haushaltsform ist eine **Szenario-Eigenschaft** und lässt sich im Dialog „Plan-Einstellungen"
|
||||
auch bei einem bestehenden Szenario nachträglich ändern (nicht pro Phase – innerhalb eines Szenarios
|
||||
gilt sie durchgehend). Wechselt ein Szenario dabei von `COUPLE` auf `SINGLE`, schneidet
|
||||
`PlanProfileFields` die Personen auf eine zusammen und `PATCH /api/scenarios/<id>` löscht Person B
|
||||
aus der Datenbank. Elemente mit `ownerRole = "PERSON_B"` bleiben bestehen. In der Berechnung
|
||||
liefert `personByRole` dann `null`:
|
||||
|
||||
@@ -2247,7 +2335,21 @@ angeboten (fette Ränder fest eingebaut); die Simulationsparameter werden **nich
|
||||
(ephemer im Dialog). Ein historischer Backtest (Stufe B) und korrelierte/vollständigere Modelle
|
||||
(Stufe C) sind offen.
|
||||
|
||||
## 9.16 Kleinere Beobachtungen
|
||||
## 9.16 Diff ist live gegen die Vorlage
|
||||
|
||||
Die Abweichungs-Markierung vergleicht **immer den aktuellen Stand** des Eltern-Szenarios. Ändert
|
||||
man die Vorlage nachträglich, verschiebt sich die Markierung in allen Kindern rückwirkend: Setzt
|
||||
man in der Vorlage einen Wert auf das, was ein Szenario ohnehin hatte, verschwindet dort die
|
||||
gelbe Markierung, ohne dass das Szenario angefasst wurde.
|
||||
|
||||
Das ist logisch korrekt („weicht ab von der Vorlage"), kann aber überraschen. Die Alternative –
|
||||
ein eingefrorener Snapshot beim Kopieren – wäre schnell veraltet und würde Abweichungen anzeigen,
|
||||
die keine mehr sind. Bewusster Entscheid zugunsten des Live-Vergleichs.
|
||||
|
||||
Verwandt: Wird ein Element in der Vorlage gelöscht, gilt das Gegenstück im Kind ab dann als
|
||||
**neu** (der Herkunfts-Verweis zeigt ins Leere). Auch das folgt aus dem Live-Vergleich.
|
||||
|
||||
## 9.17 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
|
||||
|
||||
Reference in New Issue
Block a user