// Die Übersicht der offenen Punkte (0.39). // // Sie ersetzt den FPT-Assistenten. Getestet wird die Regel dahinter: Wann gilt etwas als // offen, wann als erledigt -- und die beiden Sonderzustände am Anfang, in denen eine Zahl // eine Lüge wäre. import { describe, it, expect } from "vitest"; import { computePlan } from "@/lib/calculations"; import { reviewPlan, unconfirmedCells, isCellConfirmed } from "@/lib/review"; import type { PhaseData } from "@/lib/elements"; import type { PlanInput } from "@/lib/types"; function plan(over: Partial = {}): PlanInput { return { id: "plan", name: "T", householdType: "SINGLE", inflationRateDefault: 0, initialCash: 0, startYear: 2026, persons: [{ id: "A", role: "PERSON_A", name: null, age: 40, retirementAge: 65 }], phases: [], elements: [], ...over, }; } function phase(id: string, seq: number, years: number, ratesConfirmed = true) { return { id, sequenceNumber: seq, name: id, durationYears: years, cashTransition: { mode: "NONE" as const }, ratesConfirmed }; } let n = 0; function el(category: string, phaseValues: Record = {}) { return { id: `e${n++}`, category: category as never, name: category, ownerRole: "HOUSEHOLD" as never, orderIndex: n, phaseValues, transitionValues: {}, baseData: {}, }; } const ok = { confirmed: true }; describe("Die beiden Startzustände", () => { // Ohne Elemente und ohne Phasen gibt es naturgemäss nichts Offenes -- der Plan ist trotzdem // leer. Eine «0» wäre dort eine Lüge, deshalb eigene Zustände. it("meldet einen Plan ohne Elemente als NO_ELEMENTS, nicht als fertig", () => { const p = plan(); const r = reviewPlan(p, computePlan(p)); expect(r.stage).toBe("NO_ELEMENTS"); expect(r.total).toBe(0); }); it("meldet Elemente ohne Lebensphasen als NO_PHASES", () => { const p = plan({ elements: [el("OTHER_ASSET")] }); expect(reviewPlan(p, computePlan(p)).stage).toBe("NO_PHASES"); }); }); describe("Offene Punkte je Phase", () => { it("zählt jede unbestätigte Zelle", () => { const p = plan({ phases: [phase("p1", 1, 10)], elements: [el("INCOME", { p1: {} }), el("EXPENSE", { p1: {} }), el("OTHER_ASSET", { p1: ok })], }); const c = computePlan(p); expect(unconfirmedCells(p, c, "p1")).toHaveLength(2); const r = reviewPlan(p, c); expect(r.stage).toBe("OPEN"); expect(r.total).toBe(2); expect(r.groups[0].reasons[0]).toContain("2 Annahmen"); }); it("zählt die nicht verteilte Sparquote eigens", () => { // Man kann jede Zelle angeschaut und die Quote trotzdem nie verteilt haben -- dann bliebe // alles still auf dem Cash-Konto. const p = plan({ phases: [phase("p1", 1, 10, false)], elements: [el("INCOME", { p1: ok })], }); const r = reviewPlan(p, computePlan(p)); expect(r.total).toBe(1); expect(r.groups[0].reasons[0]).toContain("Sparquote nicht verteilt"); }); it("ist fertig, wenn alles bestätigt und verteilt ist", () => { const p = plan({ phases: [phase("p1", 1, 10)], elements: [el("INCOME", { p1: ok }), el("EXPENSE", { p1: ok })], }); const r = reviewPlan(p, computePlan(p)); expect(r.stage).toBe("DONE"); expect(r.total).toBe(0); expect(r.groups).toHaveLength(0); }); it("verlangt keine Bestätigung für eine Zelle ohne Annahmen", () => { // Seit 0.40 stehen Bestände in den Stammdaten und Raten im Verteil-Dialog. Eine // «Sonstige Schuld» trägt in der Phasenzelle damit gar nichts mehr -- eine Bestätigung // wäre eine Frage ohne Gegenstand. const p = plan({ phases: [phase("p1", 1, 10)], elements: [el("OTHER_DEBT", { p1: {} })], }); const c = computePlan(p); expect(unconfirmedCells(p, c, "p1")).toHaveLength(0); expect(reviewPlan(p, c).stage).toBe("DONE"); }); it("verlangt keine Bestätigung für ein verkauftes Element", () => { // Ein verkauftes Haus trägt in der Folgephase keine Annahmen mehr. const p = plan({ phases: [phase("p1", 1, 5), phase("p2", 2, 5)], elements: [ el("INCOME", { p1: ok, p2: ok }), { ...el("REAL_ESTATE", { p1: { ...ok, purchasePrice: 800000, mortgage: 500000 }, p2: {} }), transitionValues: { p1: { decision: "SELL" as const, salePrice: 900000 } }, }, ], }); const c = computePlan(p); // In p2 ist die Immobilie verkauft -- nur das Einkommen zählt, und das ist bestätigt. expect(unconfirmedCells(p, c, "p2")).toHaveLength(0); }); }); describe("Bestätigen friert nichts ein", () => { // Der wichtigste Punkt: Der Haken steht NEBEN den Werten. Eine bestätigte Phase erbt // weiterhin live aus der Vorphase -- sonst wäre die Feld-Vererbung hinfällig. it("lässt die Vererbung unberührt", () => { const p = plan({ phases: [phase("p1", 1, 10), phase("p2", 2, 10)], elements: [ { ...el("OTHER_ASSET"), baseData: { startValue: 100000, expectedReturn: 5 }, // p2 ist bestätigt, hat aber KEINEN eigenen Renditewert -- sie muss erben. phaseValues: { p1: ok, p2: ok }, }, ], }); const c = computePlan(p); const p2 = c.phases[1].elements.find((e) => e.category === "OTHER_ASSET")!; // 100'000 über 20 Jahre zu 5 % -- die Rendite gilt in beiden Phasen. Auf den Franken // genau geht es nicht: Der Rechenkern rundet an jeder Phasengrenze. expect(p2.endValue).toBeCloseTo(100000 * Math.pow(1.05, 20), -1); expect(isCellConfirmed(p, "p2", p.elements[0].id)).toBe(true); }); });