TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1// Calculs hypothécaires illustratifs (Canada/Québec). Hypothèses éditables ; aucun taux n'est « réel » sauf indication datée et sourcée.2export interface MortgageInput {3 price: number;4 downPayment: number; // $5 rateAnnualPct: number; // taux affiché (composé semestriellement au Canada pour les prêts fixes)6 amortYears: number; // 25 ou 307 paymentsPerYear?: 12 | 26 | 52;8 municipalTaxAnnual?: number | null;9 schoolTaxAnnual?: number | null;10 insuranceMonthly?: number | null;11 condoFeesMonthly?: number | null;12 heatingMonthly?: number | null;13}14export interface MortgageResult {15 principal: number; cmhcPremium: number; cmhcRatePct: number; loan: number; payment: number; paymentMonthlyEq: number;16 totalInterest: number; totalPaid: number; monthlyAllIn: number; minDownPayment: number; downPaymentPct: number;17 warnings: string[]; schedule: { year: number; interest: number; principal: number; balance: number }[];18 stressTest: { qualifyingRatePct: number; payment: number };19}2021/** Mise de fonds minimale au Canada : 5 % jusqu'à 500 k$, 10 % sur la tranche 500 k$–1,5 M$, 20 % au-delà de 1,5 M$ (règle en vigueur depuis le 15 déc. 2024). */22export function minDownPayment(price: number): number {23 if (price > 1_500_000) return price * 0.2;24 if (price <= 500_000) return price * 0.05;25 return 500_000 * 0.05 + (price - 500_000) * 0.10;26}2728/** Prime SCHL/Sagen/Canada Guaranty selon le rapport prêt-valeur (barème standard). */29export function cmhcRatePct(ltv: number): number {30 if (ltv <= 0.65) return 0.6;31 if (ltv <= 0.75) return 1.7;32 if (ltv <= 0.80) return 2.4;33 if (ltv <= 0.85) return 2.8;34 if (ltv <= 0.90) return 3.1;35 if (ltv <= 0.95) return 4.0;36 return NaN;37}3839/** Taux périodique équivalent à partir d'un taux nominal composé semestriellement (norme canadienne). */40export function periodicRate(rateAnnualPct: number, paymentsPerYear: number): number {41 const semi = rateAnnualPct / 100 / 2;42 return Math.pow(1 + semi, 2 / paymentsPerYear) - 1;43}4445export function paymentFor(loan: number, rateAnnualPct: number, amortYears: number, paymentsPerYear = 12): number {46 const r = periodicRate(rateAnnualPct, paymentsPerYear); const n = amortYears * paymentsPerYear;47 if (r === 0) return loan / n;48 return (loan * r) / (1 - Math.pow(1 + r, -n));49}5051export function computeMortgage(i: MortgageInput): MortgageResult {52 const ppy = i.paymentsPerYear ?? 12;53 const warnings: string[] = [];54 const minDp = minDownPayment(i.price);55 const dp = Math.min(Math.max(i.downPayment, 0), i.price);56 if (dp < minDp - 1) warnings.push(`Mise de fonds inférieure au minimum réglementaire (${Math.round(minDp).toLocaleString("fr-CA")} $).`);57 const principal = i.price - dp;58 const ltv = principal / i.price;59 let premiumRate = 0;60 if (ltv > 0.8) {61 premiumRate = cmhcRatePct(ltv);62 if (Number.isNaN(premiumRate)) { warnings.push("Rapport prêt-valeur > 95 % : non assurable."); premiumRate = 0; }63 if (i.price >= 1_500_000) warnings.push("Prix ≥ 1,5 M$ : assurance prêt hypothécaire non disponible, 20 % de mise de fonds requis.");64 if (i.amortYears > 25) warnings.push("Prêt assuré : amortissement de 30 ans réservé aux premiers acheteurs ou aux constructions neuves.");65 }66 const cmhcPremium = principal * premiumRate / 100;67 const loan = principal + cmhcPremium;68 const payment = paymentFor(loan, i.rateAnnualPct, i.amortYears, ppy);69 const paymentMonthlyEq = payment * ppy / 12;70 const totalPaid = payment * i.amortYears * ppy;71 const totalInterest = totalPaid - loan;72 const extras = (i.municipalTaxAnnual ?? 0) / 12 + (i.schoolTaxAnnual ?? 0) / 12 + (i.insuranceMonthly ?? 0) + (i.condoFeesMonthly ?? 0) + (i.heatingMonthly ?? 0);73 // Tableau d'amortissement annuel74 const r = periodicRate(i.rateAnnualPct, ppy);75 let bal = loan; const schedule: MortgageResult["schedule"] = [];76 for (let y = 1; y <= i.amortYears; y++) {77 let yi = 0, yp = 0;78 for (let k = 0; k < ppy; k++) { const int = bal * r; const pr = Math.min(payment - int, bal); yi += int; yp += pr; bal -= pr; if (bal <= 0.005) { bal = 0; break; } }79 schedule.push({ year: y, interest: yi, principal: yp, balance: Math.max(0, bal) });80 if (bal === 0) break;81 }82 const qualifyingRatePct = Math.max(i.rateAnnualPct + 2, 5.25);83 return {84 principal, cmhcPremium, cmhcRatePct: premiumRate, loan, payment, paymentMonthlyEq, totalInterest, totalPaid, monthlyAllIn: paymentMonthlyEq + extras,85 minDownPayment: minDp, downPaymentPct: dp / i.price, warnings, schedule, stressTest: { qualifyingRatePct, payment: paymentFor(loan, qualifyingRatePct, i.amortYears, ppy) * ppy / 12 },86 };87}8889/** Droits de mutation (« taxe de bienvenue ») — barème provincial 2026 ; Montréal applique des tranches supérieures additionnelles (non modélisées ici). */90export function transferDuty(price: number, montreal = false): number {91 const brackets: [number, number][] = montreal92 ? [[61_500, 0.005], [307_800, 0.01], [552_300, 0.015], [1_104_700, 0.02], [2_136_500, 0.025], [3_113_000, 0.035], [Infinity, 0.04]]93 : [[61_500, 0.005], [307_800, 0.01], [Infinity, 0.015]];94 let duty = 0, prev = 0;95 for (const [cap, rate] of brackets) { if (price <= prev) break; duty += (Math.min(price, cap) - prev) * rate; prev = cap; }96 return duty;97}98