@open3cl/engine 1.6.1 → 1.6.2

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.
Files changed (107) hide show
  1. package/10_besoin_fr.spec.js +122 -0
  2. package/10_clim.spec.js +116 -0
  3. package/11_besoin_ecs.spec.js +69 -0
  4. package/11_ecs.spec.js +282 -0
  5. package/11_nadeq.spec.js +37 -0
  6. package/12.4_pac.spec.js +131 -0
  7. package/13.2_generateur_combustion.js +2 -0
  8. package/13.2_generateur_combustion.spec.js +537 -0
  9. package/13.2_generateur_combustion_bouilleur.js +4 -1
  10. package/13.2_generateur_combustion_bouilleur.spec.js +74 -0
  11. package/13.2_generateur_combustion_ch.spec.js +323 -0
  12. package/13.2_generateur_combustion_chaudiere.spec.js +85 -0
  13. package/13.2_generateur_pac.spec.js +64 -0
  14. package/13_rendement_distribution_ecs.spec.js +38 -0
  15. package/14_generateur_ecs.js +4 -4
  16. package/14_generateur_ecs.spec.js +745 -0
  17. package/15_conso_aux.spec.js +225 -0
  18. package/16.2_production_enr.spec.js +188 -0
  19. package/16_conso_eclairage.spec.js +62 -0
  20. package/2021_04_13_confort_ete.spec.js +159 -0
  21. package/2021_04_13_qualite_isolation.js +1 -1
  22. package/2021_04_13_qualite_isolation.spec.js +286 -0
  23. package/3.1_b.js +1 -1
  24. package/3.1_b.spec.js +367 -0
  25. package/3.2.1_mur.spec.js +812 -39
  26. package/3.2.2_plancher_bas.spec.js +643 -78
  27. package/3.2.3_plancher_haut.spec.js +452 -0
  28. package/3.3.1.4_porte.spec.js +114 -0
  29. package/3.3_baie_vitree.spec.js +449 -295
  30. package/3.4_pont_thermique.spec.js +890 -0
  31. package/3_deperdition.spec.js +397 -0
  32. package/4_ventilation.js +1 -1
  33. package/4_ventilation.spec.js +406 -0
  34. package/5_conso_ventilation.spec.js +186 -0
  35. package/6.2_surface_sud_equivalente.spec.js +138 -0
  36. package/8_intermittence.spec.js +33 -0
  37. package/9_besoin_ch.js +2 -2
  38. package/9_besoin_ch.spec.js +381 -0
  39. package/9_chauffage.js +4 -0
  40. package/9_chauffage.spec.js +478 -92
  41. package/9_conso_ch.spec.js +502 -248
  42. package/9_emetteur_ch.spec.js +339 -0
  43. package/9_generateur_ch.spec.js +467 -81
  44. package/apport_et_besoin.spec.js +126 -0
  45. package/conso.js +1 -1
  46. package/conso.spec.js +471 -82
  47. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  48. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  49. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  50. package/core/conf/infrastructure/application.config.spec.js +35 -0
  51. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  52. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  53. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  54. package/core/util/infrastructure/object-util.spec.js +20 -0
  55. package/core/util/logger/log-service.js +16 -7
  56. package/core/util/logger/log-service.spec.js +101 -0
  57. package/dpe-sanitizer.service.spec.js +138 -0
  58. package/engine.js +4 -1
  59. package/engine.spec.js +854 -0
  60. package/enums.spec.js +46 -0
  61. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  62. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  63. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  64. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  65. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  66. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  67. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  68. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  69. package/features/dpe/infrastructure/tv.store.js +28 -9
  70. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  71. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  72. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  73. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  74. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  75. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  76. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  77. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  78. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  79. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  80. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  81. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  82. package/features/engine/domain/constants.spec.js +14 -0
  83. package/features/engine/domain/contexte.builder.spec.js +21 -0
  84. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  85. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  86. package/features/engine/domain/engine.service.spec.js +227 -4
  87. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  88. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  89. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  90. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  91. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  92. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  93. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  94. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  95. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  96. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  97. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  98. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  99. package/ficheTechnique.spec.js +26 -0
  100. package/index.spec.js +54 -0
  101. package/package.json +5 -2
  102. package/preprocessor/index.js +20 -0
  103. package/preprocessor/index.spec.js +15 -0
  104. package/tv.spec.js +38 -0
  105. package/utils.js +43 -1
  106. package/utils.spec.js +518 -2
  107. package/output.js +0 -1
