feat: core financial engine (PMT, amortization, simulation calculations)
This commit is contained in:
267
src/lib/calculations.ts
Normal file
267
src/lib/calculations.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { pmt } from './financial';
|
||||
import type {
|
||||
SimulationState,
|
||||
SimulationResults,
|
||||
LoanResult,
|
||||
LoanInput,
|
||||
MonthlyRow,
|
||||
YearlyRow,
|
||||
YearlySummary,
|
||||
} from './types';
|
||||
|
||||
const MAX_YEARS = 30;
|
||||
|
||||
// ─── Main computation ───────────────────────────────────────────────────────
|
||||
|
||||
export function computeSimulation(state: SimulationState): SimulationResults {
|
||||
// 1. Financing
|
||||
const notaryFees = state.propertyPrice * state.notaryFeesRate;
|
||||
const guaranteeFees = state.propertyPrice * state.guaranteeRate;
|
||||
const totalFinancing =
|
||||
state.propertyPrice + notaryFees + guaranteeFees + state.bankFees + state.worksCost;
|
||||
const totalCredit = Math.max(0, totalFinancing - state.downPayment);
|
||||
|
||||
// 2. Main loan amount
|
||||
const otherLoansTotal = state.loans
|
||||
.filter((l) => !l.isMainLoan && l.enabled)
|
||||
.reduce((sum, l) => sum + l.amount, 0);
|
||||
const mainLoanAmount = Math.max(0, totalCredit - otherLoansTotal);
|
||||
|
||||
// 3. Per-loan amortization
|
||||
const loanResults: LoanResult[] = state.loans
|
||||
.filter((l) => l.enabled)
|
||||
.map((loan) => {
|
||||
const amount = loan.isMainLoan ? mainLoanAmount : loan.amount;
|
||||
return computeLoan(loan, amount);
|
||||
});
|
||||
|
||||
// 4. Max duration
|
||||
const maxDuration = loanResults.length > 0
|
||||
? Math.max(...loanResults.map((lr) => lr.loan.duration))
|
||||
: 0;
|
||||
|
||||
// 5. Yearly summaries
|
||||
const projectedChargesMonthly =
|
||||
state.projectedCharges +
|
||||
state.propertyTax +
|
||||
state.projectedUtilities +
|
||||
state.projectedHomeInsurance;
|
||||
|
||||
const currentChargesMonthly =
|
||||
state.currentUtilities + state.currentCharges + state.currentHomeInsurance;
|
||||
|
||||
const yearlySummaries: YearlySummary[] = [];
|
||||
let resalePrice = state.propertyPrice * (1 + state.firstYearDepreciation);
|
||||
|
||||
for (let y = 1; y <= MAX_YEARS; y++) {
|
||||
// Aggregate loan data for this year
|
||||
let totalInterest = 0;
|
||||
let totalPrincipal = 0;
|
||||
let totalInsurance = 0;
|
||||
let totalRemaining = 0;
|
||||
let totalPayment = 0;
|
||||
|
||||
for (const lr of loanResults) {
|
||||
const yearData = lr.yearly.find((yr) => yr.year === y);
|
||||
if (yearData) {
|
||||
totalInterest += yearData.interest;
|
||||
totalPrincipal += yearData.principal;
|
||||
totalInsurance += yearData.insurance;
|
||||
totalRemaining += yearData.remaining;
|
||||
totalPayment += yearData.payment;
|
||||
} else if (y <= lr.loan.duration) {
|
||||
// Year exists but no data (shouldn't happen with correct computation)
|
||||
} else {
|
||||
// After loan duration — find last remaining
|
||||
const lastYear = lr.yearly[lr.yearly.length - 1];
|
||||
if (lastYear) {
|
||||
totalRemaining += lastYear.remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Monthly projected costs
|
||||
const monthlyInterest = totalInterest / 12;
|
||||
const monthlyPrincipal = totalPrincipal / 12;
|
||||
const monthlyLoanInsurance = totalInsurance / 12;
|
||||
const monthlyTotal = monthlyInterest + monthlyPrincipal + projectedChargesMonthly + monthlyLoanInsurance;
|
||||
const capitalRatio =
|
||||
monthlyInterest + monthlyPrincipal + projectedChargesMonthly + monthlyLoanInsurance > 0
|
||||
? monthlyPrincipal / (monthlyInterest + monthlyPrincipal + projectedChargesMonthly + monthlyLoanInsurance)
|
||||
: 0;
|
||||
|
||||
// Current costs (rent evolves)
|
||||
const currentRent = state.currentRent * Math.pow(1 + state.rentEvolutionRate, y - 1);
|
||||
const currentMonthlyTotal = currentRent + currentChargesMonthly;
|
||||
|
||||
// Difference
|
||||
const difference = monthlyTotal - currentMonthlyTotal;
|
||||
|
||||
// Resale
|
||||
if (y > 1) {
|
||||
resalePrice = resalePrice * (1 + state.annualAppreciation);
|
||||
}
|
||||
const netResale = resalePrice * (1 - state.saleFees) - totalRemaining - state.downPayment;
|
||||
|
||||
yearlySummaries.push({
|
||||
year: y,
|
||||
totalInterest,
|
||||
totalPrincipal,
|
||||
totalInsurance,
|
||||
totalRemaining,
|
||||
totalPayment,
|
||||
monthlyInterest,
|
||||
monthlyPrincipal,
|
||||
monthlyCharges: projectedChargesMonthly,
|
||||
monthlyLoanInsurance,
|
||||
monthlyTotal,
|
||||
capitalRatio,
|
||||
yearlyTotal: monthlyTotal * 12,
|
||||
currentRent,
|
||||
currentCharges: currentChargesMonthly,
|
||||
currentMonthlyTotal,
|
||||
currentYearlyTotal: currentMonthlyTotal * 12,
|
||||
difference,
|
||||
resalePrice,
|
||||
netResale,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Global totals
|
||||
const totalCapitalAll = loanResults.reduce((s, lr) => s + lr.effectiveAmount, 0);
|
||||
const totalInterestAll = loanResults.reduce((s, lr) => s + lr.totalInterest, 0);
|
||||
const totalInsuranceAll = loanResults.reduce((s, lr) => s + lr.totalInsurance, 0);
|
||||
const grandTotal = totalCapitalAll + totalInterestAll + totalInsuranceAll;
|
||||
|
||||
// Year 1 snapshot
|
||||
const year1 = yearlySummaries[0];
|
||||
const monthlyPaymentYear1 = year1 ? year1.monthlyInterest + year1.monthlyPrincipal + year1.monthlyLoanInsurance : 0;
|
||||
|
||||
return {
|
||||
notaryFees,
|
||||
guaranteeFees,
|
||||
totalFinancing,
|
||||
totalCredit,
|
||||
mainLoanAmount,
|
||||
loanResults,
|
||||
yearlySummaries,
|
||||
totalCapital: totalCapitalAll,
|
||||
totalInterest: totalInterestAll,
|
||||
totalInsurance: totalInsuranceAll,
|
||||
grandTotal,
|
||||
monthlyPaymentYear1,
|
||||
projectedMonthlyCharges: projectedChargesMonthly,
|
||||
projectedMonthlyTotal: year1?.monthlyTotal ?? 0,
|
||||
currentMonthlyTotal: year1?.currentMonthlyTotal ?? 0,
|
||||
maxDuration,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Loan-level computation ─────────────────────────────────────────────────
|
||||
|
||||
function computeLoan(loan: LoanInput, amount: number): LoanResult {
|
||||
if (amount <= 0 || loan.duration <= 0) {
|
||||
return emptyLoanResult(loan, amount);
|
||||
}
|
||||
|
||||
const monthlyRate = loan.rate / 12;
|
||||
const deferralMonths = Math.round(loan.deferral * 12);
|
||||
const totalMonths = Math.round(loan.duration * 12);
|
||||
const repaymentMonths = totalMonths - deferralMonths;
|
||||
const monthlyPmt = repaymentMonths > 0 ? pmt(monthlyRate, repaymentMonths, amount) : 0;
|
||||
const monthlyInsurance = amount * loan.insuranceRate / 12;
|
||||
|
||||
let remaining = amount;
|
||||
const monthly: MonthlyRow[] = [];
|
||||
|
||||
for (let m = 1; m <= totalMonths; m++) {
|
||||
const interest = remaining * monthlyRate;
|
||||
let principal = 0;
|
||||
|
||||
if (m > deferralMonths && repaymentMonths > 0) {
|
||||
principal = monthlyPmt - interest;
|
||||
// Protect against floating-point overshoot on the last payment
|
||||
if (principal > remaining) {
|
||||
principal = remaining;
|
||||
}
|
||||
remaining = Math.max(0, remaining - principal);
|
||||
}
|
||||
|
||||
monthly.push({
|
||||
month: m,
|
||||
monthInYear: ((m - 1) % 12) + 1,
|
||||
year: Math.ceil(m / 12),
|
||||
interest,
|
||||
principal,
|
||||
insurance: monthlyInsurance,
|
||||
remaining,
|
||||
payment: interest + principal + monthlyInsurance,
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregate to yearly
|
||||
const yearly: YearlyRow[] = [];
|
||||
const years = Math.ceil(totalMonths / 12);
|
||||
for (let y = 1; y <= years; y++) {
|
||||
const rows = monthly.filter((r) => r.year === y);
|
||||
if (rows.length === 0) continue;
|
||||
yearly.push({
|
||||
year: y,
|
||||
interest: rows.reduce((s, r) => s + r.interest, 0),
|
||||
principal: rows.reduce((s, r) => s + r.principal, 0),
|
||||
insurance: rows.reduce((s, r) => s + r.insurance, 0),
|
||||
remaining: rows[rows.length - 1].remaining,
|
||||
payment: rows.reduce((s, r) => s + r.payment, 0),
|
||||
});
|
||||
}
|
||||
|
||||
// Pad yearly to MAX_YEARS for easy aggregation
|
||||
for (let y = years + 1; y <= MAX_YEARS; y++) {
|
||||
yearly.push({
|
||||
year: y,
|
||||
interest: 0,
|
||||
principal: 0,
|
||||
insurance: 0,
|
||||
remaining: 0,
|
||||
payment: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const totalInterest = monthly.reduce((s, r) => s + r.interest, 0);
|
||||
const totalInsurance = monthly.reduce((s, r) => s + r.insurance, 0);
|
||||
|
||||
return {
|
||||
loan,
|
||||
effectiveAmount: amount,
|
||||
monthlyPayment: monthlyPmt,
|
||||
monthlyInsurance,
|
||||
monthly,
|
||||
yearly,
|
||||
totalInterest,
|
||||
totalInsurance,
|
||||
totalCost: amount + totalInterest + totalInsurance,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyLoanResult(loan: LoanInput, amount: number): LoanResult {
|
||||
const yearly: YearlyRow[] = Array.from({ length: MAX_YEARS }, (_, i) => ({
|
||||
year: i + 1,
|
||||
interest: 0,
|
||||
principal: 0,
|
||||
insurance: 0,
|
||||
remaining: 0,
|
||||
payment: 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
loan,
|
||||
effectiveAmount: Math.max(0, amount),
|
||||
monthlyPayment: 0,
|
||||
monthlyInsurance: 0,
|
||||
monthly: [],
|
||||
yearly,
|
||||
totalInterest: 0,
|
||||
totalInsurance: 0,
|
||||
totalCost: 0,
|
||||
};
|
||||
}
|
||||
174
src/lib/defaults.ts
Normal file
174
src/lib/defaults.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { SimulationState, LoanInput } from './types';
|
||||
|
||||
let loanIdCounter = 0;
|
||||
export function generateLoanId(): string {
|
||||
return `loan_${++loanIdCounter}_${Date.now()}`;
|
||||
}
|
||||
|
||||
// ─── Default loans ──────────────────────────────────────────────────────────
|
||||
|
||||
export function defaultPTZ(): LoanInput {
|
||||
return {
|
||||
id: generateLoanId(),
|
||||
name: 'Prêt Taux Zéro',
|
||||
amount: 90000,
|
||||
rate: 0,
|
||||
duration: 20,
|
||||
deferral: 0,
|
||||
insuranceRate: 0.003,
|
||||
isMainLoan: false,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultActionLogement(): LoanInput {
|
||||
return {
|
||||
id: generateLoanId(),
|
||||
name: 'Action Logement',
|
||||
amount: 30000,
|
||||
rate: 0.01,
|
||||
duration: 25,
|
||||
deferral: 0,
|
||||
insuranceRate: 0.003,
|
||||
isMainLoan: false,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultPrimoBoost(): LoanInput {
|
||||
return {
|
||||
id: generateLoanId(),
|
||||
name: 'Primo Boost',
|
||||
amount: 25000,
|
||||
rate: 0.0199,
|
||||
duration: 25,
|
||||
deferral: 0,
|
||||
insuranceRate: 0.003,
|
||||
isMainLoan: false,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultMainLoan(): LoanInput {
|
||||
return {
|
||||
id: generateLoanId(),
|
||||
name: 'Prêt Immobilier Classique',
|
||||
amount: 0, // Auto-calculated
|
||||
rate: 0.0325,
|
||||
duration: 25,
|
||||
deferral: 0,
|
||||
insuranceRate: 0.003,
|
||||
isMainLoan: true,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyLoan(): LoanInput {
|
||||
return {
|
||||
id: generateLoanId(),
|
||||
name: 'Nouveau prêt',
|
||||
amount: 0,
|
||||
rate: 0,
|
||||
duration: 25,
|
||||
deferral: 0,
|
||||
insuranceRate: 0.003,
|
||||
isMainLoan: false,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Default state for "Neuf" ───────────────────────────────────────────────
|
||||
|
||||
export function defaultStateNeuf(): SimulationState {
|
||||
return {
|
||||
propertyType: 'neuf',
|
||||
propertyPrice: 396000,
|
||||
notaryFeesRate: 0.025,
|
||||
guaranteeRate: 0.01,
|
||||
bankFees: 1280,
|
||||
worksCost: 0,
|
||||
downPayment: 15000,
|
||||
|
||||
currentRent: 1216,
|
||||
rentEvolutionRate: 0.012,
|
||||
currentUtilities: 160,
|
||||
currentCharges: 100,
|
||||
currentHomeInsurance: 32,
|
||||
|
||||
projectedCharges: 150,
|
||||
propertyTax: 100,
|
||||
projectedUtilities: 80,
|
||||
projectedHomeInsurance: 35,
|
||||
|
||||
loans: [defaultPTZ(), defaultActionLogement(), defaultPrimoBoost(), defaultMainLoan()],
|
||||
|
||||
firstYearDepreciation: -0.05,
|
||||
annualAppreciation: 0.01,
|
||||
saleFees: 0.07,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Default state for "Ancien" ─────────────────────────────────────────────
|
||||
|
||||
export function defaultStateAncien(): SimulationState {
|
||||
return {
|
||||
propertyType: 'ancien',
|
||||
propertyPrice: 396000,
|
||||
notaryFeesRate: 0.08,
|
||||
guaranteeRate: 0.01,
|
||||
bankFees: 1280,
|
||||
worksCost: 0,
|
||||
downPayment: 15000,
|
||||
|
||||
currentRent: 1216,
|
||||
rentEvolutionRate: 0.012,
|
||||
currentUtilities: 160,
|
||||
currentCharges: 100,
|
||||
currentHomeInsurance: 32,
|
||||
|
||||
projectedCharges: 150,
|
||||
propertyTax: 100,
|
||||
projectedUtilities: 80,
|
||||
projectedHomeInsurance: 35,
|
||||
|
||||
// No PTZ for ancien
|
||||
loans: [defaultActionLogement(), defaultPrimoBoost(), defaultMainLoan()],
|
||||
|
||||
firstYearDepreciation: -0.05,
|
||||
annualAppreciation: 0.01,
|
||||
saleFees: 0.07,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Loan presets ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface LoanPreset {
|
||||
label: string;
|
||||
description: string;
|
||||
create: () => LoanInput;
|
||||
neufOnly?: boolean;
|
||||
}
|
||||
|
||||
export const LOAN_PRESETS: LoanPreset[] = [
|
||||
{
|
||||
label: 'Prêt Taux Zéro (PTZ)',
|
||||
description: 'Prêt à 0% pour primo-accédants (neuf uniquement)',
|
||||
create: defaultPTZ,
|
||||
neufOnly: true,
|
||||
},
|
||||
{
|
||||
label: 'Action Logement',
|
||||
description: 'Prêt employeur à taux réduit',
|
||||
create: defaultActionLogement,
|
||||
},
|
||||
{
|
||||
label: 'Primo Boost',
|
||||
description: 'Prêt aidé pour primo-accédants',
|
||||
create: defaultPrimoBoost,
|
||||
},
|
||||
{
|
||||
label: 'Prêt personnalisé',
|
||||
description: 'Ajouter un prêt avec des paramètres libres',
|
||||
create: createEmptyLoan,
|
||||
},
|
||||
];
|
||||
54
src/lib/financial.ts
Normal file
54
src/lib/financial.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Core financial functions replicating Excel's PMT, IPMT, PPMT.
|
||||
* All functions return positive values (Excel returns negative for payments).
|
||||
*/
|
||||
|
||||
/**
|
||||
* PMT — Fixed periodic payment for a loan.
|
||||
* Equivalent to Excel: -PMT(rate, nper, pv)
|
||||
*/
|
||||
export function pmt(rate: number, nper: number, pv: number): number {
|
||||
if (pv <= 0 || nper <= 0) return 0;
|
||||
if (rate === 0) return pv / nper;
|
||||
const factor = Math.pow(1 + rate, nper);
|
||||
return (pv * rate * factor) / (factor - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* IPMT — Interest portion of a specific payment period.
|
||||
* Equivalent to Excel: -IPMT(rate, per, nper, pv)
|
||||
* @param per 1-based period number
|
||||
*/
|
||||
export function ipmt(rate: number, per: number, nper: number, pv: number): number {
|
||||
if (rate === 0 || pv <= 0 || nper <= 0) return 0;
|
||||
const bal = remainingBalance(rate, per - 1, nper, pv);
|
||||
return bal * rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* PPMT — Principal portion of a specific payment period.
|
||||
* Equivalent to Excel: -PPMT(rate, per, nper, pv)
|
||||
* @param per 1-based period number
|
||||
*/
|
||||
export function ppmt(rate: number, per: number, nper: number, pv: number): number {
|
||||
if (pv <= 0 || nper <= 0) return 0;
|
||||
return pmt(rate, nper, pv) - ipmt(rate, per, nper, pv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaining balance after `per` payments.
|
||||
* @param per Number of payments already made (0 = initial balance)
|
||||
*/
|
||||
export function remainingBalance(rate: number, per: number, nper: number, pv: number): number {
|
||||
if (pv <= 0 || nper <= 0) return 0;
|
||||
if (per <= 0) return pv;
|
||||
if (per >= nper) return 0;
|
||||
|
||||
if (rate === 0) {
|
||||
return pv - pmt(rate, nper, pv) * per;
|
||||
}
|
||||
|
||||
const payment = pmt(rate, nper, pv);
|
||||
const factor = Math.pow(1 + rate, per);
|
||||
return pv * factor - payment * (factor - 1) / rate;
|
||||
}
|
||||
49
src/lib/formatters.ts
Normal file
49
src/lib/formatters.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Format a number as Euro currency (French locale).
|
||||
* 396000 → "396 000 €"
|
||||
*/
|
||||
export function formatCurrency(value: number, decimals = 0): string {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number as a short currency (for charts).
|
||||
* 396000 → "396k €"
|
||||
*/
|
||||
export function formatCurrencyShort(value: number): string {
|
||||
if (Math.abs(value) >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(1)}M €`;
|
||||
}
|
||||
if (Math.abs(value) >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(0)}k €`;
|
||||
}
|
||||
return `${value.toFixed(0)} €`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a rate as percentage.
|
||||
* 0.0325 → "3,25 %"
|
||||
*/
|
||||
export function formatPercent(value: number, decimals = 2): string {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a plain number with French locale.
|
||||
* 1234.56 → "1 234,56"
|
||||
*/
|
||||
export function formatNumber(value: number, decimals = 0): string {
|
||||
return new Intl.NumberFormat('fr-FR', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
}
|
||||
156
src/lib/types.ts
Normal file
156
src/lib/types.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// ─── Property Type ───────────────────────────────────────────────────────────
|
||||
|
||||
export type PropertyType = 'neuf' | 'ancien';
|
||||
|
||||
// ─── Loan Input ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LoanInput {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
rate: number; // Annual rate as decimal (3.25% → 0.0325)
|
||||
duration: number; // Years
|
||||
deferral: number; // Deferred period in years
|
||||
insuranceRate: number; // Annual rate on initial capital (0.3% → 0.003)
|
||||
isMainLoan: boolean; // If true, amount = totalCredit - sum(others)
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// ─── Simulation State (all inputs) ──────────────────────────────────────────
|
||||
|
||||
export interface SimulationState {
|
||||
// Project
|
||||
propertyType: PropertyType;
|
||||
propertyPrice: number;
|
||||
notaryFeesRate: number;
|
||||
guaranteeRate: number;
|
||||
bankFees: number;
|
||||
worksCost: number;
|
||||
downPayment: number;
|
||||
|
||||
// Current costs (tenant)
|
||||
currentRent: number;
|
||||
rentEvolutionRate: number; // Annual evolution (1.2% → 0.012)
|
||||
currentUtilities: number; // EDF / fibre / eau
|
||||
currentCharges: number;
|
||||
currentHomeInsurance: number;
|
||||
|
||||
// Projected costs (owner)
|
||||
projectedCharges: number; // Copropriété
|
||||
propertyTax: number; // Taxe foncière (monthly)
|
||||
projectedUtilities: number; // EDF / fibre / eau
|
||||
projectedHomeInsurance: number;
|
||||
|
||||
// Loans
|
||||
loans: LoanInput[];
|
||||
|
||||
// Resale parameters
|
||||
firstYearDepreciation: number; // e.g., -0.05 (5% loss first year)
|
||||
annualAppreciation: number; // e.g., 0.01 (1% per year)
|
||||
saleFees: number; // e.g., 0.07 (7% agency + notary)
|
||||
}
|
||||
|
||||
// ─── Monthly Amortization (per loan) ────────────────────────────────────────
|
||||
|
||||
export interface MonthlyRow {
|
||||
month: number; // 1-based global month
|
||||
monthInYear: number; // 1-12
|
||||
year: number; // 1-based year
|
||||
interest: number;
|
||||
principal: number;
|
||||
insurance: number;
|
||||
remaining: number;
|
||||
payment: number; // interest + principal + insurance
|
||||
}
|
||||
|
||||
// ─── Yearly Amortization (per loan) ─────────────────────────────────────────
|
||||
|
||||
export interface YearlyRow {
|
||||
year: number;
|
||||
interest: number;
|
||||
principal: number;
|
||||
insurance: number;
|
||||
remaining: number;
|
||||
payment: number;
|
||||
}
|
||||
|
||||
// ─── Loan Computation Result ────────────────────────────────────────────────
|
||||
|
||||
export interface LoanResult {
|
||||
loan: LoanInput;
|
||||
effectiveAmount: number; // Actual amount (auto-calculated for main)
|
||||
monthlyPayment: number; // Monthly principal + interest (excl. insurance)
|
||||
monthlyInsurance: number;
|
||||
monthly: MonthlyRow[];
|
||||
yearly: YearlyRow[];
|
||||
totalInterest: number;
|
||||
totalInsurance: number;
|
||||
totalCost: number; // amount + totalInterest + totalInsurance
|
||||
}
|
||||
|
||||
// ─── Yearly Summary (aggregated across all loans) ───────────────────────────
|
||||
|
||||
export interface YearlySummary {
|
||||
year: number;
|
||||
|
||||
// Credits aggregated
|
||||
totalInterest: number;
|
||||
totalPrincipal: number;
|
||||
totalInsurance: number;
|
||||
totalRemaining: number;
|
||||
totalPayment: number;
|
||||
|
||||
// Projected monthly costs (as owner)
|
||||
monthlyInterest: number;
|
||||
monthlyPrincipal: number;
|
||||
monthlyCharges: number; // charges + tax + utilities + home insurance
|
||||
monthlyLoanInsurance: number;
|
||||
monthlyTotal: number;
|
||||
capitalRatio: number; // Part of payment building equity
|
||||
yearlyTotal: number;
|
||||
|
||||
// Current monthly costs (as tenant)
|
||||
currentRent: number;
|
||||
currentCharges: number; // utilities + charges + insurance
|
||||
currentMonthlyTotal: number;
|
||||
currentYearlyTotal: number;
|
||||
|
||||
// Comparison
|
||||
difference: number; // projected - current (negative = savings)
|
||||
|
||||
// Resale projection
|
||||
resalePrice: number;
|
||||
netResale: number; // After fees - remaining debt - down payment
|
||||
}
|
||||
|
||||
// ─── Final Simulation Results ───────────────────────────────────────────────
|
||||
|
||||
export interface SimulationResults {
|
||||
// Financing
|
||||
notaryFees: number;
|
||||
guaranteeFees: number;
|
||||
totalFinancing: number;
|
||||
totalCredit: number;
|
||||
mainLoanAmount: number;
|
||||
|
||||
// Per-loan results
|
||||
loanResults: LoanResult[];
|
||||
|
||||
// Yearly summaries (up to 30 years)
|
||||
yearlySummaries: YearlySummary[];
|
||||
|
||||
// Global totals
|
||||
totalCapital: number;
|
||||
totalInterest: number;
|
||||
totalInsurance: number;
|
||||
grandTotal: number;
|
||||
|
||||
// Year 1 monthly snapshot
|
||||
monthlyPaymentYear1: number;
|
||||
projectedMonthlyCharges: number;
|
||||
projectedMonthlyTotal: number;
|
||||
currentMonthlyTotal: number;
|
||||
|
||||
// Max duration across all loans
|
||||
maxDuration: number;
|
||||
}
|
||||
Reference in New Issue
Block a user