PDF-Berichte (Roadmap 11)
Deploy App / deploy (push) Successful in 3m5s

Neuer Unterpunkt "Berichte" je Plan: Liste plus Assistent (Titel, Notiz,
nominal ODER real, Plan-/Ist-Daten, bis zu drei Szenarien, gespeicherte
Analysen). Layout immer gleich, Auswahl bestimmt nur die Bausteine.

Die PDF-Datei wird ALS DATEI abgelegt (BYTEA in Postgres, nicht im
Container-Dateisystem): Ein Bericht muss in drei Jahren byte-identisch
wieder herunterladbar sein -- eine Neuerzeugung koennte das nach
Aenderungen an Plan, Rechenkern oder Layout nicht garantieren.

Kennzahlen je Szenario inkl. offener Entscheide. Deren Zaehlung liegt neu
als reine Funktion in decisions.ts, die Matrix UND Bericht benutzen --
sonst nennen beide verschiedene Zahlen.

Zu jeder Kennzahl ihre Grundlage als Verweis; die vollstaendigen Annahmen
einmal je Szenario. Haftungsausschluss ist verpflichtend (per Test).

Technik: pdfkit in der Node-Runtime statt Headless-Browser;
@react-pdf/renderer bricht mit React 19. Als externes Paket deklariert,
weil pdfkit Font-Metriken ueber Dateipfade laedt.

