@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,406 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler le module de ventilation :
5
+ * - `tv` : accès aux tables `debits_ventilation` / `q4pa_conv`, on contrôle les lignes ;
6
+ * - `requestInput` / `requestInputID` : passe-plats vers les données d'entrée ;
7
+ * - `bug_for_bug_compat` : désactivé pour isoler le comportement nominal ;
8
+ * - `calc_pvent` (5_conso_ventilation.js) : espion, la conso de ventilation est testée ailleurs.
9
+ */
10
+ vi.mock('./utils.js', () => ({
11
+ tv: vi.fn(),
12
+ requestInput: (de, du, field) => de[field],
13
+ requestInputID: (de, du, field) => de[`enum_${field}_id`],
14
+ bug_for_bug_compat: false
15
+ }));
16
+
17
+ vi.mock('./5_conso_ventilation.js', () => ({
18
+ default: vi.fn()
19
+ }));
20
+
21
+ const { default: calc_ventilation, calc_hperm } = await import('./4_ventilation.js');
22
+ const { tv } = await import('./utils.js');
23
+ const { default: calc_pvent } = await import('./5_conso_ventilation.js');
24
+
25
+ /** Ligne figée de la table `debits_ventilation`. */
26
+ const ROW_DEBITS = {
27
+ qvarep_conv: '1.2',
28
+ qvasouf_conv: '0.5',
29
+ smea_conv: '20',
30
+ tv_debits_ventilation_id: '3'
31
+ };
32
+
33
+ beforeEach(() => {
34
+ tv.mockReset();
35
+ vi.mocked(calc_pvent).mockReset();
36
+ tv.mockImplementation((table) => {
37
+ if (table === 'debits_ventilation') return { ...ROW_DEBITS };
38
+ if (table === 'q4pa_conv') return { q4pa_conv: '1.3', tv_q4pa_conv_id: '9' };
39
+ return null;
40
+ });
41
+ });
42
+
43
+ /**
44
+ * Déperdition par renouvellement d'air dû aux infiltrations (Hperm)
45
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
46
+ */
47
+ describe('calc_hperm - déperdition par perméabilité', () => {
48
+ test('plusieurs façades exposées (pfe=1) : e=0,07 et f=15', () => {
49
+ const di = { q4pa_conv: 1.3, smea_conv: 20, qvasouf_conv: 0.5, qvarep_conv: 1.2 };
50
+ calc_hperm(di, 100, 2.5, 80, 1);
51
+ // valeur de référence de régression
52
+ expect(di.hperm).toBeCloseTo(124.24055432137072, 9);
53
+ });
54
+
55
+ test('une seule façade exposée (pfe=0) : e=0,02 et f=20', () => {
56
+ const di = { q4pa_conv: 1.3, smea_conv: 20, qvasouf_conv: 0.5, qvarep_conv: 1.2 };
57
+ calc_hperm(di, 100, 2.5, 80, 0);
58
+ // valeur de référence de régression
59
+ expect(di.hperm).toBeCloseTo(31.494527138357977, 9);
60
+ });
61
+
62
+ test('le nombre de façades exposées modifie le résultat', () => {
63
+ const diMulti = { q4pa_conv: 1.3, smea_conv: 20, qvasouf_conv: 0.5, qvarep_conv: 1.2 };
64
+ const diSingle = { q4pa_conv: 1.3, smea_conv: 20, qvasouf_conv: 0.5, qvarep_conv: 1.2 };
65
+ calc_hperm(diMulti, 100, 2.5, 80, 1);
66
+ calc_hperm(diSingle, 100, 2.5, 80, 0);
67
+ expect(diMulti.hperm).not.toBeCloseTo(diSingle.hperm, 5);
68
+ });
69
+ });
70
+
71
+ /** Fabrique un objet ventilation avec les données d'entrée utiles. */
72
+ function ventilation(de = {}) {
73
+ return { donnee_entree: { enum_type_ventilation_id: '1', ...de } };
74
+ }
75
+
76
+ const CG = {
77
+ hsp: 2.5,
78
+ enum_periode_construction_id: '1',
79
+ enum_methode_application_dpe_log_id: '1'
80
+ };
81
+
82
+ describe('calc_ventilation - orchestration', () => {
83
+ test('surface ventilée absente : la surface habitable Sh est utilisée', () => {
84
+ const vt = ventilation({ q4pa_conv_saisi: 1.3 });
85
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
86
+
87
+ // Hvent = 0,34 * qvarep_conv * surface_ventile = 0,34 * 1,2 * 100
88
+ expect(vt.donnee_intermediaire.hvent).toBeCloseTo(40.8, 9);
89
+ });
90
+
91
+ test('surface ventilée collective : divisée par la clé de répartition', () => {
92
+ const vt = ventilation({
93
+ surface_ventile: 200,
94
+ cle_repartition_ventilation: 2,
95
+ q4pa_conv_saisi: 1.3
96
+ });
97
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
98
+
99
+ // surface_ventile = 200 / 2 = 100 => Hvent = 0,34 * 1,2 * 100
100
+ expect(vt.donnee_intermediaire.hvent).toBeCloseTo(40.8, 9);
101
+ });
102
+
103
+ test('Q4Pa mesuré saisi : utilisé directement, sans lecture de la table q4pa_conv', () => {
104
+ const vt = ventilation({ q4pa_conv_saisi: 0.6 });
105
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
106
+
107
+ expect(vt.donnee_intermediaire.q4pa_conv).toBe(0.6);
108
+ expect(tv).not.toHaveBeenCalledWith('q4pa_conv', expect.anything());
109
+ });
110
+
111
+ test('Q4Pa non saisi : lu dans la table q4pa_conv', () => {
112
+ const vt = ventilation();
113
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
114
+
115
+ expect(tv).toHaveBeenCalledWith('q4pa_conv', expect.any(Object));
116
+ expect(vt.donnee_intermediaire.q4pa_conv).toBe(1.3);
117
+ });
118
+
119
+ test('les débits conventionnels intermédiaires sont supprimés du résultat', () => {
120
+ const vt = ventilation({ q4pa_conv_saisi: 1.3 });
121
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
122
+
123
+ expect(vt.donnee_intermediaire.qvarep_conv).toBeUndefined();
124
+ expect(vt.donnee_intermediaire.qvasouf_conv).toBeUndefined();
125
+ expect(vt.donnee_intermediaire.smea_conv).toBeUndefined();
126
+ });
127
+
128
+ test('la consommation de ventilation est déléguée à calc_pvent', () => {
129
+ const vt = ventilation({ q4pa_conv_saisi: 1.3 });
130
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
131
+
132
+ expect(calc_pvent).toHaveBeenCalledWith(
133
+ vt.donnee_intermediaire,
134
+ vt.donnee_entree,
135
+ vt.donnee_utilisateur,
136
+ 'th'
137
+ );
138
+ });
139
+
140
+ test('surface ventilée différente de Sh sans clé de répartition : divisée par 1', () => {
141
+ // Branche `de.cle_repartition_ventilation || 1` : surface_ventile conservée telle quelle
142
+ const vt = ventilation({ surface_ventile: 50, q4pa_conv_saisi: 1.3 });
143
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
144
+
145
+ // surface_ventile = 50 / 1 => Hvent = 0,34 * 1,2 * 50
146
+ expect(vt.donnee_intermediaire.hvent).toBeCloseTo(0.34 * 1.2 * 50, 9);
147
+ });
148
+
149
+ test("débits de ventilation introuvables : message d'erreur, débits non renseignés", () => {
150
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
151
+ tv.mockImplementation((table) => (table === 'q4pa_conv' ? { q4pa_conv: '1.3' } : null));
152
+
153
+ const vt = ventilation({ q4pa_conv_saisi: 1.3 });
154
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
155
+
156
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('debits_ventilation'));
157
+ errorSpy.mockRestore();
158
+ });
159
+
160
+ test("valeur forfaitaire q4pa_conv introuvable : message d'erreur", () => {
161
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
162
+ tv.mockImplementation((table) => (table === 'debits_ventilation' ? { ...ROW_DEBITS } : null));
163
+
164
+ const vt = ventilation();
165
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
166
+
167
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('q4pa_conv'));
168
+ errorSpy.mockRestore();
169
+ });
170
+
171
+ test('classification des surfaces : majorité isolée et joints présents => "1"/"1"', () => {
172
+ const vt = ventilation();
173
+ const murs = [
174
+ // b === 0 => paroi ignorée dans les deux sommes
175
+ {
176
+ donnee_intermediaire: { b: 0 },
177
+ donnee_entree: { enum_type_isolation_id: '3', surface_paroi_opaque: 999 }
178
+ },
179
+ // type inconnu (1) mais période d'isolation >= 3 => considérée isolée
180
+ {
181
+ donnee_intermediaire: { b: 1 },
182
+ donnee_entree: {
183
+ enum_type_isolation_id: '1',
184
+ enum_periode_isolation_id: '3',
185
+ surface_paroi_opaque: 50
186
+ }
187
+ },
188
+ // type isolé connu (3) => isolée
189
+ {
190
+ donnee_intermediaire: { b: 1 },
191
+ donnee_entree: { enum_type_isolation_id: '3', surface_paroi_opaque: 40 }
192
+ }
193
+ ];
194
+ const phs = [
195
+ // type non isolé (2) => non isolée
196
+ {
197
+ donnee_intermediaire: { b: 1 },
198
+ donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 10 }
199
+ },
200
+ // type inconnu (1) période < 3 => non isolée
201
+ {
202
+ donnee_intermediaire: { b: 1 },
203
+ donnee_entree: {
204
+ enum_type_isolation_id: '1',
205
+ enum_periode_isolation_id: '1',
206
+ surface_paroi_opaque: 5
207
+ }
208
+ }
209
+ ];
210
+ // surface_isolee = 90, surface_non_isolee = 15 => isolation_surfaces = '1'
211
+ const portes = [
212
+ { donnee_entree: { presence_joint: 1, surface_porte: 2 } },
213
+ { donnee_entree: { presence_joint: 0, surface_porte: 1 } }
214
+ ];
215
+ const baies = [
216
+ { donnee_entree: { presence_joint: 1, surface_totale_baie: 5 } },
217
+ { donnee_entree: { presence_joint: 0, surface_totale_baie: 1 } }
218
+ ];
219
+ // avec joint = 7, sans joint = 2 => ratio 7/9 > 0,5 => presence_joints_menuiserie = '1'
220
+ calc_ventilation(vt, CG, 'th', 80, 100, murs, phs, portes, baies);
221
+
222
+ expect(tv).toHaveBeenCalledWith('q4pa_conv', {
223
+ enum_periode_construction_id: '1',
224
+ enum_methode_application_dpe_log_id: '1',
225
+ isolation_surfaces: '1',
226
+ presence_joints_menuiserie: '1'
227
+ });
228
+ });
229
+
230
+ test('classification des surfaces : majorité non isolée et sans joints => "0"/"0"', () => {
231
+ const vt = ventilation();
232
+ const murs = [
233
+ {
234
+ donnee_intermediaire: { b: 1 },
235
+ donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 }
236
+ }
237
+ ];
238
+ const baies = [{ donnee_entree: { presence_joint: 0, surface_totale_baie: 10 } }];
239
+
240
+ calc_ventilation(vt, CG, 'th', 80, 100, murs, [], [], baies);
241
+
242
+ expect(tv).toHaveBeenCalledWith(
243
+ 'q4pa_conv',
244
+ expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '0' })
245
+ );
246
+ });
247
+
248
+ test('fiche technique façades exposées incohérente : valeur de la fiche imposée', () => {
249
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
250
+ // Fiche "Non" => pfe = 0, alors que plusieurs_facade_exposee = 1 => incohérence signalée
251
+ const vt = ventilation({
252
+ q4pa_conv_saisi: 1.3,
253
+ plusieurs_facade_exposee: 1,
254
+ ficheTechniqueFacadesExposees: { valeur: 'Non', description: 'FT façades' }
255
+ });
256
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
257
+
258
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('plusieurs_facade_exposee'));
259
+ errorSpy.mockRestore();
260
+ });
261
+
262
+ test('fiche technique façades exposées cohérente : aucun message', () => {
263
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
264
+ // Fiche "Deux" => pfe = 1, cohérent avec plusieurs_facade_exposee = 1
265
+ const vt = ventilation({
266
+ q4pa_conv_saisi: 1.3,
267
+ plusieurs_facade_exposee: 1,
268
+ ficheTechniqueFacadesExposees: { valeur: 'Deux', description: 'FT façades' }
269
+ });
270
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
271
+
272
+ expect(errorSpy).not.toHaveBeenCalled();
273
+ errorSpy.mockRestore();
274
+ });
275
+
276
+ test('fiche technique ventilation post 2012 incohérente : valeur forcée à 1', () => {
277
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
278
+ const vt = ventilation({
279
+ q4pa_conv_saisi: 1.3,
280
+ ventilation_post_2012: 0,
281
+ ficheTechniqueVentilationPost2012: { description: 'FT ventilation' }
282
+ });
283
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
284
+
285
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ventilation_post_2012'));
286
+ expect(vt.donnee_entree.ventilation_post_2012).toBe(1);
287
+ errorSpy.mockRestore();
288
+ });
289
+ });
290
+
291
+ /**
292
+ * Comportement historique `bug_for_bug_compat` activé : le seuil des joints devient `>= 0,5`
293
+ * et les incohérences avec la ligne `q4pa_conv` déjà saisie dans le DPE sont signalées.
294
+ */
295
+ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => {
296
+ /**
297
+ * Recharge le module avec `bug_for_bug_compat = true`. Le double de `tv` renvoie une ligne
298
+ * `q4pa_conv` distincte selon que le matcher cible l'identifiant déjà saisi ou le matcher complet.
299
+ */
300
+ async function chargerAvecBug(rowParId) {
301
+ vi.resetModules();
302
+ const tvBug = vi.fn((table, matcher) => {
303
+ if (table === 'debits_ventilation') return { ...ROW_DEBITS };
304
+ if (table === 'q4pa_conv') {
305
+ if (matcher.tv_q4pa_conv_id) return rowParId;
306
+ return { q4pa_conv: '1.3', tv_q4pa_conv_id: '9' };
307
+ }
308
+ return null;
309
+ });
310
+ vi.doMock('./utils.js', () => ({
311
+ tv: tvBug,
312
+ requestInput: (de, du, field) => de[field],
313
+ requestInputID: (de, du, field) => de[`enum_${field}_id`],
314
+ bug_for_bug_compat: true
315
+ }));
316
+ vi.doMock('./5_conso_ventilation.js', () => ({ default: vi.fn() }));
317
+ const { default: calc_ventilation_bug } = await import('./4_ventilation.js');
318
+ return { calc_ventilation_bug, tvBug };
319
+ }
320
+
321
+ afterEach(() => {
322
+ vi.doUnmock('./utils.js');
323
+ vi.doUnmock('./5_conso_ventilation.js');
324
+ vi.resetModules();
325
+ });
326
+
327
+ /** Surfaces produisant isolation_surfaces = '1' et joints = '1'. */
328
+ function surfacesIsolantes() {
329
+ return {
330
+ murs: [
331
+ {
332
+ donnee_intermediaire: { b: 1 },
333
+ donnee_entree: { enum_type_isolation_id: '3', surface_paroi_opaque: 100 }
334
+ }
335
+ ],
336
+ baies: [{ donnee_entree: { presence_joint: 1, surface_totale_baie: 10 } }]
337
+ };
338
+ }
339
+
340
+ test('ligne DPE incohérente : les deux écarts (isolation et joints) sont signalés', async () => {
341
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
342
+ const { calc_ventilation_bug } = await chargerAvecBug({
343
+ q4pa_conv: '1.3',
344
+ tv_q4pa_conv_id: '9',
345
+ isolation_surfaces: '0',
346
+ presence_joints_menuiserie: '0',
347
+ description: 'ventilation'
348
+ });
349
+
350
+ const { murs, baies } = surfacesIsolantes();
351
+ const vt = {
352
+ donnee_entree: {
353
+ enum_type_ventilation_id: '1',
354
+ tv_q4pa_conv_id: 9,
355
+ description: 'ventilation'
356
+ }
357
+ };
358
+ calc_ventilation_bug(vt, CG, 'th', 80, 100, murs, [], [], baies);
359
+
360
+ // Deux messages : isolation_surfaces et presence_joints_menuiserie
361
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('isolation_surfaces'));
362
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('presence_joints_menuiserie'));
363
+ errorSpy.mockRestore();
364
+ });
365
+
366
+ test('ligne DPE cohérente : aucun écart signalé', async () => {
367
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
368
+ const { calc_ventilation_bug } = await chargerAvecBug({
369
+ q4pa_conv: '1.3',
370
+ tv_q4pa_conv_id: '9',
371
+ isolation_surfaces: '1',
372
+ presence_joints_menuiserie: '1'
373
+ });
374
+
375
+ const { murs, baies } = surfacesIsolantes();
376
+ const vt = {
377
+ donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: 9 }
378
+ };
379
+ calc_ventilation_bug(vt, CG, 'th', 80, 100, murs, [], [], baies);
380
+
381
+ expect(errorSpy).not.toHaveBeenCalled();
382
+ errorSpy.mockRestore();
383
+ });
384
+
385
+ test('ligne DPE saisie introuvable : aucune comparaison effectuée', async () => {
386
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
387
+ // rowParId = null => la branche `if (rowQ4paConv)` est fausse
388
+ const { calc_ventilation_bug } = await chargerAvecBug(null);
389
+
390
+ // Baie sans joint => ratio < 0,5 => branche '0' du seuil `>= 0,5` (mode bug)
391
+ const murs = [
392
+ {
393
+ donnee_intermediaire: { b: 1 },
394
+ donnee_entree: { enum_type_isolation_id: '3', surface_paroi_opaque: 100 }
395
+ }
396
+ ];
397
+ const baies = [{ donnee_entree: { presence_joint: 0, surface_totale_baie: 10 } }];
398
+ const vt = {
399
+ donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: 9 }
400
+ };
401
+ calc_ventilation_bug(vt, CG, 'th', 80, 100, murs, [], [], baies);
402
+
403
+ expect(errorSpy).not.toHaveBeenCalled();
404
+ errorSpy.mockRestore();
405
+ });
406
+ });
@@ -0,0 +1,186 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * `requestInput` est mocké en simple passe-plat : il retourne la valeur portée par les données
5
+ * d'entrée `de` pour le champ demandé. Cela isole `calc_pvent` de la logique de `utils`.
6
+ */
7
+ vi.mock('./utils.js', () => ({
8
+ requestInput: (de, du, field) => de[field]
9
+ }));
10
+
11
+ const { default: calc_pvent } = await import('./5_conso_ventilation.js');
12
+
13
+ /**
14
+ * 5. Consommation des auxiliaires de ventilation
15
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §5, pages 41-42
16
+ */
17
+ describe('calc_pvent - consommation des auxiliaires de ventilation', () => {
18
+ let di;
19
+ let du;
20
+
21
+ beforeEach(() => {
22
+ di = {};
23
+ du = {};
24
+ });
25
+
26
+ test('ne calcule rien si le type de ventilation est indéfini', () => {
27
+ calc_pvent(di, {}, du, 'maison');
28
+ expect(di).toEqual({});
29
+ });
30
+
31
+ test('ventilation naturelle : consommation nulle', () => {
32
+ const de = { type_ventilation: 'ventilation naturelle par conduit', ventilation_post_2012: 0 };
33
+ calc_pvent(di, de, du, 'maison');
34
+ expect(di.conso_auxiliaire_ventilation).toBe(0);
35
+ });
36
+
37
+ test('maison, VMC simple flux auto après 2012 : Pvent = 35 W', () => {
38
+ const de = { type_ventilation: 'vmc sf auto réglable après 2012', ventilation_post_2012: 1 };
39
+ calc_pvent(di, de, du, 'maison');
40
+ // pvent_moy_maison['simple flux auto'][1] * coef(non hybride = 1) = 35
41
+ expect(di.pvent_moy).toBe(35);
42
+ expect(di.conso_auxiliaire_ventilation).toBeCloseTo(8.76 * 35, 10);
43
+ });
44
+
45
+ test('maison, VMC double flux avant 2013 : Pvent = 80 W', () => {
46
+ const de = { type_ventilation: 'vmc df sans échangeur avant 2013', ventilation_post_2012: 0 };
47
+ calc_pvent(di, de, du, 'maison');
48
+ expect(di.pvent_moy).toBe(80);
49
+ });
50
+
51
+ test("maison, ventilation hybride : ratio de temps d'utilisation appliqué (14/168)", () => {
52
+ const de = { type_ventilation: 'ventilation hybride après 2012', ventilation_post_2012: 1 };
53
+ calc_pvent(di, de, du, 'maison');
54
+ // hybride => type 'simple flux auto', post_2012 forcé à 0, coef = 14 / (24*7)
55
+ const coef = 14 / (24 * 7);
56
+ expect(di.pvent_moy).toBeCloseTo(65 * coef, 10);
57
+ });
58
+
59
+ test('immeuble, VMC double flux : Pvent proratisé au débit et à la surface ventilée', () => {
60
+ di.qvarep_conv = 2;
61
+ const de = {
62
+ type_ventilation: 'vmc df collective avec échangeur avant 2013',
63
+ ventilation_post_2012: 0,
64
+ surface_ventile: 50
65
+ };
66
+ calc_pvent(di, de, du, 'immeuble');
67
+ // pvent_immeuble['double flux'][0] * qvarep_conv * surface_ventile * coef(1) = 1.1 * 2 * 50
68
+ expect(di.pvent_moy).toBeCloseTo(1.1 * 2 * 50, 10);
69
+ expect(di.conso_auxiliaire_ventilation).toBeCloseTo(8.76 * 110, 10);
70
+ });
71
+
72
+ test("immeuble, ventilation hybride : ratio de temps d'utilisation immeuble (28/168)", () => {
73
+ // Branche `th !== 'maison'` du ratio hybride (28 au lieu de 14)
74
+ di.qvarep_conv = 2;
75
+ const de = {
76
+ type_ventilation: 'ventilation hybride de 2001 à 2012',
77
+ ventilation_post_2012: 1,
78
+ surface_ventile: 50
79
+ };
80
+ calc_pvent(di, de, du, 'immeuble');
81
+ // hybride => type 'simple flux auto', post_2012 forcé à 0 => pvent_immeuble = 0.46
82
+ const coef = 28 / (24 * 7);
83
+ expect(di.pvent_moy).toBeCloseTo(0.46 * 2 * 50 * coef, 10);
84
+ });
85
+ });
86
+
87
+ /**
88
+ * Couverture exhaustive des libellés de type de ventilation (branches du switch).
89
+ * Chaque libellé doit être routé vers le bon groupe (naturelle => conso nulle,
90
+ * sinon simple flux auto / hygro / double flux avec une consommation positive).
91
+ */
92
+ describe('calc_pvent - routage de tous les types de ventilation', () => {
93
+ const naturelles = [
94
+ 'ventilation par ouverture des fenêtres',
95
+ "ventilation par entrées d'air hautes et basses",
96
+ 'ventilation naturelle par conduit',
97
+ "ventilation naturelle par conduit avec entrées d'air hygro",
98
+ 'puits climatique sans échangeur avant 2013',
99
+ 'puits climatique sans échangeur à partir de 2013',
100
+ 'puits climatique avec échangeur avant 2013',
101
+ 'puits climatique avec échangeur à partir de 2013'
102
+ ];
103
+
104
+ test.each(naturelles)('ventilation naturelle "%s" : consommation nulle', (type_ventilation) => {
105
+ const di = {};
106
+ calc_pvent(di, { type_ventilation, ventilation_post_2012: 0 }, {}, 'maison');
107
+ expect(di.conso_auxiliaire_ventilation).toBe(0);
108
+ expect(di.pvent_moy).toBeUndefined();
109
+ });
110
+
111
+ const hybrides = [
112
+ 'ventilation hybride avant 2001',
113
+ 'ventilation hybride de 2001 à 2012',
114
+ 'ventilation hybride après 2012',
115
+ "ventilation hybride avec entrées d'air hygro avant 2001",
116
+ "ventilation hybride avec entrées d'air hygro de 2001 à 2012",
117
+ "ventilation hybride avec entrées d'air hygro après 2012"
118
+ ];
119
+
120
+ test.each(hybrides)(
121
+ 'ventilation hybride "%s" : simple flux auto avec ratio 14/168 (maison)',
122
+ (type_ventilation) => {
123
+ const di = {};
124
+ // Même avec ventilation_post_2012 = 1, l'hybride force post_2012 = 0 (=> 65 W)
125
+ calc_pvent(di, { type_ventilation, ventilation_post_2012: 1 }, {}, 'maison');
126
+ expect(di.pvent_moy).toBeCloseTo(65 * (14 / (24 * 7)), 10);
127
+ }
128
+ );
129
+
130
+ const simpleFluxAuto = [
131
+ 'ventilation mécanique sur conduit existant avant 2013',
132
+ 'ventilation mécanique sur conduit existant à partir de 2013',
133
+ 'vmc sf auto réglable avant 1982',
134
+ 'vmc sf auto réglable de 1982 à 2000',
135
+ 'vmc sf auto réglable de 2001 à 2012',
136
+ 'vmc sf auto réglable après 2012',
137
+ 'vmc sf gaz avant 2001',
138
+ 'vmc sf gaz de 2001 à 2012',
139
+ 'vmc sf gaz après 2012'
140
+ ];
141
+
142
+ test.each(simpleFluxAuto)(
143
+ 'simple flux auto "%s" : 65 W (maison, avant 2012)',
144
+ (type_ventilation) => {
145
+ const di = {};
146
+ calc_pvent(di, { type_ventilation, ventilation_post_2012: 0 }, {}, 'maison');
147
+ expect(di.pvent_moy).toBe(65);
148
+ }
149
+ );
150
+
151
+ const simpleFluxHygro = [
152
+ 'vmc sf hygro b avant 2001',
153
+ 'vmc sf hygro b de 2001 à 2012',
154
+ 'vmc sf hygro b après 2012',
155
+ 'vmc sf hygro a avant 2001',
156
+ 'vmc sf hygro a de 2001 à 2012',
157
+ 'vmc sf hygro a après 2012',
158
+ 'vmc basse pression auto-réglable',
159
+ 'vmc basse pression hygro a',
160
+ 'vmc basse pression hygro b'
161
+ ];
162
+
163
+ test.each(simpleFluxHygro)(
164
+ 'simple flux hygro "%s" : 50 W (maison, avant 2012)',
165
+ (type_ventilation) => {
166
+ const di = {};
167
+ calc_pvent(di, { type_ventilation, ventilation_post_2012: 0 }, {}, 'maison');
168
+ expect(di.pvent_moy).toBe(50);
169
+ }
170
+ );
171
+
172
+ const doubleFlux = [
173
+ 'vmc df individuelle avec échangeur avant 2013',
174
+ 'vmc df individuelle avec échangeur à partir de 2013',
175
+ 'vmc df collective avec échangeur avant 2013',
176
+ 'vmc df collective avec échangeur à partir de 2013',
177
+ 'vmc df sans échangeur avant 2013',
178
+ 'vmc df sans échangeur après 2012'
179
+ ];
180
+
181
+ test.each(doubleFlux)('double flux "%s" : 80 W (maison, avant 2012)', (type_ventilation) => {
182
+ const di = {};
183
+ calc_pvent(di, { type_ventilation, ventilation_post_2012: 0 }, {}, 'maison');
184
+ expect(di.pvent_moy).toBe(80);
185
+ });
186
+ });