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%
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