@open3cl/engine 1.6.1 → 1.7.0

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 (109) 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.js +121 -1
  18. package/15_conso_aux.spec.js +550 -0
  19. package/16.2_production_enr.spec.js +188 -0
  20. package/16_conso_eclairage.spec.js +62 -0
  21. package/2021_04_13_confort_ete.spec.js +159 -0
  22. package/2021_04_13_qualite_isolation.js +1 -1
  23. package/2021_04_13_qualite_isolation.spec.js +286 -0
  24. package/3.1_b.js +1 -1
  25. package/3.1_b.spec.js +367 -0
  26. package/3.2.1_mur.spec.js +812 -39
  27. package/3.2.2_plancher_bas.spec.js +643 -78
  28. package/3.2.3_plancher_haut.spec.js +452 -0
  29. package/3.3.1.4_porte.spec.js +114 -0
  30. package/3.3_baie_vitree.spec.js +449 -295
  31. package/3.4_pont_thermique.spec.js +890 -0
  32. package/3_deperdition.spec.js +397 -0
  33. package/4_ventilation.js +1 -1
  34. package/4_ventilation.spec.js +406 -0
  35. package/5_conso_ventilation.spec.js +186 -0
  36. package/6.2_surface_sud_equivalente.js +2 -1
  37. package/6.2_surface_sud_equivalente.spec.js +138 -0
  38. package/8_intermittence.spec.js +33 -0
  39. package/9_besoin_ch.js +2 -2
  40. package/9_besoin_ch.spec.js +381 -0
  41. package/9_chauffage.js +4 -0
  42. package/9_chauffage.spec.js +478 -92
  43. package/9_conso_ch.spec.js +502 -248
  44. package/9_emetteur_ch.spec.js +339 -0
  45. package/9_generateur_ch.spec.js +467 -81
  46. package/apport_et_besoin.spec.js +126 -0
  47. package/conso.js +21 -7
  48. package/conso.spec.js +491 -82
  49. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  50. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  51. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  52. package/core/conf/infrastructure/application.config.spec.js +35 -0
  53. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  54. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  55. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  56. package/core/util/infrastructure/object-util.spec.js +20 -0
  57. package/core/util/logger/log-service.js +16 -7
  58. package/core/util/logger/log-service.spec.js +101 -0
  59. package/dpe-sanitizer.service.spec.js +138 -0
  60. package/engine.js +20 -1
  61. package/engine.spec.js +949 -0
  62. package/enums.spec.js +46 -0
  63. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  64. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  65. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  66. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  67. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  68. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  69. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  70. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  71. package/features/dpe/infrastructure/tv.store.js +28 -9
  72. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  73. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  74. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  75. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  76. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  77. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  78. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  79. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  80. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  81. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  82. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  83. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  84. package/features/engine/domain/constants.spec.js +14 -0
  85. package/features/engine/domain/contexte.builder.spec.js +21 -0
  86. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  87. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  88. package/features/engine/domain/engine.service.spec.js +227 -4
  89. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  90. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  91. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  92. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  93. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  94. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  95. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  96. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  97. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  98. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  99. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  100. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  101. package/ficheTechnique.spec.js +26 -0
  102. package/index.spec.js +54 -0
  103. package/package.json +5 -2
  104. package/preprocessor/index.js +20 -0
  105. package/preprocessor/index.spec.js +15 -0
  106. package/tv.spec.js +38 -0
  107. package/utils.js +43 -1
  108. package/utils.spec.js +518 -2
  109. package/output.js +0 -1
