@open3cl/engine 1.6.0 → 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 (108) 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/README.md +18 -9
  45. package/apport_et_besoin.spec.js +126 -0
  46. package/conso.js +1 -1
  47. package/conso.spec.js +471 -82
  48. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  49. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  50. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  51. package/core/conf/infrastructure/application.config.spec.js +35 -0
  52. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  53. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  54. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  55. package/core/util/infrastructure/object-util.spec.js +20 -0
  56. package/core/util/logger/log-service.js +16 -7
  57. package/core/util/logger/log-service.spec.js +101 -0
  58. package/dpe-sanitizer.service.spec.js +138 -0
  59. package/engine.js +7 -5
  60. package/engine.spec.js +854 -0
  61. package/enums.spec.js +46 -0
  62. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  63. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  64. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  65. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  66. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  67. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  68. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  69. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  70. package/features/dpe/infrastructure/tv.store.js +28 -9
  71. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  72. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  73. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  74. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  75. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  76. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  77. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  78. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  79. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  80. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  81. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  82. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  83. package/features/engine/domain/constants.spec.js +14 -0
  84. package/features/engine/domain/contexte.builder.spec.js +21 -0
  85. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  86. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  87. package/features/engine/domain/engine.service.spec.js +227 -4
  88. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  89. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  90. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  91. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  92. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  93. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  94. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  95. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  96. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  97. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  98. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  99. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  100. package/ficheTechnique.spec.js +26 -0
  101. package/index.spec.js +54 -0
  102. package/package.json +5 -2
  103. package/preprocessor/index.js +20 -0
  104. package/preprocessor/index.spec.js +15 -0
  105. package/tv.spec.js +38 -0
  106. package/utils.js +43 -1
  107. package/utils.spec.js +518 -2
  108. package/output.js +0 -1
@@ -5,6 +5,7 @@ import { ContexteBuilder } from '../contexte.builder.js';
5
5
  import { SurfaceSudEquivalenteService } from './surface-sud-equivalente.service.js';
6
6
  import { getAdemeFileJson } from '../../../../../test/test-helpers.js';
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 {SurfaceSudEquivalenteService} **/
10
11
  let service;
@@ -206,7 +207,7 @@ describe('Calcul de la surface sud équivalente du logement', () => {
206
207
  });
207
208
  });
208
209
 
