SPB Git

spb/valoplex Public

ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.

TypeScript 90.3% Python 7.1% CSS 2.5%
4.6 KB · 124 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2import { describe, expect, it } from "vitest";3import {4  buildProforma,5  droitsMutation,6  paiementMensuel,7  soldeApresMois,8  tauxMensuelCanadien,9  tgaReference,10} from "./proforma";1112describe("tauxMensuelCanadien", () => {13  it("composition semestrielle : 6 % annuel → ~0,4939 % mensuel", () => {14    expect(tauxMensuelCanadien(6)).toBeCloseTo(0.0049386, 6);15  });16});1718describe("paiementMensuel (hypothèque canadienne)", () => {19  it("cas de référence : 100 000 $ à 6 % sur 25 ans ≈ 639,81 $", () => {20    expect(paiementMensuel(100000, 6, 25)).toBeCloseTo(639.81, 0);21  });22  it("principal nul → 0", () => {23    expect(paiementMensuel(0, 5, 25)).toBe(0);24  });25});2627describe("droitsMutation", () => {28  it("barème QC standard : 500 000 $ hors Montréal", () => {29    // 61 500×0,5% + (307 800−61 500)×1% + (500 000−307 800)×1,5%30    const attendu = Math.round(61500 * 0.005 + (307800 - 61500) * 0.01 + (500000 - 307800) * 0.015);31    expect(droitsMutation(500000, false)).toBe(attendu);32  });33  it("Montréal > QC standard pour un prix élevé", () => {34    expect(droitsMutation(2000000, true)).toBeGreaterThan(droitsMutation(2000000, false));35  });36  it("identiques sous 307 800 $", () => {37    expect(droitsMutation(250000, true)).toBe(droitsMutation(250000, false));38  });39});4041describe("tgaReference", () => {42  it("croît avec le gabarit", () => {43    expect(tgaReference(2)).toBeLessThan(tgaReference(6));44    expect(tgaReference(6)).toBeLessThan(tgaReference(20));45  });46});4748describe("soldeApresMois", () => {49  it("décroît avec le temps et atteint ~0 à l'échéance", () => {50    const s12 = soldeApresMois(300000, 5, 25, 12);51    const s60 = soldeApresMois(300000, 5, 25, 60);52    expect(s12).toBeLessThan(300000);53    expect(s60).toBeLessThan(s12);54    expect(soldeApresMois(300000, 5, 25, 300)).toBeLessThan(1);55  });56});5758describe("buildProforma", () => {59  const pf = buildProforma(1_095_300, 3, 2_160_600, "Montréal");6061  it("le RNE implicite correspond au TGA de référence appliqué à la valeur", () => {62    expect(pf.rne).toBeCloseTo(1_095_300 * (pf.params.tgaPct / 100), 0);63    expect(pf.tgaImplicitePct).toBeCloseTo(pf.params.tgaPct, 1);64  });6566  it("le loyer implicite reconstruit exactement les revenus bruts", () => {67    expect(pf.loyerMoyenMensuel * 12 * 3).toBeCloseTo(pf.revenusBruts, 6);68  });6970  it("la cascade comptable est cohérente : RE + dépenses = RNE", () => {71    expect(pf.revenusEffectifs + pf.totalDepenses).toBeCloseTo(pf.rne, 6);72  });7374  it("cashflow = RNE − service de la dette", () => {75    expect(pf.cashflowAnnuel).toBeCloseTo(pf.rne - pf.serviceDetteAnnuel, 6);76  });7778  it("liquidités = mise de fonds + mutation + notaire + inspection", () => {79    expect(pf.liquiditesRequises).toBeCloseTo(80      pf.miseDeFonds + pf.droitsMutation + pf.fraisNotaire + pf.fraisInspection, 6);81  });8283  it("un taux plus élevé réduit le cashflow et le DSCR", () => {84    const cher = buildProforma(1_095_300, 3, 2_160_600, "Montréal", { tauxHypoPct: 7 });85    expect(cher.cashflowAnnuel).toBeLessThan(pf.cashflowAnnuel);86    expect(cher.dscr).toBeLessThan(pf.dscr);87  });8889  it("plus de mise de fonds améliore le cashflow", () => {90    const gros = buildProforma(1_095_300, 3, 2_160_600, "Montréal", { miseDeFondsPct: 40 });91    expect(gros.cashflowAnnuel).toBeGreaterThan(pf.cashflowAnnuel);92  });9394  it("capital an 1 + intérêts an 1 = service de la dette", () => {95    expect(pf.capitalAn1 + pf.interetsAn1).toBeCloseTo(pf.serviceDetteAnnuel, 4);96  });9798  it("équité 5 ans = valeur projetée − solde", () => {99    expect(pf.equite5Ans).toBeCloseTo(pf.valeur5Ans - pf.soldeHypo5Ans, 4);100  });101102  it("au loyer de point mort, le cashflow est nul (reconstruit)", () => {103    // RB' au point mort → RNE' = service de dette104    const rbPM = pf.loyerPointMort * 12 * 3;105    const denom = 1 - pf.params.vacancePct / 100 - pf.params.gestionPct / 100 - pf.params.entretienPct / 100;106    const F = -pf.depenses.filter((d) => !["entretien", "gestion"].includes(d.key))107      .reduce((a, d) => a + d.amount, 0);108    const rnePM = rbPM * denom - F;109    expect(rnePM).toBeCloseTo(pf.serviceDetteAnnuel, 0);110  });111112  it("le taux de point mort annule le cashflow", () => {113    if (pf.tauxPointMortPct != null) {114      const cf = pf.rne - 12 * paiementMensuel(pf.hypotheque, pf.tauxPointMortPct, pf.params.amortAns);115      expect(Math.abs(cf)).toBeLessThan(1);116    }117  });118119  it("les taxes sont dérivées de la valeur au rôle", () => {120    const taxes = pf.depenses.find((d) => d.key === "taxesMunicipales")!;121    expect(-taxes.amount).toBeCloseTo(2_160_600 * 0.011, 2);122  });123});124