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
Deploy App / deploy (push) Successful in 1m31s
This commit is contained in:
+24
-49
@@ -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);
|
||||
}
|
||||
|
||||
+1
-4
@@ -74,12 +74,9 @@ export function toPlanInput(plan: PlanWithRelations): PlanInput {
|
||||
realEstates: phase.realEstates.map((re) => ({
|
||||
id: re.id,
|
||||
name: re.name,
|
||||
marketValue: re.marketValue,
|
||||
purchasePrice: re.purchasePrice,
|
||||
mortgage: re.mortgage,
|
||||
valueGrowth: re.valueGrowth,
|
||||
amortization: re.amortization,
|
||||
salePrice: re.salePrice,
|
||||
saleTaxRate: re.saleTaxRate,
|
||||
})),
|
||||
oneTimeEvents: phase.oneTimeEvents.map((e) => ({
|
||||
id: e.id,
|
||||
|
||||
+1
-4
@@ -53,12 +53,9 @@ export interface SecurityInput {
|
||||
export interface RealEstateInput {
|
||||
id: string;
|
||||
name: string;
|
||||
marketValue: number;
|
||||
purchasePrice: number;
|
||||
mortgage: number;
|
||||
valueGrowth: number;
|
||||
amortization: number;
|
||||
salePrice: number | null;
|
||||
saleTaxRate: number;
|
||||
}
|
||||
|
||||
export interface OneTimeEventInput {
|
||||
|
||||
Reference in New Issue
Block a user