209
- describe("Test d'intégration pour surface sud équivalente", () => {
210
+ describeIntegration("Test d'intégration pour surface sud équivalente", () => {
210
211
  test.each(corpus)(
211
212
  'vérification des DI de la surface sud équivalente pour dpe %s',
212
213
  (ademeId) => {
@@ -79,6 +79,8 @@ export class GenerateurChService {
79
79
  generateurChauffage.donnee_utilisateur.generateur = this.#chTvStore.getGenerateurCombustion(
80
80
  generateurChauffageDE.enum_type_generateur_ch_id,
81
81
  generateurChauffage.donnee_intermediaire.pn /
82
+ /* c8 ignore next 2 -- repli défensif inatteignable : donnee_utilisateur.ratio_virtualisation
83
+ est déjà défini juste au-dessus (valeur de l'installation ou 1), donc toujours truthy ici. */
82
84
  ((generateurChauffage.donnee_utilisateur.ratio_virtualisation || 1) * 1000)
83
85
  );
84
86
 
@@ -4,11 +4,12 @@ import { GenerateurChService } from './generateur-ch.service.js';
4
4
  import { DpeNormalizerService } from '../../../normalizer/domain/dpe-normalizer.service.js';
5
5
  import corpus from '../../../../../test/corpus-sano.json';
6
6
  import { getAdemeFileJson } from '../../../../../test/test-helpers.js';
7
- import { TypeGenerateur } from '../../../dpe/domain/models/installation-chauffage.model.js';
7
+ import { TypeGenerateur } from '../../../dpe/domain/models/type-generateur.model.js';
8
8
  import { ChTvStore } from '../../../dpe/infrastructure/ch/chTv.store.js';
9
9
  import { ContexteBuilder } from '../contexte.builder.js';
10
10
  import { TvStore } from '../../../dpe/infrastructure/tv.store.js';
11
11
  import { EmetteurChService } from './emetteur-ch.service.js';
12
+ import { describeIntegration } from '../../../../../test/helpers/integration-test.js';
12
13
 
13
14
  /** @type {GenerateurChService} **/
14
15
  let service;
@@ -563,7 +564,121 @@ describe('Calcul des caractéristiques des générateurs de chauffage', () => {
563
564
  });
564
565
  });
565
566
 
566
- describe("Test d'intégration des installations de chauffage", () => {
567
+ test('Générateur à combustion sans pn : calcul de la puissance nominale et filtrage des émetteurs', () => {
568
+ // Type 76 (chaudière fioul) : générateur à combustion ET chaudière, pour que le calcul
569
+ // des températures de fonctionnement (utilisant les émetteurs filtrés) soit exécuté.
570
+ vi.spyOn(chTvStore, 'getCombustionGenerateurs').mockReturnValue([76]);
571
+ vi.spyOn(chTvStore, 'getPacGenerateurs').mockReturnValue([2]);
572
+ vi.spyOn(chTvStore, 'getGenerateurCombustion').mockReturnValue({});
573
+ // pn absent dans donnee_intermediaire -> execute doit appeler this.pn(ctx, logement).
574
+ const pnSpy = vi.spyOn(service, 'pn').mockReturnValue(20000);
575
+ vi.spyOn(emetteurChService, 'temperatureFonctionnement').mockReturnValue({
576
+ temp_fonc_30: 18,
577
+ temp_fonc_100: 25
578
+ });
579
+
580
+ /** @type {GenerateurChauffage} */
581
+ const generateurChauffage = {
582
+ donnee_entree: {
583
+ enum_methode_saisie_carac_sys_id: 4,
584
+ enum_type_generateur_ch_id: 76,
585
+ enum_lien_generateur_emetteur_id: 'L1',
586
+ presence_ventouse: 1
587
+ },
588
+ // pas de pn : oblige au calcul de la puissance nominale
589
+ donnee_intermediaire: { rpn: 0.95, rpint: 0.68, qp0: 125 }
590
+ };
591
+
592
+ // Deux émetteurs : un seul est lié au générateur (même enum_lien_generateur_emetteur_id).
593
+ const emetteurLie = {
594
+ donnee_entree: { enum_lien_generateur_emetteur_id: 'L1', surface_chauffee: 80 },
595
+ donnee_intermediaire: { rendement_emission: 0.86 }
596
+ };
597
+ const emetteurNonLie = {
598
+ donnee_entree: { enum_lien_generateur_emetteur_id: 'L2', surface_chauffee: 40 },
599
+ donnee_intermediaire: { rendement_emission: 0.9 }
600
+ };
601
+
602
+ const installationChauffage = {
603
+ donnee_entree: { ratio_virtualisation: 1 },
604
+ generateur_chauffage_collection: { generateur_chauffage: [generateurChauffage] },
605
+ emetteur_chauffage_collection: { emetteur_chauffage: [emetteurLie, emetteurNonLie] }
606
+ };
607
+
608
+ /** @type {Logement} */
609
+ const logement = {
610
+ installation_chauffage_collection: { installation_chauffage: [installationChauffage] },
611
+ sortie: { deperdition: { deperdition_enveloppe: 300 } }
612
+ };
613
+
614
+ /** @type {Contexte} */
615
+ const ctx = { altitude: { id: 1 }, zoneClimatique: { id: 1 } };
616
+
617
+ service.execute(ctx, logement, installationChauffage);
618
+
619
+ // La puissance nominale a été calculée puis affectée
620
+ expect(pnSpy).toHaveBeenCalledWith(ctx, logement);
621
+ expect(generateurChauffage.donnee_intermediaire.pn).toBe(20000);
622
+
623
+ // getGenerateurCombustion utilise pn / (ratio * 1000) = 20000 / 1000 = 20
624
+ expect(chTvStore.getGenerateurCombustion).toHaveBeenCalledWith(76, 20);
625
+
626
+ // Seul l'émetteur lié est transmis au calcul des températures de fonctionnement
627
+ expect(emetteurChService.temperatureFonctionnement).toHaveBeenCalledWith(
628
+ ctx,
629
+ generateurChauffage.donnee_entree,
630
+ [emetteurLie]
631
+ );
632
+ });
633
+
634
+ test('execute : installation sans collection de générateurs ne fait rien', () => {
635
+ // Valeur de repli [] quand generateur_chauffage_collection est absente.
636
+ const combustionSpy = vi.spyOn(chTvStore, 'getCombustionGenerateurs');
637
+
638
+ /** @type {Contexte} */
639
+ const ctx = { zoneClimatique: { id: 1 } };
640
+
641
+ expect(() => service.execute(ctx, {}, { donnee_entree: {} })).not.toThrow();
642
+ expect(combustionSpy).not.toHaveBeenCalled();
643
+ });
644
+
645
+ test('execute : ratio de virtualisation par défaut à 1 quand non renseigné', () => {
646
+ vi.spyOn(chTvStore, 'getCombustionGenerateurs').mockReturnValue([1]);
647
+ vi.spyOn(chTvStore, 'getPacGenerateurs').mockReturnValue([2]);
648
+ vi.spyOn(chTvStore, 'getGenerateurCombustion').mockReturnValue({});
649
+
650
+ /** @type {GenerateurChauffage} */
651
+ const generateurChauffage = {
652
+ donnee_entree: {
653
+ enum_methode_saisie_carac_sys_id: 4,
654
+ enum_type_generateur_ch_id: 1,
655
+ presence_ventouse: 1
656
+ },
657
+ donnee_intermediaire: { rpn: 0.95, rpint: 0.68, qp0: 125, pn: 15000 }
658
+ };
659
+
660
+ const installationChauffage = {
661
+ // donnee_entree sans ratio_virtualisation : repli sur 1
662
+ donnee_entree: {},
663
+ generateur_chauffage_collection: { generateur_chauffage: [generateurChauffage] }
664
+ };
665
+
666
+ /** @type {Logement} */
667
+ const logement = {
668
+ installation_chauffage_collection: { installation_chauffage: [installationChauffage] }
669
+ };
670
+
671
+ /** @type {Contexte} */
672
+ const ctx = { zoneClimatique: { id: 1 } };
673
+
674
+ service.execute(ctx, logement, installationChauffage);
675
+
676
+ expect(generateurChauffage.donnee_utilisateur.ratio_virtualisation).toBe(1);
677
+ // pn / (ratio * 1000) = 15000 / 1000 = 15
678
+ expect(chTvStore.getGenerateurCombustion).toHaveBeenCalledWith(1, 15);
679
+ });
680
+
681
+ describeIntegration("Test d'intégration des installations de chauffage", () => {
567
682
  test.each(corpus)('vérification des DI qp0 des installations CH pour dpe %s', (ademeId) => {
568
683
  let dpeRequest = getAdemeFileJson(ademeId);
569
684
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -38,4 +38,18 @@ describe('Calcul des installations de chauffage', () => {
38
38
  service.execute(ctx, logement);
39
39
  expect(generateurChService.execute).toHaveBeenCalledWith(ctx, logement, installationCh);
40
40
  });
41
+
42
+ test("Aucun générateur traité en l'absence d'installation de chauffage", () => {
43
+ vi.spyOn(generateurChService, 'execute').mockReturnThis();
44
+
45
+ /** @type {Contexte} */
46
+ const ctx = { zoneClimatique: { id: 1 } };
47
+
48
+ // Collection absente : la valeur de repli [] doit être utilisée (aucun appel au service générateur)
49
+ service.execute(ctx, {});
50
+ // Collection présente mais sans tableau d'installations
51
+ service.execute(ctx, { installation_chauffage_collection: {} });
52
+
53
+ expect(generateurChService.execute).not.toHaveBeenCalled();
54
+ });
41
55
  });
@@ -5,6 +5,7 @@ import corpus from '../../../../../../test/corpus-sano.json';
5
5
  import { getAdemeFileJson } from '../../../../../../test/test-helpers.js';
6
6
  import { FrTvStore } from '../../../../dpe/infrastructure/froid/frTv.store.js';
7
7
  import { ConsoFroidService } from './conso-froid.service.js';
8
+ import { describeIntegration } from '../../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {ConsoFroidService} **/
10
11
  let service;
@@ -117,7 +118,87 @@ describe('Calcul des consos en froid du logement', () => {
117
118
  }
118
119
  );
119
120
 
120
- describe("Test d'intégration pour le besoin en froid", () => {
121
+ test('consoFroid : données intermédiaires absentes -> EER lu dans la table (repli {})', () => {
122
+ // climatisation.donnee_intermediaire absente : repli sur {} puis lecture de l'EER forfaitaire.
123
+ vi.spyOn(tvStore, 'getEer').mockReturnValue(90);
124
+
125
+ /** @type {Contexte} */
126
+ const contexte = { zoneClimatique: { id: 1 }, surfaceHabitable: 100 };
127
+
128
+ /** @type {Climatisation} */
129
+ const climatisation = {
130
+ donnee_entree: { enum_methode_saisie_carac_sys_id: 6, enum_periode_installation_fr_id: 2 }
131
+ };
132
+
133
+ /** @type {ApportEtBesoin} */
134
+ const apportEtBesoin = { besoin_fr: 1000, besoin_fr_depensier: 1500 };
135
+
136
+ const consoFroid = service.consoFroid(contexte, apportEtBesoin, climatisation);
137
+
138
+ expect(tvStore.getEer).toHaveBeenCalledWith(1, 2);
139
+ // conso = 0.9 * besoin / eer
140
+ expect(consoFroid.conso_fr).toBeCloseTo((0.9 * 1000) / 90, 5);
141
+ expect(consoFroid.conso_fr_depensier).toBeCloseTo((0.9 * 1500) / 90, 5);
142
+ });
143
+
144
+ test('execute : renseigne les données intermédiaires de chaque climatisation', () => {
145
+ // On isole execute du calcul détaillé : consoFroid est doublée pour ne vérifier que
146
+ // l'itération sur les climatisations et l'affectation des champs (dont l'initialisation ??=).
147
+ vi.spyOn(service, 'consoFroid').mockReturnValue({
148
+ besoin_fr: 10,
149
+ besoin_fr_depensier: 15,
150
+ conso_fr: 2,
151
+ conso_fr_depensier: 3
152
+ });
153
+
154
+ /** @type {Contexte} */
155
+ const ctx = { zoneClimatique: { id: 1 }, surfaceHabitable: 100 };
156
+
157
+ // Première climatisation sans donnee_intermediaire (branche ??= {}),
158
+ // seconde avec un objet préexistant (branche ??= conservée).
159
+ const climSansDI = { donnee_entree: {} };
160
+ const climAvecDI = { donnee_entree: {}, donnee_intermediaire: { eer: 42 } };
161
+
162
+ /** @type {Logement} */
163
+ const logement = {
164
+ sortie: { apport_et_besoin: { besoin_fr: 1000, besoin_fr_depensier: 1500 } },
165
+ climatisation_collection: { climatisation: [climSansDI, climAvecDI] }
166
+ };
167
+
168
+ service.execute(ctx, logement);
169
+
170
+ expect(service.consoFroid).toHaveBeenCalledTimes(2);
171
+ expect(service.consoFroid).toHaveBeenCalledWith(
172
+ ctx,
173
+ logement.sortie.apport_et_besoin,
174
+ climSansDI
175
+ );
176
+
177
+ for (const clim of [climSansDI, climAvecDI]) {
178
+ expect(clim.donnee_intermediaire).toMatchObject({
179
+ besoin_fr: 10,
180
+ besoin_fr_depensier: 15,
181
+ conso_fr: 2,
182
+ conso_fr_depensier: 3
183
+ });
184
+ }
185
+ // L'objet préexistant a été conservé (eer non écrasé par ??=)
186
+ expect(climAvecDI.donnee_intermediaire.eer).toBe(42);
187
+ });
188
+
189
+ test("execute : aucune conso calculée en l'absence de climatisation", () => {
190
+ // Valeur de repli [] quand climatisation_collection est absente.
191
+ vi.spyOn(service, 'consoFroid');
192
+
193
+ /** @type {Contexte} */
194
+ const ctx = { zoneClimatique: { id: 1 }, surfaceHabitable: 100 };
195
+
196
+ service.execute(ctx, { sortie: { apport_et_besoin: {} } });
197
+
198
+ expect(service.consoFroid).not.toHaveBeenCalled();
199
+ });
200
+
201
+ describeIntegration("Test d'intégration pour le besoin en froid", () => {
121
202
  test.each(corpus)('vérification des sorties besoin_fr et conso_fr pour dpe %s', (ademeId) => {
122
203
  /**
123
204
  * @type {Dpe}
@@ -0,0 +1,14 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { PRECISION } from './constants.js';
3
+
4
+ describe('Constantes du moteur', () => {
5
+ test('PRECISION vaut 100000', () => {
6
+ expect(PRECISION).toBe(100000);
7
+ });
8
+
9
+ test('PRECISION est un entier positif utilisable comme facteur d’arrondi', () => {
10
+ expect(typeof PRECISION).toBe('number');
11
+ expect(Number.isInteger(PRECISION)).toBe(true);
12
+ expect(PRECISION).toBeGreaterThan(0);
13
+ });
14
+ });
@@ -160,6 +160,27 @@ describe('Generateur du contexte du calcul', () => {
160
160
  });
161
161
  });
162
162
 
163
+ test("DPE appartement généré à partir des données d'un DPE immeuble", () => {
164
+ // Méthode 10 : appartement issu des données immeuble.
165
+ // -> typeDpe APPARTEMENT_A_PARTIR_IMMEUBLE et surface habitable = celle de l'immeuble.
166
+ const dpe = {
167
+ logement: {
168
+ meteo: {},
169
+ caracteristique_generale: {
170
+ enum_methode_application_dpe_log_id: '10',
171
+ surface_habitable_logement: 48.9,
172
+ surface_habitable_immeuble: 105
173
+ }
174
+ }
175
+ };
176
+
177
+ expect(contexteBuilder.fromDpe(dpe)).toMatchObject({
178
+ typeHabitation: TypeHabitation.APPARTEMENT,
179
+ typeDpe: TypeDpe.APPARTEMENT_A_PARTIR_IMMEUBLE,
180
+ surfaceHabitable: 105
181
+ });
182
+ });
183
+
163
184
  test('Contexte avec calcul ilpa', () => {
164
185
  const dpe = {
165
186
  logement: {
@@ -121,6 +121,17 @@ describe('Calcul des informations techniques des générateurs ECS', () => {
121
121
  });
122
122
  });
123
123
 
124
+ test("execute ne fait rien quand l'installation n'a pas de collection de générateurs", () => {
125
+ // Valeur de repli [] quand generateur_ecs_collection est absente : aucune mutation.
126
+ const pertesStockageSpy = vi.spyOn(service, 'pertesStockage');
127
+
128
+ /** @type {InstallationEcs} */
129
+ const installationEcs = {};
130
+ expect(() => service.execute(installationEcs)).not.toThrow();
131
+ expect(installationEcs).toStrictEqual({});
132
+ expect(pertesStockageSpy).not.toHaveBeenCalled();
133
+ });
134
+
124
135
  test("Determination du type d'énergie électrique du générateur", () => {
125
136
  vi.spyOn(tvStore, 'getElectriqueEcsGenerateurs').mockReturnValue([1]);
126
137
  expect(service.generateurElectrique({ enum_type_generateur_ecs_id: 1 })).toBeTruthy();
@@ -1,10 +1,11 @@
1
- import { beforeEach, describe, expect, test } from 'vitest';
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
2
  import { GenerateurEcsService } from './generateur-ecs.service.js';
3
3
  import { InstallationEcsService } from './installation-ecs.service.js';
4
4
  import corpus from '../../../../../test/corpus-sano.json';
5
5
  import { expect_or, getAdemeFileJson } from '../../../../../test/test-helpers.js';
6
6
  import { DpeNormalizerService } from '../../../normalizer/domain/dpe-normalizer.service.js';
7
7
  import { ContexteBuilder } from '../contexte.builder.js';
8
+ import { describeIntegration } from '../../../../../test/helpers/integration-test.js';
8
9
 
9
10
  /** @type {InstallationEcsService} **/
10
11
  let service;
@@ -247,7 +248,59 @@ describe('Calcul des besoins et des pertes des installations ECS', () => {
247
248
  expect(service.isInstallationIndividuelle({ enum_type_installation_id: 4 })).toBeFalsy();
248
249
  });
249
250
 
250
- describe("Test d'intégration des installations ECS", () => {
251
+ test('execute : parcourt les installations ECS et renseigne besoins et pertes', () => {
252
+ // Le calcul des Qgw par générateur est délégué au GenerateurEcsService : on le double
253
+ // pour isoler la logique d'orchestration de l'installation.
254
+ vi.spyOn(generateurEcsService, 'execute').mockReturnValue(undefined);
255
+
256
+ /** @type {Contexte} */
257
+ const ctx = { surfaceHabitable: 100 };
258
+
259
+ /** @type {InstallationEcs} */
260
+ const installationEcs = {
261
+ donnee_entree: { enum_type_installation_id: 1, surface_habitable: 100 },
262
+ generateur_ecs_collection: {
263
+ generateur_ecs: [
264
+ {
265
+ donnee_entree: { position_volume_chauffe_stockage: 1 },
266
+ donnee_utilisateur: { Qgw: 500 }
267
+ }
268
+ ]
269
+ }
270
+ };
271
+
272
+ /** @type {Logement} */
273
+ const logement = { installation_ecs_collection: { installation_ecs: [installationEcs] } };
274
+
275
+ service.execute(ctx, logement, { besoin_ecs: 1000, besoin_ecs_depensier: 1500 });
276
+
277
+ // Délégation du calcul par générateur
278
+ expect(generateurEcsService.execute).toHaveBeenCalledWith(installationEcs);
279
+
280
+ // Besoins proratisés (surface installation === surface logement -> ratio 1)
281
+ expect(installationEcs.donnee_intermediaire).toStrictEqual({
282
+ besoin_ecs: 1000,
283
+ besoin_ecs_depensier: 1500
284
+ });
285
+
286
+ // Pertes de distribution individuelle : 0.1 * besoin (en Wh) ; installation individuelle -> QdwCol* = 0
287
+ expect(installationEcs.donnee_utilisateur.QdwIndVc.conventionnel).toBeCloseTo(100000, 5);
288
+ expect(installationEcs.donnee_utilisateur.QdwIndVc.depensier).toBeCloseTo(150000, 5);
289
+ expect(installationEcs.donnee_utilisateur.QdwColVc.conventionnel).toBe(0);
290
+ // Générateur en volume chauffé -> stockage récupérable = Qgw
291
+ expect(installationEcs.donnee_utilisateur.QgwRecuperable).toBe(500);
292
+ });
293
+
294
+ test('execute : aucune installation traitée quand la collection ECS est absente', () => {
295
+ // Valeur de repli [] quand installation_ecs_collection est absente.
296
+ const generateurSpy = vi.spyOn(generateurEcsService, 'execute');
297
+
298
+ service.execute({ surfaceHabitable: 100 }, {}, { besoin_ecs: 0, besoin_ecs_depensier: 0 });
299
+
300
+ expect(generateurSpy).not.toHaveBeenCalled();
301
+ });
302
+
303
+ describeIntegration("Test d'intégration des installations ECS", () => {
251
304
  test.each(corpus)('vérification des DI des installations ECS pour dpe %s', (ademeId) => {
252
305
  let dpeRequest = getAdemeFileJson(ademeId);
253
306
  dpeRequest = normalizerService.normalize(dpeRequest);
@@ -1,7 +1,230 @@
1
- import { describe, test } from 'vitest';
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { EngineService } from './engine.service.js';
3
+ import { logger } from '../../../core/util/logger/log-service.js';
2
4
 
3
- describe.skip('Application de la méthode 3CL', () => {
4
- test('test complet de calcul (validation ADEME?)', () => {
5
- // const dpe = EngineService.execute(originalDpe);=
5
+ /**
6
+ * Tests unitaires isolés de `EngineService`.
7
+ *
8
+ * Les quatre services injectés (`DeperditionEnveloppeService`,
9
+ * `ApportEtBesoinService`, `ConsoService` et `ContexteBuilder`) sont remplacés
10
+ * par des doubles de test (spies `vi.fn`) afin de n'exercer que la logique
11
+ * d'orchestration du moteur : nettoyage des données calculées, construction du
12
+ * contexte, délégation aux services et agrégation des sorties. Aucune donnée
13
+ * réelle (tables, DPE complet) n'est nécessaire.
14
+ */
15
+
16
+ /** @type {{ deperditions: import('vitest').Mock }} */
17
+ let deperditionService;
18
+
19
+ /** @type {{ execute: import('vitest').Mock }} */
20
+ let apportEtBesoinService;
21
+
22
+ /** @type {{ execute: import('vitest').Mock }} */
23
+ let consoService;
24
+
25
+ /** @type {{ fromDpe: import('vitest').Mock }} */
26
+ let contextBuilder;
27
+
28
+ /** @type {EngineService} */
29
+ let service;
30
+
31
+ /**
32
+ * Construit une enveloppe minimale : toutes les collections existent mais leurs
33
+ * tableaux internes sont absents (exerce le court-circuit des `?.`).
34
+ */
35
+ function enveloppeMinimale() {
36
+ return {
37
+ mur_collection: {},
38
+ baie_vitree_collection: {},
39
+ ets_collection: {},
40
+ plancher_bas_collection: {},
41
+ plancher_haut_collection: {},
42
+ pont_thermique_collection: {},
43
+ porte_collection: {}
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Construit un DPE minimal : collections présentes mais vides. Utilisé pour
49
+ * exercer les branches « tableau absent » de `#removeComputedData`.
50
+ */
51
+ function dpeMinimal() {
52
+ return {
53
+ numero_dpe: 'DPE-MIN',
54
+ logement: {
55
+ caracteristique_generale: {},
56
+ enveloppe: enveloppeMinimale(),
57
+ climatisation_collection: {},
58
+ ventilation_collection: {},
59
+ installation_ecs_collection: {},
60
+ installation_chauffage_collection: {}
61
+ }
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Construit un DPE complet : chaque collection possède un tableau peuplé avec
67
+ * des `donnee_intermediaire`. Exerce la branche « tableau présent » de
68
+ * `#removeComputedData` ainsi que les `?.` imbriqués.
69
+ */
70
+ function dpeComplet() {
71
+ return {
72
+ numero_dpe: 'DPE-FULL',
73
+ logement: {
74
+ caracteristique_generale: {},
75
+ sortie: { deperdition: 'ancienne valeur' },
76
+ enveloppe: {
77
+ mur_collection: { mur: [{ donnee_intermediaire: {} }] },
78
+ baie_vitree_collection: {
79
+ baie_vitree: [
80
+ {
81
+ donnee_intermediaire: {},
82
+ baie_vitree_double_fenetre: { donnee_intermediaire: {} }
83
+ },
84
+ // baie sans double fenêtre : exerce la branche falsy du `?.`
85
+ { donnee_intermediaire: {} }
86
+ ]
87
+ },
88
+ ets_collection: { ets: { donnee_intermediaire: {} } },
89
+ plancher_bas_collection: { plancher_bas: [{ donnee_intermediaire: {} }] },
90
+ plancher_haut_collection: { plancher_haut: [{ donnee_intermediaire: {} }] },
91
+ pont_thermique_collection: { pont_thermique: [{ donnee_intermediaire: {} }] },
92
+ porte_collection: { porte: [{ donnee_intermediaire: {} }] }
93
+ },
94
+ climatisation_collection: { climatisation: [{ donnee_intermediaire: {} }] },
95
+ ventilation_collection: { ventilation: [{ donnee_intermediaire: {} }] },
96
+ installation_ecs_collection: {
97
+ installation_ecs: [
98
+ {
99
+ donnee_intermediaire: {},
100
+ generateur_ecs_collection: {
101
+ generateur_ecs: [{ donnee_intermediaire: {} }]
102
+ }
103
+ }
104
+ ]
105
+ },
106
+ installation_chauffage_collection: {
107
+ installation_chauffage: [
108
+ {
109
+ donnee_intermediaire: {},
110
+ emetteur_chauffage_collection: {
111
+ emetteur_chauffage: [{ donnee_intermediaire: {} }]
112
+ }
113
+ }
114
+ ]
115
+ },
116
+ production_elec_enr: { donnee_intermediaire: {} }
117
+ }
118
+ };
119
+ }
120
+
121
+ describe('EngineService (orchestration de la méthode 3CL)', () => {
122
+ beforeEach(() => {
123
+ deperditionService = { deperditions: vi.fn().mockReturnValue('DEPERDITION') };
124
+ apportEtBesoinService = { execute: vi.fn().mockReturnValue('APPORT_ET_BESOIN') };
125
+ consoService = { execute: vi.fn() };
126
+ contextBuilder = { fromDpe: vi.fn().mockReturnValue('CTX') };
127
+ service = new EngineService(
128
+ deperditionService,
129
+ apportEtBesoinService,
130
+ consoService,
131
+ contextBuilder
132
+ );
133
+ vi.spyOn(logger, 'info').mockImplementation(() => {});
134
+ });
135
+
136
+ test('trace le numéro de DPE traité', () => {
137
+ service.execute(dpeMinimal());
138
+ expect(logger.info).toHaveBeenCalledWith('Process DPE DPE-MIN');
139
+ });
140
+
141
+ test('construit le contexte à partir du DPE nettoyé puis délègue aux services', () => {
142
+ const resultat = service.execute(dpeMinimal());
143
+
144
+ // Le contexte est construit une fois à partir du DPE traité
145
+ expect(contextBuilder.fromDpe).toHaveBeenCalledTimes(1);
146
+ const dpeTraite = contextBuilder.fromDpe.mock.calls[0][0];
147
+
148
+ // Les services reçoivent le contexte construit et le logement traité
149
+ expect(deperditionService.deperditions).toHaveBeenCalledWith('CTX', dpeTraite.logement);
150
+ expect(apportEtBesoinService.execute).toHaveBeenCalledWith('CTX', dpeTraite.logement);
151
+ expect(consoService.execute).toHaveBeenCalledWith('CTX', dpeTraite.logement);
152
+
153
+ // Les résultats des services sont agrégés dans la sortie
154
+ expect(resultat.logement.sortie.deperdition).toBe('DEPERDITION');
155
+ expect(resultat.logement.sortie.apport_et_besoin).toBe('APPORT_ET_BESOIN');
156
+ });
157
+
158
+ test('initialise la structure de sortie avec toutes les clés attendues', () => {
159
+ const resultat = service.execute(dpeMinimal());
160
+
161
+ expect(resultat.logement.sortie).toEqual({
162
+ deperdition: 'DEPERDITION',
163
+ apport_et_besoin: 'APPORT_ET_BESOIN',
164
+ ef_conso: undefined,
165
+ ep_conso: undefined,
166
+ emission_ges: undefined,
167
+ cout: undefined,
168
+ production_electricite: undefined,
169
+ sortie_par_energie_collection: undefined,
170
+ confort_ete: undefined,
171
+ qualite_isolation: undefined
172
+ });
173
+ });
174
+
175
+ test('supprime les données de calcul intermédiaires du DPE retourné', () => {
176
+ const resultat = service.execute(dpeMinimal());
177
+ expect(resultat.logement.donnees_de_calcul).toBeUndefined();
178
+ });
179
+
180
+ test("n'altère pas le DPE d'entrée (travail sur une copie profonde)", () => {
181
+ const original = dpeComplet();
182
+ const resultat = service.execute(original);
183
+
184
+ // L'entrée conserve ses données calculées d'origine
185
+ expect(original.logement.sortie).toEqual({ deperdition: 'ancienne valeur' });
186
+ expect(original.logement.enveloppe.mur_collection.mur[0].donnee_intermediaire).toBeDefined();
187
+ // La sortie est bien un nouvel objet
188
+ expect(resultat).not.toBe(original);
189
+ });
190
+
191
+ test('nettoie toutes les données intermédiaires quand les collections sont peuplées', () => {
192
+ const resultat = service.execute(dpeComplet());
193
+ const env = resultat.logement.enveloppe;
194
+
195
+ expect(env.mur_collection.mur[0].donnee_intermediaire).toBeUndefined();
196
+ expect(env.baie_vitree_collection.baie_vitree[0].donnee_intermediaire).toBeUndefined();
197
+ expect(
198
+ env.baie_vitree_collection.baie_vitree[0].baie_vitree_double_fenetre.donnee_intermediaire
199
+ ).toBeUndefined();
200
+ expect(env.ets_collection.ets.donnee_intermediaire).toBeUndefined();
201
+ expect(env.plancher_bas_collection.plancher_bas[0].donnee_intermediaire).toBeUndefined();
202
+ expect(env.plancher_haut_collection.plancher_haut[0].donnee_intermediaire).toBeUndefined();
203
+ expect(env.pont_thermique_collection.pont_thermique[0].donnee_intermediaire).toBeUndefined();
204
+ expect(env.porte_collection.porte[0].donnee_intermediaire).toBeUndefined();
205
+
206
+ expect(
207
+ resultat.logement.climatisation_collection.climatisation[0].donnee_intermediaire
208
+ ).toBeUndefined();
209
+ expect(
210
+ resultat.logement.ventilation_collection.ventilation[0].donnee_intermediaire
211
+ ).toBeUndefined();
212
+
213
+ const ecs = resultat.logement.installation_ecs_collection.installation_ecs[0];
214
+ expect(ecs.donnee_intermediaire).toBeUndefined();
215
+ expect(ecs.generateur_ecs_collection.generateur_ecs[0].donnee_intermediaire).toBeUndefined();
216
+
217
+ const ch = resultat.logement.installation_chauffage_collection.installation_chauffage[0];
218
+ expect(ch.donnee_intermediaire).toBeUndefined();
219
+ expect(
220
+ ch.emetteur_chauffage_collection.emetteur_chauffage[0].donnee_intermediaire
221
+ ).toBeUndefined();
222
+
223
+ expect(resultat.logement.production_elec_enr.donnee_intermediaire).toBeUndefined();
224
+ });
225
+
226
+ test('reste robuste lorsque les collections ne contiennent aucun élément', () => {
227
+ // Aucun tableau interne ni production_elec_enr : les `?.` court-circuitent
228
+ expect(() => service.execute(dpeMinimal())).not.toThrow();
6
229
  });
7
230
  });