@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
package/3.2.1_mur.spec.js CHANGED
@@ -1,47 +1,820 @@
1
- import calc_mur from './3.2.1_mur.js';
2
- import { describe, expect, test } from 'vitest';
3
-
4
- describe('Recherche de bugs dans le calcul de déperdition des murs', () => {
5
- /**
6
- * @see : https://redfroggy.atlassian.net/browse/KAR-119
7
- */
8
- test('calcul de déperdition pour les murs de 2213E0696993Z', () => {
9
- const zc = 8; // H3
10
- const pc_id = 2; // Période de construction (1948)
11
- const ej = 0;
12
- const mur = {
13
- donnee_entree: {
14
- description:
15
- "Mur 2 Est - Inconnu donnant sur des circulations sans ouverture directe sur l'extérieur",
16
- reference: '2021_08_24_18_02_58_7233440008111783',
17
- tv_coef_reduction_deperdition_id: 78,
18
- surface_aiu: 22,
19
- surface_aue: 15,
20
- enum_cfg_isolation_lnc_id: '2',
21
- enum_type_adjacence_id: '14', // Circulation sans ouverture directe sur l'extérieur
22
- enum_orientation_id: '3', // Est
23
- surface_paroi_totale: 10.5,
24
- surface_paroi_opaque: 10.5,
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `calc_mur` :
5
+ * - `enums` : mapping minimal période de construction / période d'isolation ;
6
+ * - `b` (3.1_b.js) : simple espion (le coefficient b n'intervient pas dans le calcul de Umur ici) ;
7
+ * - `tv` : accès aux tables `umur0` / `umur`, on contrôle la ligne retournée par table ;
8
+ * - `requestInput` / `requestInputID` : passe-plats vers les données d'entrée ;
9
+ * - `getKeyByValue` : implémentation déterministe (indépendante des vraies enums) ;
10
+ * - `getThicknessFromDescription` : contrôlé par test (épaisseur extraite de la description) ;
11
+ * - `bug_for_bug_compat` : exposé via un getter afin de pouvoir le basculer par test.
12
+ */
13
+ let bugForBugCompat = false;
14
+
15
+ vi.mock('./enums.js', () => ({
16
+ default: {
17
+ periode_construction: { 1: 'avant 1948', 3: '1948-1974', 5: '1989-2000' },
18
+ periode_isolation: { 2: '1975-1977', 6: '2006-2012' }
19
+ }
20
+ }));
21
+
22
+ vi.mock('./3.1_b.js', () => ({
23
+ default: vi.fn()
24
+ }));
25
+
26
+ vi.mock('./utils.js', () => ({
27
+ tv: vi.fn(),
28
+ requestInput: (de, du, field) => de[field],
29
+ requestInputID: (de, du, field) => de[`enum_${field}_id`],
30
+ getKeyByValue: (object, value) => Object.keys(object).find((key) => object[key] === value),
31
+ getThicknessFromDescription: vi.fn(() => 0),
32
+ get bug_for_bug_compat() {
33
+ return bugForBugCompat;
34
+ }
35
+ }));
36
+
37
+ const { default: calc_mur } = await import('./3.2.1_mur.js');
38
+ const { tv, getThicknessFromDescription } = await import('./utils.js');
39
+
40
+ /** Table `umur0` figée : U0 forfaitaire de la paroi nue. */
41
+ const ROW_UMUR0 = { umur0: '2', tv_umur0_id: '1' };
42
+ /** Table `umur` figée : U forfaitaire de la paroi isolée. */
43
+ const ROW_UMUR = { umur: '0.5', tv_umur_id: '6' };
44
+
45
+ beforeEach(() => {
46
+ bugForBugCompat = false;
47
+ tv.mockReset();
48
+ getThicknessFromDescription.mockReset();
49
+ getThicknessFromDescription.mockReturnValue(0);
50
+ tv.mockImplementation((table) => {
51
+ if (table === 'umur0') return { ...ROW_UMUR0 };
52
+ if (table === 'umur') return { ...ROW_UMUR };
53
+ return null;
54
+ });
55
+ });
56
+
57
+ /**
58
+ * 3.2.1 Coefficient de transmission des murs (Umur)
59
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.1
60
+ */
61
+ describe('calc_mur - mur non isolé', () => {
62
+ test('type de paroi inconnu : Umur0 = 2,5 et Umur = min(Umur0 ; 2,5)', () => {
63
+ const mur = {
64
+ donnee_entree: {
65
+ methode_saisie_u: 'non isolé',
66
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
67
+ enum_type_doublage_id: '2'
68
+ }
69
+ };
70
+
71
+ calc_mur(mur, 'h1a', '1', '0');
72
+
73
+ expect(mur.donnee_intermediaire.umur0).toBe(2.5);
74
+ expect(mur.donnee_intermediaire.umur).toBe(2.5);
75
+ });
76
+ });
77
+
78
+ describe('calc_mur - calcul de Umur0 par table forfaitaire (tv_umur0)', () => {
79
+ test('matériau concerné par les épaisseurs : épaisseur intégrée au matcher', () => {
80
+ const mur = {
81
+ donnee_entree: {
82
+ methode_saisie_u: 'non isolé',
83
+ methode_saisie_u0:
84
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
85
+ enum_materiaux_structure_mur_id: '5',
86
+ epaisseur_structure: 30,
87
+ enum_type_doublage_id: '2'
88
+ }
89
+ };
90
+
91
+ calc_mur(mur, 'h1a', '1', '0');
92
+
93
+ expect(tv).toHaveBeenCalledWith('umur0', {
94
+ enum_materiaux_structure_mur_id: '5',
95
+ epaisseur_structure: 30
96
+ });
97
+ expect(mur.donnee_intermediaire.umur0).toBe(2);
98
+ expect(mur.donnee_intermediaire.umur).toBe(2);
99
+ });
100
+
101
+ test.each([['1'], ['6'], ['20'], ['27']])(
102
+ 'matériau %s non concerné par les épaisseurs : matcher sans épaisseur',
103
+ (materiau) => {
104
+ const mur = {
105
+ donnee_entree: {
106
+ methode_saisie_u: 'non isolé',
107
+ methode_saisie_u0:
108
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
109
+ enum_materiaux_structure_mur_id: materiau,
110
+ epaisseur_structure: 30,
111
+ enum_type_doublage_id: '2'
112
+ }
113
+ };
114
+
115
+ calc_mur(mur, 'h1a', '1', '0');
116
+
117
+ expect(tv).toHaveBeenCalledWith('umur0', {
118
+ enum_materiaux_structure_mur_id: materiau
119
+ });
120
+ }
121
+ );
122
+
123
+ test('épaisseur absente : récupération depuis la description', () => {
124
+ getThicknessFromDescription.mockReturnValue(25);
125
+ const mur = {
126
+ donnee_entree: {
127
+ methode_saisie_u: 'non isolé',
128
+ methode_saisie_u0:
129
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
130
+ enum_materiaux_structure_mur_id: '5',
131
+ description: 'mur de 25 cm',
132
+ enum_type_doublage_id: '2'
133
+ }
134
+ };
135
+
136
+ calc_mur(mur, 'h1a', '1', '0');
137
+
138
+ expect(getThicknessFromDescription).toHaveBeenCalledWith('mur de 25 cm');
139
+ expect(tv).toHaveBeenCalledWith('umur0', {
140
+ enum_materiaux_structure_mur_id: '5',
141
+ epaisseur_structure: 25
142
+ });
143
+ });
144
+
145
+ test('bug_for_bug : épaisseur absente et introuvable dans la description, reprise depuis tv_umur0_id', () => {
146
+ bugForBugCompat = true;
147
+ getThicknessFromDescription.mockReturnValue(0);
148
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
149
+ tv.mockImplementation((table, matcher) => {
150
+ if (table === 'umur0' && matcher.tv_umur0_id) {
151
+ return { epaisseur_structure: 18, tv_umur0_id: '1' };
152
+ }
153
+ if (table === 'umur0') return { ...ROW_UMUR0, tv_umur0_id: matcher.tv_umur0_id ?? '1' };
154
+ return null;
155
+ });
156
+ const mur = {
157
+ donnee_entree: {
158
+ methode_saisie_u: 'non isolé',
159
+ methode_saisie_u0:
160
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
161
+ enum_materiaux_structure_mur_id: '5',
162
+ description: 'mur sans épaisseur',
25
163
  tv_umur0_id: 1,
26
- enum_materiaux_structure_mur_id: '1', // Inconnu
27
- enum_methode_saisie_u0_id: '2', // déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire
28
- paroi_ancienne: 0,
29
- enum_type_doublage_id: '2', // absence de doublage
30
- enum_type_isolation_id: '1', // inconnu
31
- enum_periode_isolation_id: '2', // 1948-1974
32
- tv_umur_id: 6, //
33
- enum_methode_saisie_u_id: '8' // année de construction saisie (table forfaitaire)
164
+ enum_type_doublage_id: '2'
165
+ }
166
+ };
167
+
168
+ calc_mur(mur, 'h1a', '1', '0');
169
+
170
+ // L'épaisseur 18 est récupérée depuis la ligne tv_umur0_id puis utilisée dans le matcher final
171
+ expect(error).toHaveBeenCalled();
172
+ expect(tv).toHaveBeenLastCalledWith('umur0', {
173
+ enum_materiaux_structure_mur_id: '5',
174
+ epaisseur_structure: 18
175
+ });
176
+ error.mockRestore();
177
+ });
178
+
179
+ test('bug_for_bug : épaisseur > 80 recalculée en la divisant par 10 (valeur en millimètres)', () => {
180
+ bugForBugCompat = true;
181
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
182
+ // du.tv_umur0_id_avant = mur.donnee_entree.tv_umur0_id = 5.
183
+ // Le 1er appel (épaisseur 200) renvoie tv_umur0_id 1 (mismatch) => recalcul avec épaisseur 20.
184
+ tv.mockImplementation((table, matcher) => {
185
+ if (table !== 'umur0') return null;
186
+ return { umur0: '2', tv_umur0_id: matcher.epaisseur_structure === 20 ? '5' : '1' };
187
+ });
188
+ const mur = {
189
+ donnee_entree: {
190
+ methode_saisie_u: 'non isolé',
191
+ methode_saisie_u0:
192
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
193
+ enum_materiaux_structure_mur_id: '5',
194
+ epaisseur_structure: 200,
195
+ tv_umur0_id: 5,
196
+ enum_type_doublage_id: '2',
197
+ description: 'mur'
198
+ }
199
+ };
200
+
201
+ calc_mur(mur, 'h1a', '1', '0');
202
+
203
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('epaisseur > 80'));
204
+ // Après division par 10, l'épaisseur 20 donne la bonne ligne (tv_umur0_id 5)
205
+ expect(mur.donnee_entree.tv_umur0_id).toBe(5);
206
+ expect(mur.donnee_intermediaire.umur0).toBe(2);
207
+ error.mockRestore();
208
+ });
209
+
210
+ test('bug_for_bug : épaisseur > 80 toujours incohérente après division : Umur0 non trouvé', () => {
211
+ bugForBugCompat = true;
212
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
213
+ // Aucun appel ne renvoie tv_umur0_id = 5 : la ligne finit par être invalidée (row = undefined)
214
+ tv.mockImplementation((table) => (table === 'umur0' ? { umur0: '2', tv_umur0_id: '1' } : null));
215
+ const mur = {
216
+ donnee_entree: {
217
+ methode_saisie_u: 'non isolé',
218
+ methode_saisie_u0:
219
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
220
+ enum_materiaux_structure_mur_id: '5',
221
+ epaisseur_structure: 200,
222
+ tv_umur0_id: 5,
223
+ enum_type_doublage_id: '2',
224
+ description: 'mur'
225
+ }
226
+ };
227
+
228
+ calc_mur(mur, 'h1a', '1', '0');
229
+
230
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('umur0'));
231
+ // di.umur0 n'est jamais défini par tv_umur0 => Math.min(2.5, undefined) = NaN
232
+ expect(mur.donnee_intermediaire.umur0).toBeNaN();
233
+ error.mockRestore();
234
+ });
235
+
236
+ test('Umur0 introuvable dans la table : erreur émise', () => {
237
+ tv.mockImplementation((table) => (table === 'umur0' ? null : { ...ROW_UMUR }));
238
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
239
+ const mur = {
240
+ donnee_entree: {
241
+ methode_saisie_u: 'non isolé',
242
+ methode_saisie_u0:
243
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
244
+ enum_materiaux_structure_mur_id: '5',
245
+ epaisseur_structure: 30,
246
+ enum_type_doublage_id: '2'
247
+ }
248
+ };
249
+
250
+ calc_mur(mur, 'h1a', '1', '0');
251
+
252
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('umur0'));
253
+ error.mockRestore();
254
+ });
255
+ });
256
+
257
+ describe('calc_mur - saisie de Umur0 (calc_umur0)', () => {
258
+ test('saisie directe de Umur0 : valeur reprise depuis umur0_saisi', () => {
259
+ const mur = {
260
+ donnee_entree: {
261
+ methode_saisie_u: 'non isolé',
262
+ methode_saisie_u0:
263
+ 'saisie direct u0 justifiée à partir des documents justificatifs autorisés',
264
+ umur0_saisi: 1.2,
265
+ enum_type_doublage_id: '2'
266
+ }
267
+ };
268
+
269
+ calc_mur(mur, 'h1a', '1', '0');
270
+
271
+ expect(mur.donnee_intermediaire.umur0).toBe(1.2);
272
+ expect(mur.donnee_intermediaire.umur).toBe(1.2);
273
+ });
274
+
275
+ test('Umur0 non saisi car U saisi : Umur0 non défini par la méthode', () => {
276
+ const mur = {
277
+ donnee_entree: {
278
+ methode_saisie_u: 'non isolé',
279
+ methode_saisie_u0: 'u0 non saisi car le u est saisi connu et justifié.',
280
+ enum_type_doublage_id: '2'
281
+ }
282
+ };
283
+
284
+ calc_mur(mur, 'h1a', '1', '0');
285
+
286
+ // di.umur0 reste indéfini => Math.min(2.5, undefined) = NaN
287
+ expect(mur.donnee_intermediaire.umur0).toBeNaN();
288
+ });
289
+
290
+ test('methode_saisie_u0 inconnue : avertissement émis', () => {
291
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
292
+ const mur = {
293
+ donnee_entree: {
294
+ methode_saisie_u: 'non isolé',
295
+ methode_saisie_u0: 'valeur inexistante',
296
+ enum_type_doublage_id: '2'
297
+ }
298
+ };
299
+
300
+ calc_mur(mur, 'h1a', '1', '0');
301
+
302
+ expect(warn).toHaveBeenCalledWith('methode_saisie_u0 inconnue:', 'valeur inexistante');
303
+ warn.mockRestore();
304
+ });
305
+ });
306
+
307
+ describe('calc_mur - prise en compte du doublage', () => {
308
+ test("doublage indéterminé ou lame d'air < 15 mm (type 3) : +0,1 de résistance", () => {
309
+ const mur = {
310
+ donnee_entree: {
311
+ methode_saisie_u: 'non isolé',
312
+ methode_saisie_u0:
313
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
314
+ enum_materiaux_structure_mur_id: '5',
315
+ epaisseur_structure: 30,
316
+ enum_type_doublage_id: '3'
317
+ }
318
+ };
319
+
320
+ calc_mur(mur, 'h1a', '1', '0');
321
+
322
+ // Umur0 table = 2 ; 1 / (1/2 + 0,1) = 1,6666...
323
+ // valeur de référence de régression
324
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(1.6666666666666667, 9);
325
+ });
326
+
327
+ test.each([['4'], ['5']])(
328
+ "doublage avec lame d'air ou connu (type %s) : +0,21 de résistance",
329
+ (typeDoublage) => {
330
+ const mur = {
331
+ donnee_entree: {
332
+ methode_saisie_u: 'non isolé',
333
+ methode_saisie_u0:
334
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
335
+ enum_materiaux_structure_mur_id: '5',
336
+ epaisseur_structure: 30,
337
+ enum_type_doublage_id: typeDoublage
338
+ }
339
+ };
340
+
341
+ calc_mur(mur, 'h1a', '1', '0');
342
+
343
+ // Umur0 table = 2 ; 1 / (1/2 + 0,21) = 1,40845...
344
+ // valeur de référence de régression
345
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(1.4084507042253522, 9);
346
+ }
347
+ );
348
+
349
+ test.each([
350
+ ['doublage connu (plâtre, brique...)', 5],
351
+ ["doublage indéterminé avec lame d'air", 4],
352
+ ["doublage indéterminé ou lame d'air", 3]
353
+ ])(
354
+ 'bug_for_bug : type de doublage déduit de la description "%s"',
355
+ (description, resistanceType) => {
356
+ bugForBugCompat = true;
357
+ const mur = {
358
+ donnee_entree: {
359
+ methode_saisie_u: 'non isolé',
360
+ methode_saisie_u0:
361
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
362
+ enum_materiaux_structure_mur_id: '5',
363
+ epaisseur_structure: 30,
364
+ enum_type_doublage_id: '1',
365
+ description
366
+ }
367
+ };
368
+
369
+ calc_mur(mur, 'h1a', '1', '0');
370
+
371
+ const attendu = resistanceType === 3 ? 1 / (1 / 2 + 0.1) : 1 / (1 / 2 + 0.21);
372
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(attendu, 9);
373
+ }
374
+ );
375
+
376
+ test('bug_for_bug : DPE minorant Umur0 à 2,5 avant doublage : avertissement (Umur0 avant)', () => {
377
+ bugForBugCompat = true;
378
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
379
+ // umur0_avant est aligné sur le Umur0 minoré à 2,5 avec doublage type 3 => 1/(1/2 + 0,1)
380
+ const mur = {
381
+ donnee_entree: {
382
+ methode_saisie_u: 'non isolé',
383
+ methode_saisie_u0:
384
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
385
+ enum_materiaux_structure_mur_id: '5',
386
+ epaisseur_structure: 30,
387
+ enum_type_doublage_id: '3'
388
+ },
389
+ donnee_intermediaire: { umur0: 1 / (1 / 2 + 0.1) }
390
+ };
391
+
392
+ calc_mur(mur, 'h1a', '1', '0');
393
+
394
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('minoré à 2.5'));
395
+ error.mockRestore();
396
+ });
397
+
398
+ test('bug_for_bug : DPE minorant Umur0 à 2,5 avant doublage : avertissement (Umur avant)', () => {
399
+ bugForBugCompat = true;
400
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
401
+ // umur_avant est aligné sur le Umur0 minoré à 2,5 avec doublage type 4 => 1/(1/2 + 0,21)
402
+ const mur = {
403
+ donnee_entree: {
404
+ methode_saisie_u: 'non isolé',
405
+ methode_saisie_u0:
406
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
407
+ enum_materiaux_structure_mur_id: '5',
408
+ epaisseur_structure: 30,
409
+ enum_type_doublage_id: '4'
410
+ },
411
+ donnee_intermediaire: { umur: 1 / (1 / 2 + 0.21) }
412
+ };
413
+
414
+ calc_mur(mur, 'h1a', '1', '0');
415
+
416
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('minoré à 2.5'));
417
+ error.mockRestore();
418
+ });
419
+ });
420
+
421
+ describe('calc_mur - enduit isolant sur paroi ancienne', () => {
422
+ test('paroi ancienne avec enduit : +0,7 de résistance', () => {
423
+ const mur = {
424
+ donnee_entree: {
425
+ methode_saisie_u: 'non isolé',
426
+ methode_saisie_u0:
427
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
428
+ enum_materiaux_structure_mur_id: '5',
429
+ epaisseur_structure: 30,
430
+ enum_type_doublage_id: '2',
431
+ paroi_ancienne: 1, // renommé en enduit_isolant_paroi_ancienne
432
+ enduit_isolant_paroi_ancienne: 1
433
+ }
434
+ };
435
+
436
+ calc_mur(mur, 'h1a', '1', '0');
437
+
438
+ // Umur0 = 2 ; 1 / (1/2 + 0,7) = 0,8333...
439
+ // valeur de référence de régression
440
+ expect(mur.donnee_intermediaire.umur0).toBeCloseTo(0.8333333333333334, 9);
441
+ expect(mur.donnee_intermediaire.umur0Enduit).toBeCloseTo(0.8333333333333334, 9);
442
+ });
443
+
444
+ test("bug_for_bug : le DPE ignore l'enduit, la valeur Umur0 d'origine est conservée", () => {
445
+ bugForBugCompat = true;
446
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
447
+ // umur0_avant = min(2.5, umur0) = 2 (Umur0 table), le DPE n'a pas appliqué l'enduit
448
+ const mur = {
449
+ donnee_entree: {
450
+ methode_saisie_u: 'non isolé',
451
+ methode_saisie_u0:
452
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
453
+ enum_materiaux_structure_mur_id: '5',
454
+ epaisseur_structure: 30,
455
+ enum_type_doublage_id: '2',
456
+ enduit_isolant_paroi_ancienne: 1,
457
+ description: 'mur'
34
458
  },
35
- donnee_intermediaire: {
36
- b: 0.35,
37
- umur: 2.5,
38
- umur0: 2.5
459
+ donnee_intermediaire: { umur0: 2 }
460
+ };
461
+
462
+ calc_mur(mur, 'h1a', '1', '0');
463
+
464
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('paroi ancienne'));
465
+ // La valeur d'origine (2) est conservée, bornée à 2,5
466
+ expect(mur.donnee_intermediaire.umur0).toBe(2);
467
+ error.mockRestore();
468
+ });
469
+ });
470
+
471
+ describe('calc_mur - isolation par épaisseur ou résistance', () => {
472
+ test("épaisseur d'isolation saisie : Umur = 1 / (1/Umur0 + e/0,04)", () => {
473
+ const mur = {
474
+ donnee_entree: {
475
+ methode_saisie_u: 'epaisseur isolation saisie justifiée par mesure ou observation',
476
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
477
+ enum_type_doublage_id: '2',
478
+ epaisseur_isolation: 10
39
479
  }
40
480
  };
41
- calc_mur(mur, zc, pc_id, ej);
42
481
 
43
- expect(mur.donnee_intermediaire.b).toBe(0.35);
482
+ calc_mur(mur, 'h1a', '1', '0');
483
+
484
+ // Umur0 = 2,5 ; e = 0,1 ; 1 / (1/2,5 + 0,1/0,04) = 0,3448...
485
+ // valeur de référence de régression
486
+ expect(mur.donnee_intermediaire.umur).toBeCloseTo(0.3448275862068966, 9);
487
+ });
488
+
489
+ test("épaisseur d'isolation absente : repli sur Umur = min(Umur0 ; 2,5) et erreur", () => {
490
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
491
+ const mur = {
492
+ donnee_entree: {
493
+ methode_saisie_u:
494
+ 'epaisseur isolation saisie justifiée à partir des documents justificatifs autorisés',
495
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
496
+ enum_type_doublage_id: '2',
497
+ description: 'mur sans épaisseur isolation'
498
+ }
499
+ };
500
+
501
+ calc_mur(mur, 'h1a', '1', '0');
502
+
503
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('epaisseur_isolation'));
44
504
  expect(mur.donnee_intermediaire.umur).toBe(2.5);
45
- expect(mur.donnee_intermediaire.umur0).toBe(2.5);
505
+ error.mockRestore();
506
+ });
507
+
508
+ test("résistance d'isolation saisie : Umur = 1 / (1/Umur0 + R)", () => {
509
+ const mur = {
510
+ donnee_entree: {
511
+ methode_saisie_u:
512
+ "resistance isolation saisie justifiée observation de l'isolant installé et mesure de son épaisseur",
513
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
514
+ enum_type_doublage_id: '2',
515
+ resistance_isolation: 1
516
+ }
517
+ };
518
+
519
+ calc_mur(mur, 'h1a', '1', '0');
520
+
521
+ // Umur0 = 2,5 ; R = 1 ; 1 / (1/2,5 + 1) = 0,7142...
522
+ // valeur de référence de régression
523
+ expect(mur.donnee_intermediaire.umur).toBeCloseTo(0.7142857142857143, 9);
524
+ });
525
+
526
+ test("résistance d'isolation absente : repli sur Umur = min(Umur0 ; 2,5) et erreur", () => {
527
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
528
+ const mur = {
529
+ donnee_entree: {
530
+ methode_saisie_u:
531
+ 'resistance isolation saisie justifiée à partir des documents justificatifs autorisés',
532
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
533
+ enum_type_doublage_id: '2',
534
+ description: 'mur sans résistance isolation'
535
+ }
536
+ };
537
+
538
+ calc_mur(mur, 'h1a', '1', '0');
539
+
540
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('resistance_isolation'));
541
+ expect(mur.donnee_intermediaire.umur).toBe(2.5);
542
+ error.mockRestore();
543
+ });
544
+ });
545
+
546
+ describe('calc_mur - table forfaitaire (Umur)', () => {
547
+ test('isolation inconnue : Umur = min(Umur table ; Umur0)', () => {
548
+ const mur = {
549
+ donnee_entree: {
550
+ methode_saisie_u: 'isolation inconnue (table forfaitaire)',
551
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
552
+ enum_type_doublage_id: '2'
553
+ }
554
+ };
555
+
556
+ calc_mur(mur, 'h1a', '1', '0');
557
+
558
+ expect(tv).toHaveBeenCalledWith(
559
+ 'umur',
560
+ expect.objectContaining({
561
+ enum_periode_construction_id: '1',
562
+ enum_zone_climatique_id: 'h1a',
563
+ effet_joule: '0'
564
+ }),
565
+ mur.donnee_entree
566
+ );
567
+ // min(0,5 ; 2,5) = 0,5
568
+ expect(mur.donnee_intermediaire.umur).toBe(0.5);
569
+ });
570
+
571
+ test("année d'isolation différente : la période d'isolation renseignée est utilisée", () => {
572
+ const mur = {
573
+ donnee_entree: {
574
+ methode_saisie_u:
575
+ "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)",
576
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
577
+ enum_type_doublage_id: '2',
578
+ enum_periode_isolation_id: '6'
579
+ }
580
+ };
581
+
582
+ calc_mur(mur, 'h1a', '1', '0');
583
+
584
+ expect(tv).toHaveBeenCalledWith(
585
+ 'umur',
586
+ expect.objectContaining({ enum_periode_construction_id: '6' }),
587
+ mur.donnee_entree
588
+ );
589
+ });
590
+
591
+ test("année d'isolation différente sans période d'isolation : repli sur la période de construction", () => {
592
+ const mur = {
593
+ donnee_entree: {
594
+ methode_saisie_u:
595
+ "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)",
596
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
597
+ enum_type_doublage_id: '2'
598
+ }
599
+ };
600
+
601
+ calc_mur(mur, 'h1a', '5', '0');
602
+
603
+ expect(tv).toHaveBeenCalledWith(
604
+ 'umur',
605
+ expect.objectContaining({ enum_periode_construction_id: '5' }),
606
+ mur.donnee_entree
607
+ );
608
+ });
609
+
610
+ test('Umur introuvable dans la table : erreur émise', () => {
611
+ tv.mockImplementation((table) => (table === 'umur' ? null : { ...ROW_UMUR0 }));
612
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
613
+ const mur = {
614
+ donnee_entree: {
615
+ methode_saisie_u: 'isolation inconnue (table forfaitaire)',
616
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
617
+ enum_type_doublage_id: '2'
618
+ }
619
+ };
620
+
621
+ calc_mur(mur, 'h1a', '1', '0');
622
+
623
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('umur'));
624
+ error.mockRestore();
625
+ });
626
+ });
627
+
628
+ describe('calc_mur - année de construction saisie (table forfaitaire)', () => {
629
+ test("période d'isolation renseignée : elle est utilisée", () => {
630
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
631
+ const mur = {
632
+ donnee_entree: {
633
+ methode_saisie_u: 'année de construction saisie (table forfaitaire)',
634
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
635
+ enum_type_doublage_id: '2',
636
+ enum_periode_isolation_id: '6'
637
+ }
638
+ };
639
+
640
+ calc_mur(mur, 'h1a', '5', '0');
641
+
642
+ expect(tv).toHaveBeenCalledWith(
643
+ 'umur',
644
+ expect.objectContaining({ enum_periode_construction_id: '6' }),
645
+ mur.donnee_entree
646
+ );
647
+ warn.mockRestore();
648
+ });
649
+
650
+ test.each([
651
+ ['1', 'avant 1948'],
652
+ ['3', '1948-1974']
653
+ ])(
654
+ "construction ancienne (%s = %s) sans année d'isolation : isolation forcée à 1975-1977 + avertissement",
655
+ (pcId) => {
656
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
657
+ // pi_id est parseInt(getKeyByValue(...)) = 2 (nombre) ; on renvoie un tv_umur_id différent
658
+ tv.mockImplementation((table, matcher) => {
659
+ if (table === 'umur0') return { ...ROW_UMUR0 };
660
+ if (table === 'umur') {
661
+ return {
662
+ umur: '0.5',
663
+ tv_umur_id: matcher.enum_periode_construction_id === 2 ? '9' : '6'
664
+ };
665
+ }
666
+ return null;
667
+ });
668
+ const mur = {
669
+ donnee_entree: {
670
+ methode_saisie_u: 'année de construction saisie (table forfaitaire)',
671
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
672
+ enum_type_doublage_id: '2'
673
+ }
674
+ };
675
+
676
+ calc_mur(mur, 'h1a', pcId, '0');
677
+
678
+ // parseInt(getKeyByValue(periode_isolation, '1975-1977'), 10) = 2 (nombre)
679
+ expect(tv).toHaveBeenCalledWith(
680
+ 'umur',
681
+ expect.objectContaining({ enum_periode_construction_id: 2 }),
682
+ mur.donnee_entree
683
+ );
684
+ expect(warn).toHaveBeenCalled();
685
+ warn.mockRestore();
686
+ }
687
+ );
688
+
689
+ test('bug_for_bug : second appel forfaitaire avec la période de construction', () => {
690
+ bugForBugCompat = true;
691
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
692
+ tv.mockImplementation((table, matcher) => {
693
+ if (table === 'umur0') return { ...ROW_UMUR0 };
694
+ // Relecture via tv_umur_id : effet Joule et période cohérents (pas de correction)
695
+ if (table === 'umur' && matcher.tv_umur_id) {
696
+ return { effet_joule: '0', enum_periode_construction_id: '1' };
697
+ }
698
+ if (table === 'umur') {
699
+ // 1er appel : période d'isolation (2) => tv_umur_id 9 ; 2e appel bug : période '1' => 3
700
+ return {
701
+ umur: '0.5',
702
+ tv_umur_id: matcher.enum_periode_construction_id === '1' ? '3' : '9'
703
+ };
704
+ }
705
+ return null;
706
+ });
707
+ const mur = {
708
+ donnee_entree: {
709
+ methode_saisie_u: 'année de construction saisie (table forfaitaire)',
710
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
711
+ enum_type_doublage_id: '2',
712
+ description: 'mur'
713
+ }
714
+ };
715
+
716
+ calc_mur(mur, 'h1a', '1', '0');
717
+
718
+ // Le dernier appel forfaitaire réutilise la période de construction d'origine ('1')
719
+ expect(tv).toHaveBeenLastCalledWith(
720
+ 'umur',
721
+ expect.objectContaining({ enum_periode_construction_id: '1' }),
722
+ mur.donnee_entree
723
+ );
724
+ warn.mockRestore();
725
+ });
726
+
727
+ test("construction récente sans année d'isolation : pas de bascule ni d'avertissement", () => {
728
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
729
+ const mur = {
730
+ donnee_entree: {
731
+ methode_saisie_u: 'année de construction saisie (table forfaitaire)',
732
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
733
+ enum_type_doublage_id: '2'
734
+ }
735
+ };
736
+
737
+ calc_mur(mur, 'h1a', '5', '0');
738
+
739
+ expect(tv).toHaveBeenCalledWith(
740
+ 'umur',
741
+ expect.objectContaining({ enum_periode_construction_id: '5' }),
742
+ mur.donnee_entree
743
+ );
744
+ expect(warn).not.toHaveBeenCalled();
745
+ warn.mockRestore();
746
+ });
747
+ });
748
+
749
+ /**
750
+ * Compatibilité "bug for bug" du calcul forfaitaire de Umur : la ligne relue via `tv_umur_id`
751
+ * peut corriger l'effet Joule et la période de construction utilisés.
752
+ */
753
+ describe('calc_mur - bug_for_bug sur tv_umur (récupération depuis tv_umur_id)', () => {
754
+ beforeEach(() => {
755
+ bugForBugCompat = true;
756
+ });
757
+
758
+ test('effet_joule et période de construction du DPE conservés', () => {
759
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
760
+ tv.mockImplementation((table, matcher) => {
761
+ if (table === 'umur0') return { ...ROW_UMUR0 };
762
+ if (table === 'umur' && matcher.tv_umur_id) {
763
+ return { effet_joule: '1', enum_periode_construction_id: '5|6' };
764
+ }
765
+ if (table === 'umur') return { ...ROW_UMUR };
766
+ return null;
767
+ });
768
+ const mur = {
769
+ donnee_entree: {
770
+ methode_saisie_u: 'isolation inconnue (table forfaitaire)',
771
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
772
+ enum_type_doublage_id: '2',
773
+ tv_umur_id: 6,
774
+ description: 'mur test'
775
+ }
776
+ };
777
+
778
+ calc_mur(mur, 'h1a', '1', '0');
779
+
780
+ expect(error).toHaveBeenCalled();
781
+ // Le matcher final utilise l'effet Joule ('1') et la période ('5') issus du DPE
782
+ expect(tv).toHaveBeenLastCalledWith(
783
+ 'umur',
784
+ expect.objectContaining({ effet_joule: '1', enum_periode_construction_id: '5' }),
785
+ mur.donnee_entree
786
+ );
787
+ error.mockRestore();
788
+ });
789
+ });
790
+
791
+ describe('calc_mur - saisie directe de Umur', () => {
792
+ test('Umur pris depuis umur_saisi et Umur0 depuis la donnée intermédiaire', () => {
793
+ const mur = {
794
+ donnee_entree: {
795
+ methode_saisie_u:
796
+ 'saisie direct u justifiée (à partir des documents justificatifs autorisés)',
797
+ umur_saisi: 0.15
798
+ },
799
+ donnee_intermediaire: { umur0: 1.8 }
800
+ };
801
+
802
+ calc_mur(mur, 'h1a', '1', '0');
803
+
804
+ expect(mur.donnee_intermediaire.umur).toBe(0.15);
805
+ expect(mur.donnee_intermediaire.umur0).toBe(1.8);
806
+ });
807
+ });
808
+
809
+ describe('calc_mur - méthode de saisie de U inconnue', () => {
810
+ test('aucun calcul de Umur et avertissement émis', () => {
811
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
812
+ const mur = { donnee_entree: { methode_saisie_u: 'valeur inexistante' } };
813
+
814
+ calc_mur(mur, 'h1a', '1', '0');
815
+
816
+ expect(mur.donnee_intermediaire.umur).toBeUndefined();
817
+ expect(warn).toHaveBeenCalledWith('methode_saisie_u inconnue:', 'valeur inexistante');
818
+ warn.mockRestore();
46
819
  });
47
820
  });