@open3cl/engine 1.8.8 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/conso.js CHANGED
@@ -122,6 +122,129 @@ function cout_electricite(cef) {
122
122
  else return 56 + 0.15989 * cef;
123
123
  }
124
124
 
125
+ /**
126
+ * Date d'entrée en vigueur du barème des prix des énergies mis à jour.
127
+ * Constatée sur les DPE publiés par l'ADEME (dataset dpe03existant) : barème 2021 jusqu'au
128
+ * 30/06/2024, barème mis à jour pour les DPE établis à partir du 01/07/2024 (et toujours en 2026).
129
+ */
130
+ export const DATE_BAREME_COUT_2024 = '2024-07-01';
131
+
132
+ function cout_gaz_naturel_2024(cef) {
133
+ if (cef < 5009) return 0.1312 * cef;
134
+ else if (cef < 50055) return 182 + 0.09488 * cef;
135
+ else return 288 + 0.09274 * cef;
136
+ }
137
+
138
+ function cout_electricite_2024(cef) {
139
+ if (cef < 1000) return 0.34721 * cef;
140
+ else if (cef < 2500) return 158 + 0.18954 * cef;
141
+ else if (cef < 5000) return 158 + 0.18949 * cef;
142
+ else if (cef < 15000) return 119 + 0.19726 * cef;
143
+ else return 78 + 0.20001 * cef;
144
+ }
145
+
146
+ /**
147
+ * Barème des prix des énergies applicable aux DPE établis à partir du 01/07/2024.
148
+ * Mêmes tranches qu'en 2021 ; valeurs calées sur les sorties Tribu (moteur CSTB) et les DPE ADEME.
149
+ */
150
+ const coef_cout_2024 = {
151
+ 'fioul domestique': 0.14821,
152
+ 'réseau de chauffage urbain': 0.08921,
153
+ propane: 0.15672,
154
+ gpl: 0.15672,
155
+ butane: 0.23429,
156
+ charbon: 0.02787,
157
+ 'bois – granulés (pellets) ou briquettes': 0.09897,
158
+ 'bois – bûches': 0.042,
159
+ 'bois – plaquettes forestières': 0.042,
160
+ 'bois – plaquettes d’industrie': 0.042,
161
+ 'gaz naturel': cout_gaz_naturel_2024,
162
+ "électricité d'origine renouvelable utilisée dans le bâtiment": cout_electricite_2024,
163
+ 'électricité ch': cout_electricite_2024,
164
+ 'électricité ecs': cout_electricite_2024,
165
+ 'électricité fr': cout_electricite_2024,
166
+ 'électricité éclairage': cout_electricite_2024,
167
+ 'électricité auxiliaire': cout_electricite_2024
168
+ };
169
+
170
+ /**
171
+ * Sélection du barème des prix des énergies selon la date d'établissement du DPE.
172
+ * @param dateDpe {string} date d'établissement du DPE (AAAA-MM-JJ)
173
+ * @returns {object} barème 2021 (Annexe 7) avant le 01/07/2024, barème mis à jour ensuite
174
+ */
175
+ export function getCoefCout(dateDpe) {
176
+ return new Date(dateDpe) >= new Date(DATE_BAREME_COUT_2024) ? coef_cout_2024 : coef_cout;
177
+ }
178
+
179
+ /** Clé (symbole) portant le barème des générateurs d'installations collectives d'un immeuble. */
180
+ const COUT_COLLECTIF = Symbol('cout_collectif');
181
+
182
+ /** Énergies dont le coût suit un barème par tranches, regroupées par énergie facturée. */
183
+ const ENERGIES_A_TRANCHES = {
184
+ 'électricité ch': 'électricité',
185
+ 'électricité ecs': 'électricité',
186
+ 'électricité fr': 'électricité',
187
+ 'électricité éclairage': 'électricité',
188
+ 'électricité auxiliaire': 'électricité',
189
+ "électricité d'origine renouvelable utilisée dans le bâtiment": 'électricité renouvelable',
190
+ 'gaz naturel': 'gaz naturel'
191
+ };
192
+
193
+ /**
194
+ * Table « masque » : consommation conservée pour les clés du groupe, annulée pour les autres.
195
+ * Permet de totaliser la consommation d'une énergie avec les fonctions d'agrégation existantes.
196
+ * @param groupe {string} énergie facturée (valeur de ENERGIES_A_TRANCHES)
197
+ * @returns {object}
198
+ */
199
+ export function masqueEnergie(groupe) {
200
+ const cles = Object.values(enums.type_energie)
201
+ .filter((label) => label !== 'électricité')
202
+ .concat(Object.keys(ENERGIES_A_TRANCHES));
203
+ return Object.fromEntries(
204
+ cles.map((cle) => [cle, ENERGIES_A_TRANCHES[cle] === groupe ? (c) => c : () => 0])
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Barème « prix moyen » pour les DPE établis à partir du 01/07/2024 (constaté sur Tribu et sur
210
+ * les DPE publiés par l'ADEME) : pour l'électricité et le gaz naturel, la tranche est déterminée
211
+ * sur la consommation annuelle TOTALE de l'énergie (tous usages) et le coût est réparti entre les
212
+ * postes au prorata de leur consommation.
213
+ * Immeuble : consommation des usages individuels rapportée au logement (N × f(C / N)) ;
214
+ * chauffage / ECS des installations collectives tarifés sur leur total (f(C)).
215
+ *
216
+ * @param base {object} barème (prix unitaires et fonctions par tranches)
217
+ * @param consoGroupe {function(masque): {total: number, collectif: number}} consommations EF
218
+ * @param nbLogements {number} nombre de logements (1 hors immeuble)
219
+ * @param genCollectifs {Set} générateurs de chauffage / ECS d'installations collectives
220
+ * @returns {object}
221
+ */
222
+ export function coefCoutProrata(base, consoGroupe, nbLogements, genCollectifs) {
223
+ const coef = { ...base };
224
+ const coefCollectif = { ...base };
225
+ new Set(Object.values(ENERGIES_A_TRANCHES)).forEach((groupe) => {
226
+ const { total, collectif } = consoGroupe(masqueEnergie(groupe));
227
+ const individuel = total - collectif;
228
+ Object.keys(ENERGIES_A_TRANCHES)
229
+ .filter((cle) => ENERGIES_A_TRANCHES[cle] === groupe)
230
+ .forEach((cle) => {
231
+ const f = base[cle];
232
+ const prix = individuel > 0 ? (nbLogements * f(individuel / nbLogements)) / individuel : 0;
233
+ const prixCollectif = collectif > 0 ? f(collectif) / collectif : 0;
234
+ coef[cle] = (c) => prix * c;
235
+ coefCollectif[cle] = (c) => prixCollectif * c;
236
+ });
237
+ });
238
+ coef[COUT_COLLECTIF] = { generateurs: genCollectifs, coef: coefCollectif };
239
+ return coef;
240
+ }
241
+
242
+ /** Barème d'un générateur : barème collectif s'il appartient à une installation collective. */
243
+ function coefPourGenerateur(coef, gen) {
244
+ const collectif = coef?.[COUT_COLLECTIF];
245
+ return collectif?.generateurs.has(gen) ? collectif.coef : coef;
246
+ }
247
+
125
248
  function getConso(coef, type_energie, conso) {
126
249
  // is coef is a function, execute it
127
250
  if (!coef) return conso;
@@ -197,7 +320,8 @@ export default function calc_conso(
197
320
  prorataECS,
198
321
  prorataChauffage,
199
322
  dateDpe,
200
- coeffEp
323
+ coeffEp,
324
+ nbLogements = 1
201
325
  ) {
202
326
  const gen_ch = ch.reduce((acc, ch) => {
203
327
  const generateur_chauffage = ch.generateur_chauffage_collection.generateur_chauffage;
@@ -237,6 +361,56 @@ export default function calc_conso(
237
361
  return acc.concat(generateur_ecs);
238
362
  }, []);
239
363
 
364
+ /**
365
+ * Barème des coûts : barème 2021 (par poste) avant le 01/07/2024 ; ensuite barème mis à jour
366
+ * avec tranche déterminée sur la consommation totale de l'énergie (voir coefCoutProrata).
367
+ */
368
+ const coefCout = () => {
369
+ const base = getCoefCout(dateDpe);
370
+ if (base === coef_cout) return coef_cout;
371
+ const genCollectifs = new Set();
372
+ if (nbLogements > 1) {
373
+ gen_ch
374
+ .filter((g) => g.donnee_entree.enum_type_installation_id !== '1')
375
+ .forEach((g) => genCollectifs.add(g));
376
+ ecs
377
+ .filter((inst) => (inst.donnee_entree.enum_type_installation_id ?? '1') !== '1')
378
+ .forEach((inst) =>
379
+ inst.generateur_ecs_collection.generateur_ecs.forEach((g) => genCollectifs.add(g))
380
+ );
381
+ }
382
+ const consoGroupe = (masque) => {
383
+ const tot = calc_conso_pond(
384
+ Sh,
385
+ zc_id,
386
+ vt,
387
+ gen_ch,
388
+ gen_ecs,
389
+ fr,
390
+ 'c',
391
+ masque,
392
+ prorataECS,
393
+ prorataChauffage,
394
+ ecs
395
+ );
396
+ const coll = calc_conso_pond(
397
+ 0,
398
+ zc_id,
399
+ [],
400
+ gen_ch.filter((g) => genCollectifs.has(g)),
401
+ gen_ecs.filter((g) => genCollectifs.has(g)),
402
+ [],
403
+ 'c',
404
+ masque,
405
+ prorataECS,
406
+ prorataChauffage,
407
+ []
408
+ );
409
+ return { total: tot.c_5_usages, collectif: coll.c_ch + coll.c_ecs };
410
+ };
411
+ return coefCoutProrata(base, consoGroupe, nbLogements, genCollectifs);
412
+ };
413
+
240
414
  const ret = {
241
415
  ef_conso: calc_conso_pond(
242
416
  Sh,
@@ -288,7 +462,7 @@ export default function calc_conso(
288
462
  gen_ecs,
289
463
  fr,
290
464
  'cout',
291
- coef_cout,
465
+ coefCout(),
292
466
  prorataECS,
293
467
  prorataChauffage,
294
468
  ecs,
@@ -424,7 +598,7 @@ function getEcsConso(gen_ecs, field, coef, prorataECS, prefix) {
424
598
  const conso = gen_ecs.donnee_intermediaire[field];
425
599
  const typeEnergie = getTypeEnergie(gen_ecs.donnee_entree, 'ecs');
426
600
 
427
- let coeffConsoEcs = { ...coef };
601
+ let coeffConsoEcs = { ...coefPourGenerateur(coef, gen_ecs) };
428
602
 
429
603
  if (prefix === 'emission_ges') {
430
604
  coeffConsoEcs[typeEnergie] = gen_ecs.donnee_utilisateur.coeffEmissionGes;
@@ -458,7 +632,7 @@ function getChauffageConso(gen_ch, field, coef, prorataChauffage, prefix) {
458
632
  ? gen_ch.donnee_entree.cle_repartition_ch || prorataChauffage
459
633
  : prorataChauffage;
460
634
 
461
- let coeffConsoChauffage = { ...coef };
635
+ let coeffConsoChauffage = { ...coefPourGenerateur(coef, gen_ch) };
462
636
 
463
637
  if (prefix === 'emission_ges') {
464
638
  coeffConsoChauffage[typeEnergie] = gen_ch.donnee_utilisateur.coeffEmissionGes;
package/conso.spec.js CHANGED
@@ -51,7 +51,11 @@ const {
51
51
  classe_bilan_dpe,
52
52
  classe_emission_ges,
53
53
  getCoefKey,
54
- coef_ep
54
+ coef_ep,
55
+ getCoefCout,
56
+ coefCoutProrata,
57
+ masqueEnergie,
58
+ DATE_BAREME_COUT_2024
55
59
  } = await import('./conso.js');
56
60
 
57
61
  /**
@@ -499,3 +503,185 @@ describe('calc_conso - agrégation des consommations', () => {
499
503
  expect(ids).toContain('1');
500
504
  });
501
505
  });
506
+
507
+ /**
508
+ * Barème des prix des énergies selon la date d'établissement du DPE.
509
+ * Barème 2021 : Annexe 7 de l'arrêté du 31/03/2021.
510
+ * Barème mis à jour (DPE à partir du 01/07/2024) : valeurs calées sur les sorties Tribu (CSTB)
511
+ * des autotests et sur les DPE publiés par l'ADEME (dataset dpe03existant), reproduites à ±1 €.
512
+ */
513
+ describe('getCoefCout - sélection du barème par date de DPE', () => {
514
+ test('date de bascule : 01/07/2024', () => {
515
+ expect(DATE_BAREME_COUT_2024).toBe('2024-07-01');
516
+ });
517
+
518
+ test('DPE établi avant le 01/07/2024 : barème 2021 inchangé', () => {
519
+ const coef = getCoefCout('2024-06-30');
520
+ // Annexe 7 : électricité 1000 kWh => 149 + 0,14066 × 1000
521
+ expect(coef['électricité ch'](1000)).toBeCloseTo(149 + 0.14066 * 1000, 9);
522
+ expect(coef['gaz naturel'](3000)).toBeCloseTo(0.11121 * 3000, 9);
523
+ expect(coef['fioul domestique']).toBe(0.09142);
524
+ expect(coef.propane).toBe(0.14305);
525
+ });
526
+
527
+ test.each(['2024-07-01', '2025-12-31', '2026-01-15'])(
528
+ 'DPE établi le %s : barème mis à jour',
529
+ (date) => {
530
+ const coef = getCoefCout(date);
531
+ // Tribu / ADEME : électricité 1000 kWh => 158 + 0,18954 × 1000
532
+ expect(coef['électricité ch'](1000)).toBeCloseTo(158 + 0.18954 * 1000, 9);
533
+ }
534
+ );
535
+
536
+ test('barème mis à jour : prix unitaires constants (fioul, propane, bois, réseau...)', () => {
537
+ const coef = getCoefCout('2026-01-15');
538
+ // Tribu : fioul 0,1482 €/kWh ; propane 0,1567 €/kWh (constants quelle que soit la conso)
539
+ expect(coef['fioul domestique']).toBe(0.14821);
540
+ expect(coef.propane).toBe(0.15672);
541
+ expect(coef.gpl).toBe(0.15672);
542
+ expect(coef.butane).toBe(0.23429);
543
+ expect(coef.charbon).toBe(0.02787);
544
+ expect(coef['réseau de chauffage urbain']).toBe(0.08921);
545
+ expect(coef['bois – granulés (pellets) ou briquettes']).toBe(0.09897);
546
+ expect(coef['bois – bûches']).toBe(0.042);
547
+ expect(coef['bois – plaquettes forestières']).toBe(0.042);
548
+ expect(coef['bois – plaquettes d’industrie']).toBe(0.042);
549
+ });
550
+
551
+ test.each([
552
+ // [conso, coût attendu] électricité, une valeur par tranche + limites
553
+ [500, 0.34721 * 500],
554
+ [999.99, 0.34721 * 999.99],
555
+ [1000, 158 + 0.18954 * 1000],
556
+ [2499.99, 158 + 0.18954 * 2499.99],
557
+ [2500, 158 + 0.18949 * 2500],
558
+ [4999.99, 158 + 0.18949 * 4999.99],
559
+ [5000, 119 + 0.19726 * 5000],
560
+ [14999.99, 119 + 0.19726 * 14999.99],
561
+ [15000, 78 + 0.20001 * 15000],
562
+ [20000, 78 + 0.20001 * 20000]
563
+ ])('barème mis à jour - électricité %s kWh => %s €', (conso, attendu) => {
564
+ const coef = getCoefCout('2026-01-15');
565
+ [
566
+ 'électricité ch',
567
+ 'électricité ecs',
568
+ 'électricité fr',
569
+ 'électricité éclairage',
570
+ 'électricité auxiliaire',
571
+ "électricité d'origine renouvelable utilisée dans le bâtiment"
572
+ ].forEach((cle) => expect(coef[cle](conso)).toBeCloseTo(attendu, 9));
573
+ });
574
+
575
+ test.each([
576
+ // [conso, coût attendu] gaz naturel, une valeur par tranche + limites
577
+ [3000, 0.1312 * 3000],
578
+ [5008.99, 0.1312 * 5008.99],
579
+ [5009, 182 + 0.09488 * 5009],
580
+ [50054.99, 182 + 0.09488 * 50054.99],
581
+ [50055, 288 + 0.09274 * 50055],
582
+ [100000, 288 + 0.09274 * 100000]
583
+ ])('barème mis à jour - gaz naturel %s kWh => %s €', (conso, attendu) => {
584
+ expect(getCoefCout('2026-01-15')['gaz naturel'](conso)).toBeCloseTo(attendu, 9);
585
+ });
586
+ });
587
+
588
+ describe("masqueEnergie / coefCoutProrata - tranche sur la conso totale de l'énergie", () => {
589
+ test('masque électricité : clés électriques conservées, autres énergies annulées', () => {
590
+ const masque = masqueEnergie('électricité');
591
+ expect(masque['électricité ch'](10)).toBe(10);
592
+ expect(masque['électricité auxiliaire'](10)).toBe(10);
593
+ expect(masque['gaz naturel'](10)).toBe(0);
594
+ expect(masque['réseau de chauffage urbain'](10)).toBe(0);
595
+ expect(masque["électricité d'origine renouvelable utilisée dans le bâtiment"](10)).toBe(0);
596
+ expect(masque['électricité']).toBeUndefined();
597
+ });
598
+
599
+ test('aucune consommation : prix moyen nul', () => {
600
+ const coef = coefCoutProrata(
601
+ getCoefCout('2026-01-15'),
602
+ () => ({ total: 0, collectif: 0 }),
603
+ 1,
604
+ new Set()
605
+ );
606
+ expect(coef['électricité ch'](100)).toBe(0);
607
+ expect(coef['gaz naturel'](100)).toBe(0);
608
+ // Prix constants conservés
609
+ expect(coef['fioul domestique']).toBe(0.14821);
610
+ });
611
+ });
612
+
613
+ describe('calc_conso - coût avec le barème mis à jour (DPE 2026)', () => {
614
+ const DATE_DPE_2026 = '2026-01-15';
615
+ let errorSpy;
616
+
617
+ beforeEach(() => {
618
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
619
+ });
620
+
621
+ afterEach(() => {
622
+ errorSpy.mockRestore();
623
+ });
624
+
625
+ test('DPE antérieur au 01/07/2024 : coût par poste au barème 2021 (inchangé)', () => {
626
+ const ch = [installCh({}, [genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 })])];
627
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, '2024-06-30', coef_ep);
628
+ // Chauffage tarifé seul : 122 + 0,15176 × 3000 ; éclairage seul : 0,29007 × 100
629
+ expect(res.cout.cout_ch).toBeCloseTo(122 + 0.15176 * 3000, 9);
630
+ expect(res.cout.cout_eclairage).toBeCloseTo(0.29007 * 100, 9);
631
+ });
632
+
633
+ test('maison tout électrique : tranche sur la conso électrique totale, coût réparti au prorata', () => {
634
+ const ch = [installCh({}, [genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 })])];
635
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE_2026, coef_ep);
636
+ // Conso élec totale = 3000 (ch) + 100 (éclairage = 1 × Sh) = 3100 kWh
637
+ const total = 158 + 0.18949 * 3100;
638
+ expect(res.cout.cout_5_usages).toBeCloseTo(total, 9);
639
+ expect(res.cout.cout_ch).toBeCloseTo((total * 3000) / 3100, 9);
640
+ expect(res.cout.cout_eclairage).toBeCloseTo((total * 100) / 3100, 9);
641
+ });
642
+
643
+ test('maison gaz + électricité : chaque énergie tarifée sur sa propre conso totale', () => {
644
+ const ch = [installCh({}, [genCh('2', { conso_ch: 8000, conso_ch_depensier: 8000 })])];
645
+ const ecs = [installEcs({}, [genEcs('2', { conso_ecs: 2000, conso_ecs_depensier: 2000 })])];
646
+ const res = calc_conso(100, 1, 1, [], ch, ecs, [], 1, 1, DATE_DPE_2026, coef_ep);
647
+ // Gaz total 10000 kWh => 182 + 0,09488 × 10000 ; élec (éclairage) 100 kWh => 0,34721 × 100
648
+ const gaz = 182 + 0.09488 * 10000;
649
+ expect(res.cout.cout_ch).toBeCloseTo((gaz * 8000) / 10000, 9);
650
+ expect(res.cout.cout_ecs).toBeCloseTo((gaz * 2000) / 10000, 9);
651
+ expect(res.cout.cout_eclairage).toBeCloseTo(0.34721 * 100, 9);
652
+ });
653
+
654
+ test('immeuble à chauffage individuel : barème appliqué par logement (N × f(C / N))', () => {
655
+ const ch = [installCh({}, [genCh('1', { conso_ch: 8000, conso_ch_depensier: 8000 })])];
656
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE_2026, coef_ep, 4);
657
+ // Élec totale 8100 kWh pour 4 logements : 4 × f(2025) = 4 × (158 + 0,18954 × 2025)
658
+ expect(res.cout.cout_5_usages).toBeCloseTo(4 * (158 + 0.18954 * 2025), 9);
659
+ });
660
+
661
+ test('immeuble à installations collectives : chauffage / ECS collectifs tarifés sur leur total', () => {
662
+ const ch = [
663
+ installCh({ enum_type_installation_id: '2' }, [
664
+ genCh('2', { conso_ch: 60000, conso_ch_depensier: 60000 })
665
+ ])
666
+ ];
667
+ const ecs = [
668
+ installEcs({ enum_type_installation_id: '2' }, [
669
+ genEcs('1', { conso_ecs: 4000, conso_ecs_depensier: 4000 })
670
+ ])
671
+ ];
672
+ const res = calc_conso(100, 1, 1, [], ch, ecs, [], 1, 1, DATE_DPE_2026, coef_ep, 4);
673
+ // Gaz collectif 60000 kWh => 288 + 0,09274 × 60000 (pas de division par logement)
674
+ expect(res.cout.cout_ch).toBeCloseTo(288 + 0.09274 * 60000, 9);
675
+ // ECS élec collective 4000 kWh => 158 + 0,18949 × 4000
676
+ expect(res.cout.cout_ecs).toBeCloseTo(158 + 0.18949 * 4000, 9);
677
+ // Éclairage (usage individuel) 100 kWh / 4 logements => 4 × 0,34721 × 25
678
+ expect(res.cout.cout_eclairage).toBeCloseTo(4 * 0.34721 * 25, 9);
679
+ });
680
+
681
+ test("immeuble, ECS sans type d'installation : considérée individuelle", () => {
682
+ const ecs = [installEcs({}, [genEcs('1', { conso_ecs: 4000, conso_ecs_depensier: 4000 })])];
683
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE_2026, coef_ep, 4);
684
+ // Élec totale 4100 kWh / 4 logements => 4 × (158 + 0,18954 × 1025)
685
+ expect(res.cout.cout_5_usages).toBeCloseTo(4 * (158 + 0.18954 * 1025), 9);
686
+ });
687
+ });
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.8';
36
+ const LIB_VERSION = '1.8.9';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
@@ -573,7 +573,8 @@ export function calcul_3cl(inputDpe, options) {
573
573
  prorataECS,
574
574
  prorataChauffage,
575
575
  dateDpe,
576
- coef_ep
576
+ coef_ep,
577
+ th === 'immeuble' ? Nb_lgt : 1
577
578
  );
578
579
 
579
580
  const production_electricite = productionENR.calculateEnr(
@@ -595,7 +596,8 @@ export function calcul_3cl(inputDpe, options) {
595
596
  prorataECS,
596
597
  prorataChauffage,
597
598
  dateDpe,
598
- coef_ep_1_7
599
+ coef_ep_1_7,
600
+ th === 'immeuble' ? Nb_lgt : 1
599
601
  );
600
602
 
601
603
  productionENR.calculateEnr(
@@ -618,7 +620,8 @@ export function calcul_3cl(inputDpe, options) {
618
620
  prorataECS,
619
621
  prorataChauffage,
620
622
  dateDpe,
621
- coef_ep_2_3
623
+ coef_ep_2_3,
624
+ th === 'immeuble' ? Nb_lgt : 1
622
625
  );
623
626
 
624
627
  productionENR.calculateEnr(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.8",
3
+ "version": "1.8.9",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
package/utils.js CHANGED
@@ -277,10 +277,15 @@ export function requestInput(de, du, field, type) {
277
277
  return enums[field][de[enum_name]];
278
278
  } else {
279
279
  // not enums
280
+ // `!type` couvre aussi NaN, 0 et '' (valeurs falsy)
280
281
  if (!type) {
281
282
  console.error(`requestInput: type is not defined for ${field}`);
282
283
  return null;
283
284
  }
285
+ if (!['string', 'number', 'boolean'].includes(typeof type)) {
286
+ console.error(`requestInput: type has an unexpected type (${typeof type}) for ${field}`);
287
+ return null;
288
+ }
284
289
  du[field] = type;
285
290
  return de[field];
286
291
  }
package/utils.spec.js CHANGED
@@ -287,6 +287,59 @@ describe('Utils unit tests', () => {
287
287
  spy.mockRestore();
288
288
  });
289
289
 
290
+ test.each([
291
+ ['NaN', Number.NaN],
292
+ ['0', 0],
293
+ ['une chaîne vide', '']
294
+ ])(
295
+ 'requestInput : champ non enum avec %s comme type est traité comme un type absent',
296
+ (_libelle, type) => {
297
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
298
+ const de = { epaisseur: 5 };
299
+ const du = {};
300
+ expect(requestInput(de, du, 'epaisseur', type)).toBeNull();
301
+ expect(spy).toHaveBeenCalledWith('requestInput: type is not defined for epaisseur');
302
+ // Le type invalide n'est pas publié dans donnee_utilisateur
303
+ expect(du).toStrictEqual({});
304
+ spy.mockRestore();
305
+ }
306
+ );
307
+
308
+ test.each([
309
+ ['un objet', { a: 1 }, 'object'],
310
+ ['un tableau', ['number'], 'object'],
311
+ ['une fonction', () => 'number', 'function'],
312
+ ['un symbole', Symbol('number'), 'symbol'],
313
+ ['un bigint', 10n, 'bigint']
314
+ ])(
315
+ 'requestInput : champ non enum avec %s comme type journalise une erreur et retourne null',
316
+ (_libelle, type, typeAttendu) => {
317
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
318
+ const de = { epaisseur: 5 };
319
+ const du = {};
320
+ expect(requestInput(de, du, 'epaisseur', type)).toBeNull();
321
+ expect(spy).toHaveBeenCalledWith(
322
+ `requestInput: type has an unexpected type (${typeAttendu}) for epaisseur`
323
+ );
324
+ expect(du).toStrictEqual({});
325
+ spy.mockRestore();
326
+ }
327
+ );
328
+
329
+ test.each([
330
+ ['une chaîne', 'string'],
331
+ ['un nombre', 1],
332
+ ['un booléen', true]
333
+ ])('requestInput : champ non enum avec %s comme type est accepté', (_libelle, type) => {
334
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
335
+ const de = { epaisseur: 5 };
336
+ const du = {};
337
+ expect(requestInput(de, du, 'epaisseur', type)).toBe(5);
338
+ expect(du.epaisseur).toBe(type);
339
+ expect(spy).not.toHaveBeenCalled();
340
+ spy.mockRestore();
341
+ });
342
+
290
343
  test('requestInputID : sans type publie les clés et retourne l identifiant', () => {
291
344
  const de = { enum_materiaux_id: '1' };
292
345
  const du = {};