@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 { DpeNormalizerService } from '../../../../normalizer/domain/dpe-normaliz
5
5
  import { DeperditionBaieVitreeService } from './deperdition-baie-vitree.service.js';
6
6
  import { beforeEach, describe, expect, test, vi } from 'vitest';
7
7
  import { BaieVitreeTvStore } from '../../../../dpe/infrastructure/enveloppe/baieVitreeTv.store.js';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {DeperditionBaieVitreeService} **/
10
11
  let service;
@@ -117,6 +118,32 @@ describe('Calcul de déperdition des baies vitrées', () => {
117
118
  expect(tvStore.getSw).toHaveBeenNthCalledWith(2, '2', '6', '4', '1', '3');
118
119
  expect(sw).toBe(18.8 * 18.8);
119
120
  });
121
+
122
+ test('Doit retomber sur un facteur 1 si le sw de la double-fenêtre est introuvable', () => {
123
+ // sw de la baie principale saisi, mais celui de la double-fenêtre indéterminé
124
+ // (getSw indéfini) => le facteur solaire équivalent reste celui de la baie principale.
125
+ vi.spyOn(tvStore, 'getSw').mockReturnValue(undefined);
126
+ /**
127
+ * @type {BaieVitree}
128
+ */
129
+ let bv = {
130
+ donnee_entree: {
131
+ sw_saisi: 0.5,
132
+ double_fenetre: 1
133
+ },
134
+ baie_vitree_double_fenetre: {
135
+ donnee_entree: {
136
+ enum_type_materiaux_menuiserie_id: '4',
137
+ enum_type_vitrage_id: '2',
138
+ enum_type_baie_id: '6'
139
+ }
140
+ }
141
+ };
142
+
143
+ let sw = service.sw(bv);
144
+ expect(tvStore.getSw).toHaveBeenCalledOnce();
145
+ expect(sw).toBe(0.5);
146
+ });
120
147
  });
121
148
 
122
149
  describe('Determination du coefficient uw', () => {
@@ -166,6 +193,30 @@ describe('Calcul de déperdition des baies vitrées', () => {
166
193
  expect(tvStore.getUw).toHaveBeenNthCalledWith(2, '6', '4', 2.5);
167
194
  expect(uw).toBe(1 / (1 / 18.8 + 1 / 18.8 + 0.07));
168
195
  });
196
+
197
+ test('Doit retomber sur un uw de 1 pour la double-fenêtre si celui-ci est introuvable', () => {
198
+ // uw de la baie principale saisi, uw de la double-fenêtre indéterminé (getUw indéfini)
199
+ // => le terme 1/uw_double est remplacé par 1/1 dans la mise en série.
200
+ vi.spyOn(tvStore, 'getUw').mockReturnValue(undefined);
201
+ /**
202
+ * @type {BaieVitree}
203
+ */
204
+ let bv = {
205
+ donnee_entree: {
206
+ uw_saisi: 3,
207
+ double_fenetre: 1
208
+ },
209
+ baie_vitree_double_fenetre: {
210
+ donnee_entree: {
211
+ enum_type_materiaux_menuiserie_id: '4',
212
+ enum_type_baie_id: '6'
213
+ }
214
+ }
215
+ };
216
+
217
+ let uw = service.uw(bv, 2.4);
218
+ expect(uw).toBe(1 / (1 / 3 + 1 / 1 + 0.07));
219
+ });
169
220
  });
170
221
 