Spezifikation 0.25 (3.11 neu), 9 Tests (212 -> 221).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 08:30:18 +02:00
parent 489390a4a3
commit d9ef980edf
17 changed files with 1988 additions and 47 deletions
+114 -3
View File
@@ -4,7 +4,7 @@
| | |
|---|---|
| **Dokument** | Funktionale und Technische Spezifikation FPT |
| **Version** | 0.24 |
| **Version** | 0.25 |
| **Datum** | 2026-07-18 |
| **Status** | Lebendes Dokument |
| **Codestand** | Arbeitsstand nach `1d046e9` inkl. zwei Monte-Carlo-Fragestellungen (Branch `main`) |
@@ -17,6 +17,7 @@
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 0.25 | 2026-07-21 | Claude (Opus 4.8) | **PDF-Berichte** (Roadmap Nr. 11, neues Kapitel 3.11). Neuer Unterpunkt **«Berichte»** auf Plan-Ebene: Liste der erzeugten Berichte plus Assistent zum Anlegen (Titel, Notiz, nominal **oder** real, Plan- oder effektive Daten, bis zu **drei** Szenarien, beliebige gespeicherte Analysen). Das **Layout ist immer gleich**; die Auswahl bestimmt nur, welche Bausteine erscheinen: Deckblatt mit Zusammenfassung und drei Kernaussagen, dann je Szenario Kennzahlen, Vermögensverlauf, Lebensphasen und Annahmen, danach Vergleich, Plan/Ist, Analysen und die Hinweise. **Die PDF-Datei wird als Datei abgelegt** (BYTEA in Postgres, nicht im Container-Dateisystem, das jeder Deploy neu baut): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein eine Neuerzeugung könnte das nach Änderungen an Plan, Rechenkern oder Layout nicht garantieren. **Kennzahlen je Szenario:** Endvermögen, Kapitalreichweite, Vermögen und Vorsorgekapital bei Pensionierung, AHV- und PK-Rente sowie die **offenen Entscheide** die einzige unmittelbar handlungsleitende Zahl. Damit Bericht und Matrix nie verschiedene Zahlen nennen, liegt deren Zählung neu als reine Funktion in `decisions.ts`, die beide benutzen. **Zu jeder Kennzahl steht ihre Grundlage** als kurzer Verweis; die vollständigen Annahmen (Startwerte, Renditen, Raten je Element) stehen **einmal** je Szenario, statt bei jeder Kennzahl wiederholt zu werden. Ein **Haftungsausschluss** ist verpflichtend und durch einen Test gesichert ein formal gesetztes PDF wird sonst als Beratung gelesen. **Technik:** `pdfkit` in der Node-Runtime statt Headless-Browser (kein Chromium im Image); `@react-pdf/renderer` schied aus, weil es mit React 19 / Next 16 bricht. Diagramme entstehen als **echte Vektoren** aus den gespeicherten Zahlen genau dafür wurden die Analysen in 0.24 als Zahlen und nicht als Bilder abgelegt. `pdfkit` ist als externes Paket deklariert, weil es Font-Metriken über Dateipfade lädt und gebündelt erst in der Produktion bräche. Neue Tabelle `Report`, Endpunkte unter `/api/plans/<id>/reports`, neue Module `report.ts`, `report-pdf.ts`, `decisions.ts`; 9 Tests ergänzt (212 → 221). |
| 0.24 | 2026-07-20 | Claude (Opus 4.8) | **Navigation auf Plan-Ebene und gespeicherte Analysen** (neues Kapitel 3.10). Die Seitenleiste ist neu zweistufig: Unter jedem Plan liegen die Unterpunkte **Szenarien**, **Effektive Werte** und **Analysen**; ein Klick auf den Plan-Namen öffnet ein **Plan-Dashboard** (Kennzahlen Haushaltsdaten direkt, gerechnete Werte ausdrücklich «laut Basisszenario», dazu die Ist-Abweichung, falls erfasst). Die **Szenario-Liste** zeigt je Szenario Version, Elementzahl, Endvermögen und Ruinalter mit den Aktionen Historie und Matrix; das Basisszenario ist hervorgehoben, der Szenario-Baum in der Seitenleiste bleibt daneben erhalten. Die **Analysen-Ansicht** bietet vier umklappende Kacheln (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren Umklappen auch per Antippen für Touch). **Grafiken** öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst **eines** wählen; der Szenario-Vergleich zieht mit zu den Grafiken, der CSV-Export auf die Matrix. **Gespeicherte Analysen:** Jede Grafik, MC-Simulation und Einflussfaktoren-Berechnung kann festgehalten werden als **Zahlen, nicht als Bild** (read-only, es wird nichts neu gerechnet). Das hält den Datensatz klein und macht ihn druckfähig: Der spätere PDF-Bericht (Roadmap Nr. 11) zeichnet daraus vektoriell in Druckauflösung, was ein Screenshot nicht könnte. Bewusst **nicht** gespeichert wird `finalWealthSorted` (megabyteweise). Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form, sodass eine Nur-Lese-Ansicht genügt. Neue Tabelle `SavedAnalysis`, neue Endpunkte unter `/api/plans/<id>/analyses` und `/dashboard`; neue Komponenten `PlanViews`, `SavedAnalysisView`, `SaveAnalysisButton`, neues Modul `analyses.ts`. Kein Eingriff in den Rechenkern; Testbestand 212 (Migration um `SavedAnalysis` erweitert). |
| 0.23 | 2026-07-20 | Claude (Opus 4.8) | **V7: Der Haushalt liegt am Plan, die Annahmen am Szenario.** **Haushaltsform**, **Personen** (Name, Alter) und **Planstartjahr** wandern vom Szenario auf den **Plan**; das **Pensionsalter** bleibt szenario-eigen (es ist der Kern jedes Früh-/Spätpensionierungs-Szenarios), ebenso Inflation und Cash-Anfangswert. Begründung: Diese Angaben beschreiben den Haushalt, nicht eine Planungsvariante unterscheiden sie sich, ist es ein anderer **Plan**, kein anderes Szenario. Neue Tabelle `PlanPerson` (Rolle, Name, Alter je Plan); `Person` behält nur noch Rolle und Pensionsalter; `Plan` bekommt `householdType` und `startYear`. **Der Rechenkern bleibt unberührt:** `toPlanInput()` fügt Plan- und Szenario-Ebene wieder zu einem unveränderten `PlanInput` zusammen, die 43 Golden Tests laufen durch. **Nebeneffekt, der ein reales Problem löst:** Weil das Startjahr nun plan-weit ist, landet ein erfasster Ist-Satz für 2031 in **allen** Szenarien zwingend auf demselben Planjahr vorher war das nicht garantiert. Das **Wiederherstellen einer Szenario-Version** setzt folgerichtig nur noch das Szenario-Eigene zurück; plan-weite Angaben über eine Version *eines* Szenarios zu überschreiben, hätte die übrigen stillschweigend mitverändert. Der Profil-Dialog kennzeichnet neu je Feld, ob es **plan-weit** oder **nur dieses Szenario** gilt. Die Migration übernimmt die Werte aus dem **Basisszenario**; zwei neue Tests spielen dafür echte V6-Daten ein und prüfen die Übernahme inkl. abweichender Nebenszenarien (210 → 212). |
| 0.22 | 2026-07-20 | Claude (Opus 4.8) | **Fehlerbehebung: Cash-Vorbelegung im Ist-Wizard.** Der Wizard für die effektiven Werte zeigte als geplanten Cash-Bestand den Stand am **Phasenende** statt am gewählten Stichtag -- in einer Phase von 2026 bis 2036 also für 2031 den Wert von 2036. Ursache: Der Dialog las `cashBridge.cashEnd`, weil `computePlan` den Cash-Bestand bisher nur **je Phase** auswies. `YearPoint` trägt neu ein Feld `cash` (Stand am Jahresende), analog zu `wealthNominal`; der Wizard liest daraus. Die Vorbelegung der ELEMENTE war nie betroffen -- die stammte schon immer aus dem Jahresverlauf. Zwei Regressionstests decken den gemeldeten Fall ab (208 -> 210). Rein additiv, die 43 Golden Tests laufen unverändert. |
@@ -1545,6 +1546,111 @@ Version, und Löschen entfernt ihn ersatzlos.
Referenz: `src/components/PlanViews.tsx`, `src/components/SavedAnalysisView.tsx`,
`src/components/SaveAnalysisButton.tsx`, `src/lib/analyses.ts`.
## 3.11 PDF-Berichte
Roadmap Nr. 11. Neuer Unterpunkt **«Berichte»** auf Plan-Ebene: eine Liste der erzeugten
Berichte, von dort aus lässt sich ein neuer anlegen.
### 3.11.1 Was ein Bericht ist
Ein Bericht ist ein **festes Dokument**, kein Blick auf den aktuellen Stand. Die erzeugte
PDF-Datei wird **als Datei abgelegt** und lässt sich jederzeit unverändert wieder
herunterladen auch nachdem der Plan weiterentwickelt wurde. Das ist der Zweck: ein
verlässlicher Audit-Trail. Wer heute einen Bericht verschickt, muss in drei Jahren exakt
dasselbe Dokument vorweisen können.
Deshalb wird **nicht** die Definition gespeichert und das PDF bei Bedarf neu erzeugt: Eine
Neuerzeugung könnte nach Änderungen an Plan, Rechenkern oder Layout nicht mehr dieselben
Bytes liefern.
Abgelegt werden drei Dinge: die gewählten **Parameter**, das eingefrorene **Berichtsmodell**
(alle Zahlen, für die Liste und zur Nachvollziehbarkeit) und die **PDF-Datei** selbst. In der
Datenbank, nicht im Dateisystem der Anwendungscontainer wird bei jedem Deploy neu gebaut,
nur das Datenbank-Volume überlebt.
### 3.11.2 Auswahl beim Anlegen
| Angabe | Möglichkeiten |
|---|---|
| Titel und Notiz | frei; die Notiz erscheint auf dem Deckblatt |
| **Werte** | nominal **oder** real eine Leitgrösse für den ganzen Bericht |
| **Grundlage** | Plandaten oder **effektive Werte** (nur wenn erfasst) |
| **Szenarien** | 1 bis **3** (Konstante `MAX_REPORT_SCENARIOS`) |
| **Analysen** | beliebig viele der gespeicherten Auswertungen |
Zwei bewusste Begrenzungen:
**Nur eine Leitgrösse.** «Beides» würde jede Tabellenspalte verdoppeln dasselbe Problem wie
in der Matrix ([9.29](#929-acht-zahlen-je-zelle--und-wie-wir-sie-vermeiden)).
**Höchstens drei Szenarien.** Darüber wird die Vergleichstabelle unlesbar; vergleichbare
Werkzeuge stellen bewusst nur zwei gegenüber. Das Basisszenario führt den Bericht an und
liefert die Zusammenfassung.
### 3.11.3 Aufbau des Dokuments
Das Layout ist **immer gleich**; die Auswahl bestimmt nur, welche Bausteine erscheinen.
| Seite | Inhalt |
|---|---|
| 1 | Deckblatt · **Das Wichtigste in Kürze** (Kennzahlen + drei Kernaussagen) · Ausgangslage |
| je Szenario | Kennzahlen · Vermögensverlauf · Lebensphasen · **Annahmen** |
| bei ≥ 2 Szenarien | Vergleichstabelle |
| bei Ist-Daten | Plan gegenüber effektiven Werten inkl. Abweichung |
| bei gewählten Analysen | die eingefrorenen Auswertungen |
| Schluss | **Wichtige Hinweise** (Haftungsausschluss) |
Die **Zusammenfassung zuerst** ist kein Geschmacksentscheid: Überkomplexität ist die
häufigste Kritik an Beraterberichten, weshalb sich die einseitige Übersicht als eigenes
Format etabliert hat.
### 3.11.4 Kennzahlen und ihre Grundlage
Je Szenario: **Endvermögen** · **Kapital reicht bis** · **Vermögen bei Pensionierung** ·
**Vorsorgekapital bei Pensionierung** (PK + 3a) · **AHV-Rente** · **PK-Rente** ·
**offene Entscheide**.
Die letzte ist die einzige unmittelbar handlungsleitende Zahl sie zählt die noch nicht
getroffenen Übergangs-Entscheide. Damit Bericht und Matrix nie verschiedene Zahlen nennen,
liegt die Zählung neu als reine Funktion in `lib/decisions.ts`, die **beide** benutzen.
**Zu jeder Kennzahl steht ihre Grundlage** aber als **kurzer Verweis**, nicht als
wiederholte Tabelle. Die vollständigen Annahmen (Inflation, Pensionsalter, Startwerte,
Renditen, Sparraten je Element) stehen **einmal** je Szenario in einem eigenen Abschnitt.
Sie bei jeder Kennzahl zu wiederholen würde den Bericht aufblähen, ohne etwas hinzuzufügen.
### 3.11.5 Haftungsausschluss
Ein formal gesetztes PDF wird als Beratung gelesen. Der Bericht schliesst deshalb
**verpflichtend** mit Hinweisen: dass es sich um eine Projektion auf Basis eigener Annahmen
handelt, keine Anlage-, Steuer- oder Vorsorgeberatung; dass Abweichungen erheblich sein
können; dass laufende Einkommens- und Vermögenssteuern **nicht** modelliert sind
([9.14](#914-keine-steuerschätzung)); und dass Monte-Carlo-Wahrscheinlichkeiten die Streuung
**um** die Annahmen messen, nicht deren Richtigkeit. Ein Test stellt sicher, dass der
Abschnitt nicht wegfallen kann.
### 3.11.6 Technik
Erzeugt wird mit **pdfkit** in der Node-Runtime. Bewusst **kein Headless-Browser**: Chromium
würde das Image um Hunderte Megabyte und Systembibliotheken aufblähen. `@react-pdf/renderer`
schied aus, weil es React-Interna nutzt, die mit React 19 / Next 16 brechen.
Weil das Layout ohnehin fix ist, kostet programmatisches Setzen nichts und die Diagramme
entstehen als **echte Vektoren** aus den gespeicherten Zahlen. Genau dafür wurden die
gespeicherten Analysen als Zahlen und nicht als Bilder abgelegt
([3.10.3](#3103-gespeicherte-analysen)): Ein Bildschirm-Abbild wäre im Druck unscharf.
`pdfkit` ist in `next.config.ts` als **externes Paket** deklariert. Es lädt seine
Font-Metriken zur Laufzeit über Dateipfade; gebündelt stimmt `__dirname` nicht mehr und der
Bericht bräche erst in der Produktion.
Die Trennung ist zweistufig: `report.ts` baut aus Plan und Konfiguration ein reines
**Berichtsmodell** (ohne PDF-Kenntnisse, testbar), `report-pdf.ts` zeichnet es. Damit ist
das, was eingefroren wird, dasselbe, was geprüft wird.
Referenz: `src/lib/report.ts`, `src/lib/report-pdf.ts`, `src/lib/decisions.ts`,
`src/components/ReportsView.tsx`.
---
# 4. Berechnungsmodell
@@ -2747,7 +2853,7 @@ Referenz: `src/lib/livesim.ts`, `src/lib/sensitivity.ts` (`applyElementDriver`,
FPT/
├── prisma/
│ ├── schema.prisma Datenmodell
│ └── migrations/ 16 Migrationen (chronologisch, siehe 5.4.6)
│ └── migrations/ 17 Migrationen (chronologisch, siehe 5.4.6)
├── src/
│ ├── app/
│ │ ├── api/ Route Handlers (siehe Kapitel 6)
@@ -2789,6 +2895,9 @@ PlanComputed ← an den Client geliefert
| `montecarlo.ts` | Monte-Carlo-Simulation (Sampler + Treiber), Szenario-Vergleich und Element-Gruppierung über die Herkunfts-Kette. Keine I/O, läuft im Browser. |
| `sensitivity.ts` | Sensitivitätsanalyse / Tornado: Treiber-Katalog, Parameter-Transformationen, `computeTornado`; zusätzlich `applyElementDriver` / `tunableElements` für die einzeln regelbaren Element-Renditen. Rein, läuft im Browser. |
| `actuals.ts` | Effektive Werte: Zuordnung auf die Szenario-Elemente über die Herkunfts-Kette, Einspielen in den Rechenkern, Bestand/Fluss. Rein. |
| `report.ts` | Berichtsmodell: Kennzahlen, Annahmen, Vergleich, Plan/Ist -- rein, ohne PDF-Kenntnisse (Kap. 3.11). |
| `report-pdf.ts` | Zeichnet das Berichtsmodell mit pdfkit. Nur serverseitig. |
| `decisions.ts` | Offene Übergangs-Entscheide -- von Matrix UND Bericht benutzt, damit beide dieselbe Zahl nennen. |
| `analyses.ts` | Gespeicherte Analysen: Typen, generische Ergebnis-Form, Speicher-Helfer (Kap. 3.10). |
| `dataview.ts` | Bündelt Plan-Sicht und Ist-Sicht für Matrix, Grafiken und Analysewerkzeuge. Rein. |
| `ratefields.ts` | Ratenfelder je Kategorie, Reichweite der Übernahme über Lebensphasen, Aufbau der Schreibvorgänge (Kap. 3.6.11). Rein. |
@@ -3008,6 +3117,7 @@ sondern zu leeren Werten.
| `20260719210000_scenario_versioning` | Tabelle `ScenarioVersion` (Snapshot als JSONB, A.B eindeutig je Szenario) und `Scenario.currentMajor` |
| `20260720090000_actuals` | Tabelle `ActualsSet` (effektive Werte je Plan, Werte als JSONB, Cash separat) |
| `20260720140000_plan_level_profile` | **V7**: Haushaltsform, Personen (Name/Alter) und Startjahr vom Szenario auf den Plan; neue Tabelle `PlanPerson`; `Person` behält nur das Pensionsalter. Datenübernahme aus dem Basisszenario. |
| `20260721090000_reports` | Tabelle `Report`: gewählte Parameter, eingefrorenes Modell und die PDF-Datei als `BYTEA` |
| `20260720160000_saved_analyses` | Tabelle `SavedAnalysis` (Eingaben + Ergebnis als JSONB, denormalisierte Kerndaten) |
**Zur V6-Migration:** Sie benennt die bisherige `Plan`-Tabelle in `Scenario` um dadurch
@@ -3367,12 +3477,13 @@ Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests
| `ratefields.test.ts` | 17 | Erkennung geänderter Raten, Reichweite (diese/folgende/alle), Erhalt der übrigen Werte in den Zielphasen |
| `versioning.test.ts` | 22 | Nummerierung und Zusammenfassung je Sitzung, unveränderte Stände, Benutzertrennung, Bauplan des Wiederherstellens, Auswirkung auf Kind-Szenarien |
| `versioning-coverage.test.ts` | 3 | statischer Wächter: jeder schreibende Endpunkt löst eine Version aus |
| `report.test.ts` | 9 | Zusammenfassung, Basis-Angabe zu JEDER Kennzahl, Szenario-Deckelung, Plan/Ist-Block, Haftungsausschluss, gültige PDF-Datei |
| `livesim.test.ts` | 15 | Element-Regler bewegt genau ein Element; Aufschlüsselung deckt sich mit dem Sammelregler; neutrale Stellung verändert den Plan nicht; Ruinmeldung |
| `phaseplan.test.ts` | 8 | Ableitung der Lebensabschnitte aus den Pensionierungszeitpunkten (Einzel/Paar/bereits pensioniert); letzter Teil immer offen |
| `bridges.test.ts` | 10 | Vermögens- und Cash-Brücke gehen über sieben Plankonstellationen ohne Restgrösse auf; Umbuchungen bleiben aus der Vermögensbrücke heraus |
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
| `migrations.test.ts` | 3 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-**Datenübernahme** (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen) |
| **Total** | **212** | |
| **Total** | **221** | |
## 8.2 Testfälle