@open3cl/engine 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/10_besoin_fr.spec.js +122 -0
  2. package/10_clim.spec.js +116 -0
  3. package/11_besoin_ecs.spec.js +69 -0
  4. package/11_ecs.spec.js +282 -0
  5. package/11_nadeq.spec.js +37 -0
  6. package/12.4_pac.spec.js +131 -0
  7. package/13.2_generateur_combustion.js +2 -0
  8. package/13.2_generateur_combustion.spec.js +537 -0
  9. package/13.2_generateur_combustion_bouilleur.js +4 -1
  10. package/13.2_generateur_combustion_bouilleur.spec.js +74 -0
  11. package/13.2_generateur_combustion_ch.spec.js +323 -0
  12. package/13.2_generateur_combustion_chaudiere.spec.js +85 -0
  13. package/13.2_generateur_pac.spec.js +64 -0
  14. package/13_rendement_distribution_ecs.spec.js +38 -0
  15. package/14_generateur_ecs.js +4 -4
  16. package/14_generateur_ecs.spec.js +745 -0
  17. package/15_conso_aux.spec.js +225 -0
  18. package/16.2_production_enr.spec.js +188 -0
  19. package/16_conso_eclairage.spec.js +62 -0
  20. package/2021_04_13_confort_ete.spec.js +159 -0
  21. package/2021_04_13_qualite_isolation.js +1 -1
  22. package/2021_04_13_qualite_isolation.spec.js +286 -0
  23. package/3.1_b.js +1 -1
  24. package/3.1_b.spec.js +367 -0
  25. package/3.2.1_mur.spec.js +812 -39
  26. package/3.2.2_plancher_bas.spec.js +643 -78
  27. package/3.2.3_plancher_haut.spec.js +452 -0
  28. package/3.3.1.4_porte.spec.js +114 -0
  29. package/3.3_baie_vitree.spec.js +449 -295
  30. package/3.4_pont_thermique.spec.js +890 -0
  31. package/3_deperdition.spec.js +397 -0
  32. package/4_ventilation.js +1 -1
  33. package/4_ventilation.spec.js +406 -0
  34. package/5_conso_ventilation.spec.js +186 -0
  35. package/6.2_surface_sud_equivalente.spec.js +138 -0
  36. package/8_intermittence.spec.js +33 -0
  37. package/9_besoin_ch.js +2 -2
  38. package/9_besoin_ch.spec.js +381 -0
  39. package/9_chauffage.js +4 -0
  40. package/9_chauffage.spec.js +478 -92
  41. package/9_conso_ch.spec.js +502 -248
  42. package/9_emetteur_ch.spec.js +339 -0
  43. package/9_generateur_ch.spec.js +467 -81
  44. package/README.md +18 -9
  45. package/apport_et_besoin.spec.js +126 -0
  46. package/conso.js +1 -1
  47. package/conso.spec.js +471 -82
  48. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  49. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  50. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  51. package/core/conf/infrastructure/application.config.spec.js +35 -0
  52. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  53. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  54. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  55. package/core/util/infrastructure/object-util.spec.js +20 -0
  56. package/core/util/logger/log-service.js +16 -7
  57. package/core/util/logger/log-service.spec.js +101 -0
  58. package/dpe-sanitizer.service.spec.js +138 -0
  59. package/engine.js +7 -5
  60. package/engine.spec.js +854 -0
  61. package/enums.spec.js +46 -0
  62. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  63. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  64. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  65. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  66. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  67. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  68. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  69. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  70. package/features/dpe/infrastructure/tv.store.js +28 -9
  71. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  72. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  73. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  74. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  75. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  76. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  77. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  78. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  79. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  80. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  81. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  82. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  83. package/features/engine/domain/constants.spec.js +14 -0
  84. package/features/engine/domain/contexte.builder.spec.js +21 -0
  85. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  86. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  87. package/features/engine/domain/engine.service.spec.js +227 -4
  88. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  89. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  90. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  91. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  92. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  93. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  94. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  95. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  96. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  97. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  98. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  99. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  100. package/ficheTechnique.spec.js +26 -0
  101. package/index.spec.js +54 -0
  102. package/package.json +5 -2
  103. package/preprocessor/index.js +20 -0
  104. package/preprocessor/index.spec.js +15 -0
  105. package/tv.spec.js +38 -0
  106. package/utils.js +43 -1
  107. package/utils.spec.js +518 -2
  108. package/output.js +0 -1