package/enums.spec.js ADDED
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import enums from './enums.js';
3
+
4
+ /**
5
+ * `enums.js` est un gros mapping figé (identifiant -> libellé) issu du modèle
6
+ * de données ADEME. On ne fige pas l'intégralité du contenu : on vérifie
7
+ * uniquement des invariants structurels et la présence de quelques enums clés.
8
+ */
9
+ describe('Table des enums (enums.js)', () => {
10
+ test('exporte un objet non vide par défaut', () => {
11
+ expect(enums).toBeTypeOf('object');
12
+ expect(enums).not.toBeNull();
13
+ expect(Object.keys(enums).length).toBeGreaterThan(0);
14
+ });
15
+
16
+ test('quelques enums clés sont présents et non vides', () => {
17
+ ['version', 'modele_dpe', 'usage_fonctionnel_batiment', 'consentement_formulaire'].forEach(
18
+ (cle) => {
19
+ expect(enums).toHaveProperty(cle);
20
+ expect(Object.keys(enums[cle]).length).toBeGreaterThan(0);
21
+ }
22
+ );
23
+ });
24
+
25
+ test('chaque enum est un objet dont les libellés sont des chaînes non vides', () => {
26
+ Object.entries(enums).forEach(([nomEnum, valeurs]) => {
27
+ expect(valeurs, `enum ${nomEnum}`).toBeTypeOf('object');
28
+ Object.values(valeurs).forEach((libelle) => {
29
+ expect(typeof libelle).toBe('string');
30
+ expect(libelle.length).toBeGreaterThan(0);
31
+ });
32
+ });
33
+ });
34
+
35
+ test('les identifiants (clés) sont uniques au sein de chaque enum', () => {
36
+ Object.entries(enums).forEach(([nomEnum, valeurs]) => {
37
+ const cles = Object.keys(valeurs);
38
+ expect(new Set(cles).size, `enum ${nomEnum}`).toBe(cles.length);
39
+ });
40
+ });
41
+
42
+ test('contenu attendu pour l’enum modele_dpe', () => {
43
+ expect(enums.modele_dpe['1']).toBe('dpe 3cl 2021 méthode logement');
44
+ expect(enums.consentement_formulaire['0']).toBe('absence de consentement');
45
+ });
46
+ });
@@ -2,7 +2,6 @@
2
2
  * @type {{[key: string]: string}}
3
3
  */
