Pensionierungs-Bildschirm, Ampel mit drei Zustaenden, Planungshorizont
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getOwnedElement } from "@/lib/queries";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { touchScenario } from "@/lib/versioning-db";
|
||||
import { RETIREMENT_CATEGORIES, retirementDecisionSchema } from "@/lib/retirement-decision";
|
||||
|
||||
// Speichert den Pensionierungs-Entscheid eines Elements (AHV, PK, Säule 3a).
|
||||
//
|
||||
// Bewusst OHNE Phasenbezug in der Route: Der Entscheid gilt für die Pensionierung des
|
||||
// Besitzers, wo immer die auf der Zeitachse gerade liegt. Genau das unterscheidet ihn vom
|
||||
// Übergangs-Entscheid (`/transition/<fromPhaseId>`), der an einer konkreten Grenze hängt.
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ elementId: string }> }
|
||||
) {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
const { elementId } = await params;
|
||||
|
||||
const element = await getOwnedElement(elementId, userId);
|
||||
if (!element) return NextResponse.json({ error: "Element nicht gefunden." }, { status: 404 });
|
||||
if (!RETIREMENT_CATEGORIES.includes(element.category)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Nur AHV, Pensionskasse und Säule 3a kennen einen Pensionierungs-Entscheid." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = retirementDecisionSchema.safeParse(body);
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
|
||||
await prisma.financialElement.update({
|
||||
where: { id: elementId },
|
||||
data: { retirementDecision: parsed.data },
|
||||
});
|
||||
|
||||
await touchScenario(element.scenarioId, userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -34,7 +34,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
inflationRateDefault: source.inflationRateDefault,
|
||||
initialCash: source.initialCash,
|
||||
persons: {
|
||||
create: source.persons.map((p) => ({ role: p.role, retirementAge: p.retirementAge })),
|
||||
create: source.persons.map((p) => ({
|
||||
role: p.role,
|
||||
retirementAge: p.retirementAge,
|
||||
planningHorizonAge: p.planningHorizonAge,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -64,6 +68,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
name: el.name,
|
||||
ownerRole: el.ownerRole,
|
||||
orderIndex: el.orderIndex,
|
||||
// Ohne das waere die Kopie eines Szenarios genau fuer den Zweck unbrauchbar, fuer
|
||||
// den man sie am haeufigsten anlegt: ein anderes Pensionierungs-Szenario.
|
||||
retirementDecision: el.retirementDecision ?? undefined,
|
||||
sourceElementId: el.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getOwnedScenario, toPlanInput } from "@/lib/queries";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { touchScenario } from "@/lib/versioning-db";
|
||||
import { planHorizonChange } from "@/lib/retirement";
|
||||
import { MAX_PLANNING_HORIZON_AGE, MIN_PLANNING_HORIZON_AGE } from "@/lib/constants";
|
||||
|
||||
// Planungshorizont setzen: bis zu welchem Alter gerechnet wird.
|
||||
//
|
||||
// Wie beim Pensionsalter gilt: Zahl stellen, Struktur folgt. Die LETZTE Lebensphase wird so
|
||||
// verlängert oder gekürzt, dass der Plan genau bis zum Horizont läuft. Vorher ergab sich das
|
||||
// Planende stillschweigend aus der Summe der Phasendauern -- zwei Szenarien konnten dadurch
|
||||
// unbemerkt verschieden weit rechnen und waren nicht vergleichbar.
|
||||
|
||||
const bodySchema = z.object({
|
||||
role: z.enum(["PERSON_A", "PERSON_B"]),
|
||||
horizonAge: z.number().int().min(MIN_PLANNING_HORIZON_AGE).max(MAX_PLANNING_HORIZON_AGE),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ scenarioId: string }> }) {
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
const { scenarioId } = await params;
|
||||
|
||||
const scenario = await getOwnedScenario(scenarioId, userId);
|
||||
if (!scenario) return NextResponse.json({ error: "Szenario nicht gefunden." }, { status: 404 });
|
||||
|
||||
const parsed = bodySchema.safeParse(await request.json().catch(() => ({})));
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
const { role, horizonAge } = parsed.data;
|
||||
|
||||
const planInput = toPlanInput(scenario);
|
||||
const person = planInput.persons.find((p) => p.role === role);
|
||||
if (!person) return NextResponse.json({ error: "Diese Person gibt es in diesem Szenario nicht." }, { status: 400 });
|
||||
if (horizonAge <= person.retirementAge) {
|
||||
return NextResponse.json(
|
||||
{ error: "Der Planungshorizont muss nach der Pensionierung liegen." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const change = planHorizonChange(planInput, horizonAge, role);
|
||||
if (!change) return NextResponse.json({ error: "Es gibt keine Lebensphase, die sich anpassen liesse." }, { status: 400 });
|
||||
if (change.blocked) return NextResponse.json({ error: change.blocked }, { status: 400 });
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.person.update({ where: { id: person.id }, data: { planningHorizonAge: horizonAge } }),
|
||||
prisma.phase.update({ where: { id: change.lastPhaseId }, data: { durationYears: change.newDuration } }),
|
||||
]);
|
||||
|
||||
await touchScenario(scenario.id, userId);
|
||||
return NextResponse.json({ ok: true, lastPhaseDuration: change.newDuration });
|
||||
}
|
||||
Reference in New Issue
Block a user