This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", ...init?.headers },
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
(body && typeof body.error === "string" && body.error) ||
|
||||
(body && body.error ? JSON.stringify(body.error) : `Fehler ${response.status}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(url: string) => request<T>(url),
|
||||
post: <T>(url: string, data?: unknown) =>
|
||||
request<T>(url, { method: "POST", body: JSON.stringify(data ?? {}) }),
|
||||
put: <T>(url: string, data: unknown) =>
|
||||
request<T>(url, { method: "PUT", body: JSON.stringify(data) }),
|
||||
patch: <T>(url: string, data: unknown) =>
|
||||
request<T>(url, { method: "PATCH", body: JSON.stringify(data) }),
|
||||
delete: <T>(url: string) => request<T>(url, { method: "DELETE" }),
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
|
||||
const SESSION_COOKIE_NAME = "fpt_session";
|
||||
const SESSION_DURATION = "30d";
|
||||
|
||||
function getSecretKey() {
|
||||
const secret = process.env.SESSION_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("SESSION_SECRET ist nicht gesetzt.");
|
||||
}
|
||||
return new TextEncoder().encode(secret);
|
||||
}
|
||||
|
||||
export async function createSessionToken(): Promise<string> {
|
||||
return new SignJWT({ auth: true })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(SESSION_DURATION)
|
||||
.sign(getSecretKey());
|
||||
}
|
||||
|
||||
export async function verifySessionToken(token: string): Promise<boolean> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, getSecretKey());
|
||||
return payload.auth === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { SESSION_COOKIE_NAME };
|
||||
@@ -0,0 +1,311 @@
|
||||
import { AHV_COUPLE_CAP_FACTOR, AHV_MAX_PENSION_PER_YEAR } from "@/lib/constants";
|
||||
import type { HouseholdInput, PhaseInput, PlanInput } from "@/lib/types";
|
||||
|
||||
export interface SecurityComputed {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerTag: string;
|
||||
startValue: number;
|
||||
endValue: number;
|
||||
yearly: number[]; // Index 0 = Startwert, Index durationYears = Endwert
|
||||
}
|
||||
|
||||
export interface RealEstateComputed {
|
||||
id: string;
|
||||
name: string;
|
||||
startNet: number;
|
||||
endNetIfKept: number;
|
||||
sold: boolean;
|
||||
saleNetProceeds: number | null;
|
||||
taxAmount: number;
|
||||
endContribution: number; // was tatsaechlich in die Endvermoegens-Summe der Phase einfliesst
|
||||
marketValues: number[];
|
||||
mortgages: number[];
|
||||
}
|
||||
|
||||
export interface RetirementComputed {
|
||||
perPerson: {
|
||||
personId: string;
|
||||
ahvAmount: number;
|
||||
pkPensionAmount: number;
|
||||
lumpSumAmount: number;
|
||||
lumpSumNet: number;
|
||||
}[];
|
||||
combinedAhv: number;
|
||||
ahvCapped: boolean;
|
||||
pkTotal: number;
|
||||
totalPensionIncome: number; // combinedAhv + pkTotal, fliesst als Einkommen in die Phase ein
|
||||
lumpSumGrossTotal: number;
|
||||
lumpSumNetTotal: number; // fliesst als Einmalbetrag in das Endvermoegen der Phase ein
|
||||
}
|
||||
|
||||
export interface PhaseComputed {
|
||||
id: string;
|
||||
name: string;
|
||||
sequenceNumber: number;
|
||||
durationYears: number;
|
||||
incomeFromEntries: number;
|
||||
expenseTotal: number;
|
||||
retirement: RetirementComputed | null;
|
||||
effectiveIncome: number; // incomeFromEntries + retirement.totalPensionIncome
|
||||
savingsQuota: number; // effectiveIncome - expenseTotal
|
||||
allocatedSavings: number; // Summe der jaehrlichen Sparbeitraege auf Wertschriften
|
||||
savingsWarning: boolean;
|
||||
securities: SecurityComputed[];
|
||||
realEstates: RealEstateComputed[];
|
||||
oneTimeNet: number;
|
||||
startWealthNominal: number;
|
||||
endWealthNominal: number;
|
||||
cumulativeInflationStart: number;
|
||||
cumulativeInflationEnd: number;
|
||||
startWealthReal: number;
|
||||
endWealthReal: number;
|
||||
yearlyNominal: number[]; // Laenge durationYears, Werte am Ende von Jahr 1..durationYears
|
||||
yearlyReal: number[];
|
||||
}
|
||||
|
||||
export interface PlanComputed {
|
||||
phases: PhaseComputed[];
|
||||
nachlass: number;
|
||||
totalSavingsWarnings: number;
|
||||
}
|
||||
|
||||
export function computeSecurityYearlyValues(
|
||||
startValue: number,
|
||||
expectedReturn: number,
|
||||
annualContribution: number,
|
||||
durationYears: number
|
||||
): number[] {
|
||||
const values = [startValue];
|
||||
for (let year = 1; year <= durationYears; year++) {
|
||||
const previous = values[year - 1];
|
||||
values.push(previous * (1 + expectedReturn / 100) + annualContribution);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function computeRealEstateYearly(
|
||||
marketValue: number,
|
||||
mortgage: number,
|
||||
valueGrowth: number,
|
||||
amortization: number,
|
||||
durationYears: number
|
||||
): { marketValues: number[]; mortgages: number[] } {
|
||||
const marketValues = [marketValue];
|
||||
const mortgages = [mortgage];
|
||||
for (let year = 1; year <= durationYears; year++) {
|
||||
marketValues.push(marketValues[year - 1] * (1 + valueGrowth / 100));
|
||||
mortgages.push(Math.max(0, mortgages[year - 1] - amortization));
|
||||
}
|
||||
return { marketValues, mortgages };
|
||||
}
|
||||
|
||||
function computeRetirement(
|
||||
household: HouseholdInput,
|
||||
phase: PhaseInput
|
||||
): RetirementComputed | null {
|
||||
if (phase.retirementInfos.length === 0) return null;
|
||||
|
||||
const perPerson = phase.retirementInfos.map((info) => ({
|
||||
personId: info.personId,
|
||||
ahvAmount: info.ahvAmount,
|
||||
pkPensionAmount: info.pkPensionAmount,
|
||||
lumpSumAmount: info.lumpSumAmount,
|
||||
lumpSumNet: info.lumpSumAmount * (1 - info.lumpSumTaxRate / 100),
|
||||
}));
|
||||
|
||||
const ahvSum = perPerson.reduce((sum, p) => sum + p.ahvAmount, 0);
|
||||
const ahvCap = AHV_MAX_PENSION_PER_YEAR * AHV_COUPLE_CAP_FACTOR;
|
||||
const isCoupleBothRetired = household.householdType === "COUPLE" && phase.retirementInfos.length === 2;
|
||||
const combinedAhv = isCoupleBothRetired ? Math.min(ahvSum, ahvCap) : ahvSum;
|
||||
const ahvCapped = isCoupleBothRetired && ahvSum > ahvCap;
|
||||
|
||||
const pkTotal = perPerson.reduce((sum, p) => sum + p.pkPensionAmount, 0);
|
||||
const lumpSumGrossTotal = perPerson.reduce((sum, p) => sum + p.lumpSumAmount, 0);
|
||||
const lumpSumNetTotal = perPerson.reduce((sum, p) => sum + p.lumpSumNet, 0);
|
||||
|
||||
return {
|
||||
perPerson,
|
||||
combinedAhv,
|
||||
ahvCapped,
|
||||
pkTotal,
|
||||
totalPensionIncome: combinedAhv + pkTotal,
|
||||
lumpSumGrossTotal,
|
||||
lumpSumNetTotal,
|
||||
};
|
||||
}
|
||||
|
||||
function computePhase(
|
||||
phase: PhaseInput,
|
||||
household: HouseholdInput,
|
||||
cumulativeInflationStart: number
|
||||
): PhaseComputed {
|
||||
const incomeFromEntries = phase.incomeEntries.reduce((sum, e) => sum + e.amount, 0);
|
||||
const expenseTotal = phase.expenseEntries.reduce((sum, e) => sum + e.amount, 0);
|
||||
const retirement = computeRetirement(household, phase);
|
||||
const effectiveIncome = incomeFromEntries + (retirement?.totalPensionIncome ?? 0);
|
||||
const savingsQuota = effectiveIncome - expenseTotal;
|
||||
const allocatedSavings = phase.securities.reduce((sum, s) => sum + s.annualContribution, 0);
|
||||
const savingsWarning = allocatedSavings > savingsQuota;
|
||||
|
||||
const securities: SecurityComputed[] = phase.securities.map((s) => {
|
||||
const yearly = computeSecurityYearlyValues(
|
||||
s.startValue,
|
||||
s.expectedReturn,
|
||||
s.annualContribution,
|
||||
phase.durationYears
|
||||
);
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
ownerTag: s.ownerTag,
|
||||
startValue: yearly[0],
|
||||
endValue: yearly[phase.durationYears],
|
||||
yearly,
|
||||
};
|
||||
});
|
||||
|
||||
const realEstates: RealEstateComputed[] = phase.realEstates.map((re) => {
|
||||
const { marketValues, mortgages } = computeRealEstateYearly(
|
||||
re.marketValue,
|
||||
re.mortgage,
|
||||
re.valueGrowth,
|
||||
re.amortization,
|
||||
phase.durationYears
|
||||
);
|
||||
const startNet = marketValues[0] - mortgages[0];
|
||||
const endNetIfKept = marketValues[phase.durationYears] - mortgages[phase.durationYears];
|
||||
const sold = re.salePrice != null;
|
||||
let saleNetProceeds: number | null = null;
|
||||
let taxAmount = 0;
|
||||
if (sold) {
|
||||
// Vereinfachung gemaess TDD 3.3: Gewinn = Verkaufspreis - urspruenglich erfasster Startwert
|
||||
const gain = Math.max(0, re.salePrice! - re.marketValue);
|
||||
taxAmount = gain * (re.saleTaxRate / 100);
|
||||
saleNetProceeds = re.salePrice! - taxAmount;
|
||||
}
|
||||
return {
|
||||
id: re.id,
|
||||
name: re.name,
|
||||
startNet,
|
||||
endNetIfKept,
|
||||
sold,
|
||||
saleNetProceeds,
|
||||
taxAmount,
|
||||
endContribution: sold ? saleNetProceeds! : endNetIfKept,
|
||||
marketValues,
|
||||
mortgages,
|
||||
};
|
||||
});
|
||||
|
||||
const oneTimeNet = phase.oneTimeEvents.reduce(
|
||||
(sum, e) => sum + (e.type === "INCOME" ? e.amount : -e.amount),
|
||||
0
|
||||
);
|
||||
|
||||
const startWealthNominal =
|
||||
securities.reduce((sum, s) => sum + s.startValue, 0) +
|
||||
realEstates.reduce((sum, re) => sum + re.startNet, 0);
|
||||
|
||||
const endWealthNominal =
|
||||
securities.reduce((sum, s) => sum + s.endValue, 0) +
|
||||
realEstates.reduce((sum, re) => sum + re.endContribution, 0) +
|
||||
oneTimeNet +
|
||||
(retirement?.lumpSumNetTotal ?? 0);
|
||||
|
||||
const inflationRate = phase.inflationRate ?? household.inflationRateDefault;
|
||||
// TDD Kapitel 3.5: kumulierte Inflation ist ein Produkt ueber die Phasen (ein Faktor
|
||||
// pro Phase), nicht ueber einzelne Jahre. Bewusst woertlich gemaess Spezifikation umgesetzt.
|
||||
const cumulativeInflationEnd = cumulativeInflationStart * (1 + inflationRate / 100);
|
||||
|
||||
const yearlyNominal: number[] = [];
|
||||
for (let year = 1; year <= phase.durationYears; year++) {
|
||||
let value =
|
||||
securities.reduce((sum, s) => sum + s.yearly[year], 0) +
|
||||
realEstates.reduce((sum, re) => sum + (re.marketValues[year] - re.mortgages[year]), 0);
|
||||
if (year === phase.durationYears) {
|
||||
// Einmalige Ereignisse, Verkaufserloese und Kapitalbezuege schlagen erst am Ende
|
||||
// der Phase zu Buche (siehe Phasenuebergang, TDD Kapitel 10).
|
||||
value += oneTimeNet + (retirement?.lumpSumNetTotal ?? 0);
|
||||
const soldReplacement = realEstates.reduce(
|
||||
(sum, re) => sum + (re.sold ? re.saleNetProceeds! - (re.marketValues[year] - re.mortgages[year]) : 0),
|
||||
0
|
||||
);
|
||||
value += soldReplacement;
|
||||
}
|
||||
yearlyNominal.push(value);
|
||||
}
|
||||
// Vereinfachung: innerhalb einer Phase wird fuer den Realwert durchgehend die am
|
||||
// Phasenende gueltige kumulierte Inflation verwendet (siehe cumulativeInflationEnd oben).
|
||||
const yearlyReal = yearlyNominal.map((v) => v / cumulativeInflationEnd);
|
||||
|
||||
return {
|
||||
id: phase.id,
|
||||
name: phase.name,
|
||||
sequenceNumber: phase.sequenceNumber,
|
||||
durationYears: phase.durationYears,
|
||||
incomeFromEntries,
|
||||
expenseTotal,
|
||||
retirement,
|
||||
effectiveIncome,
|
||||
savingsQuota,
|
||||
allocatedSavings,
|
||||
savingsWarning,
|
||||
securities,
|
||||
realEstates,
|
||||
oneTimeNet,
|
||||
startWealthNominal,
|
||||
endWealthNominal,
|
||||
cumulativeInflationStart,
|
||||
cumulativeInflationEnd,
|
||||
startWealthReal: startWealthNominal / cumulativeInflationStart,
|
||||
endWealthReal: endWealthNominal / cumulativeInflationEnd,
|
||||
yearlyNominal,
|
||||
yearlyReal,
|
||||
};
|
||||
}
|
||||
|
||||
export function computePlan(plan: PlanInput, household: HouseholdInput): PlanComputed {
|
||||
const orderedPhases = [...plan.phases].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
||||
|
||||
let cumulativeInflation = 1;
|
||||
const phases: PhaseComputed[] = [];
|
||||
for (const phase of orderedPhases) {
|
||||
const computed = computePhase(phase, household, cumulativeInflation);
|
||||
cumulativeInflation = computed.cumulativeInflationEnd;
|
||||
phases.push(computed);
|
||||
}
|
||||
|
||||
const nachlass = phases.length > 0 ? phases[phases.length - 1].endWealthNominal : 0;
|
||||
const totalSavingsWarnings = phases.filter((p) => p.savingsWarning).length;
|
||||
|
||||
return { phases, nachlass, totalSavingsWarnings };
|
||||
}
|
||||
|
||||
export function planToCsv(plan: PlanInput, planComputed: PlanComputed): string {
|
||||
const header = [
|
||||
"Phase",
|
||||
"Dauer (Jahre)",
|
||||
"Startvermoegen (nominal)",
|
||||
"Endvermoegen (nominal)",
|
||||
"Endvermoegen (real)",
|
||||
"Einkommen",
|
||||
"Ausgaben",
|
||||
"Sparquote",
|
||||
"Verplante Sparbeitraege",
|
||||
"Einmalige Ereignisse (netto)",
|
||||
];
|
||||
const rows = planComputed.phases.map((p) => [
|
||||
p.name,
|
||||
String(p.durationYears),
|
||||
p.startWealthNominal.toFixed(2),
|
||||
p.endWealthNominal.toFixed(2),
|
||||
p.endWealthReal.toFixed(2),
|
||||
p.effectiveIncome.toFixed(2),
|
||||
p.expenseTotal.toFixed(2),
|
||||
p.savingsQuota.toFixed(2),
|
||||
p.allocatedSavings.toFixed(2),
|
||||
p.oneTimeNet.toFixed(2),
|
||||
]);
|
||||
return [header, ...rows].map((r) => r.join(";")).join("\n");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// AHV-Maximalrente (Einzelperson, CHF/Jahr). Aendert sich periodisch durch Anpassungen
|
||||
// des Bundes -- deshalb hier als einzelner konfigurierbarer Systemparameter gefuehrt
|
||||
// (TDD Kapitel 3.4), nicht hart im Code verteilt.
|
||||
export const AHV_MAX_PENSION_PER_YEAR = 30240;
|
||||
|
||||
// Faktor fuer die Plafonierung der AHV-Rente bei Ehepaaren (TDD Kapitel 3.4).
|
||||
export const AHV_COUPLE_CAP_FACTOR = 1.5;
|
||||
@@ -0,0 +1,28 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
// Nur von API-Routes (Node.js-Runtime) verwendet -- niemals von middleware.ts
|
||||
// importieren, da dort (Edge-Runtime) kein Datenbankzugriff moeglich ist.
|
||||
|
||||
// Es gibt genau eine AppCredential-Zeile. Solange keine existiert, ist die App
|
||||
// "unconfigured" und der naechste Login-Versuch legt das Passwort fest.
|
||||
export async function getAppCredential() {
|
||||
return prisma.appCredential.findFirst();
|
||||
}
|
||||
|
||||
export async function setAppPassword(password: string) {
|
||||
const existing = await getAppCredential();
|
||||
if (existing) {
|
||||
// Sollte durch die UI (Passwort-Setup nur beim ersten Login sichtbar) nicht
|
||||
// vorkommen, wird aber sicherheitshalber serverseitig verhindert.
|
||||
throw new Error("Es ist bereits ein Passwort gesetzt.");
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
return prisma.appCredential.create({ data: { passwordHash } });
|
||||
}
|
||||
|
||||
export async function verifyAppPassword(password: string): Promise<boolean> {
|
||||
const credential = await getAppCredential();
|
||||
if (!credential) return false;
|
||||
return bcrypt.compare(password, credential.passwordHash);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import type { HouseholdInput, PlanInput } from "@/lib/types";
|
||||
|
||||
export const phaseInclude = {
|
||||
incomeEntries: true,
|
||||
expenseEntries: true,
|
||||
securities: true,
|
||||
realEstates: true,
|
||||
oneTimeEvents: true,
|
||||
retirementInfos: true,
|
||||
} satisfies Prisma.PhaseInclude;
|
||||
|
||||
export const planInclude = {
|
||||
phases: {
|
||||
include: phaseInclude,
|
||||
orderBy: { sequenceNumber: "asc" },
|
||||
},
|
||||
} satisfies Prisma.PlanInclude;
|
||||
|
||||
export type PlanWithRelations = Prisma.PlanGetPayload<{ include: typeof planInclude }>;
|
||||
export type PhaseWithRelations = Prisma.PhaseGetPayload<{ include: typeof phaseInclude }>;
|
||||
export type HouseholdWithPersons = Prisma.HouseholdGetPayload<{ include: { persons: true } }>;
|
||||
|
||||
export function toHouseholdInput(household: HouseholdWithPersons): HouseholdInput {
|
||||
return {
|
||||
id: household.id,
|
||||
householdType: household.householdType,
|
||||
inflationRateDefault: household.inflationRateDefault,
|
||||
persons: household.persons.map((p) => ({
|
||||
id: p.id,
|
||||
role: p.role,
|
||||
age: p.age,
|
||||
retirementAge: p.retirementAge,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
||||
return {
|
||||
id: plan.id,
|
||||
name: plan.name,
|
||||
parentPlanId: plan.parentPlanId,
|
||||
branchFromPhaseId: plan.branchFromPhaseId,
|
||||
phases: plan.phases.map((phase) => ({
|
||||
id: phase.id,
|
||||
sequenceNumber: phase.sequenceNumber,
|
||||
name: phase.name,
|
||||
durationYears: phase.durationYears,
|
||||
inflationRate: phase.inflationRate,
|
||||
incomeMode: phase.incomeMode,
|
||||
incomeEntries: phase.incomeEntries.map((e) => ({
|
||||
id: e.id,
|
||||
personId: e.personId,
|
||||
label: e.label,
|
||||
amount: e.amount,
|
||||
})),
|
||||
expenseEntries: phase.expenseEntries.map((e) => ({
|
||||
id: e.id,
|
||||
label: e.label,
|
||||
amount: e.amount,
|
||||
})),
|
||||
securities: phase.securities.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
startValue: s.startValue,
|
||||
expectedReturn: s.expectedReturn,
|
||||
annualContribution: s.annualContribution,
|
||||
ownerTag: s.ownerTag,
|
||||
saleTaxRate: s.saleTaxRate,
|
||||
})),
|
||||
realEstates: phase.realEstates.map((re) => ({
|
||||
id: re.id,
|
||||
name: re.name,
|
||||
marketValue: re.marketValue,
|
||||
mortgage: re.mortgage,
|
||||
valueGrowth: re.valueGrowth,
|
||||
amortization: re.amortization,
|
||||
salePrice: re.salePrice,
|
||||
saleTaxRate: re.saleTaxRate,
|
||||
})),
|
||||
oneTimeEvents: phase.oneTimeEvents.map((e) => ({
|
||||
id: e.id,
|
||||
type: e.type,
|
||||
amount: e.amount,
|
||||
description: e.description,
|
||||
})),
|
||||
retirementInfos: phase.retirementInfos.map((r) => ({
|
||||
id: r.id,
|
||||
personId: r.personId,
|
||||
ahvAmount: r.ahvAmount,
|
||||
pkPensionAmount: r.pkPensionAmount,
|
||||
lumpSumAmount: r.lumpSumAmount,
|
||||
lumpSumTaxRate: r.lumpSumTaxRate,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHouseholdOrNull(): Promise<HouseholdWithPersons | null> {
|
||||
return prisma.household.findFirst({ include: { persons: true } });
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Domain-Typen fuer die Berechnungslogik (lib/calculations.ts) und die API-Payloads.
|
||||
// Bewusst von den generierten Prisma-Typen entkoppelt, damit die Berechnungslogik
|
||||
// unabhaengig von der konkreten DB-Repraesentation testbar bleibt.
|
||||
|
||||
export type HouseholdType = "SINGLE" | "COUPLE";
|
||||
export type PersonRole = "PERSON_A" | "PERSON_B";
|
||||
export type IncomeMode = "PER_PERSON" | "HOUSEHOLD";
|
||||
export type OwnerTag = "PERSON_A" | "PERSON_B" | "HOUSEHOLD";
|
||||
export type OneTimeEventType = "INCOME" | "EXPENSE";
|
||||
export type TransitionDecision = "CARRY_OVER" | "SELL";
|
||||
export type PositionType = "SECURITY" | "REAL_ESTATE";
|
||||
|
||||
export interface PersonInput {
|
||||
id: string;
|
||||
role: PersonRole;
|
||||
age: number;
|
||||
retirementAge: number;
|
||||
}
|
||||
|
||||
export interface HouseholdInput {
|
||||
id: string;
|
||||
householdType: HouseholdType;
|
||||
inflationRateDefault: number;
|
||||
persons: PersonInput[];
|
||||
}
|
||||
|
||||
export interface IncomeEntryInput {
|
||||
id: string;
|
||||
personId: string | null;
|
||||
label: string | null;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface ExpenseEntryInput {
|
||||
id: string;
|
||||
label: string | null;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface SecurityInput {
|
||||
id: string;
|
||||
name: string;
|
||||
startValue: number;
|
||||
expectedReturn: number;
|
||||
annualContribution: number;
|
||||
ownerTag: OwnerTag;
|
||||
saleTaxRate: number;
|
||||
}
|
||||
|
||||
export interface RealEstateInput {
|
||||
id: string;
|
||||
name: string;
|
||||
marketValue: number;
|
||||
mortgage: number;
|
||||
valueGrowth: number;
|
||||
amortization: number;
|
||||
salePrice: number | null;
|
||||
saleTaxRate: number;
|
||||
}
|
||||
|
||||
export interface OneTimeEventInput {
|
||||
id: string;
|
||||
type: OneTimeEventType;
|
||||
amount: number;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface RetirementInfoInput {
|
||||
id: string;
|
||||
personId: string;
|
||||
ahvAmount: number;
|
||||
pkPensionAmount: number;
|
||||
lumpSumAmount: number;
|
||||
lumpSumTaxRate: number;
|
||||
}
|
||||
|
||||
export interface PhaseInput {
|
||||
id: string;
|
||||
sequenceNumber: number;
|
||||
name: string;
|
||||
durationYears: number;
|
||||
inflationRate: number | null;
|
||||
incomeMode: IncomeMode;
|
||||
incomeEntries: IncomeEntryInput[];
|
||||
expenseEntries: ExpenseEntryInput[];
|
||||
securities: SecurityInput[];
|
||||
realEstates: RealEstateInput[];
|
||||
oneTimeEvents: OneTimeEventInput[];
|
||||
retirementInfos: RetirementInfoInput[];
|
||||
}
|
||||
|
||||
export interface PlanInput {
|
||||
id: string;
|
||||
name: string;
|
||||
parentPlanId: string | null;
|
||||
branchFromPhaseId: string | null;
|
||||
phases: PhaseInput[];
|
||||
}
|
||||
Reference in New Issue
Block a user