TypeScript 90.1%
JavaScript 3.2%
Python 3.2%
CSS 1.8%
HTML 1.8%
1// UQO Éval — laboratoire pédagogique d'évaluation immobilière2/**3 * Rapport d'évaluation détaillé — format professionnel (présentation bancaire).4 * Structure calquée sur un rapport d'évaluation : page titre, sommaire exécutif,5 * mandat et portée, définition de la valeur marchande, méthodologie, description6 * de la propriété, grille d'ajustement des comparables, analyse hédonique,7 * réconciliation, hypothèses et conditions limitatives, certification.8 * Ton sobre, encadrés honnêtes : AVM statistique — n'est PAS un rapport OEAQ.9 * Généré dans le cadre des cours IMM1003 et IMM1033 (UQO), avec logo officiel UQO.10 */11import PDFDocument from "pdfkit";12import path from "path";13import type { UnitEstimate } from "./estimator";1415const INK = "#16181b";16const INK2 = "#4d5551";17const INK3 = "#8b928c";18const SURFACE2 = "#f9fafa";19const GREEN = "#005F85"; // bleu UQO (PMS 7469)20const GREEN_DEEP = "#00465F";21const LIME = "#2BCCAE"; // turquoise UQO2223const W = 612;24const H = 792;25const M = 56;26const CW = W - 2 * M;2728const F = (f: string) => path.join(process.cwd(), "assets", "fonts", f);29const UQO_LOGO_BLEU = path.join(process.cwd(), "assets", "uqo", "UQO_couleur.png");30const UQO_RATIO = 2123 / 853; // ratio natif du logo — jamais déformé3132/** Mention pédagogique obligatoire sur tous les rapports UQO Éval. */33const COURS =34 "Rapport généré dans le cadre des cours IMM1003 — Éléments d'évaluation immobilière et " +35 "IMM1033 — Méthodes du coût en évaluation immobilière, enseignés par Simon-Pierre Boucher (UQO).";36const fmt = (v: number | null | undefined) =>37 v == null38 ? "—"39 : new Intl.NumberFormat("fr-CA", { style: "currency", currency: "CAD", maximumFractionDigits: 0 }).format(v);40const signed = (v: number) => `${v >= 0 ? "+" : "−"}${fmt(Math.abs(Math.round(v)))}`;41const CONF_TXT: Record<string, string> = {42 A: "Très fiable", B: "Fiable", C: "Indicative", D: "Peu fiable — marché mince",43};4445type Doc = InstanceType<typeof PDFDocument>;4647interface Ctx {48 doc: Doc;49 page: number;50 dossier: string;51 generated: string;52}5354function header(c: Ctx) {55 const { doc } = c;56 doc.image(UQO_LOGO_BLEU, M, 22, { height: 14 });57 doc.font("SG-Bold").fontSize(10).fillColor(INK)58 .text("Éval", M + 14 * UQO_RATIO + 4, 26, { lineBreak: false });59 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)60 .text("RAPPORT D'ÉVALUATION DÉTAILLÉ — CADRE PÉDAGOGIQUE UQO", M + 90, 29, { characterSpacing: 0.8, lineBreak: false });61 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)62 .text(`DOSSIER ${c.dossier}`, W - M - 160, 29, { width: 160, align: "right" });63 doc.rect(M, 44, CW, 0.8).fill(INK);64}6566function footer(c: Ctx, total: number) {67 const { doc } = c;68 doc.rect(M, H - 52, CW, 0.8).fill(INK);69 doc.font("JB-Reg").fontSize(6).fillColor(INK3).text(70 "AVM PÉDAGOGIQUE — NE CONSTITUE PAS UN RAPPORT D'ÉVALUATION AU SENS DES NORMES DE L'OEAQ · UQO-EVAL.APP",71 M, H - 44, { width: CW - 50, characterSpacing: 0.3, lineBreak: false });72 doc.font("JB-Reg").fontSize(6).fillColor(INK3).text(73 "COURS IMM1003 · IMM1033 — SIMON-PIERRE BOUCHER, UQO",74 M, H - 35, { width: CW - 50, characterSpacing: 0.3, lineBreak: false });75 doc.font("JB-Bold").fontSize(7).fillColor(INK)76 .text(`${c.page} / ${total}`, W - M - 40, H - 45, { width: 40, align: "right" });77}7879function newPage(c: Ctx, total: number) {80 c.doc.addPage({ size: "LETTER", margin: 0 });81 c.page += 1;82 c.doc.rect(0, 0, W, H).fill("#ffffff");83 header(c);84 footer(c, total);85 return 62; // y de départ86}8788function sectionTitle(c: Ctx, num: string, title: string, y: number): number {89 const { doc } = c;90 doc.font("JB-Bold").fontSize(8).fillColor(GREEN)91 .text(num, M, y + 2, { lineBreak: false });92 doc.font("SG-Bold").fontSize(13).fillColor(INK)93 .text(title.toUpperCase(), M + 30, y, { lineBreak: false });94 doc.rect(M, y + 19, CW, 1.4).fill(INK);95 doc.rect(M, y + 21.5, 60, 2.2).fill(LIME);96 return y + 32;97}9899function para(c: Ctx, txt: string, y: number, opts?: { size?: number; color?: string; width?: number }): number {100 const { doc } = c;101 doc.font("Inter").fontSize(opts?.size ?? 8.6).fillColor(opts?.color ?? INK2)102 .text(txt, M, y, { width: opts?.width ?? CW, lineGap: 2.2, align: "justify" });103 return doc.y + 8;104}105106function kvTable(c: Ctx, rows: [string, string][], y: number, colW = CW): number {107 const { doc } = c;108 const rh = 17;109 rows.forEach(([k, v], i) => {110 const ry = y + i * rh;111 if (i % 2 === 0) doc.rect(M, ry, colW, rh).fill(SURFACE2);112 doc.font("JB-Bold").fontSize(6.6).fillColor(INK3)113 .text(k.toUpperCase(), M + 8, ry + 5, { characterSpacing: 0.5, lineBreak: false });114 doc.font("SG-Bold").fontSize(8.6).fillColor(INK)115 .text(v, M + colW * 0.42, ry + 4, { width: colW * 0.58 - 10, height: 11, ellipsis: true });116 });117 doc.rect(M, y, colW, rows.length * rh).lineWidth(0.9).stroke(INK);118 return y + rows.length * rh + 10;119}120121export async function buildProReport(122 data: UnitEstimate,123 marketIndex: { month: string; idx: number }[]124): Promise<Buffer> {125 const u = data.unit!;126 const s = u.specs;127 const r = data.result;128 const generated = new Date().toISOString().slice(0, 10);129 const dossier = `UQO-${u.id.slice(0, 8)}-${generated.replaceAll("-", "")}`;130 const TOTAL = 6;131132 const doc = new PDFDocument({ size: "LETTER", margin: 0, info: { Title: `UQO Éval — Rapport d'évaluation détaillé — ${u.adresse ?? ""}`, Author: "UQO Éval — cours IMM1003 · IMM1033 (Simon-Pierre Boucher, UQO)" } });133 doc.registerFont("SG-Bold", F("SpaceGrotesk-Bold.ttf"));134 doc.registerFont("SG-Med", F("SpaceGrotesk-Medium.ttf"));135 doc.registerFont("JB-Reg", F("JetBrainsMono-Regular.ttf"));136 doc.registerFont("JB-Bold", F("JetBrainsMono-Bold.ttf"));137 doc.registerFont("Inter", F("Inter-Regular.ttf"));138139 const chunks: Buffer[] = [];140 doc.on("data", (ch: Buffer) => chunks.push(ch));141 const done = new Promise<Buffer>((res) => doc.on("end", () => res(Buffer.concat(chunks))));142 const c: Ctx = { doc, page: 1, dossier, generated };143144 /* ------------------------------- PAGE TITRE ------------------------------- */145 doc.rect(0, 0, W, H).fill("#ffffff");146 doc.rect(0, 0, W, 6).fill(INK);147 doc.rect(0, 6, W, 2).fill(LIME);148 // logo officiel UQO (bleu PMS 7469, proportions natives) + wordmark Éval149 doc.image(UQO_LOGO_BLEU, M, 62, { height: 38 });150 doc.font("SG-Bold").fontSize(26).fillColor(INK)151 .text("Éval", M + 38 * UQO_RATIO + 10, 72, { lineBreak: false });152 doc.font("JB-Reg").fontSize(7.5).fillColor(INK3)153 .text("LABORATOIRE PÉDAGOGIQUE D'ÉVALUATION IMMOBILIÈRE · UNIVERSITÉ DU QUÉBEC EN OUTAOUAIS", M, 108, { characterSpacing: 1.2 });154 doc.font("JB-Bold").fontSize(7).fillColor(GREEN)155 .text("COURS IMM1003 — ÉLÉMENTS D'ÉVALUATION IMMOBILIÈRE · IMM1033 — MÉTHODES DU COÛT EN ÉVALUATION IMMOBILIÈRE", M, 122, { characterSpacing: 0.6 });156157 doc.rect(M, 168, CW, 1.4).fill(INK);158 doc.font("SG-Bold").fontSize(30).fillColor(INK)159 .text("RAPPORT D'ÉVALUATION\nDÉTAILLÉ", M, 188, { lineGap: 4 });160 doc.font("Inter").fontSize(10.5).fillColor(INK2)161 .text("Approche par comparaison directe et modélisation hédonique de masse", M, 262);162 doc.rect(M, 288, CW, 1.4).fill(INK);163164 // bloc propriété165 doc.font("JB-Bold").fontSize(7.5).fillColor(INK3).text("PROPRIÉTÉ ÉVALUÉE", M, 318, { characterSpacing: 1.2 });166 doc.font("SG-Bold").fontSize(17).fillColor(INK)167 .text(`${u.adresse ?? ""}${s.apt ? ` app. ${s.apt}` : ""}`, M, 332);168 doc.font("SG-Med").fontSize(12).fillColor(INK2)169 .text(`${u.municipalite ?? ""} (Québec) · Matricule ${s.matricule ?? "—"}`, M, doc.y + 2);170171 // encadré conclusion172 const by = 420;173 doc.roundedRect(M, by, CW, 128, 6).lineWidth(1.6).fillAndStroke(SURFACE2, INK);174 doc.font("JB-Bold").fontSize(7.5).fillColor(GREEN_DEEP)175 .text("CONCLUSION DE LA VALEUR MARCHANDE ESTIMÉE", M + 20, by + 16, { characterSpacing: 1.2 });176 doc.font("SG-Bold").fontSize(38).fillColor(INK).text(fmt(r.estimate), M + 20, by + 32);177 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("FOURCHETTE (P10-P90)", M + 20, by + 84, { lineBreak: false });178 doc.font("SG-Bold").fontSize(11).fillColor(INK).text(`${fmt(r.low)} → ${fmt(r.high)}`, M + 20, by + 96, { lineBreak: false });179 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("DATE D'EFFET", M + 260, by + 84, { lineBreak: false });180 doc.font("SG-Bold").fontSize(11).fillColor(INK).text(generated, M + 260, by + 96, { lineBreak: false });181 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("FIABILITÉ", M + 380, by + 84, { lineBreak: false });182 doc.font("SG-Bold").fontSize(11).fillColor(INK)183 .text(`${r.confidenceLevel} · ${r.confidencePct} %`, M + 380, by + 96, { lineBreak: false });184185 // préparé pour / par186 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ POUR", M, 585, { characterSpacing: 1 });187 doc.rect(M, 610, 220, 0.9).fill(INK2);188 doc.font("Inter").fontSize(7).fillColor(INK3).text("(étudiante ou étudiant / groupe-cours)", M, 615);189 doc.font("JB-Bold").fontSize(7).fillColor(INK3).text("PRÉPARÉ PAR", M + 300, 585, { characterSpacing: 1 });190 doc.font("SG-Bold").fontSize(10).fillColor(INK).text("UQO Éval — moteur d'évaluation statistique", M + 300, 597);191 doc.font("Inter").fontSize(8).fillColor(INK2).text("Simon-Pierre Boucher, enseignant (UQO)", M + 300, 611);192 doc.font("Inter").fontSize(8).fillColor(INK2).text(`Dossier ${dossier} · Généré le ${generated}`, M + 300, 623);193194 // avis195 doc.roundedRect(M, 668, CW, 54, 5).lineWidth(1).stroke(INK);196 doc.font("JB-Bold").fontSize(6.5).fillColor(INK).text("AVIS IMPORTANT", M + 12, 678, { characterSpacing: 1 });197 doc.font("Inter").fontSize(7.4).fillColor(INK2).text(198 "Ce rapport est produit automatiquement par un modèle d'évaluation de masse (AVM). Il est structuré selon les usages de la " +199 "pratique de l'évaluation afin d'en faciliter la lecture, mais il ne constitue pas un rapport d'évaluation préparé par un " +200 "évaluateur agréé (OEAQ) et aucune inspection de la propriété n'a été effectuée. Voir les hypothèses et conditions limitatives, section 7.",201 M + 12, 689, { width: CW - 24, lineGap: 1.5 });202 footer(c, TOTAL);203204 /* ------------------------- PAGE 2 : SOMMAIRE EXÉCUTIF ------------------------- */205 let y = newPage(c, TOTAL);206 y = sectionTitle(c, "1.", "Sommaire exécutif", y);207 y = kvTable(c, [208 ["Adresse", `${u.adresse ?? ""}${s.apt ? ` app. ${s.apt}` : ""}, ${u.municipalite ?? ""}`],209 ["Identifiant provincial", u.id],210 ["Matricule", s.matricule ?? "—"],211 ["Usage prédominant (CUBF)", `${s.cubf ?? "—"} — ${s.cubfLibelle ?? "—"}`],212 ["Droit évalué (présumé)", "Pleine propriété, libre de toute charge"],213 ["Date d'effet de l'estimation", generated],214 ["Méthodes utilisées", "Comparaison directe (ajustée) et modèle hédonique de masse"],215 ["Valeur estimée", fmt(r.estimate)],216 ["Fourchette P10-P90", `${fmt(r.low)} → ${fmt(r.high)}`],217 ["Indice de fiabilité", `${r.confidenceLevel} (${r.confidencePct} %) — ${CONF_TXT[r.confidenceLevel]}`],218 ["Évaluation municipale (rôle 2026)", fmt(u.valeurRole)],219 ["Écart estimation / rôle", u.valeurRole ? `${((r.estimate / u.valeurRole - 1) * 100).toFixed(1)} %` : "—"],220 ], y);221 y = para(c,222 "La valeur conclue résulte de la réconciliation pondérée de deux approches indépendantes : un modèle hédonique de " +223 `masse entraîné sur environ 690 000 transactions québécoises (pondération ${(r.modelWeight * 100).toFixed(0)} %) et l'analyse de ` +224 `${r.nCompsUsed} ventes comparables récentes situées à proximité du sujet, individuellement ajustées pour les conditions de ` +225 `marché, la superficie habitable et l'âge (pondération ${((1 - r.modelWeight) * 100).toFixed(0)} %). La dispersion des comparables ` +226 `ajustés est de ±${r.compsDispersionPct ?? "—"} %. L'indice de fiabilité intègre le nombre de comparables, leur cohérence et la ` +227 "largeur de la fourchette d'incertitude du modèle.", y);228229 y = sectionTitle(c, "2.", "Mandat, portée et définition de la valeur", y + 4);230 y = para(c,231 "Mandat — Estimer la valeur marchande la plus probable de la propriété identifiée ci-dessus, à la date d'effet, aux fins " +232 "d'information et d'appui à une démarche de financement. Portée — Analyse automatisée des données publiques du rôle " +233 "d'évaluation foncière (MAMH) et des transactions publiées ; aucune inspection, aucune vérification de titres.", y);234 y = para(c,235 "Définition de la valeur marchande — Le prix le plus probable, à la date d'effet, moyennant paiement comptant ou " +236 "conditions équivalentes, pour lequel la propriété devrait se vendre sur un marché concurrentiel et ouvert, dans le cadre " +237 "d'une transaction sans lien de dépendance entre un acheteur et un vendeur prudents et informés, aucune des parties " +238 "n'étant contrainte d'agir et un délai de mise en marché raisonnable étant accordé.", y);239 y = para(c,240 "Approches écartées — L'approche du coût n'est pas appliquée (absence de données fiables sur le coût de remplacement " +241 "déprécié sans inspection). L'approche du revenu n'est pas appliquée (propriété à vocation principalement résidentielle ; " +242 "données de loyers non disponibles dans les sources publiques utilisées).", y);243244 /* --------------------- PAGE 3 : DESCRIPTION DE LA PROPRIÉTÉ --------------------- */245 y = newPage(c, TOTAL);246 y = sectionTitle(c, "3.", "Description de la propriété", y);247 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.1 Bâtiment", M, y); y += 16;248 y = kvTable(c, [249 ["Année de construction", u.anneeConstruction ? `${u.anneeConstruction}${s.anneeEstimee === "E" ? " (estimée au rôle)" : " (réelle)"}` : "—"],250 ["Aire d'étages (habitable)", u.aireEtagesM2 ? `${u.aireEtagesM2} m² (${Math.round(u.aireEtagesM2 * 10.7639)} pi²)` : "—"],251 ["Nombre d'étages", s.nbEtages != null ? String(s.nbEtages) : "—"],252 ["Genre de construction", s.genreConstruction ?? "—"],253 ["Lien physique", s.lienPhysique ?? "—"],254 ["Nombre de logements", u.nbLogements != null ? String(u.nbLogements) : "—"],255 ["Locaux non résidentiels", s.nbLocauxNonResid != null ? String(s.nbLocauxNonResid) : "—"],256 ], y);257 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.2 Terrain", M, y); y += 16;258 y = kvTable(c, [259 ["Superficie du terrain", u.superficieTerrainM2 ? `${u.superficieTerrainM2} m² (${Math.round(u.superficieTerrainM2 * 10.7639)} pi²)` : "—"],260 ["Mesure frontale", s.frontTerrainM ? `${s.frontTerrainM} m` : "—"],261 ["Unité de voisinage (rôle)", s.uniteVoisinage || "—"],262 ["Arrondissement", s.arrond || "—"],263 ], y);264 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("3.3 Données du rôle d'évaluation", M, y); y += 16;265 y = kvTable(c, [266 ["Valeur du terrain (rôle 2026)", fmt(s.valeurTerrain)],267 ["Valeur du bâtiment (rôle 2026)", fmt(s.valeurBatiment)],268 ["Valeur totale (rôle 2026)", fmt(u.valeurRole)],269 ["Valeur au rôle antérieur", fmt(s.valeurAnterieure)],270 ["Conditions du marché considérées au", s.datCondMarche ?? "—"],271 ], y);272 y = para(c,273 "Les caractéristiques ci-dessus proviennent du rôle d'évaluation foncière en vigueur (source : MAMH, données ouvertes). " +274 "Elles n'ont pas fait l'objet d'une vérification sur place.", y);275276 /* --------------------- PAGE 4 : GRILLE DES COMPARABLES --------------------- */277 y = newPage(c, TOTAL);278 y = sectionTitle(c, "4.", "Approche par comparaison — grille d'ajustement", y);279 const comps = r.comps.slice(0, 4);280 const colW = (CW - 118) / comps.length;281 const rowsG: [string, (ci: number) => string][] = [282 ["Adresse", (i) => `${comps[i].street ?? ""}`],283 ["Municipalité", (i) => comps[i].city ?? "—"],284 ["Date de vente", (i) => comps[i].date],285 ["Prix de vente", (i) => fmt(comps[i].amount)],286 ["Distance du sujet", (i) => comps[i].distanceM < 1000 ? `${Math.round(comps[i].distanceM)} m` : `${(comps[i].distanceM / 1000).toFixed(1)} km`],287 ["Aire d'étages", (i) => comps[i].floorArea ? `${comps[i].floorArea} m²` : "—"],288 ["Année constr.", (i) => comps[i].yearBuilt ? String(comps[i].yearBuilt) : "—"],289 ["Ajust. conditions de marché", (i) => signed(comps[i].adjTime)],290 ["Ajust. superficie", (i) => signed(comps[i].adjArea)],291 ["Ajust. âge", (i) => signed(comps[i].adjAge)],292 ["PRIX AJUSTÉ", (i) => fmt(Math.round(comps[i].adjustedPrice))],293 ["Pondération", (i) => `${(comps[i].weight * 100).toFixed(0)} %`],294 ];295 const rh = 20;296 // en-têtes colonnes297 doc.rect(M, y, 118, rh).fill(INK);298 doc.font("JB-Bold").fontSize(6.4).fillColor("#ffffff").text("ÉLÉMENT", M + 6, y + 7, { lineBreak: false });299 comps.forEach((_, i) => {300 doc.rect(M + 118 + i * colW, y, colW, rh).fillAndStroke(i % 2 ? SURFACE2 : "#ffffff", INK);301 doc.font("JB-Bold").fontSize(6.6).fillColor(GREEN_DEEP)302 .text(`COMPARABLE ${i + 1}`, M + 118 + i * colW + 6, y + 7, { lineBreak: false });303 });304 y += rh;305 rowsG.forEach(([label, get]) => {306 const bold = label === "PRIX AJUSTÉ";307 doc.rect(M, y, 118, rh).fillAndStroke(bold ? LIME : SURFACE2, INK);308 doc.font("JB-Bold").fontSize(6).fillColor(INK)309 .text(label.toUpperCase(), M + 6, y + 7, { width: 108, height: 8, ellipsis: true, lineBreak: false });310 comps.forEach((_, ci) => {311 doc.rect(M + 118 + ci * colW, y, colW, rh).fillAndStroke(bold ? "#f6fbe4" : "#ffffff", INK);312 doc.font(bold ? "SG-Bold" : "Inter").fontSize(bold ? 8.4 : 7.4).fillColor(bold ? INK : INK2)313 .text(get(ci), M + 118 + ci * colW + 6, y + (bold ? 5.5 : 6.5), { width: colW - 12, height: 10, ellipsis: true, lineBreak: false });314 });315 y += rh;316 });317 y += 10;318 y = para(c,319 `${r.nCompsUsed} ventes comparables ont été retenues au total (les ${comps.length} plus pondérées sont détaillées ci-dessus ; la liste ` +320 "complète figure dans le rapport standard). Chaque prix de vente est ajusté : (i) aux conditions actuelles du marché au moyen " +321 "d'un indice mensuel du prix médian au mètre carré du segment, (ii) pour l'écart de superficie habitable à raison de 50 % du " +322 "prix unitaire au m² du comparable, plafonné à ±25 %, et (iii) pour l'écart d'âge à raison de 0,5 % par année, plafonné à ±10 %. " +323 `La médiane pondérée des prix ajustés s'établit à ${fmt(r.compsEstimate)}.`, y);324325 // mini-indice de marché326 if (marketIndex.length > 3) {327 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("4.1 Indice des conditions de marché du segment", M, y);328 y += 16;329 const gw = CW, gh = 84;330 doc.roundedRect(M, y, gw, gh, 5).lineWidth(1).fillAndStroke("#ffffff", INK);331 const vals = marketIndex.map((p) => p.idx);332 const mn = Math.min(...vals), mx = Math.max(...vals), span = mx - mn || 1;333 const pts = marketIndex.map((p, i) => [334 M + 14 + (i / (marketIndex.length - 1)) * (gw - 28),335 y + gh - 18 - ((p.idx - mn) / span) * (gh - 36),336 ]);337 doc.moveTo(pts[0][0], pts[0][1]);338 pts.slice(1).forEach(([px, py]) => doc.lineTo(px, py));339 doc.lineWidth(1.8).stroke(GREEN);340 const last = pts[pts.length - 1];341 doc.circle(last[0], last[1], 3).lineWidth(1.2).fillAndStroke(LIME, INK);342 doc.font("JB-Reg").fontSize(6).fillColor(INK3)343 .text(marketIndex[0].month, M + 14, y + gh - 12, { lineBreak: false })344 .text(marketIndex[marketIndex.length - 1].month, M + gw - 60, y + gh - 12, { width: 46, align: "right" });345 doc.font("JB-Reg").fontSize(6).fillColor(INK3)346 .text(`BASE 1,00 = ${marketIndex[marketIndex.length - 1].month} · MÉDIANE $/M² LISSÉE 3 MOIS`, M + 14, y + 8, { characterSpacing: 0.5 });347 }348349 /* ------------------- PAGE 5 : HÉDONIQUE + RÉCONCILIATION ------------------- */350 y = newPage(c, TOTAL);351 y = sectionTitle(c, "5.", "Analyse hédonique de masse", y);352 y = para(c,353 "Un modèle de gradient boosting (LightGBM) est entraîné sur environ 690 000 transactions québécoises (2021-2026) appariées " +354 "au rôle d'évaluation foncière géoréférencé. Dix-huit variables décrivent chaque propriété (dimensions, âge, usage, " +355 "localisation fine, valeurs au rôle, temps). Sur un échantillon réservé de 102 943 ventes jamais vues du modèle, l'erreur " +356 "médiane est de 11,0 % et 72 % des estimations tombent à ±20 % du prix réel (ratio médian 0,995, sans biais systématique — " +357 "validation selon la norme IAAO sur études de ratios). L'incertitude propre à la propriété est quantifiée par deux modèles de " +358 "régression quantile (10e et 90e centiles).", y);359 y = kvTable(c, [360 ["Estimation du modèle hédonique", fmt(r.modelEstimate)],361 ["Intervalle P10-P90 du modèle", `${fmt(r.low)} → ${fmt(r.high)} (recentré)`],362 ["Erreur médiane du modèle (validation)", "11,0 % (MdAPE, holdout aléatoire)"],363 ], y);364365 y = sectionTitle(c, "6.", "Réconciliation et conclusion", y + 4);366 const rh2 = 20;367 const recRows: [string, string, string][] = [368 ["Approche", "Indication de valeur", "Pondération"],369 ["Modèle hédonique de masse", fmt(r.modelEstimate), `${(r.modelWeight * 100).toFixed(0)} %`],370 ["Comparaison directe (médiane pondérée)", fmt(r.compsEstimate), `${((1 - r.modelWeight) * 100).toFixed(0)} %`],371 ["VALEUR CONCLUE", fmt(r.estimate), "100 %"],372 ];373 recRows.forEach(([a, b, cc], i) => {374 const head = i === 0;375 const last = i === recRows.length - 1;376 doc.rect(M, y, CW, rh2).fillAndStroke(head ? INK : last ? LIME : i % 2 ? SURFACE2 : "#ffffff", INK);377 doc.font(head || last ? "JB-Bold" : "Inter").fontSize(head ? 6.8 : last ? 8.6 : 8)378 .fillColor(head ? "#ffffff" : INK)379 .text(head ? a.toUpperCase() : a, M + 8, y + 6, { lineBreak: false });380 doc.text(b, M + CW * 0.52, y + 6, { lineBreak: false });381 doc.text(cc, M + CW * 0.84, y + 6, { lineBreak: false });382 y += rh2;383 });384 y += 10;385 y = para(c,386 `Compte tenu de la qualité de l'appariement des données, du nombre et de la cohérence des comparables (dispersion ±${r.compsDispersionPct ?? "—"} %) ` +387 `et de la performance validée du modèle, la valeur marchande estimée de la propriété au ${generated} est conclue à ` +388 `${fmt(r.estimate)}, à l'intérieur d'une fourchette raisonnable de ${fmt(r.low)} à ${fmt(r.high)}. Indice de fiabilité : ` +389 `${r.confidenceLevel} (${r.confidencePct} %) — ${CONF_TXT[r.confidenceLevel]}.`, y);390391 doc.font("SG-Bold").fontSize(9.5).fillColor(INK).text("6.1 Évolution de la valeur estimée (2021-2026)", M, y);392 y += 16;393 const hist = u.history.filter((h) => h.value != null);394 y = kvTable(c, hist.map((h) => [String(h.year), fmt(h.value)] as [string, string]), y, CW * 0.6);395396 /* ------------------- PAGE 6 : CONDITIONS + CERTIFICATION ------------------- */397 y = newPage(c, TOTAL);398 y = sectionTitle(c, "7.", "Hypothèses et conditions limitatives", y);399 const conds = [400 "Le présent rapport est généré automatiquement par le moteur d'évaluation statistique UQO Éval (modèle d'évaluation de masse, AVM), dans un cadre pédagogique universitaire. Il ne constitue pas un rapport d'évaluation au sens des normes de pratique professionnelle de l'Ordre des évaluateurs agréés du Québec (OEAQ) et ne peut être présenté comme tel.",401 "Aucune inspection physique de la propriété n'a été effectuée. L'état d'entretien, la qualité des finis, les rénovations, les dépendances et tout vice apparent ou caché ne sont pas considérés.",402 "Les caractéristiques utilisées proviennent du rôle d'évaluation foncière (MAMH, données ouvertes) et de transactions publiées. Leur exactitude et leur mise à jour ne sont pas garanties.",403 "Le droit évalué est présumé être la pleine propriété, libre de toute hypothèque, servitude, charge ou restriction inhabituelle. Aucune vérification de titres n'a été réalisée.",404 "La conformité aux lois, règlements de zonage et permis est présumée. Aucune étude environnementale n'a été effectuée ; la propriété est présumée exempte de contamination.",405 "La valeur conclue vaut à la date d'effet uniquement. Les marchés immobiliers évoluent ; l'estimation ne doit pas être utilisée à une autre date.",406 "La fourchette P10-P90 et l'indice de fiabilité font partie intégrante de la conclusion et doivent être communiqués avec elle.",407 "Ce rapport est destiné à un usage pédagogique, universitaire et expérimental (cours IMM1003 et IMM1033, UQO). Toute autre utilisation ou distribution engage la seule responsabilité de l'utilisateur.",408 "Pour toute décision réelle (financement, transaction), une évaluation par un évaluateur agréé (OEAQ) est requise.",409 ];410 conds.forEach((txt, i) => {411 doc.font("JB-Bold").fontSize(7).fillColor(GREEN).text(`7.${i + 1}`, M, y + 1, { lineBreak: false });412 doc.font("Inter").fontSize(7.9).fillColor(INK2)413 .text(txt, M + 26, y, { width: CW - 26, lineGap: 1.6, align: "justify" });414 y = doc.y + 6;415 });416417 y = sectionTitle(c, "8.", "Attestation", y + 2);418 y = para(c,419 "J'atteste que les données, méthodes et pondérations décrites dans ce rapport sont celles réellement employées par le moteur " +420 "UQO Éval pour produire la conclusion de valeur ; que la méthodologie complète, ses métriques de validation et ses limites " +421 "sont publiées à uqo-eval.app/methodologie ; et que ni UQO Éval ni son exploitant n'ont d'intérêt présent ou futur dans la " +422 "propriété évaluée, ni de lien de dépendance avec les parties à une éventuelle transaction. " + COURS, y);423 y += 14;424 doc.rect(M, y + 26, 200, 0.9).fill(INK);425 doc.image(UQO_LOGO_BLEU, M, y + 32, { height: 16 });426 doc.font("SG-Bold").fontSize(10).fillColor(INK).text("Éval", M + 16 * UQO_RATIO + 5, y + 36, { lineBreak: false });427 doc.font("Inter").fontSize(8).fillColor(INK2)428 .text("Simon-Pierre Boucher, enseignant — UQO", M, y + 52)429 .text(`Dossier ${dossier} · ${generated}`, M, y + 64);430 doc.font("JB-Reg").fontSize(6.5).fillColor(INK3)431 .text("ANNEXE DISPONIBLE : RAPPORT STANDARD (PLAN DES COMPARABLES, LISTE COMPLÈTE DES VENTES) — UQO-EVAL.APP", M, y + 86, { characterSpacing: 0.5 });432433 doc.end();434 return done;435}436