@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
@@ -5,6 +5,7 @@ import { ContexteBuilder } from '../../contexte.builder.js';
5
5
  import { DpeNormalizerService } from '../../../../normalizer/domain/dpe-normalizer.service.js';
6
6
  import { TvStore } from '../../../../dpe/infrastructure/tv.store.js';
7
7
  import { beforeEach, describe, expect, test } from 'vitest';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {DeperditionMurService} **/
10
11
  let service;
@@ -333,6 +334,121 @@ describe('Calcul de déperdition des murs', () => {
333
334
  expect(di.umur0).toBeCloseTo(0.81545);
334
335
  expect(di.umur).toBeCloseTo(0.81545);
335
336
  });
337
+
338
+ test('Isolation inconnue (méthode 2) : umur est plafonné par la valeur forfaitaire de la table', () => {
339
+ // enum_methode_saisie_u_id = '2' => on prend le min entre umur_nu et la valeur tabulée.
340
+ /** @type {Contexte} */
341
+ const ctx = {
342
+ effetJoule: false,
343
+ enumPeriodeConstructionId: '2',
344
+ zoneClimatique: { id: '3' }
345
+ };
346
+ /** @type {MurDE} */
347
+ const de = {
348
+ enum_materiaux_structure_mur_id: '12',
349
+ enum_methode_saisie_u_id: '2',
350
+ enum_methode_saisie_u0_id: '2',
351
+ epaisseur_structure: 25,
352
+ enum_type_doublage_id: '2',
353
+ enum_type_isolation_id: '2'
354
+ };
355
+
356
+ const di = service.execute(ctx, de);
357
+ // Valeurs de référence de régression
358
+ expect(di.umur0).toBeCloseTo(2.3);
359
+ expect(di.umur).toBeCloseTo(2.3);
360
+ });
361
+
362
+ test("Épaisseur d'isolation saisie mais manquante (méthode 4) : repli sur min(umur0, 2.5)", () => {
363
+ // Sans epaisseur_isolation, un avertissement est émis et umur = min(umur0, 2.5).
364
+ /** @type {Contexte} */
365
+ const ctx = {
366
+ effetJoule: false,
367
+ enumPeriodeConstructionId: '2',
368
+ zoneClimatique: { id: '3' }
369
+ };
370
+ /** @type {MurDE} */
371
+ const de = {
372
+ enum_materiaux_structure_mur_id: '12',
373
+ enum_methode_saisie_u_id: '4',
374
+ enum_methode_saisie_u0_id: '2',
375
+ epaisseur_structure: 25,
376
+ enum_type_doublage_id: '2',
377
+ enum_type_isolation_id: '2',
378
+ description: 'mur sans épaisseur isolation'
379
+ };
380
+
381
+ const di = service.execute(ctx, de);
382
+ expect(di.umur0).toBeCloseTo(2.3);
383
+ expect(di.umur).toBeCloseTo(2.3);
384
+ });
385
+
386
+ test("Résistance d'isolation saisie (méthode 5) : umur calculé à partir de la résistance", () => {
387
+ /** @type {Contexte} */
388
+ const ctx = {
389
+ effetJoule: false,
390
+ enumPeriodeConstructionId: '2',
391
+ zoneClimatique: { id: '3' }
392
+ };
393
+ /** @type {MurDE} */
394
+ const de = {
395
+ enum_materiaux_structure_mur_id: '12',
396
+ enum_methode_saisie_u_id: '5',
397
+ enum_methode_saisie_u0_id: '2',
398
+ epaisseur_structure: 25,
399
+ enum_type_doublage_id: '2',
400
+ enum_type_isolation_id: '3',
401
+ resistance_isolation: 2.5
402
+ };
403
+
404
+ const di = service.execute(ctx, de);
405
+ expect(di.umur0).toBeCloseTo(2.3);
406
+ // 1 / (1 / 2.3 + 2.5) => valeur de référence de régression
407
+ expect(di.umur).toBeCloseTo(0.34074);
408
+ });
409
+
410
+ test('umur0 via matériau seul (méthode u0 = 1) sans épaisseur de structure', () => {
411
+ /** @type {Contexte} */
412
+ const ctx = {
413
+ effetJoule: false,
414
+ enumPeriodeConstructionId: '2',
415
+ zoneClimatique: { id: '3' }
416
+ };
417
+ /** @type {MurDE} */
418
+ const de = {
419
+ enum_materiaux_structure_mur_id: '12',
420
+ enum_methode_saisie_u_id: '1',
421
+ enum_methode_saisie_u0_id: '1',
422
+ enum_type_doublage_id: '2',
423
+ enum_type_isolation_id: '2'
424
+ };
425
+
426
+ const di = service.execute(ctx, de);
427
+ // Matériau seul => valeur forfaitaire plafonnée à 2.5
428
+ expect(di.umur0).toBeCloseTo(2.5);
429
+ expect(di.umur).toBeCloseTo(2.5);
430
+ });
431
+
432
+ test('umur0 saisi directement via une méthode u0 non gérée (défaut)', () => {
433
+ /** @type {Contexte} */
434
+ const ctx = {
435
+ effetJoule: false,
436
+ enumPeriodeConstructionId: '2',
437
+ zoneClimatique: { id: '3' }
438
+ };
439
+ /** @type {MurDE} */
440
+ const de = {
441
+ enum_methode_saisie_u_id: '1',
442
+ enum_methode_saisie_u0_id: '9',
443
+ umur0_saisi: 1.234,
444
+ enum_type_doublage_id: '2',
445
+ enum_type_isolation_id: '2'
446
+ };
447
+
448
+ const di = service.execute(ctx, de);
449
+ expect(di.umur0).toBeCloseTo(1.234);
450
+ expect(di.umur).toBeCloseTo(1.234);
451
+ });
336
452
  });