4
4
  export const TypeGenerateur = {
5
- COMBUSTION: 'COMBUSTION',
6
- PAC: 'PAC',
5
+ CHAUDIERE: 'CHAUDIERE',
7
6
  OTHER: 'OTHER'
8
7
  };
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TypeGenerateur } from './type-generateur.model.js';
3
+
4
+ describe('Modèle TypeGenerateur', () => {
5
+ test('expose les types de générateurs attendus', () => {
6
+ expect(TypeGenerateur).toEqual({
7
+ CHAUDIERE: 'CHAUDIERE',
8
+ OTHER: 'OTHER'
9
+ });
10
+ });
11
+
12
+ test('chaque clé correspond à une valeur chaîne identique (auto-référence)', () => {
13
+ Object.entries(TypeGenerateur).forEach(([cle, valeur]) => {
14
+ expect(typeof valeur).toBe('string');
15
+ expect(valeur).toBe(cle);
16
+ });
17
+ });
18
+
19
+ test('les valeurs sont uniques et non vides', () => {
20
+ const valeurs = Object.values(TypeGenerateur);
21
+ expect(valeurs.length).toBeGreaterThan(0);
22
+ expect(new Set(valeurs).size).toBe(valeurs.length);
23
+ valeurs.forEach((valeur) => expect(valeur.length).toBeGreaterThan(0));
24
+ });
25
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TypeHabitation, TypeDpe } from './type-habitation.model.js';
3
+
4
+ describe('Modèle TypeHabitation', () => {
5
+ test("expose les types d'habitation attendus", () => {
6
+ expect(TypeHabitation).toEqual({
7
+ MAISON: 'MAISON',
8
+ APPARTEMENT: 'APPARTEMENT',
9
+ IMMEUBLE: 'IMMEUBLE'
10
+ });
11
+ });
12
+
13
+ test('chaque clé est auto-référencée par sa valeur', () => {
14
+ Object.entries(TypeHabitation).forEach(([cle, valeur]) => {
15
+ expect(valeur).toBe(cle);
16
+ });
17
+ });
18
+ });
19
+
20
+ describe('Modèle TypeDpe', () => {
21
+ test('expose les types de DPE attendus', () => {
22
+ expect(TypeDpe).toEqual({
23
+ MAISON: 'MAISON',
24
+ APPARTEMENT: 'APPARTEMENT',
25
+ APPARTEMENT_A_PARTIR_IMMEUBLE: 'APPARTEMENT_A_PARTIR_IMMEUBLE',
26
+ IMMEUBLE: 'IMMEUBLE'
27
+ });
28
+ });
29
+
30
+ test('les valeurs sont uniques et non vides', () => {
31
+ const valeurs = Object.values(TypeDpe);
32
+ expect(new Set(valeurs).size).toBe(valeurs.length);
33
+ valeurs.forEach((valeur) => expect(valeur.length).toBeGreaterThan(0));
34
+ });
35
+
36
+ test('reprend les types communs avec TypeHabitation', () => {
37
+ expect(TypeDpe.MAISON).toBe(TypeHabitation.MAISON);
38
+ expect(TypeDpe.APPARTEMENT).toBe(TypeHabitation.APPARTEMENT);
39
+ expect(TypeDpe.IMMEUBLE).toBe(TypeHabitation.IMMEUBLE);
40
+ });
41
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TypeStockage } from './type-stockage.model.js';
3
+
4
+ describe('Modèle TypeStockage', () => {
5
+ test('expose les types de stockage attendus', () => {
6
+ expect(TypeStockage).toEqual({
7
+ INSTANTANE: 'INSTANTANE',
8
+ INTEGRE: 'INTEGRE',
9
+ INDEPENDANT: 'INDEPENDANT'
10
+ });
11
+ });
12
+
13
+ test('chaque clé est auto-référencée et les valeurs sont uniques', () => {
14
+ const valeurs = Object.values(TypeStockage);
15
+ Object.entries(TypeStockage).forEach(([cle, valeur]) => expect(valeur).toBe(cle));
16
+ expect(new Set(valeurs).size).toBe(valeurs.length);
17
+ });
18
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TypeVentilation } from './type-ventilation.model.js';
3
+
4
+ describe('Modèle TypeVentilation', () => {
5
+ test('expose les types de ventilation attendus', () => {
6
+ expect(TypeVentilation).toEqual({
7
+ SIMPLE_FLUX_AUTO: 'SIMPLE_FLUX_AUTO',
8
+ SIMPLE_FLUX_HYGRO: 'SIMPLE_FLUX_HYGRO',
9
+ DOUBLE_FLUX: 'DOUBLE_FLUX'
10
+ });
11
+ });
12
+
13
+ test('chaque clé est auto-référencée et les valeurs sont uniques', () => {
14
+ const valeurs = Object.values(TypeVentilation);
15
+ Object.entries(TypeVentilation).forEach(([cle, valeur]) => expect(valeur).toBe(cle));
16
+ expect(new Set(valeurs).size).toBe(valeurs.length);
17
+ valeurs.forEach((valeur) => expect(valeur.length).toBeGreaterThan(0));
18
+ });
19
+ });
@@ -33,8 +33,8 @@ describe('Lecture des tables de valeurs', () => {
33
33
  enum_type_generateur_ecs_id: '46|93',
34
34
  type_generateur: 'Chaudière gaz classique 1981-1985',
35
35
  pn: 'Pn',
36
- rpn: '84 + 2 logPn',
37
- rpint: '80 + 3 logPn',
36
+ rpn: '84+2*log10(Pn)',
37
+ rpint: '80+3*log10(Pn)',
38
38
  qp0_perc: '2%',
39
39
  pveil: '150'
40
40
  });
@@ -46,8 +46,8 @@ describe('Lecture des tables de valeurs', () => {
46
46
  type_generateur: 'Chaudière fioul à condensation après 2015',
47
47
  critere_pn: '70<Pn≤400',
48
48
  pn: 'Pn',
49
- rpn: '94 + logPn',
50
- rpint: '100 + logPn',
49
+ rpn: '94+log10(Pn)',
50
+ rpint: '100+log10(Pn)',
51
51
  qp0_perc: '0.60%'
52
52
  });
53
53
  });
@@ -119,15 +119,19 @@ export class BaieVitreeTvStore extends TvStore {
119
119
  (v) =>
120
120
  v.enum_type_baie_id.split('|').includes(enumTypeBaieId) &&
121
121
  v.enum_type_materiaux_menuiserie_id.split('|').includes(enumTypeMateriauxMenuiserieId)
122
- ) || [];
122
+ ) /* c8 ignore next -- .filter renvoie toujours un tableau : fallback défensif inatteignable */ ||
123
+ [];
123
124
 
124
125
  let ug1, ug2;
125
126
 