package/engine.spec.js ADDED
@@ -0,0 +1,949 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * `engine.js` est l'orchestrateur du moteur. Il enchaîne de nombreux sous-modules
5
+ * de calcul (déperdition, apport/besoin, ecs, chauffage, conso, ...). Pour le
6
+ * tester **unitairement**, TOUS ces sous-modules sont mockés : ils renvoient des
7
+ * valeurs contrôlées, ce qui permet de piloter chacune des branches de
8
+ * l'orchestrateur sans exécuter la moindre logique métier.
9
+ *
10
+ * On vérifie que l'orchestrateur :
11
+ * - garde-fou sur le modèle / l'enveloppe (retours `null`) ;
12
+ * - choisit les bonnes surfaces (Sh / Sh immeuble) selon `th` et le map_id ;
13
+ * - construit et transmet les bons arguments aux sous-modules ;
14
+ * - agrège correctement les sorties (`sortie`, `ep_conso`, coefficients).
15
+ */
16
+
17
+ // Drapeaux mutables pilotant les constantes de configuration importées depuis utils.
18
+ // Les getters garantissent une lecture « live » côté module testé.
19
+ let bugForBugCompat = false;
20
+ let useEnumAsStringFlag = false;
21
+
22
+ vi.mock('./utils.js', () => ({
23
+ add_references: vi.fn(),
24
+ get bug_for_bug_compat() {
25
+ return bugForBugCompat;
26
+ },
27
+ collectionCanBeEmpty: vi.fn(),
28
+ containsAnySubstring: vi.fn(),
29
+ isEffetJoule: vi.fn(),
30
+ get use_enum_as_string() {
31
+ return useEnumAsStringFlag;
32
+ },
33
+ useEnumAsString: vi.fn(),
34
+ xmlParser: { parse: vi.fn() }
35
+ }));
36
+
37
+ vi.mock('./enums.js', () => ({
38
+ default: {
39
+ methode_application_dpe_log: {},
40
+ modele_dpe: {},
41
+ orientation: {},
42
+ type_vitrage: {},
43
+ type_baie: {},
44
+ type_materiaux_menuiserie: {},
45
+ type_generateur_ch: {},
46
+ type_generateur_ecs: {}
47
+ }
48
+ }));
49
+
50
+ vi.mock('./conso.js', () => ({
51
+ default: vi.fn(),
52
+ classe_bilan_dpe: vi.fn(),
53
+ classe_emission_ges: vi.fn(),
54
+ coef_ep: { tag: 'coef_ep' },
55
+ coef_ep_1_7: { tag: 'coef_ep_1_7' },
56
+ coef_ep_2_3: { tag: 'coef_ep_2_3' },
57
+ COEFF_EP_1_7: 1.7,
58
+ COEFF_EP_2_3: 2.3
59
+ }));
60
+
61
+ // Dépendances de calcul mockées : elles ne doivent pas s'exécuter réellement.
62
+ vi.mock('./3_deperdition.js', () => ({ default: vi.fn() }));
63
+ vi.mock('./apport_et_besoin.js', () => ({ default: vi.fn() }));
64
+ vi.mock('./10_clim.js', () => ({ default: vi.fn() }));
65
+ vi.mock('./11_ecs.js', () => ({ default: vi.fn() }));
66
+ vi.mock('./9_besoin_ch.js', () => ({ default: vi.fn() }));
67
+ vi.mock('./9_chauffage.js', () => ({ default: vi.fn(), tauxChargeForGenerator: vi.fn() }));
68
+ vi.mock('./15_conso_aux.js', () => ({ conso_aux_distribution_ecs: vi.fn() }));
69
+ vi.mock('./2021_04_13_confort_ete.js', () => ({ default: vi.fn() }));
70
+ vi.mock('./2021_04_13_qualite_isolation.js', () => ({ default: vi.fn() }));
71
+ vi.mock('./ficheTechnique.js', () => ({ default: vi.fn() }));
72
+
73
+ // Les instances Inertie / ProductionENR sont créées au chargement du module.
74
+ // On expose des méthodes mockées récupérables dans les tests.
75
+ const calculateInertie = vi.fn();
76
+ const calculateEnr = vi.fn();
77
+ vi.mock('./7_inertie.js', () => ({ Inertie: vi.fn(() => ({ calculateInertie })) }));
78
+ vi.mock('./16.2_production_enr.js', () => ({ ProductionENR: vi.fn(() => ({ calculateEnr })) }));
79
+
80
+ // Le service de sanitisation est instancié au chargement puis appelé : `execute`
81
+ // renvoie l'entrée telle quelle.
82
+ const sanitizeExecute = vi.fn((dpe) => dpe);
83
+ vi.mock('./dpe-sanitizer.service.js', () => ({
84
+ default: vi.fn(() => ({ execute: sanitizeExecute }))
85
+ }));
86
+
87
+ const { getVersion, get_conso_coeff_1_7_2027, get_classe_ges_dpe, calcul_3cl, calcul_3cl_xml } =
88
+ await import('./engine.js');
89
+ const { classe_bilan_dpe, classe_emission_ges } = await import('./conso.js');
90
+ const calc_conso = (await import('./conso.js')).default;
91
+ const {
92
+ add_references,
93
+ collectionCanBeEmpty,
94
+ containsAnySubstring,
95
+ isEffetJoule,
96
+ useEnumAsString,
97
+ xmlParser
98
+ } = await import('./utils.js');
99
+ const enums = (await import('./enums.js')).default;
100
+ const calc_deperdition = (await import('./3_deperdition.js')).default;
101
+ const calc_apport_et_besoin = (await import('./apport_et_besoin.js')).default;
102
+ const calc_clim = (await import('./10_clim.js')).default;
103
+ const calc_ecs = (await import('./11_ecs.js')).default;
104
+ const calc_besoin_ch = (await import('./9_besoin_ch.js')).default;
105
+ const { default: calc_chauffage, tauxChargeForGenerator } = await import('./9_chauffage.js');
106
+ const { conso_aux_distribution_ecs } = await import('./15_conso_aux.js');
107
+ const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
108
+ const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
109
+ const getFicheTechnique = (await import('./ficheTechnique.js')).default;
110
+
111
+ /** Objet conso renvoyé par le mock de `calc_conso` (mêmes réf pour les 3 appels). */
112
+ let consoResult;
113
+
114
+ beforeEach(() => {
115
+ vi.clearAllMocks();
116
+ bugForBugCompat = false;
117
+ useEnumAsStringFlag = false;
118
+
119
+ // Enums par défaut : maison, modèle supporté.
120
+ enums.methode_application_dpe_log = { 1: 'dpe maison individuelle' };
121
+ enums.modele_dpe = { 1: 'dpe 3cl 2021 méthode logement', 99: 'modèle non supporté' };
122
+ enums.orientation = { 1: 'sud' };
123
+ enums.type_vitrage = { 1: 'double' };
124
+ enums.type_baie = { 1: 'fenetre' };
125
+ enums.type_materiaux_menuiserie = { 1: 'pvc' };
126
+ enums.type_generateur_ch = {};
127
+ enums.type_generateur_ecs = {};
128
+
129
+ // Retours par défaut des sous-modules (chemin nominal).
130
+ vi.mocked(sanitizeExecute).mockImplementation((dpe) => dpe);
131
+ vi.mocked(collectionCanBeEmpty).mockReturnValue(true);
132
+ vi.mocked(isEffetJoule).mockReturnValue(false);
133
+ vi.mocked(getFicheTechnique).mockReturnValue(undefined);
134
+ vi.mocked(containsAnySubstring).mockReturnValue(false);
135
+ vi.mocked(calc_deperdition).mockReturnValue({ deperdition_enveloppe: 300 });
136
+ vi.mocked(calculateInertie).mockReturnValue({ enum_classe_inertie_id: '3' });
137
+ vi.mocked(calc_apport_et_besoin).mockReturnValue({
138
+ besoin_fr: 1,
139
+ besoin_fr_depensier: 2,
140
+ besoin_ecs: 100,
141
+ besoin_ecs_depensier: 120,
142
+ nadeq: 2
143
+ });
144
+ vi.mocked(calc_besoin_ch).mockReturnValue({
145
+ besoin_ch: 50,
146
+ besoin_ch_depensier: 60,
147
+ besoin_ch_mois: { m1: 10, m2: 20 }
148
+ });
149
+ consoResult = {
150
+ ep_conso: { classe_bilan_dpe: 'D', ep_conso_5_usages: 100, ep_conso_5_usages_m2: 5 },
151
+ ef_conso: {}
152
+ };
153
+ vi.mocked(calc_conso).mockReturnValue(consoResult);
154
+ vi.mocked(calculateEnr).mockReturnValue({ pv: 1 });
155
+ vi.mocked(calc_confort_ete).mockReturnValue('confort');
156
+ vi.mocked(calc_qualite_isolation).mockReturnValue('qualite');
157
+ });
158
+
159
+ /** Construit un DPE complet exploitable par un run intégral de `calcul_3cl`. */
160
+ function makeDpe() {
161
+ return {
162
+ administratif: {
163
+ enum_modele_dpe_id: '1',
164
+ date_etablissement_dpe: '2024-01-01'
165
+ },
166
+ logement: {
167
+ meteo: {
168
+ enum_zone_climatique_id: 'zc1',
169
+ enum_classe_altitude_id: 'ca1',
170
+ batiment_materiaux_anciens: 0
171
+ },
172
+ caracteristique_generale: {
173
+ enum_methode_application_dpe_log_id: '1',
174
+ surface_habitable_logement: 80,
175
+ surface_habitable_immeuble: 200,
176
+ nombre_appartement: 4,
177
+ nombre_niveau_immeuble: 3,
178
+ hsp: 2.5,
179
+ annee_construction: 1990
180
+ },
181
+ enveloppe: {
182
+ mur_collection: { mur: [{ donnee_entree: {} }] },
183
+ plancher_haut_collection: { plancher_haut: [{ donnee_entree: {} }] },
184
+ plancher_bas_collection: { plancher_bas: [{ donnee_entree: {} }] },
185
+ baie_vitree_collection: { baie_vitree: [{ donnee_entree: { presence_joint: 1 } }] },
186
+ ets_collection: { ets: [] },
187
+ inertie: { enum_classe_inertie_id: '3' }
188
+ },
189
+ installation_chauffage_collection: {
190
+ installation_chauffage: [
191
+ { donnee_entree: {}, generateur_chauffage_collection: { generateur_chauffage: [] } }
192
+ ]
193
+ },
194
+ installation_ecs_collection: {
195
+ installation_ecs: [
196
+ {
197
+ donnee_entree: { enum_type_installation_id: '1' },
198
+ donnee_intermediaire: {},
199
+ generateur_ecs_collection: { generateur_ecs: [] }
200
+ }
201
+ ]
202
+ },
203
+ climatisation_collection: { climatisation: [{ donnee_entree: {} }] },
204
+ ventilation_collection: { ventilation: [{ donnee_entree: {} }] },
205
+ production_elec_enr: { some: 'pv' }
206
+ }
207
+ };
208
+ }
209
+
210
+ /** Construit un générateur ECS pour les tests du bloc `bug_for_bug_compat`. */
211
+ function genEcs(overrides = {}) {
212
+ return { donnee_entree: { ...overrides } };
213
+ }
214
+
215
+ describe('getVersion', () => {
216
+ test('retourne le jeton de version remplacé au build', () => {
217
+ // En source, le jeton n'est pas encore substitué.
218
+ expect(getVersion()).toBe('OPEN3CL_VERSION');
219
+ });
220
+ });
221
+
222
+ describe('get_classe_ges_dpe', () => {
223
+ test('délègue le classement DPE et GES aux fonctions de conso avec les bons arguments', () => {
224
+ vi.mocked(classe_bilan_dpe).mockReturnValue('D');
225
+ vi.mocked(classe_emission_ges).mockReturnValue('C');
226
+
227
+ const dpe = makeDpe();
228
+ dpe.logement.sortie = {
229
+ ep_conso: { ep_conso_5_usages_m2: 125 },
230
+ emission_ges: { emission_ges_5_usages_m2: 30 }
231
+ };
232
+ const res = get_classe_ges_dpe(dpe);
233
+
234
+ expect(res).toEqual({ dpeClass: 'D', gesClass: 'C' });
235
+ // Sh = surface_habitable_logement pour une maison
236
+ expect(classe_bilan_dpe).toHaveBeenCalledWith(125, 'zc1', 'ca1', 80);
237
+ expect(classe_emission_ges).toHaveBeenCalledWith(30, 'zc1', 'ca1', 80);
238
+ });
239
+
240
+ test("utilise la surface de l'immeuble pour un modèle immeuble", () => {
241
+ enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' };
242
+ vi.mocked(classe_bilan_dpe).mockReturnValue('E');
243
+
244
+ const dpe = makeDpe();
245
+ dpe.logement.sortie = {
246
+ ep_conso: { ep_conso_5_usages_m2: 125 },
247
+ emission_ges: { emission_ges_5_usages_m2: 30 }
248
+ };
249
+ get_classe_ges_dpe(dpe);
250
+
251
+ // Sh = surface_habitable_immeuble
252
+ expect(classe_bilan_dpe).toHaveBeenCalledWith(125, 'zc1', 'ca1', 200);
253
+ });
254
+
255
+ test('méthode application inconnue : th null, surface indéterminée', () => {
256
+ // calc_th ne reconnaît ni maison ni appartement ni immeuble => null
257
+ enums.methode_application_dpe_log = { 1: 'méthode farfelue' };
258
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
259
+ vi.mocked(classe_bilan_dpe).mockReturnValue('G');
260
+ vi.mocked(classe_emission_ges).mockReturnValue('G');
261
+
262
+ const dpe = makeDpe();
263
+ dpe.logement.sortie = {
264
+ ep_conso: { ep_conso_5_usages_m2: 400 },
265
+ emission_ges: { emission_ges_5_usages_m2: 80 }
266
+ };
267
+ get_classe_ges_dpe(dpe);
268
+
269
+ // th null => Sh reste indéfini
270
+ expect(classe_bilan_dpe).toHaveBeenCalledWith(400, 'zc1', 'ca1', undefined);
271
+ expect(errSpy).toHaveBeenCalled();
272
+ errSpy.mockRestore();
273
+ });
274
+ });
275
+
276
+ describe('calcul_3cl - garde-fous précoces', () => {
277
+ test('retourne null pour un modèle de DPE non supporté', () => {
278
+ const dpe = { administratif: { enum_modele_dpe_id: '99' } };
279
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
280
+
281
+ const res = calcul_3cl(dpe, { sanitize: false });
282
+
283
+ expect(res).toBeNull();
284
+ errSpy.mockRestore();
285
+ });
286
+
287
+ test("sanitise l'entrée par défaut avant de vérifier le modèle", () => {
288
+ const dpe = { administratif: { enum_modele_dpe_id: '99' } };
289
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
290
+
291
+ const res = calcul_3cl(dpe); // pas d'options => sanitize par défaut
292
+
293
+ expect(sanitizeExecute).toHaveBeenCalledWith(dpe);
294
+ expect(res).toBeNull();
295
+ errSpy.mockRestore();
296
+ });
297
+
298
+ test("sans sanitisation, applique useEnumAsString si l'option globale est active", () => {
299
+ useEnumAsStringFlag = true;
300
+ const dpe = { administratif: { enum_modele_dpe_id: '99' } };
301
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
302
+
303
+ calcul_3cl(dpe, { sanitize: false });
304
+
305
+ expect(sanitizeExecute).not.toHaveBeenCalled();
306
+ expect(useEnumAsString).toHaveBeenCalledWith(dpe);
307
+ errSpy.mockRestore();
308
+ });
309
+
310
+ test('retourne null si enveloppe absente', () => {
311
+ const dpe = makeDpe();
312
+ delete dpe.logement.enveloppe;
313
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
314
+
315
+ expect(calcul_3cl(dpe, { sanitize: false })).toBeNull();
316
+ warnSpy.mockRestore();
317
+ });
318
+
319
+ test('retourne null si mur_collection absent', () => {
320
+ const dpe = makeDpe();
321
+ dpe.logement.enveloppe.mur_collection = undefined;
322
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
323
+
324
+ expect(calcul_3cl(dpe, { sanitize: false })).toBeNull();
325
+ warnSpy.mockRestore();
326
+ });
327
+
328
+ test('plancher_haut vide + non-vidable => null', () => {
329
+ vi.mocked(collectionCanBeEmpty).mockReturnValue(false);
330
+ const dpe = makeDpe();
331
+ dpe.logement.enveloppe.plancher_haut_collection = { plancher_haut: [] };
332
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
333
+
334
+ expect(calcul_3cl(dpe, { sanitize: false })).toBeNull();
335
+ expect(collectionCanBeEmpty).toHaveBeenCalledWith(dpe.logement, 'plancher_haut', 3);
336
+ errSpy.mockRestore();
337
+ });
338
+
339
+ test('plancher_bas vide + non-vidable => null', () => {
340
+ vi.mocked(collectionCanBeEmpty).mockReturnValue(false);
341
+ const dpe = makeDpe();
342
+ // plancher_haut non vide pour atteindre le contrôle plancher_bas
343
+ dpe.logement.enveloppe.plancher_bas_collection = { plancher_bas: [] };
344
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
345
+
346
+ expect(calcul_3cl(dpe, { sanitize: false })).toBeNull();
347
+ expect(collectionCanBeEmpty).toHaveBeenCalledWith(dpe.logement, 'plancher_bas', 1);
348
+ errSpy.mockRestore();
349
+ });
350
+ });
351
+
352
+ describe('calcul_3cl_xml - garde sur le modèle de DPE', () => {
353
+ test('parse le XML puis retourne null pour un modèle non supporté', () => {
354
+ const dpe = { administratif: { enum_modele_dpe_id: '99' } };
355
+ vi.mocked(xmlParser.parse).mockReturnValue({ dpe });
356
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
357
+
358
+ const res = calcul_3cl_xml('<dpe/>', { sanitize: false });
359
+
360
+ expect(xmlParser.parse).toHaveBeenCalledWith('<dpe/>');
361
+ expect(res).toBeNull();
362
+ errSpy.mockRestore();
363
+ });
364
+ });
365
+
366
+ describe('calcul_3cl - run intégral (maison)', () => {
367
+ test('orchestre les sous-modules et agrège la sortie', () => {
368
+ const dpe = makeDpe();
369
+
370
+ const res = calcul_3cl(dpe, { sanitize: false });
371
+
372
+ // Le DPE (muté) est retourné.
373
+ expect(res).toBe(dpe);
374
+
375
+ // Surfaces : maison => Sh = surface_habitable_logement ; map '1' hors liste
376
+ // immeuble => ShChauffageAndEcs = Sh.
377
+ expect(calc_deperdition).toHaveBeenCalledWith(
378
+ dpe.logement.caracteristique_generale,
379
+ 'zc1',
380
+ 'maison',
381
+ false,
382
+ dpe,
383
+ 80
384
+ );
385
+
386
+ // Références ajoutées à l'enveloppe.
387
+ expect(add_references).toHaveBeenCalledWith(dpe.logement.enveloppe);
388
+
389
+ // Version moteur injectée.
390
+ expect(dpe.administratif.diagnostiqueur).toEqual({
391
+ version_moteur_calcul: 'Open3CL OPEN3CL_VERSION'
392
+ });
393
+
394
+ // besoin_ch fusionné, besoin_ch_mois retiré de apport_et_besoin.
395
+ expect(dpe.logement.sortie.apport_et_besoin.besoin_ch).toBe(50);
396
+ expect(dpe.logement.sortie.apport_et_besoin.besoin_ch_mois).toBeUndefined();
397
+
398
+ // Taux de charge appelé avant la 2ᵉ passe chauffage.
399
+ expect(tauxChargeForGenerator).toHaveBeenCalledWith(
400
+ dpe.logement.installation_chauffage_collection.installation_chauffage,
401
+ 300,
402
+ 'ca1',
403
+ 'zc1',
404
+ 'maison'
405
+ );
406
+
407
+ // calc_chauffage appelé 2 fois par installation (1ʳᵉ et 2ᵉ passe).
408
+ expect(calc_chauffage).toHaveBeenCalledTimes(2);
409
+ // Première passe : besoins à 0.
410
+ expect(calc_chauffage.mock.calls[0][6]).toBe(0);
411
+ expect(calc_chauffage.mock.calls[0][7]).toBe(0);
412
+ // Deuxième passe : besoins réels + besoin_ch_mois.
413
+ expect(calc_chauffage.mock.calls[1][6]).toBe(50);
414
+ expect(calc_chauffage.mock.calls[1][7]).toBe(60);
415
+ expect(calc_chauffage.mock.calls[1][13]).toEqual({ m1: 10, m2: 20 });
416
+ // ilpa = '0' (materiaux anciens = 0).
417
+ expect(calc_chauffage.mock.calls[0][12]).toBe('0');
418
+
419
+ // 3 calculs de conso (coef_ep, coef_ep_1_7, coef_ep_2_3).
420
+ expect(calc_conso).toHaveBeenCalledTimes(3);
421
+ expect(calc_conso.mock.calls[0][10]).toEqual({ tag: 'coef_ep' });
422
+ expect(calc_conso.mock.calls[1][10]).toEqual({ tag: 'coef_ep_1_7' });
423
+ expect(calc_conso.mock.calls[2][10]).toEqual({ tag: 'coef_ep_2_3' });
424
+ // prorata ECS / chauffage neutres pour une maison.
425
+ expect(calc_conso.mock.calls[0][7]).toBe(1);
426
+ expect(calc_conso.mock.calls[0][8]).toBe(1);
427
+
428
+ // 3 calculs de production ENR ; le 1ᵉʳ sans coefficient.
429
+ expect(calculateEnr).toHaveBeenCalledTimes(3);
430
+ expect(calculateEnr.mock.calls[0]).toEqual([
431
+ dpe.logement.production_elec_enr,
432
+ consoResult,
433
+ 80,
434
+ 'maison',
435
+ 'zc1'
436
+ ]);
437
+ expect(calculateEnr.mock.calls[1][5]).toBe(1.7);
438
+ expect(calculateEnr.mock.calls[2][5]).toBe(2.3);
439
+
440
+ // Sorties agrégées.
441
+ expect(dpe.logement.sortie.deperdition).toEqual({ deperdition_enveloppe: 300 });
442
+ expect(dpe.logement.sortie.confort_ete).toBe('confort');
443
+ expect(dpe.logement.sortie.qualite_isolation).toBe('qualite');
444
+ expect(dpe.logement.sortie.production_electricite).toEqual({ pv: 1 });
445
+ expect(calc_confort_ete).toHaveBeenCalledWith(
446
+ '3',
447
+ dpe.logement.enveloppe.baie_vitree_collection.baie_vitree,
448
+ [dpe.logement.enveloppe.plancher_haut_collection.plancher_haut[0]]
449
+ );
450
+
451
+ // ep_conso enrichi des coefficients 1.7 / 2.3 et de la projection 2027.
452
+ expect(dpe.logement.sortie.ep_conso.coeff_2_3_classe_bilan_dpe).toBe('D');
453
+ expect(dpe.logement.sortie.ep_conso.coeff_1_7_ep_conso_5_usages).toBe(100);
454
+ expect(dpe.logement.sortie.ep_conso.classe_bilan_dpe_2027).toBe('D');
455
+ expect(dpe.logement.sortie.ep_conso.ep_conso_5_usages_2027).toBe(100);
456
+
457
+ // ECS individuelle unique : calc_ecs appelé une fois, sans division du besoin.
458
+ expect(calc_ecs).toHaveBeenCalledTimes(1);
459
+ expect(calc_ecs.mock.calls[0][2]).toBe(100);
460
+ expect(calc_ecs.mock.calls[0][11]).toBe(false);
461
+
462
+ // Auxiliaires de distribution ECS calculés pour l'installation (maison => map hors
463
+ // liste des DPE générés à partir des données d'immeuble).
464
+ const ecs = dpe.logement.installation_ecs_collection.installation_ecs[0];
465
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(1);
466
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledWith(
467
+ ecs,
468
+ ecs.donnee_entree,
469
+ ecs.donnee_intermediaire,
470
+ 80, // Sh (maison)
471
+ 200, // surface_habitable_immeuble
472
+ 'ca1',
473
+ 'zc1',
474
+ 2, // nadeq
475
+ 3 // nombre_niveau_immeuble
476
+ );
477
+
478
+ // Climatisation présente : calc_clim appelé.
479
+ expect(calc_clim).toHaveBeenCalledTimes(1);
480
+ });
481
+
482
+ test('bâtiment matériaux anciens => ilpa = 1 ; incohérence inertie => erreur loggée', () => {
483
+ const dpe = makeDpe();
484
+ dpe.logement.meteo.batiment_materiaux_anciens = 1;
485
+ vi.mocked(calculateInertie).mockReturnValue({ enum_classe_inertie_id: '2' }); // != '3'
486
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
487
+
488
+ calcul_3cl(dpe, { sanitize: false });
489
+
490
+ expect(calc_chauffage.mock.calls[0][12]).toBe('1');
491
+ expect(errSpy).toHaveBeenCalled();
492
+ errSpy.mockRestore();
493
+ });
494
+
495
+ test('collections ECS/clim absentes et nombre_appartement absent => valeurs par défaut', () => {
496
+ const dpe = makeDpe();
497
+ dpe.logement.installation_ecs_collection.installation_ecs = undefined;
498
+ dpe.logement.climatisation_collection.climatisation = undefined;
499
+ dpe.logement.caracteristique_generale.nombre_appartement = undefined;
500
+
501
+ calcul_3cl(dpe, { sanitize: false });
502
+
503
+ // Nb_lgt par défaut = 1 (6ᵉ argument d'apport_et_besoin).
504
+ expect(calc_apport_et_besoin.mock.calls[0][5]).toBe(1);
505
+ // Pas d'ECS ni de clim à traiter.
506
+ expect(calc_ecs).not.toHaveBeenCalled();
507
+ expect(conso_aux_distribution_ecs).not.toHaveBeenCalled();
508
+ expect(calc_clim).not.toHaveBeenCalled();
509
+ });
510
+ });
511
+
512
+ describe('calcul_3cl - collections vidables (poursuite du calcul)', () => {
513
+ test('plancher_haut vide mais vidable : réinitialisé et calcul poursuivi', () => {
514
+ const dpe = makeDpe();
515
+ dpe.logement.enveloppe.plancher_haut_collection = { plancher_haut: [] };
516
+
517
+ const res = calcul_3cl(dpe, { sanitize: false });
518
+
519
+ expect(res).toBe(dpe);
520
+ expect(collectionCanBeEmpty).toHaveBeenCalledWith(dpe.logement, 'plancher_haut', 3);
521
+ expect(dpe.logement.enveloppe.plancher_haut_collection).toEqual({ plancher_haut: [] });
522
+ // ph_list vide passé au confort d'été.
523
+ expect(calc_confort_ete.mock.calls[0][2]).toEqual([]);
524
+ });
525
+
526
+ test('plancher_bas vide mais vidable : réinitialisé et calcul poursuivi', () => {
527
+ const dpe = makeDpe();
528
+ dpe.logement.enveloppe.plancher_bas_collection = { plancher_bas: [] };
529
+
530
+ const res = calcul_3cl(dpe, { sanitize: false });
531
+
532
+ expect(res).toBe(dpe);
533
+ expect(collectionCanBeEmpty).toHaveBeenCalledWith(dpe.logement, 'plancher_bas', 1);
534
+ expect(dpe.logement.enveloppe.plancher_bas_collection).toEqual({ plancher_bas: [] });
535
+ });
536
+ });
537
+
538
+ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
539
+ test('immeuble : Sh = surface immeuble ; ECS collective individualisée non divisée', () => {
540
+ enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' };
541
+ const dpe = makeDpe();
542
+ // Deux systèmes ECS individuels en immeuble => pas de division du besoin.
543
+ dpe.logement.installation_ecs_collection.installation_ecs = [
544
+ {
545
+ donnee_entree: { enum_type_installation_id: '1' },
546
+ generateur_ecs_collection: { generateur_ecs: [] }
547
+ },
548
+ {
549
+ donnee_entree: { enum_type_installation_id: '1' },
550
+ generateur_ecs_collection: { generateur_ecs: [] }
551
+ }
552
+ ];
553
+
554
+ calcul_3cl(dpe, { sanitize: false });
555
+
556
+ // th immeuble => Sh = surface_habitable_immeuble.
557
+ expect(calc_deperdition.mock.calls[0][5]).toBe(200);
558
+ expect(calc_deperdition.mock.calls[0][2]).toBe('immeuble');
559
+ // isImmeubleSystemEcsIndividuels = true, besoin non divisé.
560
+ expect(calc_ecs.mock.calls[0][11]).toBe(true);
561
+ expect(calc_ecs.mock.calls[0][2]).toBe(100);
562
+ // Auxiliaires ECS calculés par installation, avec Sh = surface immeuble.
563
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(2);
564
+ expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(200);
565
+ });
566
+
567
+ test('immeuble : systèmes ECS non tous individuels => besoin divisé par deux', () => {
568
+ enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' };
569
+ const dpe = makeDpe();
570
+ dpe.logement.installation_ecs_collection.installation_ecs = [
571
+ {
572
+ donnee_entree: { enum_type_installation_id: '1' },
573
+ generateur_ecs_collection: { generateur_ecs: [] }
574
+ },
575
+ {
576
+ donnee_entree: { enum_type_installation_id: '2' },
577
+ generateur_ecs_collection: { generateur_ecs: [] }
578
+ }
579
+ ];
580
+
581
+ calcul_3cl(dpe, { sanitize: false });
582
+
583
+ expect(calc_ecs.mock.calls[0][11]).toBe(false);
584
+ expect(calc_ecs.mock.calls[0][2]).toBe(50); // 100 / 2
585
+ });
586
+
587
+ test('maison avec deux systèmes ECS => besoin divisé par deux', () => {
588
+ const dpe = makeDpe();
589
+ dpe.logement.installation_ecs_collection.installation_ecs = [
590
+ {
591
+ donnee_entree: { enum_type_installation_id: '1' },
592
+ generateur_ecs_collection: { generateur_ecs: [] }
593
+ },
594
+ {
595
+ donnee_entree: { enum_type_installation_id: '1' },
596
+ generateur_ecs_collection: { generateur_ecs: [] }
597
+ }
598
+ ];
599
+
600
+ calcul_3cl(dpe, { sanitize: false });
601
+
602
+ // th != immeuble => isImmeubleSystemEcsIndividuels false, besoin divisé.
603
+ expect(calc_ecs.mock.calls[0][11]).toBe(false);
604
+ expect(calc_ecs.mock.calls[0][2]).toBe(50);
605
+ });
606
+
607
+ test('appartement issu immeuble (map 15) : Sh immeuble pour chauffage/ECS et prorata surfacique', () => {
608
+ enums.methode_application_dpe_log = { 15: 'dpe appartement collectif' };
609
+ const dpe = makeDpe();
610
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = '15';
611
+
612
+ calcul_3cl(dpe, { sanitize: false });
613
+
614
+ // ShChauffageAndEcs = surface immeuble (200) pour la déperdition.
615
+ expect(calc_deperdition.mock.calls[0][5]).toBe(200);
616
+ // conso : Sh = surface logement (80) et prorata = 80/200 = 0.4.
617
+ expect(calc_conso.mock.calls[0][0]).toBe(80);
618
+ expect(calc_conso.mock.calls[0][7]).toBeCloseTo(0.4, 9); // prorataECS
619
+ expect(calc_conso.mock.calls[0][8]).toBeCloseTo(0.4, 9); // prorataChauffage
620
+ // Auxiliaires ECS : Sh = surface logement, ramenée à l'immeuble dans le calcul.
621
+ expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(80);
622
+ expect(conso_aux_distribution_ecs.mock.calls[0][4]).toBe(200);
623
+ });
624
+ });
625
+
626
+ describe("calcul_3cl - auxiliaires de distribution d'ECS", () => {
627
+ // Méthodes d'application « appartement généré à partir des données DPE immeuble » :
628
+ // les auxiliaires de distribution d'ECS ne sont pas calculés pour ces DPE.
629
+ const MAP_IDS_GENERES_IMMEUBLE = ['10', '11', '12', '13', '33', '34', '38', '39', '40'];
630
+
631
+ test.each(MAP_IDS_GENERES_IMMEUBLE)(
632
+ "DPE appartement généré à partir des données d'immeuble (map %s) : auxiliaires non calculés",
633
+ (mapId) => {
634
+ enums.methode_application_dpe_log = {
635
+ [mapId]: 'dpe appartement généré à partir des données dpe immeuble chauffage collectif'
636
+ };
637
+ const dpe = makeDpe();
638
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = mapId;
639
+
640
+ calcul_3cl(dpe, { sanitize: false });
641
+
642
+ // L'installation est bien traitée par ailleurs, seul le calcul des auxiliaires est ignoré.
643
+ expect(calc_ecs).toHaveBeenCalledTimes(1);
644
+ expect(conso_aux_distribution_ecs).not.toHaveBeenCalled();
645
+ }
646
+ );
647
+
648
+ test('appartement non généré depuis un immeuble (map 15) : auxiliaires calculés', () => {
649
+ enums.methode_application_dpe_log = { 15: 'dpe appartement collectif' };
650
+ const dpe = makeDpe();
651
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = '15';
652
+
653
+ calcul_3cl(dpe, { sanitize: false });
654
+
655
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(1);
656
+ });
657
+
658
+ test('une installation ECS par appel : chaque installation est traitée', () => {
659
+ const dpe = makeDpe();
660
+ const installations = [
661
+ {
662
+ donnee_entree: { enum_type_installation_id: '1' },
663
+ donnee_intermediaire: { tag: 'ecs1' },
664
+ generateur_ecs_collection: { generateur_ecs: [] }
665
+ },
666
+ {
667
+ donnee_entree: { enum_type_installation_id: '2' },
668
+ donnee_intermediaire: { tag: 'ecs2' },
669
+ generateur_ecs_collection: { generateur_ecs: [] }
670
+ }
671
+ ];
672
+ dpe.logement.installation_ecs_collection.installation_ecs = installations;
673
+
674
+ calcul_3cl(dpe, { sanitize: false });
675
+
676
+ expect(conso_aux_distribution_ecs).toHaveBeenCalledTimes(2);
677
+ installations.forEach((ecs, i) => {
678
+ expect(conso_aux_distribution_ecs.mock.calls[i][0]).toBe(ecs);
679
+ expect(conso_aux_distribution_ecs.mock.calls[i][1]).toBe(ecs.donnee_entree);
680
+ expect(conso_aux_distribution_ecs.mock.calls[i][2]).toBe(ecs.donnee_intermediaire);
681
+ });
682
+ });
683
+
684
+ test("nombre de niveaux de l'immeuble absent : transmis tel quel", () => {
685
+ const dpe = makeDpe();
686
+ dpe.logement.caracteristique_generale.nombre_niveau_immeuble = undefined;
687
+
688
+ calcul_3cl(dpe, { sanitize: false });
689
+
690
+ expect(conso_aux_distribution_ecs.mock.calls[0][8]).toBeUndefined();
691
+ });
692
+ });
693
+
694
+ describe('calcul_3cl - présence de joint des baies vitrées', () => {
695
+ function dpeAvecBaieSansJoint() {
696
+ const dpe = makeDpe();
697
+ dpe.logement.enveloppe.baie_vitree_collection = {
698
+ baie_vitree: [
699
+ {
700
+ donnee_entree: {
701
+ description: 'baie 1',
702
+ enum_orientation_id: '1',
703
+ enum_type_vitrage_id: '1',
704
+ enum_type_baie_id: '1',
705
+ enum_type_materiaux_menuiserie_id: '1'
706
+ // presence_joint absent
707
+ }
708
+ }
709
+ ]
710
+ };
711
+ return dpe;
712
+ }
713
+
714
+ test('fiche technique "Oui" => présence de joint = 1', () => {
715
+ vi.mocked(getFicheTechnique).mockImplementation((d, id, key) =>
716
+ id === '4' && key === 'joint' ? { valeur: 'Oui' } : undefined
717
+ );
718
+ const dpe = dpeAvecBaieSansJoint();
719
+
720
+ calcul_3cl(dpe, { sanitize: false });
721
+
722
+ expect(
723
+ dpe.logement.enveloppe.baie_vitree_collection.baie_vitree[0].donnee_entree.presence_joint
724
+ ).toBe(1);
725
+ });
726
+
727
+ test('fiche technique "Non" => présence de joint = 0', () => {
728
+ vi.mocked(getFicheTechnique).mockImplementation((d, id, key) =>
729
+ id === '4' && key === 'joint' ? { valeur: 'Non' } : undefined
730
+ );
731
+ const dpe = dpeAvecBaieSansJoint();
732
+
733
+ calcul_3cl(dpe, { sanitize: false });
734
+
735
+ expect(
736
+ dpe.logement.enveloppe.baie_vitree_collection.baie_vitree[0].donnee_entree.presence_joint
737
+ ).toBe(0);
738
+ });
739
+
740
+ test('fiche technique de valeur inconnue => avertissement, présence de joint inchangée', () => {
741
+ vi.mocked(getFicheTechnique).mockImplementation((d, id, key) =>
742
+ id === '4' && key === 'joint' ? { valeur: 'Peut-être' } : undefined
743
+ );
744
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
745
+ const dpe = dpeAvecBaieSansJoint();
746
+
747
+ calcul_3cl(dpe, { sanitize: false });
748
+
749
+ expect(
750
+ dpe.logement.enveloppe.baie_vitree_collection.baie_vitree[0].donnee_entree.presence_joint
751
+ ).toBeUndefined();
752
+ expect(warnSpy).toHaveBeenCalled();
753
+ warnSpy.mockRestore();
754
+ });
755
+
756
+ test('absence de fiche technique => présence de joint reste indéterminée', () => {
757
+ // getFicheTechnique renvoie undefined (défaut)
758
+ const dpe = dpeAvecBaieSansJoint();
759
+
760
+ calcul_3cl(dpe, { sanitize: false });
761
+
762
+ expect(
763
+ dpe.logement.enveloppe.baie_vitree_collection.baie_vitree[0].donnee_entree.presence_joint
764
+ ).toBeUndefined();
765
+ });
766
+ });
767
+
768
+ describe('calcul_3cl - réalignements ECS (bug_for_bug_compat)', () => {
769
+ test('réaligne position_volume_chauffe et type de générateur ECS depuis le mixte de chauffage', () => {
770
+ bugForBugCompat = true;
771
+ enums.type_generateur_ch = { CH1: 'chaudiere gaz' };
772
+ enums.type_generateur_ecs = { ECS9: 'chaudiere gaz' };
773
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
774
+
775
+ // La fiche technique '8' impose "hors volume habitable" => position 0.
776
+ vi.mocked(getFicheTechnique).mockImplementation((d, id) =>
777
+ id === '8' ? { valeur: 'hors volume habitable', description: 'ft8' } : undefined
778
+ );
779
+ vi.mocked(containsAnySubstring).mockReturnValue(true); // => pvc = 0
780
+
781
+ const dpe = makeDpe();
782
+ dpe.logement.installation_chauffage_collection.installation_chauffage = [
783
+ {
784
+ donnee_entree: {},
785
+ generateur_chauffage_collection: {
786
+ generateur_chauffage: [
787
+ {
788
+ donnee_entree: {
789
+ reference_generateur_mixte: 'MX1',
790
+ enum_type_generateur_ch_id: 'CH1'
791
+ }
792
+ }
793
+ ]
794
+ }
795
+ }
796
+ ];
797
+ const generateur = genEcs({
798
+ description: 'gen1',
799
+ position_volume_chauffe: 1,
800
+ enum_usage_generateur_id: '3',
801
+ reference_generateur_mixte: 'MX1',
802
+ enum_type_generateur_ecs_id: 'OLD'
803
+ });
804
+ dpe.logement.installation_ecs_collection.installation_ecs = [
805
+ {
806
+ donnee_entree: { enum_type_installation_id: '1' },
807
+ generateur_ecs_collection: { generateur_ecs: [generateur] }
808
+ }
809
+ ];
810
+
811
+ calcul_3cl(dpe, { sanitize: false });
812
+
813
+ // position réalignée à 0 et type ECS aligné sur le libellé du générateur mixte.
814
+ expect(generateur.donnee_entree.position_volume_chauffe).toBe(0);
815
+ expect(generateur.donnee_entree.enum_type_generateur_ecs_id).toBe('ECS9');
816
+ expect(errSpy).toHaveBeenCalled();
817
+ errSpy.mockRestore();
818
+ });
819
+
820
+ test('couvre les branches sans réalignement (fiche absente, mixte introuvable, libellés non résolus)', () => {
821
+ bugForBugCompat = true;
822
+ enums.type_generateur_ch = { CH1: 'chaudiere gaz', CHX: 'labelX' };
823
+ enums.type_generateur_ecs = { SAME: 'labelX' };
824
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
825
+
826
+ // Fiche '8' uniquement pour la description 'withFiche' ; valeur non "hors volume".
827
+ vi.mocked(getFicheTechnique).mockImplementation((d, id, key, arr) => {
828
+ if (id === '8' && arr?.[0] === 'withFiche') return { valeur: 'oui', description: 'ft8' };
829
+ return undefined;
830
+ });
831
+ // containsAnySubstring false => pvc = 1 (égal à la position courante => pas de réalignement).
832
+ vi.mocked(containsAnySubstring).mockReturnValue(false);
833
+
834
+ const dpe = makeDpe();
835
+ dpe.logement.installation_chauffage_collection.installation_chauffage = [
836
+ {
837
+ donnee_entree: {},
838
+ generateur_chauffage_collection: {
839
+ generateur_chauffage: [
840
+ {
841
+ donnee_entree: {
842
+ reference_generateur_mixte: 'MX_labelnull',
843
+ enum_type_generateur_ch_id: 'INCONNU'
844
+ }
845
+ },
846
+ {
847
+ donnee_entree: {
848
+ reference_generateur_mixte: 'MX_chgaz',
849
+ enum_type_generateur_ch_id: 'CH1'
850
+ }
851
+ },
852
+ {
853
+ donnee_entree: {
854
+ reference_generateur_mixte: 'MX_X',
855
+ enum_type_generateur_ch_id: 'CHX'
856
+ }
857
+ }
858
+ ]
859
+ }
860
+ }
861
+ ];
862
+
863
+ const generateurs = [
864
+ // Fiche présente, pvc calculé (=1) égal à la position => pas de réalignement ;
865
+ // usage '3' mais mixte introuvable (ref inconnue).
866
+ genEcs({
867
+ description: 'withFiche',
868
+ position_volume_chauffe: 1,
869
+ enum_usage_generateur_id: '3',
870
+ reference_generateur_mixte: 'AUCUN'
871
+ }),
872
+ // Pas de description (fiche ignorée) ; usage != 3.
873
+ genEcs({ enum_usage_generateur_id: '1' }),
874
+ // Description sans fiche ; usage '3' mais pas de référence mixte.
875
+ genEcs({ description: 'noFiche', enum_usage_generateur_id: '3' }),
876
+ // Mixte trouvé mais libellé de générateur de chauffage non résolu.
877
+ genEcs({
878
+ enum_usage_generateur_id: '3',
879
+ reference_generateur_mixte: 'MX_labelnull',
880
+ enum_type_generateur_ecs_id: 'X0'
881
+ }),
882
+ // Mixte + libellé résolus mais aucun type ECS correspondant => pas de réalignement.
883
+ genEcs({
884
+ enum_usage_generateur_id: '3',
885
+ reference_generateur_mixte: 'MX_chgaz',
886
+ enum_type_generateur_ecs_id: 'X1'
887
+ }),
888
+ // Mixte + libellé + type ECS correspondant identique à l'actuel => pas de réalignement.
889
+ genEcs({
890
+ enum_usage_generateur_id: '3',
891
+ reference_generateur_mixte: 'MX_X',
892
+ enum_type_generateur_ecs_id: 'SAME'
893
+ })
894
+ ];
895
+ dpe.logement.installation_ecs_collection.installation_ecs = [
896
+ {
897
+ donnee_entree: { enum_type_installation_id: '1' },
898
+ generateur_ecs_collection: { generateur_ecs: generateurs }
899
+ }
900
+ ];
901
+
902
+ calcul_3cl(dpe, { sanitize: false });
903
+
904
+ // Aucun réalignement effectif.
905
+ expect(generateurs[0].donnee_entree.position_volume_chauffe).toBe(1);
906
+ expect(generateurs[3].donnee_entree.enum_type_generateur_ecs_id).toBe('X0');
907
+ expect(generateurs[4].donnee_entree.enum_type_generateur_ecs_id).toBe('X1');
908
+ expect(generateurs[5].donnee_entree.enum_type_generateur_ecs_id).toBe('SAME');
909
+ errSpy.mockRestore();
910
+ });
911
+ });
912
+
913
+ describe('get_conso_coeff_1_7_2027', () => {
914
+ test('projette la conso EP 2027 pour une maison', () => {
915
+ vi.mocked(classe_bilan_dpe).mockReturnValue('C');
916
+ const dpe = makeDpe();
917
+ dpe.logement.sortie = {
918
+ ep_conso: { ep_conso_5_usages: 10000 },
919
+ ef_conso: { conso_5_usages: 6000 }
920
+ };
921
+
922
+ const res = get_conso_coeff_1_7_2027(dpe);
923
+
924
+ // ep = (0.7/0.9) * (10000 - 6000) + 6000 = 9111.11...
925
+ // Valeur de référence de régression.
926
+ expect(res.ep_conso_5_usages_2027).toBeCloseTo(9111.111111111, 6);
927
+ // m2 = floor(9111.11 / 80) = 113
928
+ expect(res.ep_conso_5_usages_m2_2027).toBe(113);
929
+ expect(res.classe_bilan_dpe_2027).toBe('C');
930
+ expect(classe_bilan_dpe).toHaveBeenCalledWith(113, 'zc1', 'ca1', 80);
931
+ });
932
+
933
+ test("utilise la surface d'immeuble pour un modèle immeuble", () => {
934
+ enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' };
935
+ vi.mocked(classe_bilan_dpe).mockReturnValue('E');
936
+ const dpe = makeDpe();
937
+ dpe.logement.sortie = {
938
+ ep_conso: { ep_conso_5_usages: 30000 },
939
+ ef_conso: { conso_5_usages: 12000 }
940
+ };
941
+
942
+ const res = get_conso_coeff_1_7_2027(dpe);
943
+
944
+ // Sh = 200 => m2 = floor(ep / 200).
945
+ const ep = (0.7 / 0.9) * (30000 - 12000) + 12000;
946
+ expect(res.ep_conso_5_usages_2027).toBeCloseTo(ep, 6);
947
+ expect(classe_bilan_dpe).toHaveBeenCalledWith(Math.floor(ep / 200), 'zc1', 'ca1', 200);
948
+ });
949
+ });