SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
5.2 KB · 98 lines typescript
Raw Blame History
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