Fundament: Element-Stammdaten, Fixpunkte, Horizont in Jahren, PK-Bezugsalter
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+88
-14
@@ -11,40 +11,114 @@
|
||||
|
||||
export type SegmentType = "ERWERB" | "MIXED" | "PENSION";
|
||||
|
||||
// Ein Zeitpunkt, an dem ZWINGEND eine neue Lebensphase beginnen muss.
|
||||
//
|
||||
// Bis 0.35 gab es genau einen pro Person: das Erwerbsende. Seit 0.36 sind es bis zu vier --
|
||||
// jeder Beginn eines Renten- oder Kapitalbezugs ist einer. Der Grund ist derselbe wie beim
|
||||
// Erwerbsende: Die Rechnung leitet Erwerbsstatus und Bezuege am PHASENBEGINN ab. Faellt ein
|
||||
// Bezug mitten in eine Phase, waere der halbe Phasenertrag falsch.
|
||||
export type FixpointKind = "RETIREMENT" | "AHV" | "PENSION_FUND" | "PILLAR_3A";
|
||||
|
||||
export interface Fixpoint {
|
||||
// Jahre ab Planbeginn (1-basiert wie die Phasengrenzen: Jahr 5 = Ende des 5. Planjahres).
|
||||
year: number;
|
||||
age: number;
|
||||
role: string;
|
||||
kind: FixpointKind;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface PlanSegment {
|
||||
type: SegmentType;
|
||||
fromYear: number; // Jahre ab Planbeginn (Beginn des Abschnitts)
|
||||
// Feste Länge in Jahren (durch den nächsten Pensionierungspunkt bestimmt) ODER null für den
|
||||
// letzten, OFFENEN Abschnitt (Pension bis Lebensende -- Dauer frei wählbar).
|
||||
// Feste Länge in Jahren (durch den nächsten Fixpunkt bestimmt) ODER null für den letzten,
|
||||
// OFFENEN Abschnitt (Dauer frei wählbar bzw. durch den Planungshorizont bestimmt).
|
||||
fixedYears: number | null;
|
||||
// Welche Ereignisse den Abschnitt beenden -- für die Beschriftung der Zeitachse.
|
||||
endedBy: Fixpoint[];
|
||||
}
|
||||
|
||||
// Ein Person-Erwerbsstatus wechselt genau einmal: im Jahr `retirementAge - age` (auf 0 gekappt,
|
||||
// falls bei Planbeginn bereits pensioniert). Die distinkten dieser Zeitpunkte sind die
|
||||
// Abschnittsgrenzen; der Abschnitt nach dem letzten Zeitpunkt ist offen.
|
||||
export function planSegments(persons: { age: number; retirementAge: number }[]): PlanSegment[] {
|
||||
if (persons.length === 0) return [{ type: "PENSION", fromYear: 0, fixedYears: null }];
|
||||
export interface FixpointPerson {
|
||||
// Optional, damit Aufrufer, die nur Abschnitte brauchen (Zeitachse), nichts erfinden muessen.
|
||||
role?: string;
|
||||
name?: string | null;
|
||||
age: number;
|
||||
retirementAge: number;
|
||||
// Bezugsalter aus dem Pensionierungs-Entscheid. Fehlen sie, gilt das Erwerbsende.
|
||||
ahvStartAge?: number;
|
||||
pkWithdrawalAge?: number;
|
||||
pillar3aAges?: number[];
|
||||
}
|
||||
|
||||
const KIND_LABEL: Record<FixpointKind, string> = {
|
||||
RETIREMENT: "Erwerbsende",
|
||||
AHV: "AHV-Rente",
|
||||
PENSION_FUND: "Pensionskasse",
|
||||
PILLAR_3A: "Säule 3a",
|
||||
};
|
||||
|
||||
// Alle Fixpunkte, sortiert und ohne Duplikate im selben Jahr (mehrere Ereignisse im gleichen
|
||||
// Jahr brauchen nur EINE Phasengrenze -- sie wird dann mehrfach beschriftet).
|
||||
export function planFixpoints(persons: FixpointPerson[]): Fixpoint[] {
|
||||
const out: Fixpoint[] = [];
|
||||
const add = (person: FixpointPerson, age: number | undefined, kind: FixpointKind) => {
|
||||
if (typeof age !== "number") return;
|
||||
const year = Math.round(age - person.age);
|
||||
// Ereignisse vor oder bei Planbeginn sind keine Grenze -- sie sind bereits Geschichte.
|
||||
if (year <= 0) return;
|
||||
out.push({
|
||||
year,
|
||||
age: Math.round(age),
|
||||
role: person.role ?? "PERSON_A",
|
||||
kind,
|
||||
label: `${KIND_LABEL[kind]}${person.name ? " " + person.name : ""} (${Math.round(age)})`,
|
||||
});
|
||||
};
|
||||
for (const p of persons) {
|
||||
add(p, p.retirementAge, "RETIREMENT");
|
||||
add(p, p.ahvStartAge, "AHV");
|
||||
add(p, p.pkWithdrawalAge, "PENSION_FUND");
|
||||
for (const a of p.pillar3aAges ?? []) add(p, a, "PILLAR_3A");
|
||||
}
|
||||
return out.sort((a, b) => a.year - b.year || a.kind.localeCompare(b.kind));
|
||||
}
|
||||
|
||||
// Die distinkten Fixpunkt-Jahre sind die Abschnittsgrenzen; der Abschnitt nach dem letzten
|
||||
// ist offen.
|
||||
export function planSegments(persons: FixpointPerson[]): PlanSegment[] {
|
||||
if (persons.length === 0) return [{ type: "PENSION", fromYear: 0, fixedYears: null, endedBy: [] }];
|
||||
|
||||
const fixpoints = planFixpoints(persons);
|
||||
const retYears = persons.map((p) => Math.max(0, p.retirementAge - p.age));
|
||||
const events = [...new Set(retYears.filter((r) => r > 0))].sort((a, b) => a - b);
|
||||
const boundaries = [0, ...events];
|
||||
const years = [...new Set(fixpoints.map((f) => f.year))].sort((a, b) => a - b);
|
||||
const boundaries = [0, ...years];
|
||||
|
||||
const segments: PlanSegment[] = [];
|
||||
for (let i = 0; i < boundaries.length; i++) {
|
||||
const from = boundaries[i];
|
||||
const next = i + 1 < boundaries.length ? boundaries[i + 1] : null;
|
||||
// Eine Person arbeitet in einem Abschnitt, der bei Jahr `from` beginnt, genau dann, wenn
|
||||
// ihr Pensionierungszeitpunkt echt später liegt.
|
||||
// ihr Erwerbsende echt später liegt.
|
||||
const working = retYears.filter((r) => r > from).length;
|
||||
const type: SegmentType = working === persons.length ? "ERWERB" : working > 0 ? "MIXED" : "PENSION";
|
||||
segments.push({ type, fromYear: from, fixedYears: next === null ? null : next - from });
|
||||
segments.push({
|
||||
type,
|
||||
fromYear: from,
|
||||
fixedYears: next === null ? null : next - from,
|
||||
endedBy: next === null ? [] : fixpoints.filter((f) => f.year === next),
|
||||
});
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
// Vorgeschlagene Dauer des offenen Pensions-Abschnitts: bis das älteste Haushaltsmitglied
|
||||
// ungefähr 90 ist, mindestens aber 5 Jahre.
|
||||
export function defaultOpenDuration(persons: { age: number }[], fromYear: number): number {
|
||||
// Vorgeschlagene Dauer des offenen Abschnitts. Mit erfasstem Planungshorizont ergibt sie sich
|
||||
// daraus; ohne ihn bis etwa Alter 90, mindestens aber 5 Jahre.
|
||||
export function defaultOpenDuration(
|
||||
persons: { age: number }[],
|
||||
fromYear: number,
|
||||
horizonYears?: number | null
|
||||
): number {
|
||||
if (typeof horizonYears === "number" && horizonYears > fromYear) return horizonYears - fromYear;
|
||||
const oldestNow = persons.length > 0 ? Math.max(...persons.map((p) => p.age)) : 65;
|
||||
return Math.max(5, 90 - (oldestNow + fromYear));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user