337
453
 
338
454
  test.each([
@@ -396,7 +512,7 @@ describe('Calcul de déperdition des murs', () => {
396
512
  }
397
513
  );
398
514
 
399
- describe("Test d'intégration de mur", () => {
515
+ describeIntegration("Test d'intégration de mur", () => {
400
516
  test.each(corpus)('vérification des DI des murs pour dpe %s', (ademeId) => {
401
517
  let dpeRequest = getAdemeFileJson(ademeId);
402
518
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -5,6 +5,7 @@ import { DpeNormalizerService } from '../../../../normalizer/domain/dpe-normaliz
5
5
  import { DeperditionPlancherBasService } from './deperdition-plancher-bas.service.js';
6
6
  import { TvStore } from '../../../../dpe/infrastructure/tv.store.js';
7
7
  import { beforeEach, describe, expect, test } from 'vitest';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {DeperditionPlancherBasService} **/
10
11
  let service;
@@ -180,6 +181,138 @@ describe('Calcul de déperdition des planchers bas', () => {
180
181
  expect(di.upb0).toBeUndefined();
181
182
  expect(di.upb_final).toBeCloseTo(1.25);
182
183
  });
184
+
185
+ test('Isolation inconnue (méthode 2) : upb plafonné par la valeur forfaitaire de la table', () => {
186
+ /** @type {Contexte} */
187
+ const ctx = {
188
+ effetJoule: false,
189
+ enumPeriodeConstructionId: '6',
190
+ zoneClimatique: { id: '3' }
191
+ };
192
+ /** @type {PlancherBasDE} */
193
+ const de = {
194
+ enum_type_adjacence_id: '21',
195
+ enum_type_plancher_bas_id: '2',
196
+ enum_methode_saisie_u0_id: '2',
197
+ enum_type_isolation_id: '3',
198
+ enum_methode_saisie_u_id: '2',
199
+ enum_periode_isolation_id: '6',
200
+ calcul_ue: 0
201
+ };
202
+
203
+ const di = service.execute(ctx, de, []);
204
+ expect(di.upb0).toBeCloseTo(1.45);
205
+ // Valeur de référence de régression
206
+ expect(di.upb).toBeCloseTo(0.5);
207
+ expect(di.upb_final).toBeCloseTo(0.5);
208
+ });
209
+
210
+ test.each([
211
+ // Méthodes 7 (année d'isolation) et 8 (année de construction) : même table forfaitaire.
212
+ { enumMethodeSaisieUId: '7' },
213
+ { enumMethodeSaisieUId: '8' }
214
+ ])(
215
+ 'upb via table forfaitaire pour la méthode de saisie $enumMethodeSaisieUId',
216
+ ({ enumMethodeSaisieUId }) => {
217
+ /** @type {Contexte} */
218
+ const ctx = {
219
+ effetJoule: false,
220
+ enumPeriodeConstructionId: '6',
221
+ zoneClimatique: { id: '3' }
222
+ };
223
+ /** @type {PlancherBasDE} */
224
+ const de = {
225
+ enum_type_adjacence_id: '21',
226
+ enum_type_plancher_bas_id: '2',
227
+ enum_methode_saisie_u0_id: '2',
228
+ enum_type_isolation_id: '3',
229
+ enum_methode_saisie_u_id: enumMethodeSaisieUId,
230
+ enum_periode_isolation_id: '6',
231
+ calcul_ue: 0
232
+ };
233
+
234
+ const di = service.execute(ctx, de, []);
235
+ expect(di.upb0).toBeCloseTo(1.45);
236
+ // Valeur de référence de régression
237
+ expect(di.upb).toBeCloseTo(0.5);
238
+ }
239
+ );
240
+
241
+ test("Résistance d'isolation saisie (méthode 6) : upb calculé à partir de la résistance", () => {
242
+ /** @type {Contexte} */
243
+ const ctx = {
244
+ effetJoule: false,
245
+ enumPeriodeConstructionId: '6',
246
+ zoneClimatique: { id: '3' }
247
+ };
248
+ /** @type {PlancherBasDE} */
249
+ const de = {
250
+ enum_type_adjacence_id: '21',
251
+ enum_type_plancher_bas_id: '2',
252
+ enum_methode_saisie_u0_id: '2',
253
+ enum_type_isolation_id: '3',
254
+ enum_methode_saisie_u_id: '6',
255
+ resistance_isolation: 2.5,
256
+ calcul_ue: 0
257
+ };
258
+
259
+ const di = service.execute(ctx, de, []);
260
+ expect(di.upb0).toBeCloseTo(1.45);
261
+ // 1 / (1 / 1.45 + 2.5) => valeur de référence de régression
262
+ expect(di.upb).toBeCloseTo(0.31351);
263
+ });
264
+
265
+ test("Résistance d'isolation saisie (méthode 5) : upb calculé à partir de la résistance", () => {
266
+ /** @type {Contexte} */
267
+ const ctx = {
268
+ effetJoule: false,
269
+ enumPeriodeConstructionId: '6',
270
+ zoneClimatique: { id: '3' }
271
+ };
272
+ /** @type {PlancherBasDE} */
273
+ const de = {
274
+ enum_type_adjacence_id: '21',
275
+ enum_type_plancher_bas_id: '2',
276
+ enum_methode_saisie_u0_id: '2',
277
+ enum_type_isolation_id: '3',
278
+ enum_methode_saisie_u_id: '5',
279
+ resistance_isolation: 2.5,
280
+ calcul_ue: 0
281
+ };
282
+
283
+ const di = service.execute(ctx, de, []);
284
+ expect(di.upb0).toBeCloseTo(1.45);
285
+ // 1 / (1 / 1.45 + 2.5) => valeur de référence de régression
286
+ expect(di.upb).toBeCloseTo(0.31351);
287
+ });
288
+
289
+ test('Calcul Ue avec repli sur surface de paroi et périmètre nul (dsp par défaut = 1)', () => {
290
+ // Adjacence terre-plein, calcul_ue = 0 : le upb_final passe par la table Ue.
291
+ // Le plancher n'a ni surface_ue ni perimetre_ue => on retombe sur surface_paroi_opaque
292
+ // et un dsp forfaitaire de 1.
293
+ /** @type {Contexte} */
294
+ const ctx = {
295
+ effetJoule: false,
296
+ enumPeriodeConstructionId: '6',
297
+ zoneClimatique: { id: '3' }
298
+ };
299
+ /** @type {PlancherBasDE} */
300
+ const de = {
301
+ enum_type_adjacence_id: '5',
302
+ enum_type_plancher_bas_id: '9',
303
+ enum_methode_saisie_u0_id: '2',
304
+ enum_type_isolation_id: '2',
305
+ enum_methode_saisie_u_id: '1',
306
+ calcul_ue: 0,
307
+ surface_paroi_opaque: 40
308
+ };
309
+
310
+ const di = service.execute(ctx, de, [{ donnee_entree: de }]);
311
+ expect(di.upb0).toBeCloseTo(2);
312
+ expect(di.upb).toBeCloseTo(2);
313
+ // Valeur de référence de régression (Ue table, dsp = 1)
314
+ expect(di.upb_final).toBeCloseTo(0.74421);
315
+ });
183
316
  });
184
317
 
185
318
  test.each([
@@ -267,7 +400,7 @@ describe('Calcul de déperdition des planchers bas', () => {
267
400
  }
268
401
  );
269
402
 
270
- describe("Test d'intégration de plancher bas", () => {
403
+ describeIntegration("Test d'intégration de plancher bas", () => {
271
404
  test.each(corpus)('vérification des DI des pb pour dpe %s', (ademeId) => {
272
405
  /**
273
406
  * @type {Dpe}
@@ -5,6 +5,7 @@ import { DeperditionPlancherHautService } from './deperdition-plancher-haut.serv
5
5
  import corpus from '../../../../../../test/corpus-sano.json';
6
6
  import { TvStore } from '../../../../dpe/infrastructure/tv.store.js';
7
7
  import { beforeEach, describe, expect, test } from 'vitest';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {DeperditionPlancherHautService} **/
10
11
  let service;
@@ -193,6 +194,130 @@ describe('Calcul de déperdition des planchers haut', () => {
193
194
  expect(di.uph0).toBeCloseTo(2.5);
194
195
  expect(di.uph).toBeCloseTo(0.4);
195
196
  });
197
+
198
+ test('Plancher non isolé (méthode 1) : uph vaut uph_nu', () => {
199
+ /** @type {Contexte} */
200
+ const ctx = {
201
+ effetJoule: false,
202
+ enumPeriodeConstructionId: '2',
203
+ zoneClimatique: { id: '3' }
204
+ };
205
+ /** @type {PlancherHautDE} */
206
+ const de = {
207
+ enum_type_adjacence_id: '1',
208
+ enum_type_plancher_haut_id: '10',
209
+ enum_methode_saisie_u0_id: '2',
210
+ enum_type_isolation_id: '2',
211
+ enum_methode_saisie_u_id: '1'
212
+ };
213
+
214
+ const di = service.execute(ctx, de);
215
+ expect(di.uph0).toBeCloseTo(2.3);
216
+ expect(di.uph).toBeCloseTo(2.3);
217
+ });
218
+
219
+ test("Épaisseur d'isolation saisie (méthode 4) : uph calculé à partir de l'épaisseur", () => {
220
+ /** @type {Contexte} */
221
+ const ctx = {
222
+ effetJoule: false,
223
+ enumPeriodeConstructionId: '2',
224
+ zoneClimatique: { id: '3' }
225
+ };
226
+ /** @type {PlancherHautDE} */
227
+ const de = {
228
+ enum_type_adjacence_id: '1',
229
+ enum_type_plancher_haut_id: '10',
230
+ enum_methode_saisie_u0_id: '2',
231
+ enum_type_isolation_id: '3',
232
+ enum_methode_saisie_u_id: '4',
233
+ epaisseur_isolation: 10
234
+ };
235
+
236
+ const di = service.execute(ctx, de);
237
+ expect(di.uph0).toBeCloseTo(2.3);
238
+ // 1 / (1 / 2.3 + (10 * 0.01) / 0.04) => valeur de référence de régression
239
+ expect(di.uph).toBeCloseTo(0.34074);
240
+ });
241
+
242
+ test("Résistance d'isolation saisie (méthode 6) : uph calculé à partir de la résistance", () => {
243
+ /** @type {Contexte} */
244
+ const ctx = {
245
+ effetJoule: false,
246
+ enumPeriodeConstructionId: '2',
247
+ zoneClimatique: { id: '3' }
248
+ };
249
+ /** @type {PlancherHautDE} */
250
+ const de = {
251
+ enum_type_adjacence_id: '1',
252
+ enum_type_plancher_haut_id: '10',
253
+ enum_methode_saisie_u0_id: '2',
254
+ enum_type_isolation_id: '3',
255
+ enum_methode_saisie_u_id: '6',
256
+ resistance_isolation: 2.5
257
+ };
258
+
259
+ const di = service.execute(ctx, de);
260
+ expect(di.uph0).toBeCloseTo(2.3);
261
+ // 1 / (1 / 2.3 + 2.5) => valeur de référence de régression
262
+ expect(di.uph).toBeCloseTo(0.34074);
263
+ });
264
+
265
+ test.each([
266
+ // Isolation inconnue via table forfaitaire (méthode 2) et variantes épaisseur/résistance.
267
+ { enumMethodeSaisieUId: '2', uphExpected: 0.75 },
268
+ { enumMethodeSaisieUId: '3', epaisseurIsolation: 10, uphExpected: 0.34074 },
269
+ { enumMethodeSaisieUId: '5', resistanceIsolation: 2.5, uphExpected: 0.34074 }
270
+ ])(
271
+ 'uph pour la méthode de saisie $enumMethodeSaisieUId',
272
+ ({ enumMethodeSaisieUId, epaisseurIsolation, resistanceIsolation, uphExpected }) => {
273
+ /** @type {Contexte} */
274
+ const ctx = {
275
+ effetJoule: false,
276
+ enumPeriodeConstructionId: '2',
277
+ zoneClimatique: { id: '3' }
278
+ };
279
+ /** @type {PlancherHautDE} */
280
+ const de = {
281
+ enum_type_adjacence_id: '1',
282
+ enum_type_plancher_haut_id: '10',
283
+ enum_methode_saisie_u0_id: '2',
284
+ enum_type_isolation_id: '3',
285
+ enum_periode_isolation_id: '3',
286
+ enum_methode_saisie_u_id: enumMethodeSaisieUId,
287
+ epaisseur_isolation: epaisseurIsolation,
288
+ resistance_isolation: resistanceIsolation
289
+ };
290
+
291
+ const di = service.execute(ctx, de);
292
+ expect(di.uph0).toBeCloseTo(2.3);
293
+ // Valeurs de référence de régression
294
+ expect(di.uph).toBeCloseTo(uphExpected);
295
+ }
296
+ );
297
+
298
+ test("Adjacence 'locaux non chauffés non accessible' (7) : uph_tab pris en catégorie terrasse", () => {
299
+ // @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.3.1
300
+ /** @type {Contexte} */
301
+ const ctx = {
302
+ effetJoule: false,
303
+ enumPeriodeConstructionId: '2',
304
+ zoneClimatique: { id: '3' }
305
+ };
306
+ /** @type {PlancherHautDE} */
307
+ const de = {
308
+ enum_type_adjacence_id: '7',
309
+ enum_type_plancher_haut_id: '1',
310
+ enum_methode_saisie_u0_id: '2',
311
+ enum_type_isolation_id: '3',
312
+ enum_periode_isolation_id: '6',
313
+ enum_methode_saisie_u_id: '8'
314
+ };
315
+
316
+ const di = service.execute(ctx, de);
317
+ expect(di.uph0).toBeCloseTo(2.5);
318
+ // Valeur de référence de régression (catégorie terrasse)
319
+ expect(di.uph).toBeCloseTo(0.4);
320
+ });
196
321
  });
197
322
 
198
323
  test.each([
@@ -256,7 +381,7 @@ describe('Calcul de déperdition des planchers haut', () => {
256
381
  }
257
382
  );
258
383
 
259
- describe("Test d'intégration de plancher haut", () => {
384
+ describeIntegration("Test d'intégration de plancher haut", () => {
260
385
  test.each(corpus)('vérification des DI des ph pour dpe %s', (ademeId) => {
261
386
  /**
262
387
  * @type {Dpe}
@@ -8,6 +8,7 @@ import { PontThermiqueTvStore } from '../../../../dpe/infrastructure/enveloppe/p
8
8
  import { DeperditionMurService } from '../mur/deperdition-mur.service.js';
9
9
  import { DeperditionPlancherHautService } from '../plancher_haut/deperdition-plancher-haut.service.js';
10
10
  import { DeperditionPlancherBasService } from '../plancher_bas/deperdition-plancher-bas.service.js';
11
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
11
12
 
12
13
  /** @type {DeperditionPontThermiqueService} **/
13
14
  let service;
@@ -421,6 +422,22 @@ describe('Calcul de déperdition des ponts thermiques', () => {
421
422
  expect(tvStore.getKForMurById).not.toHaveBeenCalled();
422
423
  expect(tvStore.getKForMenuiserie).toHaveBeenCalledWith(5, 'iti', 3, 0, 5);
423
424
  });
425
+
426
+ test('Pont thermique nul pour une paroi en brique de verre (vitrage 5)', () => {
427
+ // @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.5 Menuiserie / mur
428
+ // Les liaisons avec des parois en brique de verre (type_vitrage 5) sont prises nulles.
429
+ vi.spyOn(tvStore, 'getKForMenuiserie').mockReturnValue(0.25);
430
+
431
+ const pontThermiqueDE = { reference_1: 'reference' };
432
+ const enveloppe = {
433
+ baie_vitree_collection: {
434
+ baie_vitree: [{ donnee_entree: { reference: 'reference', enum_type_vitrage_id: '5' } }]
435
+ }
436
+ };
437
+
438
+ expect(service.pontThermiqueMenuiserieMur(pontThermiqueDE, enveloppe, 3)).toBe(0);
439
+ expect(tvStore.getKForMenuiserie).not.toHaveBeenCalled();
440
+ });
424
441
  });
425
442
 
426
443
  describe("Calcul de l'isolation du mur", () => {
@@ -616,7 +633,7 @@ describe('Calcul de déperdition des ponts thermiques', () => {
616
633
  });
617
634
  });
618
635
 
619
- describe("Test d'intégration des ponts thermiques", () => {
636
+ describeIntegration("Test d'intégration des ponts thermiques", () => {
620
637
  test.each(corpus)('vérification des DI des ponts thermiques pour dpe %s', (ademeId) => {
621
638
  let dpeRequest = getAdemeFileJson(ademeId);
622
639
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -3,6 +3,7 @@ import corpus from '../../../../../../test/corpus-sano.json';
3
3
  import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
4
4
  import { beforeEach, describe, expect, test } from 'vitest';
5
5
  import { TvStore } from '../../../../dpe/infrastructure/tv.store.js';
6
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
6
7
 
7
8
  /** @type {DeperditionPorteService} **/
8
9
  let service;
@@ -99,7 +100,7 @@ describe('Calcul de déperdition des portes', () => {
99
100
  });
100
101
  });
101
102
 
102
- describe("Test d'intégration de porte", () => {
103
+ describeIntegration("Test d'intégration de porte", () => {
103
104
  test.each(corpus)('vérification des DI des portes pour dpe %s', (ademeId) => {
104
105
  const dpeRequest = getAdemeFileJson(ademeId);
105
106
  /** @type {Contexte} */
@@ -7,6 +7,8 @@ import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
7
7
  import { DpeNormalizerService } from '../../../../normalizer/domain/dpe-normalizer.service.js';
8
8
  import { ContexteBuilder } from '../../contexte.builder.js';
9
9
  import { DeperditionEnveloppeService } from '../deperdition-enveloppe.service.js';
10
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
11
+ import { logger } from '../../../../../core/util/logger/log-service.js';
10
12
 
11
13
  /** @type {DeperditionVentilationService} **/
12
14
  let service;
@@ -87,6 +89,17 @@ describe('Calcul de déperdition des portes', () => {
87
89
  expectedIsolationSurface: '1',
88
90
  expectedPresenceJointsMenuiserie: undefined
89
91
  },
92
+ {
93
+ // 1948-1974 mais moins de 50% de surface isolée => isolationSurface '0'
94
+ q4paConvSaisi: undefined,
95
+ enumPeriodeConstructionId: '2',
96
+ surfaceIsolee: 100,
97
+ surfaceNonIsolee: 200,
98
+ surfaceMenuiserieAvecJoint: 0,
99
+ surfaceMenuiserieSansJoint: 0,
100
+ expectedIsolationSurface: '0',
101
+ expectedPresenceJointsMenuiserie: undefined
102
+ },
90
103
  {
91
104
  q4paConvSaisi: undefined,
92
105
  enumPeriodeConstructionId: '3',
@@ -408,7 +421,60 @@ describe('Calcul de déperdition des portes', () => {
408
421
  );
409
422
  });
410
423
 
411
- describe("Test d'intégration des ventilations", () => {
424
+ describe('execute (agrégation des déperditions et consommations de ventilation)', () => {
425
+ /** @type {Contexte} */
426
+ const ctx = {
427
+ surfaceHabitable: 100,
428
+ hauteurSousPlafond: 2.5,
429
+ typeHabitation: TypeHabitation.MAISON,
430
+ enumPeriodeConstructionId: '3'
431
+ };
432
+
433
+ test('assemble hvent, hperm et la consommation des auxiliaires', () => {
434
+ vi.spyOn(tvStore, 'getDebitsVentilation').mockReturnValue({
435
+ qvarep_conv: 10,
436
+ qvasouf_conv: 5,
437
+ smea_conv: 2
438
+ });
439
+ vi.spyOn(tvStore, 'getQ4paConv').mockReturnValue({ q4pa_conv: 1.7 });
440
+
441
+ /** @type {VentilationDE} */
442
+ const de = {
443
+ enum_type_ventilation_id: '4',
444
+ ventilation_post_2012: true,
445
+ plusieurs_facade_exposee: false
446
+ };
447
+
448
+ const di = service.execute(ctx, de, 120, 80, 40, 10, 5);
449
+
450
+ expect(tvStore.getDebitsVentilation).toHaveBeenCalledWith('4');
451
+ // hvent = 0.34 * qvarep_conv * surfaceHabitable
452
+ expect(di.hvent).toBeCloseTo(340, 5);
453
+ // pventMoy (SF auto, maison, post 2012) = 35 => conso = 8.76 * 35
454
+ expect(di.pvent_moy).toBe(35);
455
+ expect(di.conso_auxiliaire_ventilation).toBeCloseTo(306.6, 5);
456
+ // hperm : valeur de référence de régression
457
+ expect(di.hperm).toBeCloseTo(0.10692928032279153, 9);
458
+ });
459
+
460
+ test('conso des auxiliaires nulle sans type de ventilation ni indicateur post 2012', () => {
461
+ vi.spyOn(tvStore, 'getDebitsVentilation').mockReturnValue({});
462
+ vi.spyOn(tvStore, 'getQ4paConv').mockReturnValue({ q4pa_conv: 1.7 });
463
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
464
+
465
+ /** @type {VentilationDE} */
466
+ const de = { plusieurs_facade_exposee: false };
467
+
468
+ const di = service.execute(ctx, de, 0, 0, 0, 0, 0);
469
+
470
+ expect(errorSpy).toHaveBeenCalledOnce();
471
+ // La consommation retournée vaut 0 (nombre), les clés conso ne sont donc pas ajoutées
472
+ expect(di.conso_auxiliaire_ventilation).toBeUndefined();
473
+ expect(di.pvent_moy).toBeUndefined();
474
+ });
475
+ });
476
+
477
+ describeIntegration("Test d'intégration des ventilations", () => {
412
478
  test.each(corpus)('vérification des DI de la ventilation pour dpe %s', (ademeId) => {
413
479
  let dpeRequest = getAdemeFileJson(ademeId);
414
480
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -1,4 +1,4 @@
1
- import { DpePreProcessor } from '../../../../../preprocessor/index.js';
1
+ import { DpePreProcessor } from '../../../preprocessor/index.js';
2
2
 
3
3
  /**
4
4
  * Service de normalisation du DPE d'entrée
@@ -1,3 +1,54 @@
1
- import { describe } from 'vitest';
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
2
 
3
- describe.skip('Normalisation des DPE', () => {});
3
+ // Isolation : on mocke le pré-processeur importé par le service afin de tester
4
+ // uniquement la logique de normalisation (clonage + délégation), sans dépendre
5
+ // du comportement réel de `DpePreProcessor` (testé dans son propre spec).
6
+ const preprocessMock = vi.fn();
7
+ vi.mock('../../../preprocessor/index.js', () => ({
8
+ DpePreProcessor: vi.fn().mockImplementation(() => ({
9
+ preprocess: preprocessMock
10
+ }))
11
+ }));
12
+
13
+ const { DpeNormalizerService } = await import('./dpe-normalizer.service.js');
14
+ const { DpePreProcessor } = await import('../../../preprocessor/index.js');
15
+
16
+ describe('Normalisation des DPE (DpeNormalizerService)', () => {
17
+ beforeEach(() => {
18
+ preprocessMock.mockReset();
19
+ vi.mocked(DpePreProcessor).mockClear();
20
+ });
21
+
22
+ test("clone le DPE d'entrée sans le muter puis délègue au pré-processeur", () => {
23
+ const dpeOrigine = { logement: { enveloppe: { mur: [{ id: 1 }] } } };
24
+ const service = new DpeNormalizerService();
25
+
26
+ const normalise = service.normalize(dpeOrigine);
27
+
28
+ // Le pré-processeur est instancié et reçoit le clone à normaliser.
29
+ expect(DpePreProcessor).toHaveBeenCalledTimes(1);
30
+ expect(preprocessMock).toHaveBeenCalledTimes(1);
31
+ expect(preprocessMock).toHaveBeenCalledWith(normalise);
32
+
33
+ // Le résultat est structurellement égal à l'entrée...
34
+ expect(normalise).toEqual(dpeOrigine);
35
+ // ...mais c'est bien un clone profond (aucune référence partagée).
36
+ expect(normalise).not.toBe(dpeOrigine);
37
+ expect(normalise.logement).not.toBe(dpeOrigine.logement);
38
+ expect(normalise.logement.enveloppe.mur).not.toBe(dpeOrigine.logement.enveloppe.mur);
39
+ });
40
+
41
+ test("ne modifie pas le DPE d'origine même si le pré-processeur mute le clone", () => {
42
+ // Le pré-processeur mocké mute le DPE reçu : l'original doit rester intact.
43
+ preprocessMock.mockImplementation((dpe) => {
44
+ dpe.ajoute = 'valeur';
45
+ });
46
+ const dpeOrigine = { a: 1 };
47
+ const service = new DpeNormalizerService();
48
+
49
+ const normalise = service.normalize(dpeOrigine);
50
+
51
+ expect(normalise).toEqual({ a: 1, ajoute: 'valeur' });
52
+ expect(dpeOrigine).toEqual({ a: 1 });
53
+ });
54
+ });
@@ -156,6 +156,32 @@ describe('FicheTechnique service tests', () => {
156
156
  expect(fiche.valeur).toBe('aucune');
157
157
  });
158
158
 
159
+ test('should normalize a single sous_fiche_technique into an array when filtering on classifications', async () => {
160
+ // sous_fiche_technique est un objet unique (et non un tableau) : il doit être normalisé
161
+ const dpeSingleSousFiche = {
162
+ fiche_technique_collection: {
163
+ fiche_technique: {
164
+ enum_categorie_fiche_technique_id: '10',
165
+ sous_fiche_technique_collection: {
166
+ sous_fiche_technique: {
167
+ description: 'Type de ventilation: VMC SF',
168
+ valeur: 'VMC SF'
169
+ }
170
+ }
171
+ }
172
+ }
173
+ };
174
+
175
+ // la classification 'VMC SF' correspond => la fiche est retournée
176
+ const fiche = getFicheTechnique(dpeSingleSousFiche, '10', 'ventilation', ['VMC SF']);
177
+ expect(fiche).not.toBeNull();
178
+ expect(fiche.valeur).toBe('VMC SF');
179
+
180
+ // une classification absente => aucune fiche
181
+ const aucune = getFicheTechnique(dpeSingleSousFiche, '10', 'ventilation', ['absente']);
182
+ expect(aucune).toBeNull();
183
+ });
184
+
159
185
  test('should not return a non existing fiche technique', async () => {
160
186
  let fiche = getFicheTechnique(dpe, '40', 'exposées');
161
187
  expect(fiche).toBeNull();