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:
@@ -0,0 +1,63 @@
|
||||
// Spielt ALLE Migrationen gegen ein echtes PostgreSQL (PGlite, in-process) ein und prueft das
|
||||
// Ergebnis. Faengt kaputte oder nicht-idempotente Migrations-SQL ab, bevor sie beim Deploy
|
||||
// gegen die Live-Datenbank laufen -- lokal steht sonst keine Datenbank zur Verfuegung.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { PGlite } from "@electric-sql/pglite";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const MIG = path.join(process.cwd(), "prisma", "migrations");
|
||||
|
||||
describe("Datenbank-Migrationen", () => {
|
||||
it("laufen vollstaendig durch und ergeben das erwartete Schema", async () => {
|
||||
const db = await PGlite.create();
|
||||
const dirs = readdirSync(MIG).filter((d) => !d.endsWith(".toml")).sort();
|
||||
expect(dirs.length).toBeGreaterThan(0);
|
||||
|
||||
for (const d of dirs) {
|
||||
await db.exec(readFileSync(path.join(MIG, d, "migration.sql"), "utf8"));
|
||||
}
|
||||
|
||||
const tables = (
|
||||
await db.query<{ table_name: string }>(
|
||||
`SELECT table_name FROM information_schema.tables WHERE table_schema='public'`
|
||||
)
|
||||
).rows.map((r) => r.table_name);
|
||||
|
||||
// V6-Struktur: Behaelter Plan + berechenbares Scenario.
|
||||
for (const t of ["User", "Plan", "Scenario", "Person", "Phase", "FinancialElement"]) {
|
||||
expect(tables, `Tabelle ${t} fehlt`).toContain(t);
|
||||
}
|
||||
|
||||
const cols = async (table: string) =>
|
||||
(
|
||||
await db.query<{ column_name: string }>(
|
||||
`SELECT column_name FROM information_schema.columns WHERE table_name=$1`,
|
||||
[table]
|
||||
)
|
||||
).rows.map((r) => r.column_name);
|
||||
|
||||
// Kind-Tabellen haengen am Szenario, nicht mehr am Plan.
|
||||
for (const t of ["Person", "Phase", "FinancialElement"]) {
|
||||
const c = await cols(t);
|
||||
expect(c, `${t}.scenarioId fehlt`).toContain("scenarioId");
|
||||
expect(c, `${t}.planId haette entfernt werden muessen`).not.toContain("planId");
|
||||
}
|
||||
|
||||
// Herkunfts-Verweise fuer den Diff.
|
||||
expect(await cols("Phase")).toContain("sourcePhaseId");
|
||||
expect(await cols("FinancialElement")).toContain("sourceElementId");
|
||||
|
||||
// Der Plan traegt keine Finanzdaten mehr.
|
||||
const planCols = await cols("Plan");
|
||||
expect(planCols).not.toContain("householdType");
|
||||
expect(planCols).toContain("userId");
|
||||
|
||||
// Das Szenario traegt sie.
|
||||
const scenCols = await cols("Scenario");
|
||||
for (const c of ["planId", "isBase", "parentScenarioId", "householdType", "initialCash"]) {
|
||||
expect(scenCols, `Scenario.${c} fehlt`).toContain(c);
|
||||
}
|
||||
expect(scenCols).not.toContain("userId"); // Eigentuemer haengt am Plan
|
||||
}, 60000);
|
||||
});
|
||||
Reference in New Issue
Block a user