a5d4868c58
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>
188 lines
6.2 KiB
Plaintext
188 lines
6.2 KiB
Plaintext
// FPT (Financial Planning Tool) — Datenmodell.
|
|
//
|
|
// Kernkonzept: finanzielle Elemente leben auf SZENARIO-Ebene und sind ueber alle
|
|
// Lebensphasen hinweg dieselbe Entitaet. Pro Element existiert je Lebensphase ein
|
|
// Werte-Datensatz (ElementPhaseValue) und je Uebergang ein Entscheid-Datensatz
|
|
// (ElementTransitionValue). Die kategorie-/kontextspezifischen Felder liegen als JSON,
|
|
// validiert und typisiert in der Applikationsschicht (lib/elements.ts).
|
|
//
|
|
// Rework 07/2026 (V6, Szenario-Hierarchie): Der PLAN ist neu ein schlanker Behaelter und
|
|
// traegt selbst keine Finanzdaten. Die berechenbare Einheit ist das SZENARIO -- es traegt
|
|
// das Grundprofil (Haushaltsform, Personen, Inflation, Cash-Anfangswert), die Phasenkette
|
|
// und die Elemente. Jeder Plan hat genau ein Basisszenario (isBase) und beliebig viele
|
|
// weitere Szenarien, die als Baum (parentScenarioId) darunter haengen. Kopierte Phasen und
|
|
// Elemente tragen einen Herkunfts-Verweis (sourcePhaseId / sourceElementId) auf ihr
|
|
// Gegenstueck im Eltern-Szenario -- darauf beruht die Abweichungs-Markierung im UI.
|
|
//
|
|
// Namens-Hinweis: In der Berechnungsschicht heisst die berechenbare Einheit weiterhin
|
|
// `PlanInput` / `computePlan` (sie beschreibt eine Finanzplanung -- fachlich ein Szenario).
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
passwordHash String
|
|
createdAt DateTime @default(now())
|
|
|
|
plans Plan[]
|
|
}
|
|
|
|
enum HouseholdType {
|
|
SINGLE
|
|
COUPLE
|
|
}
|
|
|
|
enum PersonRole {
|
|
PERSON_A
|
|
PERSON_B
|
|
}
|
|
|
|
enum OwnerRole {
|
|
PERSON_A
|
|
PERSON_B
|
|
HOUSEHOLD
|
|
}
|
|
|
|
enum ElementCategory {
|
|
INCOME
|
|
EXPENSE
|
|
AHV
|
|
PENSION_FUND
|
|
PILLAR_3A
|
|
REAL_ESTATE
|
|
OTHER_ASSET
|
|
OTHER_DEBT
|
|
}
|
|
|
|
// Einzelperson eines Szenarios. retirementAge ist das (szenario-eigene) Pensionsalter --
|
|
// dadurch sind Frueh-/Spaetpensionierungs-Szenarien moeglich.
|
|
model Person {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
role PersonRole
|
|
name String?
|
|
age Int
|
|
retirementAge Int
|
|
|
|
@@unique([scenarioId, role])
|
|
}
|
|
|
|
// Behaelter einer Planung. Traegt selbst KEINE Finanzdaten, nur den Namen und die Szenarien.
|
|
model Plan {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
name String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
scenarios Scenario[]
|
|
}
|
|
|
|
// Die berechenbare Einheit: Grundprofil + Phasenkette + Elemente. Genau ein Szenario je
|
|
// Plan ist das Basisszenario (isBase); weitere haengen als Baum darunter (parentScenarioId).
|
|
model Scenario {
|
|
id String @id @default(cuid())
|
|
planId String
|
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
name String
|
|
isBase Boolean @default(false)
|
|
|
|
// Szenario, aus dem dieses kopiert wurde. Zugleich die Vergleichsbasis fuer den Diff
|
|
// (Basisszenario: null). Beim Loeschen des Elternteils bleibt der Baum flach erhalten.
|
|
parentScenarioId String?
|
|
parentScenario Scenario? @relation("ScenarioTree", fields: [parentScenarioId], references: [id], onDelete: SetNull)
|
|
children Scenario[] @relation("ScenarioTree")
|
|
|
|
householdType HouseholdType
|
|
inflationRateDefault Float
|
|
initialCash Float @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
persons Person[]
|
|
phases Phase[]
|
|
elements FinancialElement[]
|
|
}
|
|
|
|
// Ein Lebensabschnitt innerhalb eines Plans. Der Phasentyp (Erwerb/Pension/Mischung)
|
|
// wird NICHT gespeichert, sondern aus Alter + Pensionsalter abgeleitet (lib/calculations).
|
|
// Die Inflation liegt seit V5 plan-weit am Plan (inflationRateDefault), nicht mehr hier.
|
|
model Phase {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
sequenceNumber Int
|
|
name String
|
|
durationYears Int
|
|
|
|
// Gegenstueck im Eltern-Szenario (lose Referenz, kein FK) -- Grundlage des Diffs.
|
|
sourcePhaseId String?
|
|
|
|
// Entscheid fuer das Cash-Konto beim UEBERGANG NACH dieser Phase (JSON, validiert in
|
|
// lib/elements.ts): 1:1 uebernehmen oder einmaliger Zufluss/einmalige Kosten. Liegt hier
|
|
// und nicht in ElementTransitionValue, weil Cash kein FinancialElement ist.
|
|
cashTransition Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
phaseValues ElementPhaseValue[]
|
|
transitionValues ElementTransitionValue[] @relation("TransitionFromPhase")
|
|
|
|
@@unique([scenarioId, sequenceNumber])
|
|
}
|
|
|
|
// Ein finanzielles Element (szenario-weit): Kategorie + optionale Personenzuordnung.
|
|
model FinancialElement {
|
|
id String @id @default(cuid())
|
|
scenarioId String
|
|
scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
|
|
category ElementCategory
|
|
name String
|
|
ownerRole OwnerRole?
|
|
orderIndex Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
// Gegenstueck im Eltern-Szenario (lose Referenz, kein FK) -- Grundlage des Diffs.
|
|
sourceElementId String?
|
|
|
|
phaseValues ElementPhaseValue[]
|
|
transitionValues ElementTransitionValue[]
|
|
}
|
|
|
|
// Werte eines Elements INNERHALB einer Lebensphase (kategorie-/kontextspezifisch, JSON).
|
|
model ElementPhaseValue {
|
|
id String @id @default(cuid())
|
|
elementId String
|
|
element FinancialElement @relation(fields: [elementId], references: [id], onDelete: Cascade)
|
|
phaseId String
|
|
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
|
data Json
|
|
|
|
@@unique([elementId, phaseId])
|
|
}
|
|
|
|
// Entscheid/Werte eines Elements beim UEBERGANG nach der Phase fromPhase (JSON).
|
|
model ElementTransitionValue {
|
|
id String @id @default(cuid())
|
|
elementId String
|
|
element FinancialElement @relation(fields: [elementId], references: [id], onDelete: Cascade)
|
|
fromPhaseId String
|
|
fromPhase Phase @relation("TransitionFromPhase", fields: [fromPhaseId], references: [id], onDelete: Cascade)
|
|
data Json
|
|
|
|
@@unique([elementId, fromPhaseId])
|
|
}
|