Automate transition: carried-over securities auto-appear in next phase, sale proceeds tracked as incoming capital with allocation status indicators
Deploy App / deploy (push) Successful in 1m27s

This commit is contained in:
2026-07-08 20:20:20 +02:00
parent 837da980f2
commit a1e689167c
11 changed files with 172 additions and 31 deletions
+1
View File
@@ -19,6 +19,7 @@ const securitySchema = z.object({
annualContribution: z.number(),
ownerTag: z.enum(["PERSON_A", "PERSON_B", "HOUSEHOLD"]),
saleTaxRate: z.number().min(0).max(100),
carriedBaseValue: z.number().default(0),
});
const realEstateSchema = z.object({
name: z.string().min(1),
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@/lib/db";
import { computeSecurityYearlyValues } from "@/lib/calculations";
const transitionItemSchema = z.object({
positionType: z.enum(["SECURITY", "REAL_ESTATE"]),
@@ -50,6 +51,9 @@ export async function GET(
}
// Speichert die Entscheidungen (Uebernehmen/Verkaufen) fuer jede Position der Vorphase.
// Uebernommene Wertschriften werden automatisch als neue Wertschrift in der Folgephase
// angelegt (Startwert = Endwert dieser Phase). Verkaufte Positionen fliessen als
// "verfuegbares Startkapital" (Phase.incomingCapital) in die Folgephase ein.
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ phaseId: string }> }
@@ -94,8 +98,74 @@ export async function PUT(
);
}
const securityById = new Map(phase.securities.map((s) => [s.id, s]));
const realEstateById = new Map(phase.realEstates.map((re) => [re.id, re]));
let incomingCapital = 0;
const securitiesToCarry: { source: (typeof phase.securities)[number]; endValue: number }[] = [];
for (const item of parsed.data.items) {
if (item.positionType === "SECURITY" && item.securityId) {
const security = securityById.get(item.securityId);
if (!security) continue;
const endValue = computeSecurityYearlyValues(
security.startValue,
security.expectedReturn,
security.annualContribution,
phase.durationYears
)[phase.durationYears];
if (item.decision === "CARRY_OVER") {
securitiesToCarry.push({ source: security, endValue });
} else {
const gain = Math.max(0, endValue - security.startValue);
const tax = gain * (security.saleTaxRate / 100);
incomingCapital += endValue - tax;
}
} else if (item.positionType === "REAL_ESTATE" && item.realEstateId) {
const realEstate = realEstateById.get(item.realEstateId);
if (!realEstate || item.decision !== "SELL") continue;
const salePrice = item.salePrice ?? 0;
const gain = Math.max(0, salePrice - realEstate.marketValue);
const tax = gain * (realEstate.saleTaxRate / 100);
incomingCapital += salePrice - tax;
}
}
const transition = await prisma.$transaction(async (tx) => {
await tx.phaseTransition.deleteMany({ where: { fromPhaseId: phaseId } });
// Vorherige automatisch uebernommene Wertschriften aus einem frueheren Speichern
// dieses Uebergangs entfernen, damit sie nicht dupliziert werden. Manuell vom
// Benutzer angelegte Wertschriften (carriedFromSecurityId = null) bleiben unberuehrt.
await tx.security.deleteMany({
where: {
phaseId: nextPhase.id,
carriedFromSecurityId: { in: phase.securities.map((s) => s.id) },
},
});
for (const { source, endValue } of securitiesToCarry) {
await tx.security.create({
data: {
phaseId: nextPhase.id,
name: source.name,
startValue: endValue,
carriedBaseValue: endValue,
expectedReturn: source.expectedReturn,
annualContribution: 0,
ownerTag: source.ownerTag,
saleTaxRate: source.saleTaxRate,
carriedFromSecurityId: source.id,
},
});
}
await tx.phase.update({
where: { id: nextPhase.id },
data: { incomingCapital },
});
return tx.phaseTransition.create({
data: {
fromPhaseId: phaseId,
@@ -114,5 +184,5 @@ export async function PUT(
});
});
return NextResponse.json({ transition });
return NextResponse.json({ transition, incomingCapital });
}