@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
@@ -0,0 +1,890 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `calc_pont_thermique` :
5
+ * - `enums` : mapping minimal période de construction ;
6
+ * - `isNil` (lodash-es) : implémentation déterministe ;
7
+ * - `tv` : accès à la table `pont_thermique`, on contrôle la ligne retournée ;
8
+ * - `requestInput` : passe-plat vers les données d'entrée ;
9
+ * - `compareReferences` : comparaison stricte des références (pas de nettoyage réel) ;
10
+ * - `bug_for_bug_compat` : désactivé pour isoler le comportement nominal.
11
+ */
12
+ vi.mock('./enums.js', () => ({
13
+ default: {
14
+ periode_construction: { 1: 'avant 1948', 5: '1989-2000' }
15
+ }
16
+ }));
17
+
18
+ vi.mock('lodash-es', () => ({
19
+ isNil: (value) => value === null || value === undefined
20
+ }));
21
+
22
+ /**
23
+ * `bug_for_bug_compat` est exposé via un accesseur adossé à un état hoisté, afin de pouvoir
24
+ * l'activer dans certains tests (reproduction de comportements bugués des DPE réels) tout en
25
+ * restant désactivé par défaut.
26
+ */
27
+ const state = vi.hoisted(() => ({ bug: false }));
28
+
29
+ vi.mock('./utils.js', () => ({
30
+ tv: vi.fn(),
31
+ requestInput: (de, du, field) => de[field],
32
+ compareReferences: (a, b) => a === b,
33
+ get bug_for_bug_compat() {
34
+ return state.bug;
35
+ }
36
+ }));
37
+
38
+ const { default: calc_pont_thermique } = await import('./3.4_pont_thermique.js');
39
+ const { tv } = await import('./utils.js');
40
+
41
+ /** Enveloppe minimale avec collections contrôlées. */
42
+ function logement({ mur = [], pb = [], ph = [], bv = [], porte = [] } = {}) {
43
+ return {
44
+ enveloppe: {
45
+ mur_collection: { mur },
46
+ plancher_bas_collection: { plancher_bas: pb },
47
+ plancher_haut_collection: { plancher_haut: ph },
48
+ baie_vitree_collection: { baie_vitree: bv },
49
+ porte_collection: { porte }
50
+ }
51
+ };
52
+ }
53
+
54
+ /** Pont thermique en méthode de saisie forfaitaire (méthode 1). */
55
+ function pontThermique(de = {}, k = 0.9) {
56
+ return {
57
+ donnee_entree: {
58
+ enum_methode_saisie_pont_thermique_id: '1',
59
+ enum_type_liaison_id: '12',
60
+ ...de
61
+ },
62
+ donnee_intermediaire: { k }
63
+ };
64
+ }
65
+
66
+ let errorSpy;
67
+ let warnSpy;
68
+
69
+ beforeEach(() => {
70
+ tv.mockReset();
71
+ state.bug = false;
72
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
73
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
74
+ });
75
+
76
+ afterEach(() => {
77
+ errorSpy.mockRestore();
78
+ warnSpy.mockRestore();
79
+ });
80
+
81
+ /**
82
+ * 3.4 Calcul des déperditions par les ponts thermiques
83
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4
84
+ */
85
+ describe('calc_pont_thermique - méthode de saisie directe', () => {
86
+ test('k justifié saisi (méthode 2) : k pris directement', () => {
87
+ const pt = { donnee_entree: { enum_methode_saisie_pont_thermique_id: '2', k_saisi: 0.5 } };
88
+ calc_pont_thermique(pt, '1', logement());
89
+ expect(pt.donnee_intermediaire.k).toBe(0.5);
90
+ expect(tv).not.toHaveBeenCalled();
91
+ });
92
+
93
+ test('k saisi absent (méthode 3) : erreur et k non défini', () => {
94
+ const pt = { donnee_entree: { enum_methode_saisie_pont_thermique_id: '3' } };
95
+ calc_pont_thermique(pt, '1', logement());
96
+ expect(pt.donnee_intermediaire.k).toBeUndefined();
97
+ expect(errorSpy).toHaveBeenCalled();
98
+ });
99
+
100
+ test('méthode de saisie non reconnue : erreur et k non défini', () => {
101
+ const pt = { donnee_entree: { enum_methode_saisie_pont_thermique_id: '9' } };
102
+ calc_pont_thermique(pt, '1', logement());
103
+ expect(pt.donnee_intermediaire.k).toBeUndefined();
104
+ expect(errorSpy).toHaveBeenCalled();
105
+ });
106
+ });
107
+
108
+ describe('calc_pont_thermique - valeur forfaitaire (méthode 1)', () => {
109
+ test('liaison refend / mur sans mur associé : k lu dans la table avec isolation ITI par défaut', () => {
110
+ tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
111
+ const pt = {
112
+ donnee_entree: {
113
+ enum_methode_saisie_pont_thermique_id: '1',
114
+ enum_type_liaison_id: '12',
115
+ type_liaison: 'refend / mur',
116
+ reference_1: 'M1',
117
+ description: 'refend'
118
+ },
119
+ donnee_intermediaire: { k: 0.9 }
120
+ };
121
+
122
+ calc_pont_thermique(pt, '1', logement());
123
+
124
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
125
+ enum_type_liaison_id: '12',
126
+ isolation_mur: '^iti'
127
+ });
128
+ expect(pt.donnee_intermediaire.k).toBe(0.5);
129
+ expect(pt.donnee_entree.tv_pont_thermique_id).toBe(42);
130
+ });
131
+
132
+ /**
133
+ * Les ponts thermiques des murs de circulation avec k intermédiaire nul restent nuls.
134
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4 (locaux de circulation)
135
+ */
136
+ test('mur de circulation (adjacence 22) avec k intermédiaire nul : k = 0 sans accès table', () => {
137
+ const mur = [
138
+ { donnee_entree: { reference: 'M1', enum_type_adjacence_id: '22' }, donnee_utilisateur: {} }
139
+ ];
140
+ const pt = {
141
+ donnee_entree: {
142
+ enum_methode_saisie_pont_thermique_id: '1',
143
+ enum_type_liaison_id: '12',
144
+ type_liaison: 'refend / mur',
145
+ reference_1: 'M1',
146
+ description: 'refend'
147
+ },
148
+ donnee_intermediaire: { k: 0 }
149
+ };
150
+
151
+ calc_pont_thermique(pt, '1', logement({ mur }));
152
+
153
+ expect(pt.donnee_intermediaire.k).toBe(0);
154
+ expect(tv).not.toHaveBeenCalled();
155
+ });
156
+
157
+ /**
158
+ * 3.4.5 Menuiserie / mur : les ponts thermiques avec les parois en structure bois sont négligés.
159
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.5
160
+ */
161
+ test('liaison menuiserie / mur sur structure bois : k = 0', () => {
162
+ const mur = [
163
+ {
164
+ donnee_entree: {
165
+ reference: 'M1',
166
+ enum_type_adjacence_id: '1',
167
+ enum_materiaux_structure_mur_id: '5',
168
+ type_isolation: 'iti'
169
+ },
170
+ donnee_utilisateur: {}
171
+ }
172
+ ];
173
+ const pt = {
174
+ donnee_entree: {
175
+ enum_methode_saisie_pont_thermique_id: '1',
176
+ enum_type_liaison_id: '20',
177
+ type_liaison: 'menuiserie / mur',
178
+ reference_1: 'M1',
179
+ description: 'menuiserie'
180
+ },
181
+ donnee_intermediaire: { k: 0.3 }
182
+ };
183
+
184
+ calc_pont_thermique(pt, '1', logement({ mur }));
185
+
186
+ expect(pt.donnee_intermediaire.k).toBe(0);
187
+ expect(tv).not.toHaveBeenCalled();
188
+ });
189
+ });
190
+
191
+ /**
192
+ * defaultValue : valeur de repli lorsque la référence ou la paroi associée n'est pas retrouvée.
193
+ * Atteinte ici via une description non reconnue (ni "A / B" ni "A-B").
194
+ */
195
+ describe('defaultValue - valeur de repli sur description non reconnue', () => {
196
+ function ptRepli(k, tv_pont_thermique_id = 7, pourcentage) {
197
+ const de = {
198
+ enum_type_liaison_id: '12',
199
+ type_liaison: 'refend / mur',
200
+ description: 'descriptionNonReconnue',
201
+ tv_pont_thermique_id
202
+ };
203
+ if (pourcentage !== undefined) de.pourcentage_valeur_pont_thermique = pourcentage;
204
+ return pontThermique(de, k);
205
+ }
206
+
207
+ test('k intermédiaire nul : repli à 0 sans accès à la table', () => {
208
+ const pt = ptRepli(0);
209
+ calc_pont_thermique(pt, '1', logement());
210
+ expect(pt.donnee_intermediaire.k).toBe(0);
211
+ expect(tv).not.toHaveBeenCalled();
212
+ });
213
+
214
+ test('valeur tabulée cohérente avec la valeur intermédiaire : valeur tabulée conservée', () => {
215
+ tv.mockReturnValue({ k: '0.9' });
216
+ const pt = ptRepli(0.9);
217
+ calc_pont_thermique(pt, '1', logement());
218
+ expect(tv).toHaveBeenCalledWith('pont_thermique', { tv_pont_thermique_id: 7 });
219
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
220
+ });
221
+
222
+ test('valeur tabulée = moitié de la valeur intermédiaire : pas d’incohérence', () => {
223
+ // k = 0,9 et tabulé 1,8 : 0,9 === 1,8 / 2 => cohérent (facteur 0.5)
224
+ tv.mockReturnValue({ k: '1.8' });
225
+ const pt = ptRepli(0.9);
226
+ calc_pont_thermique(pt, '1', logement());
227
+ expect(pt.donnee_intermediaire.k).toBe('1.8');
228
+ expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Incohérence'));
229
+ });
230
+
231
+ test('valeur tabulée incohérente : valeur intermédiaire conservée et erreur émise', () => {
232
+ // k = 0,9, tabulé 0,45 : 0,9 !== 0,45 et 0,9 !== 0,225 => incohérence
233
+ tv.mockReturnValue({ k: '0.45' });
234
+ const pt = ptRepli(0.9);
235
+ calc_pont_thermique(pt, '1', logement());
236
+ expect(pt.donnee_intermediaire.k).toBe(0.9);
237
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Incohérence'));
238
+ });
239
+
240
+ test('aucune ligne tabulée trouvée : la valeur intermédiaire sert de repli', () => {
241
+ tv.mockReturnValue(null);
242
+ const pt = ptRepli(0.9);
243
+ calc_pont_thermique(pt, '1', logement());
244
+ expect(pt.donnee_intermediaire.k).toBe(0.9);
245
+ });
246
+ });
247
+
248
+ /**
249
+ * 3.4 Reconstruction des références manquantes à partir de la description.
250
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4
251
+ */
252
+ describe('calc_pont_thermique - reconstruction des références via la description', () => {
253
+ test('description "mur / plancher" : la référence du mur est retrouvée (liaison refend, pas de 2e paroi)', () => {
254
+ tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
255
+ const mur = [
256
+ {
257
+ donnee_entree: {
258
+ reference: 'M1',
259
+ description: 'Mur Nord donnant sur extérieur',
260
+ enum_type_adjacence_id: '1',
261
+ type_isolation: 'iti'
262
+ },
263
+ donnee_utilisateur: {}
264
+ }
265
+ ];
266
+ const pt = pontThermique({
267
+ type_liaison: 'refend / mur',
268
+ description: 'Mur Nord / Refend intérieur'
269
+ });
270
+
271
+ calc_pont_thermique(pt, '1', logement({ mur }));
272
+
273
+ expect(warnSpy).toHaveBeenCalled();
274
+ expect(pt.donnee_entree.reference_1).toBe('M1');
275
+ expect(pt.donnee_intermediaire.k).toBe(0.5);
276
+ });
277
+
278
+ test('description "mur-baie" (séparateur tiret), liaison menuiserie : les deux références sont retrouvées', () => {
279
+ tv.mockReturnValue({ k: '0.3', tv_pont_thermique_id: '50' });
280
+ const mur = [
281
+ {
282
+ donnee_entree: {
283
+ reference: 'M1',
284
+ description: 'Mur Est',
285
+ enum_type_adjacence_id: '1',
286
+ enum_materiaux_structure_mur_id: '1',
287
+ type_isolation: 'iti'
288
+ },
289
+ donnee_utilisateur: {}
290
+ }
291
+ ];
292
+ const bv = [
293
+ {
294
+ donnee_entree: {
295
+ reference: 'B1',
296
+ description: 'Fenetre salon',
297
+ type_pose: 'tunnel',
298
+ presence_retour_isolation: 1,
299
+ largeur_dormant: 5
300
+ },
301
+ donnee_utilisateur: {}
302
+ }
303
+ ];
304
+ const pt = pontThermique({
305
+ type_liaison: 'menuiserie / mur',
306
+ description: 'Fenetre salon-Mur Est'
307
+ });
308
+
309
+ calc_pont_thermique(pt, '1', logement({ mur, bv }));
310
+
311
+ expect(pt.donnee_entree.reference_1).toBe('M1');
312
+ expect(pt.donnee_entree.reference_2).toBe('B1');
313
+ expect(pt.donnee_intermediaire.k).toBe(0.3);
314
+ });
315
+
316
+ test('description non retrouvée dans les murs : repli sur la valeur intermédiaire', () => {
317
+ tv.mockReturnValue({ k: '0.9' });
318
+ const mur = [
319
+ { donnee_entree: { reference: 'M1', description: 'Mur Sud' }, donnee_utilisateur: {} }
320
+ ];
321
+ const pt = pontThermique({
322
+ type_liaison: 'refend / mur',
323
+ description: 'Mur Absent / Refend',
324
+ tv_pont_thermique_id: 7
325
+ });
326
+
327
+ calc_pont_thermique(pt, '1', logement({ mur }));
328
+
329
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
330
+ expect(errorSpy).toHaveBeenCalled();
331
+ });
332
+
333
+ test('2e paroi non retrouvée dans la liaison : repli sur la valeur intermédiaire', () => {
334
+ tv.mockReturnValue({ k: '0.9' });
335
+ const mur = [
336
+ {
337
+ donnee_entree: { reference: 'M1', description: 'Mur Ouest' },
338
+ donnee_utilisateur: {}
339
+ }
340
+ ];
341
+ const pt = pontThermique({
342
+ type_liaison: 'plancher bas / mur',
343
+ description: 'Mur Ouest / Plancher inexistant',
344
+ tv_pont_thermique_id: 7
345
+ });
346
+
347
+ calc_pont_thermique(pt, '1', logement({ mur }));
348
+
349
+ expect(pt.donnee_entree.reference_1).toBe('M1');
350
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
351
+ expect(errorSpy).toHaveBeenCalled();
352
+ });
353
+
354
+ test('liaison "plancher intermédiaire lourd / mur" non supportée : avertissement, aucune 2e paroi', () => {
355
+ tv.mockReturnValue({ k: '0.4', tv_pont_thermique_id: '60' });
356
+ const mur = [
357
+ {
358
+ donnee_entree: {
359
+ reference: 'M1',
360
+ description: 'Mur porteur',
361
+ enum_type_adjacence_id: '1',
362
+ type_isolation: 'iti'
363
+ },
364
+ donnee_utilisateur: {}
365
+ }
366
+ ];
367
+ const pt = pontThermique({
368
+ type_liaison: 'plancher intermédiaire lourd / mur',
369
+ description: 'Mur porteur / Plancher intermédiaire'
370
+ });
371
+
372
+ calc_pont_thermique(pt, '1', logement({ mur }));
373
+
374
+ expect(warnSpy).toHaveBeenCalled();
375
+ expect(pt.donnee_intermediaire.k).toBe(0.4);
376
+ });
377
+ });
378
+
379
+ /**
380
+ * 3.4 Murs de circulation avec pont thermique non nul (cas devant tout de même être pris en compte).
381
+ */
382
+ describe('calc_pont_thermique - mur de circulation avec k non nul', () => {
383
+ test("type d'isolation inconnu, période avant 1948 : mur non isolé", () => {
384
+ tv.mockReturnValue({ k: '0.6', tv_pont_thermique_id: '70' });
385
+ const mur = [
386
+ {
387
+ donnee_entree: {
388
+ reference: 'M1',
389
+ enum_type_adjacence_id: '14',
390
+ type_isolation: 'inconnu',
391
+ periode_isolation: 'avant 1948'
392
+ },
393
+ donnee_utilisateur: {}
394
+ }
395
+ ];
396
+ const pt = pontThermique({
397
+ type_liaison: 'refend / mur',
398
+ reference_1: 'M1',
399
+ description: 'refend'
400
+ });
401
+
402
+ calc_pont_thermique(pt, '1', logement({ mur }));
403
+
404
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
405
+ enum_type_liaison_id: '12',
406
+ isolation_mur: '^non isolé$'
407
+ });
408
+ expect(pt.donnee_intermediaire.k).toBe(0.6);
409
+ });
410
+
411
+ test("type d'isolation inconnu, période récente (par défaut période de construction) : isolation ITI", () => {
412
+ tv.mockReturnValue({ k: '0.6', tv_pont_thermique_id: '70' });
413
+ const mur = [
414
+ {
415
+ donnee_entree: {
416
+ reference: 'M1',
417
+ enum_type_adjacence_id: '1',
418
+ type_isolation: 'inconnu'
419
+ },
420
+ donnee_utilisateur: {}
421
+ }
422
+ ];
423
+ const pt = pontThermique({
424
+ type_liaison: 'refend / mur',
425
+ reference_1: 'M1',
426
+ description: 'refend'
427
+ });
428
+
429
+ // pc_id = '5' => période de construction '1989-2000' (hors seuils "avant 1948"/"1948-1974")
430
+ calc_pont_thermique(pt, '5', logement({ mur }));
431
+
432
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
433
+ enum_type_liaison_id: '12',
434
+ isolation_mur: '^iti$'
435
+ });
436
+ });
437
+ });
438
+
439
+ /**
440
+ * 3.4.1 / 3.4.3 Ponts thermiques plancher bas / mur et plancher haut lourd / mur.
441
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.1 et §3.4.3
442
+ */
443
+ describe('calc_pont_thermique - liaisons plancher / mur', () => {
444
+ function murPlancher(murOverrides = {}, plancherOverrides = {}) {
445
+ const mur = [
446
+ {
447
+ donnee_entree: {
448
+ reference: 'M1',
449
+ enum_type_adjacence_id: '1',
450
+ type_isolation: 'iti',
451
+ ...murOverrides
452
+ },
453
+ donnee_utilisateur: {}
454
+ }
455
+ ];
456
+ const plancher = {
457
+ donnee_entree: { reference: 'P1', type_isolation: 'ite', ...plancherOverrides },
458
+ donnee_utilisateur: {}
459
+ };
460
+ return { mur, plancher };
461
+ }
462
+
463
+ function ptPlancher(type_liaison) {
464
+ return pontThermique({
465
+ type_liaison,
466
+ enum_type_liaison_id: '3',
467
+ reference_1: 'P1',
468
+ reference_2: 'M1',
469
+ description: 'plancher / mur'
470
+ });
471
+ }
472
+
473
+ test('plancher non retrouvé : repli sur la valeur intermédiaire', () => {
474
+ tv.mockReturnValue({ k: '0.9' });
475
+ const { mur } = murPlancher();
476
+ const pt = ptPlancher('plancher bas / mur');
477
+ pt.donnee_entree.tv_pont_thermique_id = 7;
478
+
479
+ calc_pont_thermique(pt, '1', logement({ mur }));
480
+
481
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
482
+ expect(errorSpy).toHaveBeenCalled();
483
+ });
484
+
485
+ test('plancher haut lourd sur plafond bois (type 9) : pont thermique négligé (k = 0)', () => {
486
+ const { mur, plancher } = murPlancher({}, { enum_type_plancher_haut_id: '9' });
487
+ const pt = ptPlancher('plancher haut lourd / mur');
488
+
489
+ calc_pont_thermique(pt, '1', logement({ mur, ph: [plancher] }));
490
+
491
+ expect(pt.donnee_intermediaire.k).toBe(0);
492
+ expect(tv).not.toHaveBeenCalled();
493
+ });
494
+
495
+ test('plancher haut lourd sur mur ossature bois (matériau 18) : pont thermique négligé (k = 0)', () => {
496
+ const { mur, plancher } = murPlancher(
497
+ { enum_materiaux_structure_mur_id: '18' },
498
+ { enum_type_plancher_haut_id: '1' }
499
+ );
500
+ const pt = ptPlancher('plancher haut lourd / mur');
501
+
502
+ calc_pont_thermique(pt, '1', logement({ mur, ph: [plancher] }));
503
+
504
+ expect(pt.donnee_intermediaire.k).toBe(0);
505
+ });
506
+
507
+ test('plancher bas entre solives bois (type 4) : pont thermique négligé (k = 0)', () => {
508
+ const { mur, plancher } = murPlancher({}, { enum_type_plancher_bas_id: '4' });
509
+ const pt = ptPlancher('plancher bas / mur');
510
+
511
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
512
+
513
+ expect(pt.donnee_intermediaire.k).toBe(0);
514
+ });
515
+
516
+ test('plancher bas sur mur ossature bois (matériau 16) : pont thermique négligé (k = 0)', () => {
517
+ const { mur, plancher } = murPlancher(
518
+ { enum_materiaux_structure_mur_id: '16' },
519
+ { enum_type_plancher_bas_id: '1' }
520
+ );
521
+ const pt = ptPlancher('plancher bas / mur');
522
+
523
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
524
+
525
+ expect(pt.donnee_intermediaire.k).toBe(0);
526
+ });
527
+
528
+ test('plancher bas isolation connue : matcher isolation_plancher construit', () => {
529
+ tv.mockReturnValue({ k: '0.35', tv_pont_thermique_id: '80' });
530
+ const { mur, plancher } = murPlancher(
531
+ {},
532
+ { enum_type_plancher_bas_id: '1', type_isolation: 'ite' }
533
+ );
534
+ const pt = ptPlancher('plancher bas / mur');
535
+
536
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
537
+
538
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
539
+ enum_type_liaison_id: '3',
540
+ isolation_mur: '^iti$',
541
+ isolation_plancher: '^ite$'
542
+ });
543
+ expect(pt.donnee_intermediaire.k).toBe(0.35);
544
+ });
545
+
546
+ test('plancher bas isolation inconnue sur terre-plein ancien : plancher non isolé', () => {
547
+ tv.mockReturnValue({ k: '0.35', tv_pont_thermique_id: '80' });
548
+ const { mur, plancher } = murPlancher(
549
+ {},
550
+ {
551
+ enum_type_plancher_bas_id: '1',
552
+ type_isolation: 'inconnu',
553
+ type_adjacence: 'terre-plein',
554
+ periode_isolation: '1989-2000'
555
+ }
556
+ );
557
+ const pt = ptPlancher('plancher bas / mur');
558
+
559
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
560
+
561
+ // terre-plein : le seuil "non isolé" s'étend jusqu'à 1989-2000
562
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
563
+ enum_type_liaison_id: '3',
564
+ isolation_mur: '^iti$',
565
+ isolation_plancher: 'non isolé'
566
+ });
567
+ });
568
+
569
+ test('plancher bas isolation inconnue hors terre-plein, période issue de la construction : plancher ITE', () => {
570
+ tv.mockReturnValue({ k: '0.35', tv_pont_thermique_id: '80' });
571
+ // periode_isolation absente => repli sur la période de construction (pc_id = '5' => 1989-2000)
572
+ const { mur, plancher } = murPlancher(
573
+ {},
574
+ {
575
+ enum_type_plancher_bas_id: '1',
576
+ type_isolation: 'inconnu',
577
+ type_adjacence: 'extérieur'
578
+ }
579
+ );
580
+ const pt = ptPlancher('plancher bas / mur');
581
+
582
+ calc_pont_thermique(pt, '5', logement({ mur, pb: [plancher] }));
583
+
584
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
585
+ enum_type_liaison_id: '3',
586
+ isolation_mur: '^iti$',
587
+ isolation_plancher: '^ite$'
588
+ });
589
+ });
590
+
591
+ test('plancher retrouvé via reference_2 : matcher construit', () => {
592
+ tv.mockReturnValue({ k: '0.35', tv_pont_thermique_id: '80' });
593
+ const { mur, plancher } = murPlancher(
594
+ {},
595
+ { enum_type_plancher_bas_id: '1', type_isolation: 'ite' }
596
+ );
597
+ // reference_1 pointe le mur, reference_2 pointe le plancher
598
+ const pt = pontThermique({
599
+ type_liaison: 'plancher bas / mur',
600
+ enum_type_liaison_id: '3',
601
+ reference_1: 'M1',
602
+ reference_2: 'P1',
603
+ description: 'mur / plancher'
604
+ });
605
+
606
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
607
+
608
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
609
+ enum_type_liaison_id: '3',
610
+ isolation_mur: '^iti$',
611
+ isolation_plancher: '^ite$'
612
+ });
613
+ });
614
+
615
+ test("plancher bas isolé mais type d'isolation inconnu : plancher ITE", () => {
616
+ tv.mockReturnValue({ k: '0.35', tv_pont_thermique_id: '80' });
617
+ const { mur, plancher } = murPlancher(
618
+ {},
619
+ { enum_type_plancher_bas_id: '1', type_isolation: "isolé mais type d'isolation inconnu" }
620
+ );
621
+ const pt = ptPlancher('plancher bas / mur');
622
+
623
+ calc_pont_thermique(pt, '1', logement({ mur, pb: [plancher] }));
624
+
625
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
626
+ enum_type_liaison_id: '3',
627
+ isolation_mur: '^iti$',
628
+ isolation_plancher: '^ite$'
629
+ });
630
+ });
631
+ });
632
+
633
+ /**
634
+ * 3.4.5 Menuiserie / mur.
635
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.4.5
636
+ */
637
+ describe('calc_pont_thermique - liaison menuiserie / mur', () => {
638
+ function murMenuiserie(murOverrides = {}, menuiserieOverrides = {}) {
639
+ const mur = [
640
+ {
641
+ donnee_entree: {
642
+ reference: 'M1',
643
+ enum_type_adjacence_id: '1',
644
+ enum_materiaux_structure_mur_id: '1',
645
+ type_isolation: 'iti',
646
+ ...murOverrides
647
+ },
648
+ donnee_utilisateur: {}
649
+ }
650
+ ];
651
+ const menuiserie = {
652
+ donnee_entree: {
653
+ reference: 'B1',
654
+ type_pose: 'tunnel',
655
+ presence_retour_isolation: 1,
656
+ largeur_dormant: 5,
657
+ ...menuiserieOverrides
658
+ },
659
+ donnee_utilisateur: {}
660
+ };
661
+ return { mur, menuiserie };
662
+ }
663
+
664
+ function ptMenuiserie() {
665
+ return pontThermique({
666
+ type_liaison: 'menuiserie / mur',
667
+ enum_type_liaison_id: '20',
668
+ reference_1: 'B1',
669
+ reference_2: 'M1',
670
+ description: 'menuiserie / mur'
671
+ });
672
+ }
673
+
674
+ test('mur en isolation répartie (ITR) : le pont thermique ne dépend pas de la menuiserie', () => {
675
+ tv.mockReturnValue({ k: '0.2', tv_pont_thermique_id: '90' });
676
+ const { mur } = murMenuiserie({ type_isolation: 'itr' });
677
+ const pt = ptMenuiserie();
678
+
679
+ calc_pont_thermique(pt, '1', logement({ mur }));
680
+
681
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
682
+ enum_type_liaison_id: '20',
683
+ isolation_mur: '^itr$'
684
+ });
685
+ expect(pt.donnee_intermediaire.k).toBe(0.2);
686
+ });
687
+
688
+ test('menuiserie non retrouvée : repli sur la valeur intermédiaire', () => {
689
+ tv.mockReturnValue({ k: '0.9' });
690
+ const { mur } = murMenuiserie();
691
+ const pt = ptMenuiserie();
692
+ pt.donnee_entree.tv_pont_thermique_id = 7;
693
+
694
+ calc_pont_thermique(pt, '1', logement({ mur }));
695
+
696
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
697
+ expect(errorSpy).toHaveBeenCalled();
698
+ });
699
+
700
+ test('menuiserie retrouvée : matcher pose / retour isolation / largeur dormant', () => {
701
+ tv.mockReturnValue({ k: '0.25', tv_pont_thermique_id: '91' });
702
+ const { mur, menuiserie } = murMenuiserie();
703
+ const pt = ptMenuiserie();
704
+
705
+ calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));
706
+
707
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
708
+ enum_type_liaison_id: '20',
709
+ isolation_mur: '^iti$',
710
+ type_pose: 'tunnel',
711
+ presence_retour_isolation: 1,
712
+ largeur_dormant: 5
713
+ });
714
+ expect(pt.donnee_intermediaire.k).toBe(0.25);
715
+ });
716
+
717
+ test('pose "sans objet" ramenée à une pose tunnel', () => {
718
+ tv.mockReturnValue({ k: '0.25', tv_pont_thermique_id: '91' });
719
+ const { mur, menuiserie } = murMenuiserie({}, { type_pose: 'sans objet' });
720
+ const pt = ptMenuiserie();
721
+
722
+ calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));
723
+
724
+ expect(tv).toHaveBeenCalledWith(
725
+ 'pont_thermique',
726
+ expect.objectContaining({ type_pose: 'tunnel' })
727
+ );
728
+ });
729
+
730
+ test('type de pose absent (sans compatibilité bug) : pose tunnel par défaut', () => {
731
+ tv.mockReturnValue({ k: '0.25', tv_pont_thermique_id: '91' });
732
+ const { mur, menuiserie } = murMenuiserie({}, { type_pose: undefined });
733
+ const pt = ptMenuiserie();
734
+
735
+ calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));
736
+
737
+ expect(tv).toHaveBeenLastCalledWith(
738
+ 'pont_thermique',
739
+ expect.objectContaining({ type_pose: 'tunnel' })
740
+ );
741
+ });
742
+
743
+ test('type de pose absent (compatibilité bug) : récupération via tv_pont_thermique_id', () => {
744
+ state.bug = true;
745
+ // 1er appel tv (récupération type_pose), 2e appel tv (ligne pont_thermique)
746
+ tv.mockReturnValueOnce({ type_pose: 'nu extérieur' });
747
+ tv.mockReturnValue({ k: '0.25', tv_pont_thermique_id: '91' });
748
+ const { mur, menuiserie } = murMenuiserie({}, { type_pose: undefined });
749
+ const pt = ptMenuiserie();
750
+ pt.donnee_entree.tv_pont_thermique_id = 7;
751
+
752
+ calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));
753
+
754
+ expect(tv).toHaveBeenLastCalledWith(
755
+ 'pont_thermique',
756
+ expect.objectContaining({ type_pose: 'nu extérieur' })
757
+ );
758
+ expect(errorSpy).toHaveBeenCalled();
759
+ });
760
+
761
+ test('type de pose absent (compatibilité bug) sans ligne tv : pose tunnel par défaut', () => {
762
+ state.bug = true;
763
+ tv.mockReturnValueOnce(null); // récupération type_pose échoue
764
+ tv.mockReturnValue({ k: '0.25', tv_pont_thermique_id: '91' });
765
+ const { mur, menuiserie } = murMenuiserie({}, { type_pose: undefined });
766
+ const pt = ptMenuiserie();
767
+ pt.donnee_entree.tv_pont_thermique_id = 7;
768
+
769
+ calc_pont_thermique(pt, '1', logement({ mur, bv: [menuiserie] }));
770
+
771
+ expect(tv).toHaveBeenLastCalledWith(
772
+ 'pont_thermique',
773
+ expect.objectContaining({ type_pose: 'tunnel' })
774
+ );
775
+ });
776
+ });
777
+
778
+ /**
779
+ * Recherche du mur par la référence de la baie vitrée (comportement bugué reproduit).
780
+ */
781
+ describe('calc_pont_thermique - récupération du mur via la baie vitrée (compatibilité bug)', () => {
782
+ test('mur retrouvé grâce à reference_paroi de la baie vitrée', () => {
783
+ state.bug = true;
784
+ tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
785
+ const mur = [
786
+ {
787
+ donnee_entree: {
788
+ reference: 'M1',
789
+ enum_type_adjacence_id: '1',
790
+ type_isolation: 'iti'
791
+ },
792
+ donnee_utilisateur: {}
793
+ }
794
+ ];
795
+ const bv = [{ donnee_entree: { reference: 'BV1', reference_paroi: '0' } }];
796
+ const pt = pontThermique({
797
+ type_liaison: 'refend / mur',
798
+ reference_1: 'BV1',
799
+ description: 'refend'
800
+ });
801
+
802
+ calc_pont_thermique(pt, '1', logement({ mur, bv }));
803
+
804
+ expect(pt.donnee_intermediaire.k).toBe(0.5);
805
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
806
+ enum_type_liaison_id: '12',
807
+ isolation_mur: '^iti$'
808
+ });
809
+ expect(errorSpy).toHaveBeenCalled();
810
+ });
811
+
812
+ test('reference_paroi ne correspondant pas à l’index du mur : aucun mur retrouvé (isolation ITI par défaut)', () => {
813
+ state.bug = true;
814
+ tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
815
+ const mur = [
816
+ {
817
+ donnee_entree: { reference: 'M1', enum_type_adjacence_id: '1', type_isolation: 'iti' },
818
+ donnee_utilisateur: {}
819
+ }
820
+ ];
821
+ // reference_paroi = 5 alors que l'index du mur est 0 => pas de correspondance
822
+ const bv = [{ donnee_entree: { reference: 'BV1', reference_paroi: '5' } }];
823
+ const pt = pontThermique({
824
+ type_liaison: 'refend / mur',
825
+ reference_1: 'BV1',
826
+ description: 'refend'
827
+ });
828
+
829
+ calc_pont_thermique(pt, '1', logement({ mur, bv }));
830
+
831
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
832
+ enum_type_liaison_id: '12',
833
+ isolation_mur: '^iti'
834
+ });
835
+ expect(pt.donnee_intermediaire.k).toBe(0.5);
836
+ });
837
+ });
838
+
839
+ /**
840
+ * Collections d'enveloppe absentes et absence de ligne tabulée finale.
841
+ */
842
+ describe('calc_pont_thermique - cas limites', () => {
843
+ test('collections absentes : chaque liste retombe sur un tableau vide (repli valeur intermédiaire)', () => {
844
+ tv.mockReturnValue({ k: '0.9' });
845
+ const logementVide = {
846
+ enveloppe: {
847
+ mur_collection: {},
848
+ plancher_bas_collection: {},
849
+ plancher_haut_collection: {},
850
+ baie_vitree_collection: {},
851
+ porte_collection: {}
852
+ }
853
+ };
854
+ const pt = pontThermique({
855
+ type_liaison: 'refend / mur',
856
+ description: 'descriptionNonReconnue',
857
+ tv_pont_thermique_id: 7
858
+ });
859
+
860
+ calc_pont_thermique(pt, '1', logementVide);
861
+
862
+ expect(pt.donnee_intermediaire.k).toBe('0.9');
863
+ });
864
+
865
+ test('aucune ligne tabulée pour le pont thermique : erreur émise', () => {
866
+ tv.mockReturnValue(null);
867
+ const mur = [
868
+ {
869
+ donnee_entree: {
870
+ reference: 'M1',
871
+ enum_type_adjacence_id: '1',
872
+ type_isolation: 'iti'
873
+ },
874
+ donnee_utilisateur: {}
875
+ }
876
+ ];
877
+ const pt = pontThermique({
878
+ type_liaison: 'refend / mur',
879
+ reference_1: 'M1',
880
+ description: 'refend'
881
+ });
882
+
883
+ calc_pont_thermique(pt, '1', logement({ mur }));
884
+
885
+ expect(errorSpy).toHaveBeenCalledWith(
886
+ '!! pas de valeur forfaitaire trouvée pour pont_thermique (k) !!'
887
+ );
888
+ expect(pt.donnee_intermediaire.k).toBeUndefined();
889
+ });
890
+ });