Rework real estate model: purchase price + mortgage + amortization only, no appreciation; amortization counts against savings quota; sale price/tax entered at transition time with correct mortgage payoff; carry over income/expenses to new phases
Deploy App / deploy (push) Successful in 1m31s

This commit is contained in:
2026-07-08 21:35:10 +02:00
parent 9609de67ae
commit e9dacc2026
12 changed files with 204 additions and 145 deletions
+24 -49
View File
@@ -14,14 +14,10 @@ export interface SecurityComputed {
export interface RealEstateComputed {
id: string;
name: string;
purchasePrice: number; // fix ueber die Haltedauer, keine Wertsteigerung im vereinfachten Modell
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[];
endNet: number;
mortgages: number[]; // Index 0 = Start, Index durationYears = Ende
}
export interface RetirementComputed {
@@ -88,20 +84,18 @@ export function computeSecurityYearlyValues(
return values;
}
export function computeRealEstateYearly(
marketValue: number,
// Vereinfachtes Modell (keine Wertsteigerung): der Kaufpreis bleibt ueber die ganze
// Haltedauer fix, nur die Hypothek sinkt jaehrlich um die Amortisationsrate.
export function computeMortgageYearly(
mortgage: number,
valueGrowth: number,
amortization: number,
durationYears: number
): { marketValues: number[]; mortgages: number[] } {
const marketValues = [floorToThousand(marketValue)];
): number[] {
const mortgages = [floorToThousand(mortgage)];
for (let year = 1; year <= durationYears; year++) {
marketValues.push(floorToThousand(marketValues[year - 1] * (1 + valueGrowth / 100)));
mortgages.push(floorToThousand(Math.max(0, mortgages[year - 1] - amortization)));
}
return { marketValues, mortgages };
return mortgages;
}
function computeRetirement(
@@ -149,7 +143,11 @@ function computePhase(
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);
// Amortisationsraten aller Immobilien zaehlen gleichwertig mit den Sparbeitraegen der
// Wertschriften gegen dieselbe verfuegbare Sparquote (ein gemeinsamer Topf).
const allocatedSavings =
phase.securities.reduce((sum, s) => sum + s.annualContribution, 0) +
phase.realEstates.reduce((sum, re) => sum + re.amortization, 0);
const savingsWarning = allocatedSavings > savingsQuota;
const securities: SecurityComputed[] = phase.securities.map((s) => {
@@ -170,34 +168,14 @@ function computePhase(
});
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 = floorToThousand(re.salePrice! - taxAmount);
}
const mortgages = computeMortgageYearly(re.mortgage, re.amortization, phase.durationYears);
const purchasePrice = floorToThousand(re.purchasePrice);
return {
id: re.id,
name: re.name,
startNet,
endNetIfKept,
sold,
saleNetProceeds,
taxAmount,
endContribution: sold ? saleNetProceeds! : endNetIfKept,
marketValues,
purchasePrice,
startNet: purchasePrice - mortgages[0],
endNet: purchasePrice - mortgages[phase.durationYears],
mortgages,
};
});
@@ -213,7 +191,7 @@ function computePhase(
const endWealthNominal =
securities.reduce((sum, s) => sum + s.endValue, 0) +
realEstates.reduce((sum, re) => sum + re.endContribution, 0) +
realEstates.reduce((sum, re) => sum + re.endNet, 0) +
oneTimeNet +
(retirement?.lumpSumNetTotal ?? 0);
@@ -226,16 +204,13 @@ function computePhase(
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);
realEstates.reduce((sum, re) => sum + (re.purchasePrice - 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).
// Einmalige Ereignisse und Kapitalbezuege schlagen erst am Ende der Phase zu
// Buche (siehe Phasenuebergang, TDD Kapitel 10). Immobilien-/Wertschriften-
// Verkaeufe wirken sich nur auf die naechste Phase aus (incomingCapital), nicht
// mehr auf das Endvermoegen dieser Phase selbst.
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);
}