Rework core model: financial elements as plan-wide entities across phases; derived phase types (Erwerb/Pension/Misch) with retirement-capped durations and per-plan retirement age; AHV (gap years + couple ceiling), PK payout/annuity, 3a, real estate, other assets/debts; horizontal timeline with retirement markers; phase x element matrix with detail panel; savings/consumption quota + available-capital key figures with red status
Deploy App / deploy (push) Successful in 1m57s
Deploy App / deploy (push) Successful in 1m57s
This commit is contained in:
+70
-166
@@ -1,5 +1,9 @@
|
||||
// FPT (Financial Planning Tool) — Datenmodell gemaess FDD/TDD Kapitel 2.
|
||||
// Get a free hosted Postgres database in seconds: `npx create-db`
|
||||
// FPT (Financial Planning Tool) — Datenmodell.
|
||||
// Kernkonzept (Rework 07/2026): finanzielle Elemente leben auf PLAN-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).
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
@@ -10,9 +14,6 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// Benutzerkonto: offene Registrierung mit Benutzername + Passwort (bcrypt-Hash).
|
||||
// Jeder Benutzer hat seinen eigenen Haushalt samt Plaenen -- Daten sind strikt
|
||||
// pro Konto isoliert.
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
@@ -32,33 +33,24 @@ enum PersonRole {
|
||||
PERSON_B
|
||||
}
|
||||
|
||||
enum IncomeMode {
|
||||
PER_PERSON
|
||||
HOUSEHOLD
|
||||
}
|
||||
|
||||
enum OwnerTag {
|
||||
enum OwnerRole {
|
||||
PERSON_A
|
||||
PERSON_B
|
||||
HOUSEHOLD
|
||||
}
|
||||
|
||||
enum OneTimeEventType {
|
||||
enum ElementCategory {
|
||||
INCOME
|
||||
EXPENSE
|
||||
}
|
||||
|
||||
enum TransitionDecision {
|
||||
CARRY_OVER
|
||||
SELL
|
||||
}
|
||||
|
||||
enum PositionType {
|
||||
SECURITY
|
||||
AHV
|
||||
PENSION_FUND
|
||||
PILLAR_3A
|
||||
REAL_ESTATE
|
||||
OTHER_ASSET
|
||||
OTHER_DEBT
|
||||
}
|
||||
|
||||
// Ein Haushalt (1 oder 2 Personen) - Wurzel-Objekt, gehoert genau einem Benutzer
|
||||
// Ein Haushalt (1 oder 2 Personen), gehoert genau einem Benutzer.
|
||||
model Household {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
@@ -72,7 +64,7 @@ model Household {
|
||||
plans Plan[]
|
||||
}
|
||||
|
||||
// Einzelperson im Haushalt (Alter, geplantes Pensionsalter)
|
||||
// Einzelperson im Haushalt. retirementAge ist die Standard-Annahme (im Plan uebersteuerbar).
|
||||
model Person {
|
||||
id String @id @default(cuid())
|
||||
householdId String
|
||||
@@ -81,22 +73,20 @@ model Person {
|
||||
age Int
|
||||
retirementAge Int
|
||||
|
||||
incomeEntries IncomeEntry[]
|
||||
retirementInfos RetirementInfo[]
|
||||
|
||||
@@unique([householdId, role])
|
||||
}
|
||||
|
||||
// Eine vollstaendige Phasenkette; kann Szenario eines anderen Plans sein
|
||||
// Eine vollstaendige Phasenkette; kann Szenario eines anderen Plans sein.
|
||||
// retirementAgeA/B uebersteuern das Pensionsalter der jeweiligen Person NUR fuer diesen
|
||||
// Plan (null = Standard aus Person.retirementAge) -- ermoeglicht Fruehpensions-Szenarien.
|
||||
model Plan {
|
||||
id String @id @default(cuid())
|
||||
householdId String
|
||||
household Household @relation(fields: [householdId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
id String @id @default(cuid())
|
||||
householdId String
|
||||
household Household @relation(fields: [householdId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
retirementAgeA Int?
|
||||
retirementAgeB Int?
|
||||
|
||||
// Szenario-Verzweigung: ein Szenario ist ein eigener Plan mit Verweis auf den Ursprungsplan
|
||||
// und die Phase, ab der die Ketten divergieren (branchFromPhaseId zeigt auf eine Phase
|
||||
// dieses neuen Plans, welche die per Deep-Copy duplizierte letzte gemeinsame Phase ist).
|
||||
parentPlanId String?
|
||||
parentPlan Plan? @relation("PlanScenarios", fields: [parentPlanId], references: [id], onDelete: SetNull)
|
||||
scenarios Plan[] @relation("PlanScenarios")
|
||||
@@ -105,151 +95,65 @@ model Plan {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
phases Phase[]
|
||||
phases Phase[]
|
||||
elements FinancialElement[]
|
||||
}
|
||||
|
||||
// Ein Lebensabschnitt innerhalb eines Plans
|
||||
// Ein Lebensabschnitt innerhalb eines Plans. Der Phasentyp (Erwerb/Pension/Mischung)
|
||||
// wird NICHT gespeichert, sondern aus Alter + Pensionsalter abgeleitet (lib/calculations).
|
||||
model Phase {
|
||||
id String @id @default(cuid())
|
||||
planId String
|
||||
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
sequenceNumber Int
|
||||
name String
|
||||
durationYears Int
|
||||
inflationRate Float?
|
||||
incomeMode IncomeMode @default(HOUSEHOLD)
|
||||
// Aus Verkaeufen im Uebergang aus der Vorphase verfuegbares Startkapital (wird beim
|
||||
// Speichern des Uebergangs automatisch gesetzt, siehe PhaseTransition).
|
||||
incomingCapital Float @default(0)
|
||||
|
||||
incomeEntries IncomeEntry[]
|
||||
expenseEntries ExpenseEntry[]
|
||||
securities Security[]
|
||||
realEstates RealEstate[]
|
||||
oneTimeEvents OneTimeEvent[]
|
||||
retirementInfos RetirementInfo[]
|
||||
|
||||
// Uebergang IN diese Phase (diese Phase ist Ziel) bzw. AUS dieser Phase (diese Phase ist Quelle)
|
||||
transitionIn PhaseTransition? @relation("TransitionTarget")
|
||||
transitionOut PhaseTransition? @relation("TransitionSource")
|
||||
id String @id @default(cuid())
|
||||
planId String
|
||||
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
sequenceNumber Int
|
||||
name String
|
||||
durationYears Int
|
||||
inflationRate Float?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
phaseValues ElementPhaseValue[]
|
||||
transitionValues ElementTransitionValue[] @relation("TransitionFromPhase")
|
||||
|
||||
@@unique([planId, sequenceNumber])
|
||||
}
|
||||
|
||||
// Einkommensposten einer Phase (pro Person oder gemeinsam, je nach Phase.incomeMode)
|
||||
model IncomeEntry {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
|
||||
label String?
|
||||
amount Float
|
||||
// Ein finanzielles Element (plan-weit): Kategorie + optionale Personenzuordnung.
|
||||
model FinancialElement {
|
||||
id String @id @default(cuid())
|
||||
planId String
|
||||
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
category ElementCategory
|
||||
name String
|
||||
ownerRole OwnerRole?
|
||||
orderIndex Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
phaseValues ElementPhaseValue[]
|
||||
transitionValues ElementTransitionValue[]
|
||||
}
|
||||
|
||||
// Ausgabenposten einer Phase (immer Haushaltsebene, generischer Gesamtbetrag)
|
||||
model ExpenseEntry {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
label String?
|
||||
amount Float
|
||||
// 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])
|
||||
}
|
||||
|
||||
// Eine Wertschrift innerhalb einer Phase (inkl. jaehrlichem Sparbeitrag)
|
||||
model Security {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
startValue Float
|
||||
expectedReturn Float
|
||||
annualContribution Float @default(0)
|
||||
ownerTag OwnerTag @default(HOUSEHOLD)
|
||||
// Steuersatz auf Verkaufsgewinn bei Uebernahme in PhaseTransitionItem (Default 0%, siehe Kap. 9)
|
||||
saleTaxRate Float @default(0)
|
||||
// Baseline-Wert bei automatischer Uebernahme aus der Vorphase (0 bei manuell angelegten
|
||||
// Wertschriften). Dient dazu, im UI zu erkennen, wie viel vom verfuegbaren Startkapital
|
||||
// bereits (on top of der Uebernahme) zugewiesen wurde.
|
||||
carriedBaseValue Float @default(0)
|
||||
// Verweist auf die Wertschrift der Vorphase, aus der automatisch uebernommen wurde
|
||||
// (nur intern zur Deduplizierung bei wiederholtem Speichern des Uebergangs, kein FK).
|
||||
carriedFromSecurityId String?
|
||||
|
||||
transitionItems PhaseTransitionItem[]
|
||||
}
|
||||
|
||||
// Eine Immobilie innerhalb einer Phase. Vereinfachtes Modell: kein Wertsteigerungsfeld,
|
||||
// der Kaufpreis bleibt ueber die Haltedauer fix -- einzig die Hypothek sinkt durch
|
||||
// Amortisation. Verkaufspreis/-steuer werden nicht hier, sondern erst im Uebergangs-
|
||||
// Screen im Moment des Verkaufs erfasst (siehe PhaseTransitionItem).
|
||||
model RealEstate {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
purchasePrice Float
|
||||
mortgage Float
|
||||
amortization Float
|
||||
// Verweist auf die Immobilie der Vorphase, aus der automatisch uebernommen wurde
|
||||
// (nur intern zur Deduplizierung bei wiederholtem Speichern des Uebergangs, kein FK).
|
||||
carriedFromRealEstateId String?
|
||||
|
||||
transitionItems PhaseTransitionItem[]
|
||||
}
|
||||
|
||||
// Einmalige Sondereinnahme/-ausgabe
|
||||
model OneTimeEvent {
|
||||
// Entscheid/Werte eines Elements beim UEBERGANG nach der Phase fromPhase (JSON).
|
||||
model ElementTransitionValue {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
type OneTimeEventType
|
||||
amount Float
|
||||
description String?
|
||||
}
|
||||
|
||||
// Renten-/Kapitalbezugsangaben (nur in Pensionierungsphasen), pro Person
|
||||
model RetirementInfo {
|
||||
id String @id @default(cuid())
|
||||
phaseId String
|
||||
phase Phase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
|
||||
personId String
|
||||
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
|
||||
ahvAmount Float @default(0)
|
||||
pkPensionAmount Float @default(0)
|
||||
lumpSumAmount Float @default(0)
|
||||
// Geschaetzte Kapitalbezugssteuer (%), direkt am auslösenden Ereignis erfasst (Kap. 9)
|
||||
lumpSumTaxRate Float @default(8)
|
||||
|
||||
@@unique([phaseId, personId])
|
||||
}
|
||||
|
||||
// Entscheidungen beim Uebergang zweier Phasen (uebernehmen/verkaufen je Position)
|
||||
model PhaseTransition {
|
||||
id String @id @default(cuid())
|
||||
fromPhaseId String @unique
|
||||
fromPhase Phase @relation("TransitionSource", fields: [fromPhaseId], references: [id], onDelete: Cascade)
|
||||
toPhaseId String @unique
|
||||
toPhase Phase @relation("TransitionTarget", fields: [toPhaseId], references: [id], onDelete: Cascade)
|
||||
|
||||
items PhaseTransitionItem[]
|
||||
}
|
||||
|
||||
// Einzelentscheidung fuer eine Position (Wertschrift oder Immobilie) beim Phasenuebergang
|
||||
model PhaseTransitionItem {
|
||||
id String @id @default(cuid())
|
||||
transitionId String
|
||||
transition PhaseTransition @relation(fields: [transitionId], references: [id], onDelete: Cascade)
|
||||
positionType PositionType
|
||||
securityId String?
|
||||
security Security? @relation(fields: [securityId], references: [id], onDelete: Cascade)
|
||||
realEstateId String?
|
||||
realEstate RealEstate? @relation(fields: [realEstateId], references: [id], onDelete: Cascade)
|
||||
decision TransitionDecision
|
||||
salePrice Float?
|
||||
// Nur bei Immobilien-Verkauf erfasst (Grundstueckgewinnsteuer in %), siehe RealEstate.
|
||||
saleTaxRate Float?
|
||||
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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user