@@ -1,89 +1,654 @@
1
- import calc_pb from './3.2.2_plancher_bas.js';
2
- import { describe, expect, test } from 'vitest';
3
-
4
- describe('Recherche de bugs dans le calcul de déperdition des planchers bas', () => {
5
- describe('calcul de déperdition pour les planchers bas de 2287E1043883T', () => {
6
- test('Mur 1 Nord', () => {
7
- const zc = 3;
8
- const pc_id = 1;
9
- const ej = 0;
10
- const pb1 = {
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `calc_pb` :
5
+ * - `enums` : mapping minimal type d'adjacence / 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 Upb ici) ;
7
+ * - `tv` : accès aux tables `upb0` / `upb` / `ue`, on contrôle la ligne retournée par table ;
8
+ * - `requestInput` : passe-plat renvoyant la donnée d'entrée demandée ;
9
+ * - `getKeyByValue` : implémentation déterministe (indépendante des vraies enums) ;
10
+ * - `getRange` : contrôlé par test pour fournir l'encadrement de Upb dans le calcul de Ue ;
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
+ type_adjacence: {
18
+ 5: 'terre-plein',
19
+ 6: 'sous-sol non chauffé',
20
+ 7: 'vide sanitaire',
21
+ 8: 'extérieur'
22
+ },
23
+ periode_construction: { 1: 'avant 1948', 3: '1948-1974', 5: '1989-2000' },
24
+ periode_isolation: { 2: '1975-1977', 6: '2006-2012' }
25
+ }
26
+ }));
27
+
28
+ vi.mock('./3.1_b.js', () => ({
29
+ default: vi.fn()
30
+ }));
31
+
32
+ vi.mock('./utils.js', () => ({
33
+ tv: vi.fn(),
34
+ requestInput: (de, du, field) => de[field],
35
+ getKeyByValue: (object, value) => Object.keys(object).find((key) => object[key] === value),
36
+ getRange: vi.fn(),
37
+ get bug_for_bug_compat() {
38
+ return bugForBugCompat;
39
+ }
40
+ }));
41
+
42
+ const { default: calc_pb } = await import('./3.2.2_plancher_bas.js');
43
+ const { tv, getRange } = await import('./utils.js');
44
+
45
+ /** Table `upb0` figée : U0 forfaitaire de la paroi nue. */
46
+ const ROW_UPB0 = { upb0: '2', tv_upb0_id: '9' };
47
+ /** Table `upb` figée : U forfaitaire de la paroi isolée. */
48
+ const ROW_UPB = { upb: '0.3', tv_upb_id: '2' };
49
+
50
+ beforeEach(() => {
51
+ bugForBugCompat = false;
52
+ tv.mockReset();
53
+ getRange.mockReset();
54
+ tv.mockImplementation((table) => {
55
+ if (table === 'upb0') return { ...ROW_UPB0 };
56
+ if (table === 'upb') return { ...ROW_UPB };
57
+ return null;
58
+ });
59
+ });
60
+
61
+ /**
62
+ * 3.2.2 Coefficient de transmission des planchers bas (Upb)
63
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2
64
+ */
65
+ describe('calc_pb - plancher non isolé (methode 1)', () => {
66
+ test('Upb égal à Upb0 (paroi nue) et adjacence non déperditive => upb_final = upb', () => {
67
+ const pb = {
68
+ donnee_entree: {
69
+ enum_methode_saisie_u_id: '1',
70
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
71
+ enum_type_plancher_bas_id: '9',
72
+ type_adjacence: 'extérieur'
73
+ }
74
+ };
75
+
76
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
77
+
78
+ expect(pb.donnee_intermediaire.upb0).toBe(2);
79
+ expect(pb.donnee_intermediaire.upb).toBe(2);
80
+ expect(pb.donnee_intermediaire.upb_final).toBe(2);
81
+ });
82
+
83
+ test('Upb0 initialisé depuis une donnée intermédiaire préexistante', () => {
84
+ const pb = {
85
+ donnee_entree: {
86
+ enum_methode_saisie_u_id: '1',
87
+ methode_saisie_u0: 'u0 non saisi car le u est saisi connu et justifié.',
88
+ type_adjacence: 'extérieur'
89
+ },
90
+ donnee_intermediaire: { upb0: 1.4 }
91
+ };
92
+
93
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
94
+
95
+ // 'u0 non saisi' ne recalcule pas Upb0 : la valeur préexistante est conservée
96
+ expect(pb.donnee_intermediaire.upb0).toBe(1.4);
97
+ expect(pb.donnee_intermediaire.upb).toBe(1.4);
98
+ });
99
+
100
+ test('Upb0 introuvable dans la table : erreur émise', () => {
101
+ tv.mockReturnValue(null);
102
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
103
+ const pb = {
104
+ donnee_entree: {
105
+ enum_methode_saisie_u_id: '1',
106
+ methode_saisie_u0:
107
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
108
+ enum_type_plancher_bas_id: '9',
109
+ type_adjacence: 'extérieur'
110
+ }
111
+ };
112
+
113
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
114
+
115
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('upb0'));
116
+ expect(pb.donnee_intermediaire.upb0).toBeUndefined();
117
+ error.mockRestore();
118
+ });
119
+ });
120
+
121
+ describe('calc_pb - calcul de Upb0 (calc_upb0)', () => {
122
+ test('saisie directe de Upb0 : valeur reprise depuis upb0_saisi sans accès table', () => {
123
+ const pb = {
124
+ donnee_entree: {
125
+ enum_methode_saisie_u_id: '1',
126
+ methode_saisie_u0:
127
+ 'saisie direct u0 justifiée à partir des documents justificatifs autorisés',
128
+ upb0_saisi: 0.8,
129
+ type_adjacence: 'extérieur'
130
+ }
131
+ };
132
+
133
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
134
+
135
+ expect(tv).not.toHaveBeenCalled();
136
+ expect(pb.donnee_intermediaire.upb0).toBe(0.8);
137
+ expect(pb.donnee_intermediaire.upb).toBe(0.8);
138
+ });
139
+
140
+ test('methode_saisie_u0 inconnue : avertissement émis', () => {
141
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
142
+ const pb = {
143
+ donnee_entree: {
144
+ enum_methode_saisie_u_id: '1',
145
+ methode_saisie_u0: 'valeur inexistante',
146
+ type_adjacence: 'extérieur'
147
+ }
148
+ };
149
+
150
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
151
+
152
+ expect(warn).toHaveBeenCalledWith('methode_saisie_u0 inconnue:', 'valeur inexistante');
153
+ warn.mockRestore();
154
+ });
155
+ });
156
+
157
+ describe('calc_pb - isolation par épaisseur / résistance', () => {
158
+ test.each([['3'], ['4']])(
159
+ "épaisseur d'isolation saisie (methode %s) : Upb = 1 / (1/Upb0 + e/0,042)",
160
+ (methode) => {
161
+ const pb = {
11
162
  donnee_entree: {
12
- description:
13
- 'Plancher 1 - Plancher lourd type entrevous terre-cuite, poutrelles béton donnant sur un sous-sol non chauffé avec isolation intrinsèque ou en sous-face (5 cm)',
14
- reference: '2022_05_11_17_10_23_1067244006611344',
15
- tv_coef_reduction_deperdition_id: 6,
16
- enum_type_adjacence_id: '6',
17
- surface_paroi_opaque: 42.08,
18
- tv_upb0_id: 11,
19
- enum_type_plancher_bas_id: '11',
20
- enum_methode_saisie_u0_id: '2',
21
- enum_type_isolation_id: '4',
22
- epaisseur_isolation: 5,
23
- enum_methode_saisie_u_id: '3',
24
- calcul_ue: 1,
25
- perimetre_ue: 27,
26
- surface_ue: 42.08,
27
- ue: 0.37117494
28
- },
29
- donnee_intermediaire: {
30
- b: 1,
31
- upb: 0.5915492957746479,
32
- upb_final: 0.37117494,
33
- upb0: 2
163
+ enum_methode_saisie_u_id: methode,
164
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
165
+ enum_type_plancher_bas_id: '9',
166
+ epaisseur_isolation: '5',
167
+ type_adjacence: 'extérieur'
34
168
  }
35
169
  };
36
170
 
171
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
172
+
173
+ // Upb0 = 2 ; e = 5 * 0,01 = 0,05 m ; Upb = 1 / (1/2 + 0,05/0,042)
174
+ // valeur de référence de régression
175
+ expect(pb.donnee_intermediaire.upb).toBeCloseTo(0.5915492957746479, 9);
176
+ }
177
+ );
178
+
179
+ test.each([['5'], ['6']])(
180
+ "résistance d'isolation saisie (methode %s) : Upb = 1 / (1/Upb0 + R)",
181
+ (methode) => {
37
182
  const pb = {
38
183
  donnee_entree: {
39
- description: 'Plancher 2 - Dalle béton donnant sur un terre-plein',
40
- reference: '2022_05_11_17_10_51_2415353009491032',
41
- tv_coef_reduction_deperdition_id: 5,
42
- enum_type_adjacence_id: '5', // Terre-Plein
43
- surface_paroi_opaque: 24.34,
44
- tv_upb0_id: 9,
45
- enum_type_plancher_bas_id: '9', // Dalle béton
46
- enum_methode_saisie_u0_id: '2', // déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire
47
- enum_type_isolation_id: '1', // inconnu
48
- enum_periode_isolation_id: '1', // avant 1948
49
- tv_upb_id: 2,
50
- enum_methode_saisie_u_id: '8', // année de construction saisie (table forfaitaire)
51
- calcul_ue: 1,
52
- perimetre_ue: 22,
53
- surface_ue: 24.34,
54
- ue: 0.61789474
55
- },
56
- donnee_intermediaire: {
57
- b: 1,
58
- upb: 2,
59
- upb_final: 0.61789474,
60
- upb0: 2
184
+ enum_methode_saisie_u_id: methode,
185
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
186
+ enum_type_plancher_bas_id: '9',
187
+ resistance_isolation: '1',
188
+ type_adjacence: 'extérieur'
61
189
  }
62
190
  };
63
- const pb_list = [pb1, pb];
64
-
65
- /**
66
- * Sortie lib (différence)
67
- * tv_upb_id: 8 (2 dans dpe origine)
68
- * ue: 0.44 (0.61789474 dans dpe origine)
69
- * upb: 0.9 (2 dans dpe origine)
70
- * upb_final: 0.44 (0.61789474 dans dpe origine)
71
- */
72
- calc_pb(pb, zc, pc_id, ej, pb_list);
73
-
74
- /**
75
- * P = 22
76
- * S = 24.34
77
- * 2S/P = (2 * 24.34) / 22 = 2,212727273 = 2 (arrondi à l'entier le plus proche)
78
- * upb0 = 2
79
- * upb = Min(upb0; upbtab) = Min(2; 2) = 2
80
- * ue = Umoyen pour tous les planchers du batiment
81
- * upb_final = ue
82
- */
83
- expect(pb.donnee_intermediaire.b).toBe(1);
84
- expect(pb.donnee_intermediaire.upb0).toBe(2);
85
- expect(pb.donnee_intermediaire.upb).toBe(2);
86
- expect(pb.donnee_intermediaire.upb_final).toBe(pb.donnee_entree.ue);
191
+
192
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
193
+
194
+ // Upb0 = 2 ; R = 1 ; Upb = 1 / (1/2 + 1)
195
+ // valeur de référence de régression
196
+ expect(pb.donnee_intermediaire.upb).toBeCloseTo(0.6666666666666666, 9);
197
+ }
198
+ );
199
+ });
200
+
201
+ describe('calc_pb - isolation inconnue / table forfaitaire (methodes 2 et 7)', () => {
202
+ test("Upb = min(Upb table, Upb0) et la période d'isolation prime", () => {
203
+ const pb = {
204
+ donnee_entree: {
205
+ enum_methode_saisie_u_id: '2',
206
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
207
+ enum_type_plancher_bas_id: '9',
208
+ enum_periode_isolation_id: '6',
209
+ type_adjacence: 'extérieur'
210
+ }
211
+ };
212
+
213
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
214
+
215
+ // La période d'isolation renseignée est utilisée comme période de la table
216
+ expect(tv).toHaveBeenCalledWith(
217
+ 'upb',
218
+ expect.objectContaining({ enum_periode_construction_id: '6' }),
219
+ pb.donnee_entree
220
+ );
221
+ // min(0,3 ; 2) = 0,3
222
+ expect(pb.donnee_intermediaire.upb).toBe(0.3);
223
+ });
224
+
225
+ test("sans période d'isolation, la période de construction est utilisée", () => {
226
+ const pb = {
227
+ donnee_entree: {
228
+ enum_methode_saisie_u_id: '7',
229
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
230
+ enum_type_plancher_bas_id: '9',
231
+ type_adjacence: 'extérieur'
232
+ }
233
+ };
234
+
235
+ calc_pb(pb, 'h1a', '5', '0', [pb]);
236
+
237
+ expect(tv).toHaveBeenCalledWith(
238
+ 'upb',
239
+ expect.objectContaining({ enum_periode_construction_id: '5' }),
240
+ pb.donnee_entree
241
+ );
242
+ });
243
+
244
+ test('Upb introuvable dans la table forfaitaire : erreur émise', () => {
245
+ tv.mockImplementation((table) => (table === 'upb0' ? { ...ROW_UPB0 } : null));
246
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
247
+ const pb = {
248
+ donnee_entree: {
249
+ enum_methode_saisie_u_id: '2',
250
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
251
+ enum_type_plancher_bas_id: '9',
252
+ type_adjacence: 'extérieur'
253
+ }
254
+ };
255
+
256
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
257
+
258
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('upb'));
259
+ error.mockRestore();
260
+ });
261
+ });
262
+
263
+ describe('calc_pb - année de construction saisie (methode 8)', () => {
264
+ test("période d'isolation renseignée : elle est utilisée", () => {
265
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
266
+ const pb = {
267
+ donnee_entree: {
268
+ enum_methode_saisie_u_id: '8',
269
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
270
+ enum_type_plancher_bas_id: '9',
271
+ enum_periode_isolation_id: '6',
272
+ type_adjacence: 'extérieur'
273
+ }
274
+ };
275
+
276
+ calc_pb(pb, 'h1a', '5', '0', [pb]);
277
+
278
+ expect(tv).toHaveBeenCalledWith(
279
+ 'upb',
280
+ expect.objectContaining({ enum_periode_construction_id: '6' }),
281
+ pb.donnee_entree
282
+ );
283
+ warn.mockRestore();
284
+ });
285
+
286
+ test.each([
287
+ ['1', 'avant 1948'],
288
+ ['3', '1948-1974']
289
+ ])(
290
+ "construction ancienne (%s = %s) sans année d'isolation : isolation forcée à 1975-1977",
291
+ (pcId) => {
292
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
293
+ const pb = {
294
+ donnee_entree: {
295
+ enum_methode_saisie_u_id: '8',
296
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
297
+ enum_type_plancher_bas_id: '9',
298
+ type_adjacence: 'extérieur'
299
+ }
300
+ };
301
+
302
+ calc_pb(pb, 'h1a', pcId, '0', [pb]);
303
+
304
+ // getKeyByValue(periode_isolation, '1975-1977') = '2' (différent de pc_id) => avertissement
305
+ expect(tv).toHaveBeenCalledWith(
306
+ 'upb',
307
+ expect.objectContaining({ enum_periode_construction_id: '2' }),
308
+ pb.donnee_entree
309
+ );
310
+ expect(warn).toHaveBeenCalled();
311
+ warn.mockRestore();
312
+ }
313
+ );
314
+
315
+ test('construction ancienne + bug_for_bug : second appel avec la période de construction', () => {
316
+ bugForBugCompat = true;
317
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
318
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
319
+ // 1er appel (période 1975-1977) renvoie tv_upb_id = 2 ; l'appel avec la période de
320
+ // construction ('1') renvoie tv_upb_id = 3 (valeur différente => déclenche le recalcul).
321
+ tv.mockImplementation((table, matcher) => {
322
+ if (table === 'upb0') return { ...ROW_UPB0 };
323
+ if (table === 'upb') {
324
+ return {
325
+ upb: '0.3',
326
+ effet_joule: '0',
327
+ tv_upb_id: matcher.enum_periode_construction_id === '1' ? '3' : '2'
328
+ };
329
+ }
330
+ return null;
331
+ });
332
+ const pb = {
333
+ donnee_entree: {
334
+ enum_methode_saisie_u_id: '8',
335
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
336
+ enum_type_plancher_bas_id: '9',
337
+ type_adjacence: 'extérieur'
338
+ }
339
+ };
340
+
341
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
342
+
343
+ // L'avertissement de bascule d'année d'isolation est émis
344
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('Année d'));
345
+ // Le dernier appel forfaitaire utilise la période de construction d'origine ('1')
346
+ expect(tv).toHaveBeenLastCalledWith(
347
+ 'upb',
348
+ expect.objectContaining({ enum_periode_construction_id: '1' }),
349
+ pb.donnee_entree
350
+ );
351
+ warn.mockRestore();
352
+ error.mockRestore();
353
+ });
354
+
355
+ test("construction récente sans année d'isolation : pas de forçage ni d'avertissement", () => {
356
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
357
+ const pb = {
358
+ donnee_entree: {
359
+ enum_methode_saisie_u_id: '8',
360
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
361
+ enum_type_plancher_bas_id: '9',
362
+ type_adjacence: 'extérieur'
363
+ }
364
+ };
365
+
366
+ calc_pb(pb, 'h1a', '5', '0', [pb]);
367
+
368
+ expect(tv).toHaveBeenCalledWith(
369
+ 'upb',
370
+ expect.objectContaining({ enum_periode_construction_id: '5' }),
371
+ pb.donnee_entree
372
+ );
373
+ expect(warn).not.toHaveBeenCalled();
374
+ warn.mockRestore();
375
+ });
376
+ });
377
+
378
+ describe('calc_pb - saisie directe de Upb (methodes 9 et 10)', () => {
379
+ test.each([['9'], ['10']])('Upb pris depuis upb_saisi (methode %s)', (methode) => {
380
+ const pb = {
381
+ donnee_entree: {
382
+ enum_methode_saisie_u_id: methode,
383
+ upb_saisi: 0.22,
384
+ type_adjacence: 'extérieur'
385
+ }
386
+ };
387
+
388
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
389
+
390
+ expect(pb.donnee_intermediaire.upb).toBe(0.22);
391
+ });
392
+ });
393
+
394
+ describe('calc_pb - méthode de saisie de U inconnue', () => {
395
+ test('aucun calcul de Upb et avertissement émis', () => {
396
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
397
+ const pb = {
398
+ donnee_entree: {
399
+ enum_methode_saisie_u_id: '99',
400
+ type_adjacence: 'extérieur'
401
+ }
402
+ };
403
+
404
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
405
+
406
+ expect(warn).toHaveBeenCalledWith('methode_saisie_u inconnue:', 99);
407
+ warn.mockRestore();
408
+ });
409
+ });
410
+
411
+ /**
412
+ * Compatibilité "bug for bug" : la méthode de saisie de U est forcée quand une donnée d'entrée
413
+ * (résistance, épaisseur ou Upb saisi) est présente mais incohérente avec la méthode déclarée.
414
+ */
415
+ describe('calc_pb - compatibilité bug for bug (forçage de la méthode de saisie)', () => {
416
+ beforeEach(() => {
417
+ bugForBugCompat = true;
418
+ });
419
+
420
+ test('résistance connue mais methode != 5/6 : méthode forcée à 5', () => {
421
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
422
+ const pb = {
423
+ donnee_entree: {
424
+ enum_methode_saisie_u_id: '1',
425
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
426
+ enum_type_plancher_bas_id: '9',
427
+ resistance_isolation: '1',
428
+ type_adjacence: 'extérieur'
429
+ }
430
+ };
431
+
432
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
433
+
434
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('resistance isolation saisie'));
435
+ // Upb calculé comme en methode 5 : 1 / (1/2 + 1)
436
+ expect(pb.donnee_intermediaire.upb).toBeCloseTo(0.6666666666666666, 9);
437
+ error.mockRestore();
438
+ });
439
+
440
+ test('épaisseur connue mais methode != 3/4 : méthode forcée à 3', () => {
441
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
442
+ const pb = {
443
+ donnee_entree: {
444
+ enum_methode_saisie_u_id: '1',
445
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
446
+ enum_type_plancher_bas_id: '9',
447
+ epaisseur_isolation: '5',
448
+ type_adjacence: 'extérieur'
449
+ }
450
+ };
451
+
452
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
453
+
454
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('epaisseur isolation saisie'));
455
+ expect(pb.donnee_intermediaire.upb).toBeCloseTo(0.5915492957746479, 9);
456
+ error.mockRestore();
457
+ });
458
+
459
+ test('Upb saisi mais methode != 9/10 : méthode forcée à 9', () => {
460
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
461
+ const pb = {
462
+ donnee_entree: {
463
+ enum_methode_saisie_u_id: '1',
464
+ upb_saisi: 0.25,
465
+ type_adjacence: 'extérieur'
466
+ }
467
+ };
468
+
469
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
470
+
471
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('saisie direct u'));
472
+ expect(pb.donnee_intermediaire.upb).toBe(0.25);
473
+ error.mockRestore();
474
+ });
475
+
476
+ test('effet_joule du DPE différent de celui attendu : valeur du DPE conservée dans le matcher', () => {
477
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
478
+ // La relecture via tv_upb_id renvoie effet_joule = '1' alors que l'effet Joule attendu est '0'
479
+ tv.mockImplementation((table, matcher) => {
480
+ if (table === 'upb0') return { ...ROW_UPB0 };
481
+ if (table === 'upb' && matcher.tv_upb_id) return { effet_joule: '1', tv_upb_id: '2' };
482
+ if (table === 'upb') return { ...ROW_UPB };
483
+ return null;
484
+ });
485
+ const pb = {
486
+ donnee_entree: {
487
+ enum_methode_saisie_u_id: '2',
488
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
489
+ enum_type_plancher_bas_id: '9',
490
+ tv_upb_id: 2,
491
+ type_adjacence: 'extérieur',
492
+ description: 'PB test'
493
+ }
494
+ };
495
+
496
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
497
+
498
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('effet_joule'));
499
+ // Le matcher forfaitaire final utilise l'effet Joule '1' issu du DPE
500
+ expect(tv).toHaveBeenLastCalledWith(
501
+ 'upb',
502
+ expect.objectContaining({ effet_joule: '1' }),
503
+ pb.donnee_entree
504
+ );
505
+ error.mockRestore();
506
+ });
507
+
508
+ test('données cohérentes avec la méthode : aucun forçage', () => {
509
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
510
+ const pb = {
511
+ donnee_entree: {
512
+ enum_methode_saisie_u_id: '5',
513
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
514
+ enum_type_plancher_bas_id: '9',
515
+ resistance_isolation: '1',
516
+ epaisseur_isolation: '5',
517
+ upb_saisi: 0.25,
518
+ type_adjacence: 'extérieur'
519
+ }
520
+ };
521
+
522
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
523
+
524
+ // methode 5 cohérente avec resistance_isolation => pas d'erreur de forçage de résistance,
525
+ // mais épaisseur et upb_saisi présents restent incohérents (couverture des branches vraies)
526
+ expect(error).toHaveBeenCalled();
527
+ error.mockRestore();
528
+ });
529
+ });
530
+
531
+ /**
532
+ * Calcul de la déperdition Ue vers un plancher bas sur terre-plein / vide sanitaire / sous-sol.
533
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 (Ue)
534
+ */
535
+ describe('calc_pb - calcul de Ue (tv_ue)', () => {
536
+ /** Renvoie une ue interpolée selon la valeur de upb du matcher. */
537
+ const mockTvUe = (ueForUpb) => {
538
+ tv.mockImplementation((table, matcher) => {
539
+ if (table === 'upb0') return { ...ROW_UPB0, upb0: '0.4' };
540
+ if (table === 'ue') return { ue: String(ueForUpb[matcher.upb]) };
541
+ return null;
87
542
  });
543
+ };
544
+
545
+ test('terre-plein, bâtiment avant 2001 (pc < 7) : interpolation linéaire de Ue', () => {
546
+ getRange.mockReturnValue([0.3, 0.5]);
547
+ mockTvUe({ 0.3: 0.2, 0.5: 0.4 });
548
+
549
+ // Deux planchers de même adjacence + un plancher d'adjacence différente (ignoré)
550
+ const autre = {
551
+ donnee_entree: { enum_type_adjacence_id: '8', surface_paroi_opaque: 99, perimetre_ue: 99 }
552
+ };
553
+ const pb1 = {
554
+ donnee_entree: { enum_type_adjacence_id: '5', surface_ue: 20, perimetre_ue: 12 }
555
+ };
556
+ const pb = {
557
+ donnee_entree: {
558
+ enum_methode_saisie_u_id: '1',
559
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
560
+ enum_type_plancher_bas_id: '9',
561
+ type_adjacence: 'terre-plein',
562
+ enum_type_adjacence_id: '5',
563
+ surface_paroi_opaque: 4 // pas de surface_ue => fallback
564
+ }
565
+ };
566
+
567
+ calc_pb(pb, 'h1a', '1', '0', [pb1, pb, autre]);
568
+
569
+ // surfaceUe = 20 (pb1.surface_ue) + 4 (pb.surface_paroi_opaque) = 24 ; perimetreUe = 12 + 0 = 12
570
+ // 2S/P = round(48/12) = 4 (valeur présente dans values_2s_p)
571
+ expect(tv).toHaveBeenCalledWith('ue', {
572
+ type_adjacence_plancher: 'terre plein bâtiment construit avant 2001',
573
+ '2s_p': '^4$',
574
+ upb: '0.3'
575
+ });
576
+ // di.upb = 0,4 ; interpolation : 0,2 + (0,4-0,3)*(0,4-0,2)/(0,5-0,3) = 0,3
577
+ // valeur de référence de régression
578
+ expect(pb.donnee_entree.ue).toBeCloseTo(0.3, 9);
579
+ expect(pb.donnee_intermediaire.upb_final).toBeCloseTo(0.3, 9);
580
+ });
581
+
582
+ test('terre-plein, bâtiment à partir de 2001 (pc >= 7) : catégorie adaptée', () => {
583
+ getRange.mockReturnValue([0.31, 0.31]);
584
+ mockTvUe({ 0.31: 0.25 });
585
+ const pb = {
586
+ donnee_entree: {
587
+ enum_methode_saisie_u_id: '1',
588
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
589
+ enum_type_plancher_bas_id: '9',
590
+ type_adjacence: 'terre-plein',
591
+ enum_type_adjacence_id: '5',
592
+ surface_ue: 10,
593
+ perimetre_ue: 10
594
+ }
595
+ };
596
+
597
+ calc_pb(pb, 'h1a', '8', '0', [pb]);
598
+
599
+ expect(tv).toHaveBeenCalledWith(
600
+ 'ue',
601
+ expect.objectContaining({
602
+ type_adjacence_plancher: 'terre plein bâtiment construit à partir de 2001'
603
+ })
604
+ );
605
+ // delta_upb = 0 => ue = ue de la borne inférieure = 0,25
606
+ expect(pb.donnee_entree.ue).toBe(0.25);
607
+ });
608
+
609
+ test('vide sanitaire / sous-sol non chauffé : catégorie plancher sur vide sanitaire', () => {
610
+ getRange.mockReturnValue([0.31, 0.31]);
611
+ mockTvUe({ 0.31: 0.18 });
612
+ const pb = {
613
+ donnee_entree: {
614
+ enum_methode_saisie_u_id: '1',
615
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
616
+ enum_type_plancher_bas_id: '9',
617
+ type_adjacence: 'vide sanitaire',
618
+ enum_type_adjacence_id: '7',
619
+ surface_ue: 10,
620
+ perimetre_ue: 10
621
+ }
622
+ };
623
+
624
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
625
+
626
+ expect(tv).toHaveBeenCalledWith(
627
+ 'ue',
628
+ expect.objectContaining({
629
+ type_adjacence_plancher: 'plancher sur vide sanitaire ou sous-sol non chauffé'
630
+ })
631
+ );
632
+ expect(pb.donnee_intermediaire.upb_final).toBe(0.18);
633
+ });
634
+
635
+ test('adjacence sous-sol non chauffé : upb_final = Ue', () => {
636
+ getRange.mockReturnValue([0.31, 0.31]);
637
+ mockTvUe({ 0.31: 0.19 });
638
+ const pb = {
639
+ donnee_entree: {
640
+ enum_methode_saisie_u_id: '1',
641
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
642
+ enum_type_plancher_bas_id: '9',
643
+ type_adjacence: 'sous-sol non chauffé',
644
+ enum_type_adjacence_id: '6',
645
+ surface_ue: 10,
646
+ perimetre_ue: 10
647
+ }
648
+ };
649
+
650
+ calc_pb(pb, 'h1a', '1', '0', [pb]);
651
+
652
+ expect(pb.donnee_intermediaire.upb_final).toBe(0.19);
88
653
  });
89
654
  });