// UQO Éval — Quantity Survey : la correspondance relevé → RSMeans est cohérente (unités compatibles) et, quand la base RSMeans est // installée, chaque ligne existe et possède un prix national. import { describe, expect, it } from "vitest"; import { QS_RSMEANS_MAP, QS_RSMEANS_UNMAPPED } from "./rsmeans-qs-map"; import { rsmLine, rsmPrice, rsmUnit, rsmeansAvailable } from "./rsmeans"; import { dimensionOf } from "./units"; describe("correspondance relevé des quantités → RSMeans", () => { it("toutes les familles du relevé sont couvertes (correspondance ou exclusion explicite)", async () => { const src = (await import("fs")).readFileSync(new URL("./quantity-survey.ts", import.meta.url), "utf8"); const codes = [...new Set([...src.matchAll(/"([A-Z]{2,4}-[A-Z0-9/-]{3,})"/g)].map((m) => m[1]).filter((c) => !c.startsWith("QC-")))]; expect(codes.length).toBeGreaterThan(40); for (const c of codes) expect(c in QS_RSMEANS_MAP || QS_RSMEANS_UNMAPPED.includes(c), c).toBe(true); }); it("numéros à 12 chiffres, facteurs > 0, unités de remplacement valides", () => { for (const [code, parts] of Object.entries(QS_RSMEANS_MAP)) { expect(parts.length, code).toBeGreaterThan(0); for (const p of parts) { expect(p.line, code).toMatch(/^\d{12}$/); expect(p.qtyFactor ?? 1, code).toBeGreaterThan(0); if (p.unit) expect(["m", "m2", "m3", "unit"]).toContain(p.unit); } } }); it.skipIf(!rsmeansAvailable())("base installée : chaque ligne existe, a un prix national et une unité compatible avec la ligne du relevé", async () => { const src = (await import("fs")).readFileSync(new URL("./quantity-survey.ts", import.meta.url), "utf8"); const unitOf: Record = Object.fromEntries([...src.matchAll(/add\("[^"]*", "([A-Z0-9/-]+)", [^;]*?, "(m|m2|m3|unit|lump)"/g)].map((m) => [m[1], m[2]])); for (const k of Object.keys(QS_RSMEANS_MAP)) { if (k.startsWith("KIT-")) unitOf[k] = "m"; if (k.startsWith("BATH-")) unitOf[k] = "unit"; if (k.startsWith("ENV-SIDING-")) unitOf[k] = "m2"; if (k.startsWith("ROOF-")) unitOf[k] = "m2"; if (k.startsWith("INT-FLOOR-")) unitOf[k] = "m2"; if (k.startsWith("OPN-")) unitOf[k] = "unit"; if (k === "FND-WALL-BLOCK-8IN") unitOf[k] = "m2"; if (k.startsWith("EXT-DECK-RAILING")) unitOf[k] = "m"; if (k.startsWith("EXT-DRIVEWAY")) unitOf[k] = "m2"; } const problems: string[] = []; for (const [code, parts] of Object.entries(QS_RSMEANS_MAP)) for (const p of parts) { const l = rsmLine(p.line); if (!l) { problems.push(`${code}: ligne ${p.line} absente`); continue; } const um = rsmUnit(l.unit); if (!um) { problems.push(`${code}: ligne ${p.line} sans unité chiffrable (${l.unit})`); continue; } const price = rsmPrice(p.line, 431, "STD"); if (!price || price.total == null) problems.push(`${code}: ligne ${p.line} sans prix national`); const qsUnit = p.unit ?? unitOf[code]; if (qsUnit && qsUnit !== "lump" && dimensionOf(qsUnit) !== dimensionOf(um.unit)) problems.push(`${code}: ${p.line} ${l.unit} (${dimensionOf(um.unit)}) ≠ ${qsUnit} (${dimensionOf(qsUnit)})`); } expect(problems).toEqual([]); }); }); describe("relevé des quantités tarifé RSMeans (base installée)", () => { it.skipIf(!rsmeansAvailable())("cottage à Gatineau : lignes « rsmeans » chiffrées, couverture élevée, total plausible, regénération sans doublon", async () => { const fs = await import("fs"); const os = await import("os"); const path = await import("path"); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "metre-qsrsm-")); process.env.METRE_DB = path.join(tmp, "metre.db"); process.env.METRE_FILES_DIR = path.join(tmp, "files"); process.env.COST_DB = path.join(tmp, "cost.db"); const db = await import("./db"); const auth = await import("./auth"); const projects = await import("./projects"); const plans = await import("./plans"); const estimates = await import("./estimates"); const d = db.getMetreDb(); const ctx = { db: d, user: auth.anonymousLogin(d, { ip: null, userAgent: null }).user, ip: null }; const p = projects.createProject(ctx, { name: "Cas RSMeans", regionCode: "QC-GAT", municipality: "Gatineau", valuationDate: "2026-10-28" }); await plans.createTemplateProject(ctx, p.id, { template: "cottage-c" }); const r = await estimates.generateQuantitySurvey(ctx, p.id, null, { pricing: "rsmeans" }); const rsm = r.estimate.lines.filter((l) => l.basis === "rsmeans"); expect(rsm.length).toBeGreaterThan(40); expect(rsm.every((l) => /^\d{12}$/.test(l.rsmeansLine ?? ""))).toBe(true); const unpriced = r.result.lines.filter((l) => !l.excluded && l.basis === "rsmeans" && l.located <= 0); expect(unpriced.map((l) => l.description), "lignes RSMeans non chiffrées").toEqual([]); expect(r.result.coverage.linesPriced / r.result.coverage.linesTotal).toBeGreaterThan(0.9); expect(r.result.totals.total).toBeGreaterThan(150_000); expect(r.result.totals.total).toBeLessThan(900_000); expect(r.result.lines.some((l) => l.provenance.source.includes("GATINEAU"))).toBe(true); const again = await estimates.generateQuantitySurvey(ctx, p.id, r.estimate.id, { pricing: "rsmeans" }); expect(again.estimate.lines.filter((l) => l.origin === "qs").length).toBe(r.estimate.lines.filter((l) => l.origin === "qs").length); fs.rmSync(tmp, { recursive: true, force: true }); }); });