126
127
  if (ugValues.length) {
127
128
  [ug1, ug2] = getRange(ug, [...new Set(ugValues.map((value) => value.ug).sort())]);
128
129
 
130
+ /* c8 ignore start -- getRange renvoie des bornes issues de ugValues : le find aboutit
131
+ toujours et le fallback `|| 0` reste défensif (inatteignable avec les données réelles). */
129
132
  const uw1 = ugValues.find((value) => value.ug === ug1)?.uw || 0;
130
133
  const uw2 = ugValues.find((value) => value.ug === ug2)?.uw || 0;
134
+ /* c8 ignore stop */
131
135
 
132
136
  const delta_uw = Number(uw2) - Number(uw1);
133
137
  const delta_ug = ug2 - ug1 || 0;
@@ -191,7 +195,9 @@ export class BaieVitreeTvStore extends TvStore {
191
195
  */
192
196
  const deltar = this.getDeltar(enumTypeFermetureId);
193
197
  const uwValues =
194
- tv['ujn'].filter((v) => Number(v.deltar).toPrecision(2) === Number(deltar).toPrecision(2)) ||
198
+ tv['ujn'].filter(
199
+ (v) => Number(v.deltar).toPrecision(2) === Number(deltar).toPrecision(2)
200
+ ) /* c8 ignore next -- .filter renvoie toujours un tableau : fallback défensif inatteignable */ ||
195
201
  [];
196
202
 
197
203
  let uw1, uw2;
@@ -199,12 +205,15 @@ export class BaieVitreeTvStore extends TvStore {
199
205
  if (uwValues.length) {
200
206
  [uw1, uw2] = getRange(uw, [...new Set(uwValues.map((value) => value.uw).sort())]);
201
207
 
208
+ /* c8 ignore start -- getRange renvoie des bornes issues de uwValues : le find aboutit
209
+ toujours et le fallback `|| 0` reste défensif (inatteignable avec les données réelles). */
202
210
  const ujn1 =
203
211
  uwValues.find((value) => Number(value.uw).toPrecision(2) === Number(uw1).toPrecision(2))
204
212
  ?.ujn || 0;
205
213
  const ujn2 =
206
214
  uwValues.find((value) => Number(value.uw).toPrecision(2) === Number(uw2).toPrecision(2))
207
215
  ?.ujn || 0;
216
+ /* c8 ignore stop */
208
217
 
209
218
  const delta_ujn = Number(ujn2) - Number(ujn1);
210
219
  const delta_uw = uw2 - uw1 || 0;
@@ -216,12 +225,15 @@ export class BaieVitreeTvStore extends TvStore {
216
225
  ujn = Number(ujn1) + (delta_ujn * (uw - uw1)) / delta_uw;
217
226
  }
218
227
 
228
+ /* c8 ignore start -- les valeurs ujn tabulées sont toujours strictement positives :
229
+ ce garde-fou `!ujn` est défensif et inatteignable avec les données réelles. */
219
230
  if (!ujn) {
220
231
  logger.error(
221
232
  `Pas de valeur forfaitaire ujn pour enumTypeFermetureId:${enumTypeFermetureId}, uw:${uw}`
222
233
  );
223
234
  return;
224
235
  }
236
+ /* c8 ignore stop */
225
237
 
226
238
  return parseFloat(ujn);
227
239
  }
@@ -222,6 +222,16 @@ describe('Lecture des tables de valeurs', () => {
222
222
  const ug = tvStore.getSw('0', '0', 0);
223
223
  expect(ug).toBeUndefined();
224
224
  });
225
+
226
+ test('uw non interpolé lorsque le ug fourni correspond exactement à une valeur tabulée', () => {
227
+ // ug passé sous forme de chaîne présente dans la table => getRange renvoie deux
228
+ // bornes égales, delta_ug vaut 0 et uw prend directement la valeur tabulée (uw1).
229
+ expect(tvStore.getUw('4', '6', '0.5')).toBeCloseTo(1.3, 2);
230
+ });
231
+
232
+ test("uw indéfini lorsqu'aucune ligne ne correspond au type de baie / matériau", () => {
233
+ expect(tvStore.getUw('9', '99', 2)).toBeUndefined();
234
+ });
225
235
  });
226
236
 
227
237
  describe('lecture des valeurs de ujn', () => {
@@ -252,6 +262,12 @@ describe('Lecture des tables de valeurs', () => {
252
262
  const ug = tvStore.getUjn('0', 0);
253
263
  expect(ug).toBeUndefined();
254
264
  });
265
+
266
+ test('ujn non interpolé lorsque le uw fourni correspond exactement à une valeur tabulée', () => {
267
+ // uw passé sous forme de chaîne présente dans la table => getRange renvoie deux
268
+ // bornes égales, delta_uw vaut 0 et ujn prend directement la valeur tabulée (ujn1).
269
+ expect(tvStore.getUjn('2', '0.8')).toBeCloseTo(0.8, 2);
270
+ });
255
271
  });
256
272
 
257
273
  describe('lecture des valeurs de deltar', () => {
@@ -3,6 +3,23 @@ import { getRange } from '../../../utils.js';
3
3
  import { logger } from '../../../core/util/logger/log-service.js';
4
4
  import { TypeHabitation } from '../domain/models/type-habitation.model.js';
5
5
 
6
+ /**
7
+ * Extrait la borne numérique d'un libellé de plage aiu/aue de la table
8
+ * `coef_reduction_deperdition`. Les bornes y sont stockées sous forme de libellés
9
+ * formatés à la française (ex: `'≤ 6,00'`, `'4,00 <'`) et doivent être converties
10
+ * en nombre pour être comparées au ratio surface_aiu / surface_aue.
11
+ *
12
+ * @param bound {string} Libellé de borne (ex: `'≤ 0,25'`, `'0,25 <'`)
13
+ * @return {number} Valeur numérique de la borne
14
+ */
15
+ export function parseAiuAueBound(bound) {
16
+ return parseFloat(
17
+ String(bound)
18
+ .replace(/,/g, '.')
19
+ .replace(/[^\d.]/g, '')
20
+ );
21
+ }
22
+
6
23
  /**
7
24
  * Accès aux données des tables de valeurs
8
25
  *
@@ -60,8 +77,8 @@ export class TvStore {
60
77
  !v.zone_climatique ||
61
78
  zc.toLowerCase().startsWith(v.zone_climatique.toLowerCase())) &&
62
79
  (!rAiuAue ||
63
- ((!v.aiu_aue_min || v.aiu_aue_min < rAiuAue) &&
64
- (!v.aiu_aue_max || v.aiu_aue_max >= rAiuAue)))
80
+ ((!v.aiu_aue_min || parseAiuAueBound(v.aiu_aue_min) < rAiuAue) &&
81
+ (!v.aiu_aue_max || parseAiuAueBound(v.aiu_aue_max) >= rAiuAue)))
65
82
  )?.b;
66
83
 
67
84
  if (!b) {
@@ -208,13 +225,14 @@ export class TvStore {
208
225
  * @return {number|undefined}
209
226
  */
210
227
  getUeByUpd(enumTypeAdjacenceId, enumPeriodeConstructionId, dsp, upb) {
211
- const ueValues =
212
- tv['ue'].filter(
213
- (v) =>
214
- parseInt(v['2s_p']) === dsp &&
215
- v.enum_type_adjacence_id.split('|').includes(enumTypeAdjacenceId) &&
216
- v.enum_periode_construction_id.split('|').includes(enumPeriodeConstructionId)
217
- ) || [];
228
+ const ueFiltered = tv['ue'].filter(
229
+ (v) =>
230
+ parseInt(v['2s_p']) === dsp &&
231
+ v.enum_type_adjacence_id.split('|').includes(enumTypeAdjacenceId) &&
232
+ v.enum_periode_construction_id.split('|').includes(enumPeriodeConstructionId)
233
+ );
234
+ /* c8 ignore next -- Array.filter renvoie toujours un tableau : repli || [] défensif inatteignable */
235
+ const ueValues = ueFiltered || [];
218
236
 
219
237
  const ueRange = [...new Set(ueValues.map((value) => value.upb).sort())];
220
238
  let ue;
@@ -223,6 +241,7 @@ export class TvStore {
223
241
  if (ueValues.length) {
224
242
  [upb1, upb2] = getRange(upb, ueRange);
225
243
 
244
+ /* c8 ignore next 2 -- upb1/upb2 sont toujours des bornes tabulées de ueValues : find() aboutit et ?.ue n'est jamais falsy (replis inatteignables) */
226
245
  const ue1 = ueValues.find((value) => value.upb === upb1)?.ue || 0;
227
246
  const ue2 = ueValues.find((value) => value.upb === upb2)?.ue || 0;
228
247
 
@@ -11,6 +11,23 @@ describe('Lecture des tables de valeurs', () => {
11
11
  tvStore = new TvStore();
12
12
  });
13
13
 
14
+ describe('lecture des valeurs de uPorte', () => {
15
+ test.each([
16
+ {
17
+ label: 'porte simple bois/PVC opaque pleine (id partagé 1|5)',
18
+ enumTypePorteId: '1',
19
+ expected: 3.5
20
+ },
21
+ { label: 'porte simple métal opaque pleine', enumTypePorteId: '9', expected: 5.8 }
22
+ ])('uPorte pour $label', ({ enumTypePorteId, expected }) => {
23
+ expect(tvStore.getUPorte(enumTypePorteId)).toBe(expected);
24
+ });
25
+
26
+ test('pas de valeur de uPorte', () => {
27
+ expect(tvStore.getUPorte('0')).toBeUndefined();
28
+ });
29
+ });
30
+
14
31
  describe('lecture des valeurs de b', () => {
15
32
  test.each([
16
33
  { enumTypeAdjacenceId: '1', label: 'extérieur', bExpected: 1 },
@@ -56,6 +73,26 @@ describe('Lecture des tables de valeurs', () => {
56
73
  expect(b).toBe(bExpected);
57
74
  }
58
75
  );
76
+
77
+ test('pas de valeur de b', () => {
78
+ // Aucune adjacence '0' dans la table : b introuvable -> undefined.
79
+ expect(tvStore.getB('0')).toBeUndefined();
80
+ });
81
+
82
+ test('b filtré par zone climatique (espace tampon solarisé)', () => {
83
+ // Adjacence 10 : les lignes portent une zone_climatique -> exerce le filtre
84
+ // zc.toLowerCase().startsWith(v.zone_climatique.toLowerCase()) (H1 <- h1a).
85
+ expect(tvStore.getB('10', undefined, '6', undefined, 'h1a')).toBe(0.95);
86
+ });
87
+
88
+ test.each([
89
+ // rAiuAue au-dessus d'une borne min : exerce parseAiuAueBound(aiu_aue_min) < rAiuAue.
90
+ { label: 'ratio dans un intervalle borné (min et max)', rAiuAue: 0.3, expected: 0.95 },
91
+ // rAiuAue très élevé : dernière ligne sans aiu_aue_max -> exerce la branche !v.aiu_aue_max.
92
+ { label: 'ratio au-dessus de la dernière borne (sans max)', rAiuAue: 60, expected: 0.2 }
93
+ ])('b filtré par le ratio aiu/aue pour $label', ({ rAiuAue, expected }) => {
94
+ expect(tvStore.getB('8', 3, '4', rAiuAue)).toBe(expected);
95
+ });
59
96
  });
60
97
 
61
98
  describe('lecture des valeurs de uVue', () => {
@@ -375,6 +412,13 @@ describe('Lecture des tables de valeurs', () => {
375
412
  }
376
413
  );
377
414
 
415
+ test('ue sans interpolation quand upb correspond exactement à une borne tabulée', () => {
416
+ // upb fourni comme borne exacte de la table ('0.83') : getRange renvoie deux bornes
417
+ // identiques, delta_upb vaut 0 et la valeur tabulée est retournée sans interpolation.
418
+ const ue = tvStore.getUeByUpd('3', '1', 4, '0.83');
419
+ expect(ue).toBe(0.37);
420
+ });
421
+
378
422
  test('pas de valeur de ue', () => {
379
423
  const ue = tvStore.getUeByUpd('0', '1', 0, 0);
380
424
  expect(ue).toBeUndefined();
@@ -15,6 +15,7 @@ import { PRECISION_PERCENT } from '../../../../../test/constant.js';
15
15
  import { DpeNormalizerService } from '../../../normalizer/domain/dpe-normalizer.service.js';
16
16
  import { ContexteBuilder } from '../contexte.builder.js';
17
17
  import { InstallationChService } from '../ch/installation-ch.service.js';
18
+ import { describeIntegration } from '../../../../../test/helpers/integration-test.js';
18
19
 
19
20
  /** @type {SurfaceSudEquivalenteService} **/
20
21
  let surfaceSudEquivalenteService;
@@ -157,7 +158,7 @@ describe('Calcul des apports et besoin du logement', () => {
157
158
  expect(installationChService.execute).toHaveBeenCalledWith(ctx, logement);
158
159
  });
159
160
 
160
- describe("Test d'intégration pour les besoins en chauffage", () => {
161
+ describeIntegration("Test d'intégration pour les besoins en chauffage", () => {
161
162
  test.each(corpus)(
162
163
  'Vérification de la DI besoin_ch des besoins en chauffage pour dpe %s',
163
164
  (ademeId) => {
@@ -50,6 +50,24 @@ describe('Calcul des besoins en chauffage du logement', () => {
50
50
  }
51
51
  );
52
52
 
53
+ test('Fraction des apports gratuits nulle quand les degrés-heures de chauffage sont nuls', () => {
54
+ // dh === 0 : aucun besoin de chauffage sur le mois, la fraction est court-circuitée à 0
55
+ // (évite aussi la division par zéro dans le calcul de Xj).
56
+ /** @type {Contexte} */
57
+ const ctx = { inertie: { id: 1 } };
58
+
59
+ /** @type { Logement } **/
60
+ const logement = {
61
+ donnees_de_calcul: {
62
+ apportsInterneCh: { Janvier: 102.5 },
63
+ apportsSolaire: { Janvier: 12.5 }
64
+ },
65
+ sortie: { deperdition: { deperdition_enveloppe: 25.9 } }
66
+ };
67
+
68
+ expect(service.fractionBesoinCh(ctx, logement, 'Janvier', 0)).toBe(0);
69
+ });
70
+
53
71
  test('Besoin de chauffage hors pertes récupérées et fraction des apports gratuits pour un mois', () => {
54
72
  /** @type {Contexte} */
55
73
  const ctx = { inertie: { id: 1 } };
@@ -9,6 +9,7 @@ import { PerteChRecupService } from './perte-ch-recup.service.js';
9
9
  import { BesoinChService } from './besoin-ch.service.js';
10
10
  import { ApportGratuitService } from '../apport_gratuit/apport-gratuit.service.js';
11
11
  import { PRECISION_PERCENT } from '../../../../../../test/constant.js';
12
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
12
13
 
13
14
  /** @type {PerteChRecupService} **/
14
15
  let service;
@@ -239,7 +240,38 @@ describe('Calcul des pertes de génération de chauffage récupérées', () => {
239
240
  }
240
241
  });
241
242
 
242
- describe("Test d'intégration des installations CH", () => {
243
+ test('Qrec vaut 0 pour un usage de générateur inconnu (aucune durée de récupération)', () => {
244
+ // enum_usage_generateur_id hors {1, 2, 3} : le switch ne définit pas Dperj (undefined),
245
+ // le produit devient NaN et la garde `|| 0` ramène la contribution à 0.
246
+ /** @type {GenerateurChauffage[]} */
247
+ const generateurs = [
248
+ {
249
+ donnee_entree: { presence_ventouse: 0, enum_usage_generateur_id: 99 },
250
+ donnee_intermediaire: { pn: 25000, qp0: 125 }
251
+ }
252
+ ];
253
+
254
+ expect(service.Qrec(generateurs, 12.5, 1532.9)).toBe(0);
255
+ });
256
+
257
+ test('Conversion en kWh des pertes de génération récupérées (execute)', () => {
258
+ // On isole execute de la formule mensuelle : pertesGenerateurChRecup est doublée pour
259
+ // ne vérifier que la conversion Wh -> kWh (division par 1000) et le mapping des 2 champs.
260
+ vi.spyOn(service, 'pertesGenerateurChRecup').mockImplementation((ctx, logement, depensier) =>
261
+ depensier ? 6000 : 3000
262
+ );
263
+
264
+ const res = service.execute({}, {});
265
+
266
+ expect(res).toStrictEqual({
267
+ pertes_generateur_ch_recup: 3,
268
+ pertes_generateur_ch_recup_depensier: 6
269
+ });
270
+ expect(service.pertesGenerateurChRecup).toHaveBeenCalledWith({}, {}, false);
271
+ expect(service.pertesGenerateurChRecup).toHaveBeenCalledWith({}, {}, true);
272
+ });
273
+
274
+ describeIntegration("Test d'intégration des installations CH", () => {
243
275
  test.each(corpus)('vérification des pertes de génération ch pour dpe %s', (ademeId) => {
244
276
  let dpeRequest = getAdemeFileJson(ademeId);
245
277
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -5,6 +5,7 @@ import { ContexteBuilder } from '../../contexte.builder.js';
5
5
  import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
6
6
  import { BesoinEcsService } from './besoin-ecs.service.js';
7
7
  import { EcsTvStore } from '../../../../dpe/infrastructure/ecs/ecsTv.store.js';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {BesoinEcsService} **/
10
11
  let service;
@@ -45,7 +46,26 @@ describe('Calcul du besoin en eau chaude sanitaire', () => {
45
46
  expect(tvStore.getTefs).toHaveBeenCalledWith('400-800m', 'h1a', 'Janvier');
46
47
  });
47
48
 
48
- describe("Test d'intégration pour le besoin en eau chaude sanitaire", () => {
49
+ test("Sommation du besoin ECS sur les douze mois de l'année", () => {
50
+ // On isole execute de la formule mensuelle : besoinEcsMois est doublé pour ne mesurer
51
+ // que la logique d'agrégation (une valeur conventionnelle, une valeur dépensière par mois).
52
+ vi.spyOn(service, 'besoinEcsMois').mockImplementation((ctx, mois, depensier) =>
53
+ depensier ? 10 : 1
54
+ );
55
+
56
+ /** @type {Contexte} */
57
+ const ctx = { altitude: { value: '400-800m' }, zoneClimatique: { value: 'h1a' }, nadeq: 2 };
58
+
59
+ const res = service.execute(ctx);
60
+
61
+ // 12 mois : conventionnel = 12 * 1, dépensier = 12 * 10
62
+ expect(res).toStrictEqual({ besoin_ecs: 12, besoin_ecs_depensier: 120 });
63
+ expect(service.besoinEcsMois).toHaveBeenCalledTimes(24);
64
+ expect(service.besoinEcsMois).toHaveBeenCalledWith(ctx, 'Janvier', false);
65
+ expect(service.besoinEcsMois).toHaveBeenCalledWith(ctx, 'Janvier', true);
66
+ });
67
+
68
+ describeIntegration("Test d'intégration pour le besoin en eau chaude sanitaire", () => {
49
69
  test.each(corpus)('vérification des sorties besoin_ecs pour dpe %s', (ademeId) => {
50
70
  /**
51
71
  * @type {Dpe}
@@ -9,6 +9,7 @@ import { mois_liste } from '../../../../../utils.js';
9
9
  import { EcsTvStore } from '../../../../dpe/infrastructure/ecs/ecsTv.store.js';
10
10
  import { GenerateurEcsService } from '../../ecs/generateur-ecs.service.js';
11
11
  import { InstallationEcsService } from '../../ecs/installation-ecs.service.js';
12
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
12
13
 
13
14
  /** @type {PerteEcsRecupService} **/
14
15
  let service;
@@ -130,7 +131,46 @@ describe('Calcul des pertes de distribution et stockage récupérées', () => {
130
131
  }
131
132
  });
132
133
 
133
- describe("Test d'intégration des installations ECS", () => {
134
+ test('Agrégation et conversion en kWh des pertes ECS récupérées (execute)', () => {
135
+ // On isole execute des formules mensuelles : les deux méthodes de calcul sont doublées
136
+ // pour ne vérifier que la conversion Wh -> kWh (division par 1000) et le mapping des 4 champs.
137
+ vi.spyOn(service, 'pertesDistributionEcsRecup').mockImplementation(
138
+ (ctx, logement, depensier) => (depensier ? 2000 : 1000)
139
+ );
140
+ vi.spyOn(service, 'pertesStockageEcsRecup').mockImplementation((ctx, logement, depensier) =>
141
+ depensier ? 4000 : 3000
142
+ );
143
+
144
+ const res = service.execute({}, {});
145
+
146
+ expect(res).toStrictEqual({
147
+ pertes_distribution_ecs_recup: 1,
148
+ pertes_distribution_ecs_recup_depensier: 2,
149
+ pertes_stockage_ecs_recup: 3,
150
+ pertes_stockage_ecs_recup_depensier: 4
151
+ });
152
+ expect(service.pertesDistributionEcsRecup).toHaveBeenCalledWith({}, {}, false);
153
+ expect(service.pertesDistributionEcsRecup).toHaveBeenCalledWith({}, {}, true);
154
+ expect(service.pertesStockageEcsRecup).toHaveBeenCalledWith({}, {}, false);
155
+ expect(service.pertesStockageEcsRecup).toHaveBeenCalledWith({}, {}, true);
156
+ });
157
+
158
+ test("Pertes nulles en l'absence d'installation ECS", () => {
159
+ // Valeur de repli [] quand installation_ecs_collection est absente : aucune installation à sommer.
160
+ vi.spyOn(tvStore, 'getData').mockReturnValue(1);
161
+
162
+ /** @type {Contexte} */
163
+ const ctx = {
164
+ altitude: { value: '400-800m' },
165
+ zoneClimatique: { value: 'h1a' },
166
+ inertie: { ilpa: 0 }
167
+ };
168
+
169
+ expect(service.pertesDistributionEcsRecup(ctx, {}, false)).toBe(0);
170
+ expect(service.pertesStockageEcsRecup(ctx, {}, false)).toBe(0);
171
+ });
172
+
173
+ describeIntegration("Test d'intégration des installations ECS", () => {
134
174
  test.each(corpus)(
135
175
  'vérification des pertes de distribution ecs recup des installations ECS pour dpe %s',
136
176
  (ademeId) => {
@@ -6,6 +6,7 @@ import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
6
6
  import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js';
7
7
  import { ApportGratuitService } from '../apport_gratuit/apport-gratuit.service.js';
8
8
  import { BesoinFroidService } from './besoin-froid.service.js';
9
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
9
10
 
10
11
  /** @type {ApportGratuitService} **/
11
12
  let apportGratuitService;
@@ -207,7 +208,49 @@ describe('Calcul des besoins en froid du logement', () => {
207
208
  );
208
209
  });
209
210
 
210
- describe("Test d'intégration pour le besoin en froid", () => {
211
+ test('Sommation du besoin en froid sur les douze mois en présence de climatisation', () => {
212
+ // Climatisation présente : execute doit agréger besoinFrMois sur les 12 mois.
213
+ // besoinFrMois est doublé pour n'évaluer que la logique d'agrégation.
214
+ vi.spyOn(service, 'besoinFrMois').mockImplementation((ctx, logement, mois, depensier) =>
215
+ depensier ? 5 : 2
216
+ );
217
+
218
+ /** @type {Contexte} */
219
+ const ctx = { zoneClimatique: { id: 1 } };
220
+
221
+ /** @type {Logement} */
222
+ const logement = { climatisation_collection: { climatisation: [{}] } };
223
+
224
+ const res = service.execute(ctx, logement);
225
+
226
+ expect(res).toStrictEqual({ besoin_fr: 24, besoin_fr_depensier: 60 });
227
+ expect(service.besoinFrMois).toHaveBeenCalledTimes(24);
228
+ });
229
+
230
+ test('Facteur d’utilisation lorsque le ratio de bilan thermique Rbth vaut exactement 1', () => {
231
+ // Branche fut = a / (a + 1) : atteinte uniquement quand Rbth === 1.
232
+ // Rbth = (aijFr + asjFr) / (GV * (tempExtMoyClim - 28) * nref)
233
+ // = 6000 / (100 * (30 - 28) * 30) = 1
234
+ vi.spyOn(tvStore, 'getData').mockReturnValue(30);
235
+ vi.spyOn(apportGratuitService, 'apportInterneMois').mockReturnValue(6000);
236
+ vi.spyOn(apportGratuitService, 'apportSolaireMois').mockReturnValue(0);
237
+
238
+ /** @type {Logement} **/
239
+ const logement = { enveloppe: {}, sortie: { deperdition: { deperdition_enveloppe: 100 } } };
240
+
241
+ /** @type {Contexte} */
242
+ const ctx = {
243
+ altitude: { value: '400-800m' },
244
+ zoneClimatique: { value: 'h1a' },
245
+ inertie: { id: 4 },
246
+ surfaceHabitable: 25
247
+ };
248
+
249
+ // Valeur de référence de régression (module réel exécuté avec ces entrées)
250
+ expect(service.besoinFrMois(ctx, logement, 'Janvier', false)).toBeCloseTo(9.608856088560886, 9);
251
+ });
252
+
253
+ describeIntegration("Test d'intégration pour le besoin en froid", () => {
211
254
  test.each(corpus)('vérification des sorties besoin_fr pour dpe %s', (ademeId) => {
212
255
  /**
213
256
  * @type {Dpe}