Extraction du cœur autonomie de calculs.trans-former.fr (12 modules, sans singleton, couplages sans mutation, console derrière debug), découpage en trois moteurs d'axe + intégrateur (mode global = calculs, mode par_axe = plateforme), moteurs commun.contexte (Corse 2A/2B, DOM 97x corrigés) et commun.geometrie, table des coefficients harmonisés (docs/CONSTANTES.md pour Jules, R-7), validation structurelle des données (schemas/), tests hors ligne : 28 assertions d'origine + épingle générée depuis le code d'astro-pro + écarts d'harmonisation figés + contrat des moteurs. dist/ committé. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019UBHYPdeQYy2m1GVJp6b12
128 lines
7.0 KiB
JavaScript
128 lines
7.0 KiB
JavaScript
// 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')
|
||
})
|