Teilverkauf (Sonstiges Vermoegen) + Sonderamortisation (Immobilie)
Deploy App / deploy (push) Successful in 1m1s
Deploy App / deploy (push) Successful in 1m1s
Roadmap Nr. 42: OTHER_ASSET am Uebergang neu mit Teilverkauf. Neben Halten und
Vollverkauf kann ein Betrag ins Cash fliessen, der Rest bleibt investiert und
waechst weiter. Der Teilverkauf erscheint als "Kapitalzufluss" im Phasenkopf
("zu investierendes Kapital"). Abgegrenzt zur laufenden Bezugsrate: der Teilverkauf
ist die einmalige Entnahme AM Uebergang, die Bezugsrate der laufende Verzehr IN der
Phase -- beide koexistieren.
Roadmap Nr. 15: REAL_ESTATE im Halten-Fall neu mit Sonderamortisation -- Einmaltilgung
der Hypothek aus dem Cash, am Restsaldo gekappt, bringt die Immobilie auf Augenhoehe
mit OTHER_DEBT (immediateRepayment). Erscheint als "Kapitalinvestition" im Phasenkopf.
Damit laesst sich die indirekte Amortisation via 3a MECHANISCH nachbilden (3a wachsen
lassen -> bei Pensionierung ins Cash -> Sonderamortisation).
Die Steuerwirkung der indirekten Amortisation (3a-Abzug, Zinsabzug) bleibt bewusst
zurueckgestellt mit dem Steuer-Buendel 13/14/23 (SPEZ 9.14).
Fuenf Regressionstests (36 -> 41). Spezifikation auf v0.6. Keine Verhaltensaenderung
fuer bestehende Plaene.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,81 @@ describe("AHV-Rentenformel (Skala 44)", () => {
|
||||
// Netto -> Brutto fuer die AHV (AHV_GROSS_FROM_NET_FACTOR).
|
||||
const BRUTTO = 1.12;
|
||||
|
||||
describe("Teilverkauf Sonstiges Vermoegen (Roadmap 42)", () => {
|
||||
function p42(decision: "HOLD" | "SELL" | "PARTIAL", amount?: number) {
|
||||
return plan({
|
||||
age: 40, retirementAge: 70, inflation: 0, initialCash: 0,
|
||||
phases: [
|
||||
{ id: "p1", durationYears: 1 },
|
||||
{ id: "p2", durationYears: 1 },
|
||||
],
|
||||
elements: [
|
||||
el(
|
||||
"OTHER_ASSET", "HOUSEHOLD",
|
||||
{ p1: { startValue: 100000, expectedReturn: 0, annualContribution: 0 }, p2: {} },
|
||||
{ p1: { decision, partialSaleAmount: amount } }
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
const asset = (pl: PlanInput, i: number) =>
|
||||
computePlan(pl).phases[i].elements.find((e) => e.category === "OTHER_ASSET")!;
|
||||
|
||||
it("Teilverkauf: Betrag fliesst ins Cash, Rest bleibt investiert", () => {
|
||||
const r = computePlan(p42("PARTIAL", 30000));
|
||||
expect(r.phases[1].capitalInflow).toBe(30000); // "zu investierendes Kapital" im Phasenkopf
|
||||
expect(r.phases[1].cashStart).toBe(30000);
|
||||
expect(asset(p42("PARTIAL", 30000), 1).startValue).toBe(70000); // Rest laeuft weiter
|
||||
});
|
||||
|
||||
it("Teilverkauf am Endwert gekappt; Vollverkauf und Halten unveraendert", () => {
|
||||
expect(computePlan(p42("PARTIAL", 999999)).phases[1].capitalInflow).toBe(100000);
|
||||
expect(computePlan(p42("SELL")).phases[1].capitalInflow).toBe(100000);
|
||||
expect(computePlan(p42("HOLD")).phases[1].capitalInflow).toBe(0);
|
||||
expect(asset(p42("HOLD"), 1).startValue).toBe(100000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sonderamortisation Hypothek (Roadmap 15)", () => {
|
||||
function p15(extra?: number) {
|
||||
return plan({
|
||||
age: 40, retirementAge: 70, inflation: 0, initialCash: 500000,
|
||||
phases: [
|
||||
{ id: "p1", durationYears: 5 },
|
||||
{ id: "p2", durationYears: 5 },
|
||||
],
|
||||
elements: [
|
||||
el(
|
||||
"REAL_ESTATE", "HOUSEHOLD",
|
||||
{ p1: { purchasePrice: 1000000, mortgage: 800000, amortization: 0 }, p2: {} },
|
||||
{ p1: { decision: "HOLD", extraAmortization: extra } }
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
const immo = (pl: PlanInput, i: number) =>
|
||||
computePlan(pl).phases[i].elements.find((e) => e.category === "REAL_ESTATE")!;
|
||||
|
||||
it("Einmaltilgung senkt Restschuld und belastet das Cash", () => {
|
||||
const r = computePlan(p15(300000));
|
||||
expect(r.phases[1].capitalInvest).toBe(300000); // erscheint als Kapitalinvestition
|
||||
expect(r.phases[1].cashStart).toBe(200000); // 500'000 - 300'000
|
||||
// Netto-Immobilienwert steigt um die Tilgung: 1 Mio - (800k - 300k) = 500'000.
|
||||
expect(immo(p15(300000), 1).startValue).toBe(500000);
|
||||
});
|
||||
|
||||
it("Einmaltilgung an der Restschuld gekappt", () => {
|
||||
const r = computePlan(p15(999999));
|
||||
expect(r.phases[1].capitalInvest).toBe(800000); // nie mehr als die Resthypothek
|
||||
expect(immo(p15(999999), 1).startValue).toBe(1000000); // schuldenfrei
|
||||
});
|
||||
|
||||
it("ohne Sonderamortisation bleibt alles beim Alten", () => {
|
||||
expect(computePlan(p15()).phases[1].cashStart).toBe(500000);
|
||||
expect(immo(p15(), 1).startValue).toBe(200000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Immobilie: Zins und Wertsteigerung", () => {
|
||||
function immoPlan(pd: PhaseData, jahre = 10) {
|
||||
return plan({
|
||||
|
||||
+12
-1
@@ -36,6 +36,7 @@ export interface ElementPhaseComputed {
|
||||
carried: boolean; // Phase >= 2: Start-/Basiswert wird aus der Vorphase fortgeschrieben
|
||||
baseValue: number; // fortgeschriebener Basiswert (read-only Anzeige ab Phase 2; ohne Zusatzeinlage)
|
||||
mortgageStart: number; // nur REAL_ESTATE: Resthypothek zu Phasenbeginn (0 sonst)
|
||||
mortgageEnd: number; // nur REAL_ESTATE: Resthypothek zu Phasenende (0 sonst)
|
||||
startValue: number; // Wert/Flow zu Phasenbeginn (Aktiven +, Schulden -, Einkommen/Ausgaben = Flow Jahr 1)
|
||||
endValue: number; // Wert/Flow am Phasenende (letztes Jahr)
|
||||
summary: string;
|
||||
@@ -361,6 +362,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
||||
carried: carry.hasCarry,
|
||||
baseValue: 0,
|
||||
mortgageStart: 0,
|
||||
mortgageEnd: 0,
|
||||
startValue: 0,
|
||||
endValue: 0,
|
||||
summary: "",
|
||||
@@ -647,6 +649,7 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
||||
}
|
||||
for (const re of realEstates) {
|
||||
re.ec.endValue = Math.round(re.value - re.mortgage);
|
||||
re.ec.mortgageEnd = Math.round(re.mortgage);
|
||||
re.ec.summary = fmt(re.ec.endValue);
|
||||
wealthEnd += re.ec.endValue;
|
||||
}
|
||||
@@ -803,6 +806,11 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
||||
if (td.decision === "SELL") {
|
||||
txInflow += ec.endValue;
|
||||
carry.status = "SOLD";
|
||||
} else if (td.decision === "PARTIAL") {
|
||||
// Teilverkauf: Betrag fliesst ins Cash, der Rest bleibt aktiv und waechst weiter.
|
||||
const part = Math.min(ec.endValue, Math.round(num(td.partialSaleAmount)));
|
||||
txInflow += part;
|
||||
carry.value = ec.endValue - part;
|
||||
} else {
|
||||
carry.value = ec.endValue;
|
||||
}
|
||||
@@ -820,7 +828,10 @@ export function computePlan(plan: PlanInput): PlanComputed {
|
||||
txInflow += Math.round(salePrice - re.mortgage - tax);
|
||||
carry.status = "SOLD";
|
||||
} else {
|
||||
carry.mortgage = re.mortgage;
|
||||
// Sonderamortisation: Einmaltilgung der Hypothek aus dem Cash, am Restsaldo gekappt.
|
||||
const extra = Math.min(re.mortgage, Math.round(num(td.extraAmortization)));
|
||||
if (extra > 0) txImmediateRepay += extra;
|
||||
carry.mortgage = re.mortgage - extra;
|
||||
carry.propertyValue = re.value;
|
||||
carry.propertyPurchase = re.purchase;
|
||||
}
|
||||
|
||||
+8
-2
@@ -88,7 +88,7 @@ export interface PhaseData {
|
||||
annualRepayment?: number;
|
||||
}
|
||||
|
||||
export type TransitionDecision = "HOLD" | "SELL";
|
||||
export type TransitionDecision = "HOLD" | "SELL" | "PARTIAL";
|
||||
export type PkPayoutMode = "CAPITAL" | "PENSION" | "COMBI";
|
||||
|
||||
export interface TransitionData {
|
||||
@@ -111,6 +111,10 @@ export interface TransitionData {
|
||||
// REAL_ESTATE / OTHER_ASSET
|
||||
decision?: TransitionDecision;
|
||||
salePrice?: number;
|
||||
// OTHER_ASSET (Teilverkauf): Betrag, der am Uebergang ins Cash fliesst; der Rest bleibt aktiv.
|
||||
partialSaleAmount?: number;
|
||||
// REAL_ESTATE (Sonderamortisation): Einmaltilgung der Hypothek am Uebergang, aus dem Cash.
|
||||
extraAmortization?: number;
|
||||
// OTHER_DEBT
|
||||
immediateRepayment?: number;
|
||||
}
|
||||
@@ -183,8 +187,10 @@ export const transitionDataSchema = z
|
||||
conversionRate: z.number().min(0).max(20).optional(),
|
||||
capitalTaxRate: z.number().min(0).max(100).optional(),
|
||||
saleTaxRate: z.number().min(0).max(100).optional(),
|
||||
decision: z.enum(["HOLD", "SELL"]).optional(),
|
||||
decision: z.enum(["HOLD", "SELL", "PARTIAL"]).optional(),
|
||||
salePrice: nonNeg.optional(),
|
||||
partialSaleAmount: nonNeg.optional(),
|
||||
extraAmortization: nonNeg.optional(),
|
||||
immediateRepayment: nonNeg.optional(),
|
||||
})
|
||||
.strip();
|
||||
|
||||
Reference in New Issue
Block a user