171
222
  describe('Determination du coefficient ug', () => {
@@ -258,6 +309,52 @@ describe('Calcul de déperdition des baies vitrées', () => {
258
309
  expect(uw).toBe(18.8);
259
310
  });
260
311
 
312
+ test("Ne recherche pas l'épaisseur la plus proche si l'épaisseur de lame n'est pas renseignée", () => {
313
+ // Double vitrage sans epaisseur_lame : e vaut NaN (falsy), la recherche de
314
+ // l'épaisseur tabulée la plus proche est court-circuitée.
315
+ vi.spyOn(tvStore, 'getUg').mockReturnValue(18.8);
316
+ const availableSpy = vi.spyOn(tvStore, 'getEpaisseurAvailableForUg').mockReturnValue([6, 8]);
317
+
318
+ /**
319
+ * @type {BaieVitreeDE}
320
+ */
321
+ let bvDE = {
322
+ enum_type_vitrage_id: '2',
323
+ vitrage_vir: '1',
324
+ enum_inclinaison_vitrage_id: '2',
325
+ enum_type_gaz_lame_id: '1'
326
+ };
327
+
328
+ let ug = service.ug(bvDE);
329
+
330
+ expect(availableSpy).toHaveBeenCalledOnce();
331
+ expect(tvStore.getUg).toHaveBeenCalledWith('2', '1', '2', '1', NaN);
332
+ expect(ug).toBe(18.8);
333
+ });
334
+
335
+ test("Ignore les épaisseurs tabulées supérieures à l'épaisseur réelle en cherchant la plus proche", () => {
336
+ // Épaisseur réelle 18 absente de la table : on retient la plus grande épaisseur
337
+ // tabulée strictement inférieure (8), en ignorant les épaisseurs supérieures (25).
338
+ vi.spyOn(tvStore, 'getUg').mockReturnValue(18.8);
339
+ vi.spyOn(tvStore, 'getEpaisseurAvailableForUg').mockReturnValue([6, 8, 25]);
340
+
341
+ /**
342
+ * @type {BaieVitreeDE}
343
+ */
344
+ let bvDE = {
345
+ enum_type_vitrage_id: '2',
346
+ vitrage_vir: '1',
347
+ enum_inclinaison_vitrage_id: '2',
348
+ enum_type_gaz_lame_id: '1',
349
+ epaisseur_lame: '18'
350
+ };
351
+
352
+ let ug = service.ug(bvDE);
353
+
354
+ expect(tvStore.getUg).toHaveBeenCalledWith('2', '1', '2', '1', 8);
355
+ expect(ug).toBe(18.8);
356
+ });
357
+
261
358
  test('Doit appeler tvStore avec un équivalent double vitrage pour un sur vitrage avec majoration de 0.1', () => {
262
359
  vi.spyOn(tvStore, 'getUg').mockReturnValue(18.8);
263
360
 
@@ -445,7 +542,33 @@ describe('Calcul de déperdition des baies vitrées', () => {
445
542
  expect(bvDI.u_menuiserie).toBe(5.6);
446
543
  });
447
544
 
448
- describe("Test d'intégration des baies vitrées", () => {
545
+ test('Transmet la valeur de la zone climatique au calcul de b quand le contexte la renseigne', () => {
546
+ // Contexte avec zoneClimatique défini : ctx.zoneClimatique?.value est bien résolu et
547
+ // transmis à b (utile notamment pour les espaces tampons solarisés, adjacence 10).
548
+ const bSpy = vi.spyOn(service, 'b').mockReturnValue(0.85);
549
+
550
+ const bv = {
551
+ donnee_entree: {
552
+ enum_type_adjacence_id: '10',
553
+ surface_aiu: 8,
554
+ surface_aue: 20,
555
+ enum_cfg_isolation_lnc_id: '9',
556
+ ug_saisi: 2.5,
557
+ uw_saisi: 2.8,
558
+ enum_type_fermeture_id: '1'
559
+ }
560
+ };
561
+
562
+ const di = service.execute({ zoneClimatique: { value: 'h1a' } }, bv);
563
+
564
+ expect(bSpy).toHaveBeenCalledWith(
565
+ expect.objectContaining({ enumTypeAdjacenceId: '10', zoneClimatique: 'h1a' })
566
+ );
567
+ expect(di.b).toBe(0.85);
568
+ expect(di.u_menuiserie).toBe(2.8);
569
+ });
570
+
571
+ describeIntegration("Test d'intégration des baies vitrées", () => {
449
572
  test.each(corpus)('vérification des DI des baies vitrées pour dpe %s', (ademeId) => {
450
573
  let dpeRequest = getAdemeFileJson(ademeId);
451
574
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -8,8 +8,9 @@ import { DeperditionPorteService } from './porte/deperdition-porte.service.js';
8
8
  import { DeperditionPlancherHautService } from './plancher_haut/deperdition-plancher-haut.service.js';
9
9
  import { DpeNormalizerService } from '../../../normalizer/domain/dpe-normalizer.service.js';
10
10
  import { TvStore } from '../../../dpe/infrastructure/tv.store.js';
11
- import { beforeEach, describe, expect, test } from 'vitest';
11
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
12
12
  import b from '../../../../3.1_b.js';
13
+ import { describeIntegration } from '../../../../../test/helpers/integration-test.js';
13
14
 
14
15
  /** @type {DeperditionPorteService} **/
15
16
  let deperditionPorteService;
@@ -215,6 +216,273 @@ describe('Calcul des déperditions', () => {
215
216
  );
216
217
  });
217
218
 
219
+ describe('deperditions (agrégation GV + ventilation) avec des doubles de test', () => {
220
+ /**
221
+ * Construit un service avec huit doubles de test dont la méthode `execute`
222
+ * renvoie le `__di` (donnée intermédiaire) attaché à chaque donnée d'entrée.
223
+ */
224
+ const buildService = () => {
225
+ const murService = { execute: vi.fn((ctx, de) => de.__di) };
226
+ const porteService = { execute: vi.fn((ctx, de) => de.__di) };
227
+ const plancherBasService = { execute: vi.fn((ctx, de) => de.__di) };
228
+ const plancherHautService = { execute: vi.fn((ctx, de) => de.__di) };
229
+ const baieVitreeService = { execute: vi.fn((ctx, bv) => bv.donnee_entree.__di) };
230
+ const espaceTamponService = { execute: vi.fn(() => ({ tag: 'ets-di' })) };
231
+ const pontThermiqueService = { execute: vi.fn((ctx, env, de) => de.__di) };
232
+ const ventilationService = { execute: vi.fn((ctx, de) => de.__di) };
233
+
234
+ const service = new DeperditionEnveloppeService(
235
+ murService,
236
+ porteService,
237
+ plancherBasService,
238
+ plancherHautService,
239
+ baieVitreeService,
240
+ espaceTamponService,
241
+ pontThermiqueService,
242
+ ventilationService
243
+ );
244
+
245
+ return {
246
+ service,
247
+ murService,
248
+ porteService,
249
+ plancherBasService,
250
+ plancherHautService,
251
+ baieVitreeService,
252
+ espaceTamponService,
253
+ pontThermiqueService,
254
+ ventilationService
255
+ };
256
+ };
257
+
258
+ test('somme les contributions de chaque paroi et alimente la ventilation avec les surfaces', () => {
259
+ const { service, espaceTamponService, ventilationService } = buildService();
260
+
261
+ const enveloppe = {
262
+ mur_collection: {
263
+ mur: [
264
+ {
265
+ // déperditif, isolé "connu" (1) => surfaceNonIsolee
266
+ donnee_entree: {
267
+ surface_paroi_opaque: 10,
268
+ enum_type_adjacence_id: '1',
269
+ enum_type_isolation_id: '1',
270
+ __di: { b: 0.5, umur: 2 }
271
+ }
272
+ },
273
+ {
274
+ // b > 0 mais adjacence 22 (local non déperditif) => non compté en surface déperditive,
275
+ // isolation hors ['1','2'] => surfaceIsolee
276
+ donnee_entree: {
277
+ surface_paroi_opaque: 20,
278
+ enum_type_adjacence_id: '22',
279
+ enum_type_isolation_id: '3',
280
+ __di: { b: 0.8, umur: 1 }
281
+ }
282
+ },
283
+ {
284
+ // b = 0 => aucune surface comptée
285
+ donnee_entree: {
286
+ surface_paroi_opaque: 5,
287
+ enum_type_adjacence_id: '1',
288
+ enum_type_isolation_id: '2',
289
+ __di: { b: 0, umur: 3 }
290
+ }
291
+ }
292
+ ]
293
+ },
294
+ porte_collection: {
295
+ porte: [
296
+ {
297
+ donnee_entree: {
298
+ surface_porte: 2,
299
+ presence_joint: true,
300
+ __di: { b: 0.5, uporte: 3 }
301
+ }
302
+ },
303
+ {
304
+ donnee_entree: {
305
+ surface_porte: 4,
306
+ presence_joint: false,
307
+ __di: { b: 0, uporte: 2 }
308
+ }
309
+ }
310
+ ]
311
+ },
312
+ plancher_bas_collection: {
313
+ plancher_bas: [
314
+ {
315
+ donnee_entree: {
316
+ surface_paroi_opaque: 8,
317
+ __di: { b: 0.9, upb_final: 1.5 }
318
+ }
319
+ }
320
+ ]
321
+ },
322
+ plancher_haut_collection: {
323
+ plancher_haut: [
324
+ {
325
+ donnee_entree: {
326
+ surface_paroi_opaque: 12,
327
+ enum_type_adjacence_id: '1',
328
+ enum_type_isolation_id: '1',
329
+ __di: { b: 0.7, uph: 1 }
330
+ }
331
+ },
332
+ {
333
+ donnee_entree: {
334
+ surface_paroi_opaque: 6,
335
+ enum_type_adjacence_id: '22',
336
+ enum_type_isolation_id: '5',
337
+ __di: { b: 0.5, uph: 2 }
338
+ }
339
+ },
340
+ {
341
+ donnee_entree: {
342
+ surface_paroi_opaque: 3,
343
+ enum_type_adjacence_id: '1',
344
+ enum_type_isolation_id: '1',
345
+ __di: { b: 0, uph: 1 }
346
+ }
347
+ }
348
+ ]
349
+ },
350
+ baie_vitree_collection: {
351
+ baie_vitree: [
352
+ {
353
+ donnee_entree: {
354
+ surface_totale_baie: 5,
355
+ presence_joint: true,
356
+ __di: { b: 0.6, u_menuiserie: 2 }
357
+ }
358
+ },
359
+ {
360
+ donnee_entree: {
361
+ surface_totale_baie: 3,
362
+ presence_joint: false,
363
+ __di: { b: 0, u_menuiserie: 1 }
364
+ }
365
+ }
366
+ ]
367
+ },
368
+ ets_collection: {
369
+ ets: { donnee_entree: {} }
370
+ },
371
+ pont_thermique_collection: {
372
+ pont_thermique: [
373
+ {
374
+ // pourcentage explicite
375
+ donnee_entree: { l: 4, pourcentage_valeur_pont_thermique: 0.5, __di: { k: 0.2 } }
376
+ },
377
+ {
378
+ // pourcentage absent => 1 par défaut
379
+ donnee_entree: { l: 2, __di: { k: 0.3 } }
380
+ }
381
+ ]
382
+ }
383
+ };
384
+
385
+ const logement = {
386
+ enveloppe,
387
+ ventilation_collection: {
388
+ ventilation: [{ donnee_entree: { __di: { hvent: 100, hperm: 50 } } }]
389
+ }
390
+ };
391
+
392
+ const ctx = { zoneClimatique: { value: 'h1a' } };
393
+ const resultat = service.deperditions(ctx, logement);
394
+
395
+ // Contributions GV (b * surface * u)
396
+ expect(resultat.deperdition_mur).toBeCloseTo(0.5 * 10 * 2 + 0.8 * 20 * 1 + 0, 9); // 26
397
+ expect(resultat.deperdition_porte).toBeCloseTo(0.5 * 2 * 3, 9); // 3
398
+ expect(resultat.deperdition_plancher_bas).toBeCloseTo(0.9 * 8 * 1.5, 9); // 10.8
399
+ expect(resultat.deperdition_plancher_haut).toBeCloseTo(0.7 * 12 * 1 + 0.5 * 6 * 2, 9); // 14.4
400
+ expect(resultat.deperdition_baie_vitree).toBeCloseTo(0.6 * 5 * 2, 9); // 6
401
+ expect(resultat.deperdition_pont_thermique).toBeCloseTo(4 * 0.2 * 0.5 + 2 * 0.3 * 1, 9); // 1
402
+ expect(resultat.hvent).toBe(100);
403
+ expect(resultat.hperm).toBe(50);
404
+
405
+ // deperdition_enveloppe = somme GV + hvent + hperm
406
+ expect(resultat.deperdition_enveloppe).toBeCloseTo(
407
+ 26 + 3 + 10.8 + 14.4 + 6 + 1 + 100 + 50,
408
+ 9
409
+ );
410
+
411
+ // L'espace tampon a bien été calculé et stocké
412
+ expect(espaceTamponService.execute).toHaveBeenCalledOnce();
413
+ expect(enveloppe.ets_collection.ets.donnee_intermediaire).toStrictEqual({ tag: 'ets-di' });
414
+
415
+ // La ventilation reçoit les surfaces agrégées :
416
+ // déperditive = 10 (mur A) + 2 (porte A) + 12 (ph A) + 5 (baie A) = 29
417
+ // isolée = 20 (mur B) + 6 (ph B) = 26
418
+ // non isolée = 10 (mur A) + 12 (ph A) = 22
419
+ // menuiserie avec joint = 2 (porte A) + 5 (baie A) = 7
420
+ // menuiserie sans joint = 4 (porte B) + 3 (baie B) = 7
421
+ expect(ventilationService.execute).toHaveBeenCalledWith(
422
+ ctx,
423
+ { __di: { hvent: 100, hperm: 50 } },
424
+ 29,
425
+ 26,
426
+ 22,
427
+ 7,
428
+ 7
429
+ );
430
+ });
431
+
432
+ test('utilise la première véranda lorsque les ETS sont dupliqués (tableau)', () => {
433
+ const { service, espaceTamponService } = buildService();
434
+
435
+ const premierEts = { donnee_entree: { reference: 'ets-1' } };
436
+ const enveloppe = {
437
+ mur_collection: {},
438
+ porte_collection: {},
439
+ plancher_bas_collection: {},
440
+ plancher_haut_collection: {},
441
+ baie_vitree_collection: {},
442
+ ets_collection: { ets: [premierEts, { donnee_entree: { reference: 'ets-2' } }] },
443
+ pont_thermique_collection: {}
444
+ };
445
+ const logement = { enveloppe, ventilation_collection: {} };
446
+
447
+ service.deperditions({ zoneClimatique: { value: 'h1a' } }, logement);
448
+
449
+ // Seule la première véranda est traitée
450
+ expect(espaceTamponService.execute).toHaveBeenCalledOnce();
451
+ expect(premierEts.donnee_intermediaire).toStrictEqual({ tag: 'ets-di' });
452
+ });
453
+
454
+ test('renvoie des déperditions nulles pour une enveloppe et une ventilation vides', () => {
455
+ const { service, espaceTamponService, ventilationService } = buildService();
456
+
457
+ const enveloppe = {
458
+ mur_collection: {},
459
+ porte_collection: {},
460
+ plancher_bas_collection: {},
461
+ plancher_haut_collection: {},
462
+ baie_vitree_collection: {},
463
+ pont_thermique_collection: {}
464
+ };
465
+ const logement = { enveloppe, ventilation_collection: {} };
466
+
467
+ const resultat = service.deperditions({ zoneClimatique: { value: 'h1a' } }, logement);
468
+
469
+ // Aucune paroi, aucun ETS, aucune ventilation
470
+ expect(espaceTamponService.execute).not.toHaveBeenCalled();
471
+ expect(ventilationService.execute).not.toHaveBeenCalled();
472
+ expect(resultat).toStrictEqual({
473
+ deperdition_mur: 0,
474
+ deperdition_plancher_bas: 0,
475
+ deperdition_plancher_haut: 0,
476
+ deperdition_baie_vitree: 0,
477
+ deperdition_pont_thermique: 0,
478
+ deperdition_porte: 0,
479
+ hperm: 0,
480
+ hvent: 0,
481
+ deperdition_enveloppe: 0
482
+ });
483
+ });
484
+ });
485
+
218
486
  describe.skip('Benchmark', () => {
219
487
  test('reworked', () => {
220
488
  const data = {
@@ -249,7 +517,7 @@ describe('Calcul des déperditions', () => {
249
517
  });
250
518
  });
251
519
 
252
- describe("Test d'intégration de calcul des deperditions", () => {
520
+ describeIntegration("Test d'intégration de calcul des deperditions", () => {
253
521
  test.each(corpus)('deperditions pour dpe %s', (ademeId) => {
254
522
  let dpeRequest = getAdemeFileJson(ademeId);
255
523
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -0,0 +1,141 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { DeperditionService } from './deperdition.service.js';
3
+ import { logger } from '../../../../core/util/logger/log-service.js';
4
+
5
+ /**
6
+ * Tests unitaires isolés de la classe de base `DeperditionService`.
7
+ *
8
+ * Le `TvStore` est remplacé par un double de test (spies `vi.fn`) afin de
9
+ * n'exercer que la logique de la classe (méthodes `b` et
10
+ * `getEnumPeriodeIsolationId`), sans dépendre des tables de valeurs réelles.
11
+ */
12
+
13
+ /** @type {{ getUVue: import('vitest').Mock, getB: import('vitest').Mock }} */
14
+ let tvStore;
15
+
16
+ /** @type {DeperditionService} */
17
+ let service;
18
+
19
+ describe('DeperditionService (classe de base des déperditions)', () => {
20
+ beforeEach(() => {
21
+ tvStore = {
22
+ getUVue: vi.fn(),
23
+ getB: vi.fn()
24
+ };
25
+ service = new DeperditionService(tvStore);
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ test('conserve la référence au tvStore injecté', () => {
30
+ expect(service.tvStore).toBe(tvStore);
31
+ });
32
+
33
+ test('execute() lève une erreur (opération non supportée sur la classe de base)', () => {
34
+ expect(() => service.execute({}, {})).toThrow('Unsupported operation');
35
+ });
36
+
37
+ describe('b() - coefficient de réduction des déperditions', () => {
38
+ test('retourne 0 pour une adjacence sur AUE sans surface AUE renseignée', () => {
39
+ const resultat = service.b({ enumTypeAdjacenceId: '8', surfaceAue: 0 });
40
+ expect(resultat).toBe(0);
41
+ expect(tvStore.getB).not.toHaveBeenCalled();
42
+ });
43
+
44
+ test('retourne 0 pour une adjacence sur AUE avec surface AUE indéfinie', () => {
45
+ const resultat = service.b({ enumTypeAdjacenceId: '15' });
46
+ expect(resultat).toBe(0);
47
+ });
48
+
49
+ test('local non chauffé non accessible (cfg isolation 1) : appel getB sans adjacence ni uVue', () => {
50
+ tvStore.getB.mockReturnValue(0.95);
51
+
52
+ const resultat = service.b({
53
+ enumTypeAdjacenceId: '9',
54
+ surfaceAue: 20,
55
+ enumCfgIsolationLncId: '1'
56
+ });
57
+
58
+ expect(resultat).toBe(0.95);
59
+ expect(tvStore.getUVue).not.toHaveBeenCalled();
60
+ // enumTypeAdjacenceId remis à undefined, uVue et rAiuAue non calculés
61
+ expect(tvStore.getB).toHaveBeenCalledWith(undefined, undefined, '1', undefined, undefined);
62
+ });
63
+
64
+ test('adjacence sur AUE avec surfaces : calcule uVue et le ratio rAiuAue', () => {
65
+ tvStore.getUVue.mockReturnValue(2.5);
66
+ tvStore.getB.mockReturnValue(0.8);
67
+
68
+ const resultat = service.b({
69
+ enumTypeAdjacenceId: '8',
70
+ surfaceAue: 40,
71
+ surfaceAiu: 20,
72
+ enumCfgIsolationLncId: '2'
73
+ });
74
+
75
+ expect(resultat).toBe(0.8);
76
+ expect(tvStore.getUVue).toHaveBeenCalledWith('8');
77
+ expect(tvStore.getB).toHaveBeenCalledWith('8', 2.5, '2', 0.5, undefined);
78
+ });
79
+
80
+ test('adjacence sur AUE : uVue vaut 0 si le tvStore ne retourne pas de valeur', () => {
81
+ tvStore.getUVue.mockReturnValue(undefined);
82
+ tvStore.getB.mockReturnValue(0.9);
83
+
84
+ service.b({
85
+ enumTypeAdjacenceId: '12',
86
+ surfaceAue: 10,
87
+ surfaceAiu: 5,
88
+ enumCfgIsolationLncId: '2'
89
+ });
90
+
91
+ expect(tvStore.getB).toHaveBeenCalledWith('12', 0, '2', 0.5, undefined);
92
+ });
93
+
94
+ test('espace tampon solarisé (adjacence 10) sans zone climatique : avertit et retourne undefined', () => {
95
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
96
+
97
+ const resultat = service.b({ enumTypeAdjacenceId: '10' });
98
+
99
+ expect(resultat).toBeUndefined();
100
+ expect(warnSpy).toHaveBeenCalledOnce();
101
+ expect(tvStore.getB).not.toHaveBeenCalled();
102
+ });
103
+
104
+ test('espace tampon solarisé (adjacence 10) avec zone climatique : transmet la zone à getB', () => {
105
+ tvStore.getB.mockReturnValue(0.7);
106
+
107
+ const resultat = service.b({ enumTypeAdjacenceId: '10', zoneClimatique: 'h1a' });
108
+
109
+ expect(resultat).toBe(0.7);
110
+ expect(tvStore.getB).toHaveBeenCalledWith('10', undefined, undefined, undefined, 'h1a');
111
+ });
112
+
113
+ test('adjacence classique (hors listes) : délègue directement à getB', () => {
114
+ tvStore.getB.mockReturnValue(1);
115
+
116
+ const resultat = service.b({ enumTypeAdjacenceId: '1' });
117
+
118
+ expect(resultat).toBe(1);
119
+ expect(tvStore.getUVue).not.toHaveBeenCalled();
120
+ expect(tvStore.getB).toHaveBeenCalledWith('1', undefined, undefined, undefined, undefined);
121
+ });
122
+ });
123
+
124
+ describe('getEnumPeriodeIsolationId()', () => {
125
+ test("retourne la période d'isolation si elle est fournie", () => {
126
+ expect(service.getEnumPeriodeIsolationId('7', { enumPeriodeConstructionId: '1' })).toBe('7');
127
+ });
128
+
129
+ test("période d'isolation inconnue et construction ≤ 74 : retourne '3' (75-77)", () => {
130
+ expect(service.getEnumPeriodeIsolationId(undefined, { enumPeriodeConstructionId: '2' })).toBe(
131
+ '3'
132
+ );
133
+ });
134
+
135
+ test("période d'isolation inconnue et construction récente : reprend la période de construction", () => {
136
+ expect(service.getEnumPeriodeIsolationId(undefined, { enumPeriodeConstructionId: '8' })).toBe(
137
+ '8'
138
+ );
139
+ });
140
+ });
141
+ });
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
4
4
  import { BaieVitreeTvStore } from '../../../../dpe/infrastructure/enveloppe/baieVitreeTv.store.js';
5
5
  import { EspaceTamponService } from './espace-tampon.service.js';
6
6
  import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
7
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
7
8
 
8
9
  /** @type {EspaceTamponService} **/
9
10
  let service;
@@ -54,7 +55,7 @@ describe('Calcul de déperdition des baies vitrées', () => {
54
55
  });
55
56
  });
56
57
 
57
- describe("Test d'intégration des ets", () => {
58
+ describeIntegration("Test d'intégration des ets", () => {
58
59
  test.each(['2273E0303205P', '2364E0984413P', '2283E0131604Y'])(
59
60
  'vérification des DI des ets pour dpe %s',
60
61
  (ademeId) => {