V7: Haushalt auf Plan-Ebene, Annahmen bleiben am Szenario
Deploy App / deploy (push) Successful in 1m52s

Haushaltsform, Personen (Name/Alter) und Startjahr wandern vom Szenario
auf den Plan. Das Pensionsalter bleibt szenario-eigen -- es ist der Kern
jedes Frueh-/Spaetpensionierungs-Szenarios.

Neue Tabelle PlanPerson; Person behaelt nur Rolle + Pensionsalter;
Plan bekommt householdType und startYear.

Der Rechenkern bleibt unberuehrt: toPlanInput fuegt beide Ebenen wieder
zu einem unveraenderten PlanInput zusammen. 43 Golden Tests unveraendert.

Nebeneffekt: Ein Ist-Satz trifft jetzt in ALLEN Szenarien dasselbe
Planjahr -- vorher war das nicht garantiert.

Wiederherstellen einer Version setzt nur noch Szenario-Eigenes zurueck.
Profil-Dialog kennzeichnet plan-weite vs. szenario-eigene Felder.

Zwei Tests spielen echte V6-Daten ein und pruefen die Uebernahme.
Spezifikation 0.23 (210 -> 212).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 21:53:12 +02:00
parent 44c6c81f5d
commit ce5f83823f
10 changed files with 290 additions and 51 deletions
@@ -0,0 +1,71 @@
-- V7: Haushaltsform, Personen (Name/Alter) und Planstartjahr wandern auf PLAN-Ebene.
--
-- Begruendung (SPEZIFIKATION 2.1, 9.30): Diese Angaben beschreiben den HAUSHALT, nicht eine
-- Planungsvariante. Unterscheiden sie sich, ist es ein anderer Plan -- kein anderes Szenario.
-- Solange sie am Szenario hingen, konnten zwei Szenarien desselben Plans verschiedene
-- Startjahre tragen; derselbe Ist-Satz waere dann je Szenario auf einem anderen Planjahr
-- gelandet.
--
-- Das PENSIONSALTER bleibt bewusst am Szenario -- es ist der Kern jedes
-- Frueh-/Spaetpensionierungs-Szenarios.
ALTER TABLE "Plan" ADD COLUMN "householdType" "HouseholdType";
ALTER TABLE "Plan" ADD COLUMN "startYear" INTEGER;
CREATE TABLE "PlanPerson" (
"id" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"role" "PersonRole" NOT NULL,
"name" TEXT,
"age" INTEGER NOT NULL,
CONSTRAINT "PlanPerson_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "PlanPerson_planId_role_key" ON "PlanPerson"("planId", "role");
ALTER TABLE "PlanPerson" ADD CONSTRAINT "PlanPerson_planId_fkey"
FOREIGN KEY ("planId") REFERENCES "Plan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- --- Datenuebernahme aus dem BASISSZENARIO ------------------------------------------------
-- Das Basisszenario ist der kanonische Stand. Weichen Nebenszenarien ab, gewinnt die Basis.
UPDATE "Plan" p
SET "householdType" = s."householdType",
"startYear" = s."startYear"
FROM "Scenario" s
WHERE s."planId" = p."id" AND s."isBase" = true;
-- Sicherheitsnetz: Plaene ohne Basisszenario (sollte es nicht geben) nehmen irgendein Szenario.
UPDATE "Plan" p
SET "householdType" = s."householdType",
"startYear" = COALESCE(p."startYear", s."startYear")
FROM "Scenario" s
WHERE s."planId" = p."id" AND p."householdType" IS NULL;
INSERT INTO "PlanPerson" ("id", "planId", "role", "name", "age")
SELECT 'pp_' || pe."id", s."planId", pe."role", pe."name", pe."age"
FROM "Person" pe
JOIN "Scenario" s ON s."id" = pe."scenarioId"
WHERE s."isBase" = true
ON CONFLICT ("planId", "role") DO NOTHING;
-- Netz fuer Plaene, deren Basisszenario keine Personen traegt.
INSERT INTO "PlanPerson" ("id", "planId", "role", "name", "age")
SELECT DISTINCT ON (s."planId", pe."role") 'pp_' || pe."id", s."planId", pe."role", pe."name", pe."age"
FROM "Person" pe
JOIN "Scenario" s ON s."id" = pe."scenarioId"
ORDER BY s."planId", pe."role", s."isBase" DESC
ON CONFLICT ("planId", "role") DO NOTHING;
-- Ein Plan ohne Haushaltsform waere nicht rechenbar.
UPDATE "Plan" SET "householdType" = 'SINGLE' WHERE "householdType" IS NULL;
ALTER TABLE "Plan" ALTER COLUMN "householdType" SET NOT NULL;
-- --- Szenario-Ebene verschlanken ----------------------------------------------------------
-- Person traegt nur noch Rolle + Pensionsalter; Name und Alter kommen vom Plan.
ALTER TABLE "Person" DROP COLUMN "name";
ALTER TABLE "Person" DROP COLUMN "age";
ALTER TABLE "Scenario" DROP COLUMN "householdType";
ALTER TABLE "Scenario" DROP COLUMN "startYear";
+24 -6
View File
@@ -66,18 +66,31 @@ enum ElementCategory {
// Einzelperson eines Szenarios. retirementAge ist das (szenario-eigene) Pensionsalter --
// dadurch sind Frueh-/Spaetpensionierungs-Szenarien moeglich.
// Szenario-EIGENE Angabe zu einer Person. Seit V7 nur noch das Pensionsalter -- es ist der
// Kern jedes Frueh-/Spaetpensionierungs-Szenarios. Name und Alter beschreiben den Haushalt
// und liegen deshalb am Plan (PlanPerson).
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])
}
// Eine Person des HAUSHALTS. Gilt fuer alle Szenarien des Plans.
model PlanPerson {
id String @id @default(cuid())
planId String
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
role PersonRole
name String?
age Int
@@unique([planId, role])
}
// Behaelter einer Planung. Traegt selbst KEINE Finanzdaten, nur den Namen und die Szenarien.
model Plan {
id String @id @default(cuid())
@@ -85,11 +98,19 @@ model Plan {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String
// Seit V7 am Plan: Diese Angaben beschreiben den HAUSHALT, nicht eine Planungsvariante.
// Unterscheiden sie sich, ist es ein anderer Plan -- kein anderes Szenario.
householdType HouseholdType
// Kalenderjahr, in dem die Planung beginnt (Jahr 1). Rein fuer die Darstellung --
// die Berechnung rechnet weiterhin in relativen Jahren ab Planbeginn.
startYear Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
scenarios Scenario[]
actuals ActualsSet[]
persons PlanPerson[]
}
// Ein erfasster Stand der WIRKLICHKEIT zu einem Stichtag (Roadmap Nr. 5).
@@ -136,12 +157,9 @@ model Scenario {
parentScenario Scenario? @relation("ScenarioTree", fields: [parentScenarioId], references: [id], onDelete: SetNull)
children Scenario[] @relation("ScenarioTree")
householdType HouseholdType
// Szenario-eigene Annahmen. Haushaltsform, Personen und Startjahr liegen seit V7 am Plan.
inflationRateDefault Float
initialCash Float @default(0)
// Kalenderjahr, in dem die Planung beginnt (Jahr 1). Rein fuer die Darstellung --
// die Berechnung rechnet weiterhin in relativen Jahren ab Planbeginn.
startYear Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt