// Tests des moteurs et helpers communs : contexte (Corse, DOM, inconnu), // géométrie (maison B de l'étude), argent, validation des données. import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { contexte, departementDepuisCodePostal, geometrie, mensualite, annuite, coutDuCredit, arrondiAuPas, additionneFourchettes, valider, VERSION, COEFFICIENTS } from '../dist/index.js' const lire = (f) => JSON.parse(readFileSync(new URL(`../fixtures/${f}`, import.meta.url), 'utf8')) const france_climat = lire('france_climat.json') test('contexte : département depuis code postal (Corse 2A/2B, DOM 97x, métropole)', () => { assert.equal(departementDepuisCodePostal('20000'), '2A') assert.equal(departementDepuisCodePostal('20190'), '2A') assert.equal(departementDepuisCodePostal('20200'), '2B') assert.equal(departementDepuisCodePostal('20620'), '2B') assert.equal(departementDepuisCodePostal('20999'), '2A') assert.equal(departementDepuisCodePostal('97100'), '971') assert.equal(departementDepuisCodePostal('97600'), '976') assert.equal(departementDepuisCodePostal('75019'), '75') assert.equal(departementDepuisCodePostal(31000), '31') assert.equal(departementDepuisCodePostal(''), null) assert.equal(departementDepuisCodePostal(null), null) }) test('contexte : 2A, 2B et 971 tombent sur leur ligne france_climat (H3), pas sur le défaut H2a', () => { for (const [cp, dept] of [ ['20000', '2A'], ['20200', '2B'], ['97100', '971'], ]) { const r = contexte.calculer({ code_postal: cp }, { france_climat }) const ligne = france_climat.find((c) => c.dept === dept) assert.ok(ligne, `fixture : ligne ${dept} attendue`) assert.equal(r.sorties.departement, dept) assert.equal(r.sorties.zone_climatique_rt, ligne.zone_rt) assert.equal(r.sorties.zone_climatique_rt, 'H3', `${dept} est en H3 dans la fixture ; l'origine donnait H2a`) assert.equal(r.version, VERSION) assert.ok(r.sources.some((s) => s.cle === `france_climat.${dept}`), 'la ligne climat est tracée en source') if (ligne.pluvio_mm_an != null) { assert.equal(r.sorties.pluviometrie_mm, ligne.pluvio_mm_an) assert.equal(r.sorties.dju, ligne.dju_base_18) assert.deepEqual(r.avertissements, [], `${cp} : aucun défaut appliqué`) } else { // DOM : la fixture ne porte ni pluviométrie ni DJU (« DOM tropical ») → défauts déclarés assert.equal(r.sorties.pluviometrie_mm, COEFFICIENTS.contexte_defaut.valeur.pluviometrie_mm) assert.ok(r.avertissements.some((a) => /pluviométrie absente/.test(a)), `${cp} : pluviométrie par défaut déclarée`) assert.ok(r.avertissements.some((a) => /DJU absent/.test(a)), `${cp} : DJU par défaut déclaré`) } } }) test('contexte : code postal inconnu → défaut déclaré en avertissement et en source', () => { const r = contexte.calculer({ code_postal: '98000' }, { france_climat }) assert.equal(r.sorties.zone_climatique_rt, COEFFICIENTS.contexte_defaut.valeur.zone_rt) assert.equal(r.avertissements.length, 1) assert.match(r.avertissements[0], /défaut/) assert.ok(r.sources.some((s) => s.cle === 'contexte_defaut')) }) test('géométrie : maison B de l\'étude (Tournefeuille, emprise 73 m², 2 niveaux, 5 m) — SHAB estimée à confronter au DPE en P1.1', () => { const r = geometrie.calculer({ emprise_m2: 73, niveaux: 2, hauteur_m: 5, pente_toit_deg: 30, mitoyennete: 0 }, {}) const s = r.sorties // SHAB = 73 × 2 × 0,80 = 116,8 m² assert.equal(s.shab_m2, 116.8) assert.equal(s.shab_estimee, true) assert.equal(s.plancher_bas_m2, 73) assert.equal(s.hauteur_etage_m, 2.5) assert.equal(s.volume_m3, 292) // périmètre 4 × √(73 × 1,3) = 38,97 m ; murs ext × 5 m = 194,83 m² ; vitrage 116,8/6 = 19,47 ; opaques 175,37 assert.equal(s.perimetre_m, 38.97) assert.equal(s.murs_exterieurs_m2, 194.83) assert.equal(s.vitrage_m2, 19.47) assert.equal(s.murs_opaques_m2, 175.37) // toiture 73 / cos 30° × 1,05 = 88,51 assert.equal(s.toiture_m2, 88.51) assert.ok(r.avertissements.some((a) => /estimée/.test(a)), 'SHAB estimée déclarée') assert.ok(r.sources.some((x) => x.cle === 'geometrie_ratio_shab_emprise')) // La surface habitable du DPE 2631E0015736Q n'est pas dans l'étude (aucune recherche web dans ce lot) : // l'écart SHAB estimée vs DPE se mesure au premier appel de l'adaptateur DPE, P1.1. console.log(` maison B : SHAB estimée ${s.shab_m2} m² (ratio 0,80) — surface DPE : à lire en P1.1 (écart visé < 20 %)`) }) test('géométrie : mitoyenneté et surface connue', () => { const seul = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5 }, {}).sorties const mitoyen = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5, mitoyennete: 2 }, {}).sorties assert.equal(mitoyen.perimetre_expose_m, +(seul.perimetre_m * 0.5).toFixed(2)) assert.ok(mitoyen.murs_exterieurs_m2 < seul.murs_exterieurs_m2) const connue = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5, surface_habitable_m2: 95 }, {}) assert.equal(connue.sorties.shab_m2, 95) assert.equal(connue.sorties.shab_estimee, false) assert.ok(!connue.avertissements.some((a) => /estimée/.test(a))) const sansRien = geometrie.calculer({ emprise_m2: 80 }, {}) assert.equal(sansRien.avertissements.length, 4, 'niveaux, hauteur, pente inconnus + SHAB estimée → quatre avertissements') }) test('argent : mensualité, annuité, coût du crédit, arrondis, fourchettes', () => { // 10 000 € à 3 % sur 10 ans → 96,56 €/mois (formule d'annuité constante) assert.equal(+mensualite(10000, 3, 10).toFixed(2), 96.56) assert.equal(+annuite(10000, 3, 10).toFixed(2), 1158.73) assert.equal(+coutDuCredit(10000, 3, 10).toFixed(2), 1587.29) assert.equal(mensualite(12000, 0, 10), 100) assert.equal(mensualite(0, 3, 10), 0) assert.equal(arrondiAuPas(1234, 50), 1250) assert.equal(arrondiAuPas(7, 0), 7) assert.deepEqual(additionneFourchettes([{ min: 10, max: 20 }, { min: null, max: 5 }, { min: 3, max: null }, { min: null, max: null }]), { min: 18, max: 28 }) }) test('valider : les fixtures passent, une donnée cassée est refusée avec son chemin', () => { for (const [table, fichier] of [ ['postes', 'postes.json'], ['options', 'options.json'], ['france_climat', 'france_climat.json'], ['algo_config', 'algo-config.json'], ]) { const v = valider(table, lire(fichier)) assert.deepEqual(v.erreurs, [], `${table} : fixture valide`) assert.equal(v.ok, true) } const cassees = lire('postes.json').map((p, i) => (i === 2 ? { ...p, axe: 'feu', Id: 'trois' } : p)) const v = valider('postes', cassees) assert.equal(v.ok, false) assert.ok(v.erreurs.some((e) => e.startsWith('postes[2].Id')), 'chemin de l\'erreur') assert.ok(v.erreurs.some((e) => /postes\[2\]\.axe.*feu/.test(e))) assert.equal(valider('postes', {}).ok, false) assert.equal(valider('postes', []).ok, false, 'minItems 1') assert.equal(valider('algo_config', { poids_axes_base: { eau: 40, energie: 40, alim: 20 } }).ok, false, 'clés requises manquantes') })