2f762175d2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.1 KiB
TypeScript
137 lines
5.1 KiB
TypeScript
import { Prisma } from "@/generated/prisma/client";
|
|
import { prisma } from "@/lib/db";
|
|
import { cashTransitionSchema, phaseDataSchema, transitionDataSchema } from "@/lib/elements";
|
|
import { retirementDecisionSchema } from "@/lib/retirement-decision";
|
|
import { normalizeProgress } from "@/lib/assistant";
|
|
import type { CashTransitionData, PhaseData, TransitionData } from "@/lib/elements";
|
|
import type { RetirementDecision } from "@/lib/retirement-decision";
|
|
import type { PlanInput } from "@/lib/types";
|
|
|
|
export const planInclude = {
|
|
// Der Haushalt (Form, Personen, Startjahr) liegt seit V7 am PLAN -- `toPlanInput` fügt ihn
|
|
// mit den szenario-eigenen Pensionsaltern wieder zu einem `PlanInput` zusammen. Der
|
|
// Rechenkern sieht davon nichts.
|
|
plan: { include: { persons: { orderBy: { role: "asc" } } } },
|
|
persons: { orderBy: { role: "asc" } },
|
|
phases: { orderBy: { sequenceNumber: "asc" } },
|
|
elements: {
|
|
orderBy: { orderIndex: "asc" },
|
|
include: { phaseValues: true, transitionValues: true },
|
|
},
|
|
} satisfies Prisma.ScenarioInclude;
|
|
|
|
export type PlanWithRelations = Prisma.ScenarioGetPayload<{ include: typeof planInclude }>;
|
|
|
|
function parsePhaseData(raw: unknown): PhaseData {
|
|
const parsed = phaseDataSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : {};
|
|
}
|
|
|
|
function parseTransitionData(raw: unknown): TransitionData {
|
|
const parsed = transitionDataSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : {};
|
|
}
|
|
|
|
function parseRetirementDecision(raw: unknown): RetirementDecision {
|
|
const parsed = retirementDecisionSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : {};
|
|
}
|
|
|
|
function parseCashTransition(raw: unknown): CashTransitionData {
|
|
const parsed = cashTransitionSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : {};
|
|
}
|
|
|
|
// Wandelt ein Szenario (DB) in die berechenbare Einheit (PlanInput) um.
|
|
export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
|
return {
|
|
id: plan.id,
|
|
name: plan.name,
|
|
householdType: plan.plan.householdType,
|
|
inflationRateDefault: plan.inflationRateDefault,
|
|
initialCash: plan.initialCash,
|
|
startYear: plan.plan.startYear,
|
|
planningHorizonYears: plan.planningHorizonYears,
|
|
assistantProgress: normalizeProgress(plan.assistantProgress),
|
|
// Name und Alter vom Plan, Pensionsalter vom Szenario. Fehlt zu einer Rolle die
|
|
// Plan-Person, greift ein Notbehelf -- die Berechnung darf daran nicht scheitern.
|
|
persons: plan.persons.map((p) => {
|
|
const hh = plan.plan.persons.find((x) => x.role === p.role);
|
|
return {
|
|
id: p.id,
|
|
role: p.role,
|
|
name: hh?.name ?? null,
|
|
age: hh?.age ?? 0,
|
|
retirementAge: p.retirementAge,
|
|
};
|
|
}),
|
|
phases: plan.phases.map((phase) => ({
|
|
id: phase.id,
|
|
sequenceNumber: phase.sequenceNumber,
|
|
name: phase.name,
|
|
durationYears: phase.durationYears,
|
|
cashTransition: parseCashTransition(phase.cashTransition),
|
|
sourcePhaseId: phase.sourcePhaseId,
|
|
})),
|
|
elements: plan.elements.map((e) => {
|
|
const phaseValues: Record<string, PhaseData> = {};
|
|
for (const pv of e.phaseValues) phaseValues[pv.phaseId] = parsePhaseData(pv.data);
|
|
const transitionValues: Record<string, TransitionData> = {};
|
|
for (const tv of e.transitionValues) transitionValues[tv.fromPhaseId] = parseTransitionData(tv.data);
|
|
return {
|
|
id: e.id,
|
|
category: e.category,
|
|
name: e.name,
|
|
ownerRole: e.ownerRole,
|
|
orderIndex: e.orderIndex,
|
|
phaseValues,
|
|
transitionValues,
|
|
retirementDecision: parseRetirementDecision(e.retirementDecision),
|
|
baseData: parsePhaseData(e.baseData),
|
|
sourceElementId: e.sourceElementId,
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
// --- Ownership-Abfragen. Der Eigentümer hängt neu am PLAN; Szenario/Phase/Element
|
|
// erben ihn über die Kette Scenario -> Plan -> User. ---
|
|
|
|
// Lädt ein Szenario inkl. Profil + Phasen + Elemente, nur wenn es dem Benutzer gehört.
|
|
export async function getOwnedScenario(scenarioId: string, userId: string) {
|
|
return prisma.scenario.findFirst({
|
|
where: { id: scenarioId, plan: { userId } },
|
|
include: planInclude,
|
|
});
|
|
}
|
|
|
|
// Wie oben, zusätzlich mit den Kopfdaten (Plan, Basis-Flag, Elternteil).
|
|
export async function getOwnedScenarioWithMeta(scenarioId: string, userId: string) {
|
|
return prisma.scenario.findFirst({
|
|
where: { id: scenarioId, plan: { userId } },
|
|
include: planInclude,
|
|
});
|
|
}
|
|
|
|
// Lädt einen Plan (Behälter) inkl. Szenario-Kopfdaten.
|
|
export async function getOwnedPlan(planId: string, userId: string) {
|
|
return prisma.plan.findFirst({
|
|
where: { id: planId, userId },
|
|
include: { scenarios: { orderBy: { createdAt: "asc" } } },
|
|
});
|
|
}
|
|
|
|
// Lädt eine Phase (Basisdaten), aber nur wenn sie dem Benutzer gehört.
|
|
export async function getOwnedPhase(phaseId: string, userId: string) {
|
|
return prisma.phase.findFirst({
|
|
where: { id: phaseId, scenario: { plan: { userId } } },
|
|
});
|
|
}
|
|
|
|
// Lädt ein Element (Basisdaten), aber nur wenn es dem Benutzer gehört.
|
|
export async function getOwnedElement(elementId: string, userId: string) {
|
|
return prisma.financialElement.findFirst({
|
|
where: { id: elementId, scenario: { plan: { userId } } },
|
|
});
|
|
}
|