@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/conso.js CHANGED
@@ -98,7 +98,7 @@ const coef_cout = {
98
98
  * @param destination {'ch' | 'ecs' | 'éclairage'}
99
99
  * @return {string} la clef à utiliser pour récupérer le coefficient depuis la table `coef_ges`
100
100
  */
101
- function getCoefKey(type_energie, destination) {
101
+ export function getCoefKey(type_energie, destination) {
102
102
  if (type_energie === 'électricité') {
103
103
  if (!['ch', 'ecs', 'éclairage'].includes(destination)) {
104
104
  console.error(`Type d'électricité inconnu: ${destination}`);
package/conso.spec.js CHANGED
@@ -1,91 +1,480 @@
1
- import calc_conso from './conso.js';
2
- import { describe, expect, test } from 'vitest';
3
-
4
- describe('Recherche de bugs dans le calcul de la consommation', () => {
5
- test('calcul de la consommation de chauffage pour 2475E2510509B', () => {
6
- const Sh = 22.76; // Surface habitable
7
- const zc_id = 1; // Zone climatique
8
- const ca_id = 1; // Classe d'altitude
9
- const vt = [];
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `conso.js` :
5
+ * - `enums` : libellés de type d'énergie / classe d'altitude / zone climatique ;
6
+ * - `16_conso_eclairage` : consommation d'éclairage forcée à 1 kWh/m² (=> conso = Sh) ;
7
+ * - `tvs` : seuils de classe DPE / GES contrôlés ;
8
+ * - `tv` (utils) : lecture des réseaux de chaleur, retour maîtrisé.
9
+ * Les tables internes du module (coef_ges, coef_cout, coef_ep...) ne sont pas mockées :
10
+ * elles font partie du code testé.
11
+ */
12
+ vi.mock('./enums.js', () => ({
13
+ default: {
14
+ type_energie: {
15
+ 1: 'électricité',
16
+ 2: 'gaz naturel',
17
+ 3: 'réseau de chauffage urbain',
18
+ 4: 'inconnu'
19
+ },
20
+ classe_altitude: { 1: 'inférieur à 400m', 2: 'supérieur à 800m' },
21
+ zone_climatique: { 1: 'h1a', 2: 'h1b' }
22
+ }
23
+ }));
24
+
25
+ vi.mock('./16_conso_eclairage.js', () => ({ default: vi.fn(() => 1) }));
26
+
27
+ vi.mock('./tv.js', () => ({
28
+ default: {
29
+ dpe_class_limit: {
30
+ 'inférieur à 400m': { 100: { A: 10, B: 20, C: 30, D: 40, E: 300, F: 400 } },
31
+ 'supérieur à 800m': {}
32
+ },
33
+ ges_class_limit: {
34
+ 'inférieur à 400m': { 100: { A: 1, B: 2, C: 3, D: 4, E: 60, F: 90 } },
35
+ 'supérieur à 800m': {}
36
+ }
37
+ }
38
+ }));
39
+
40
+ vi.mock('./utils.js', () => ({
41
+ tv: vi.fn((table, matcher) => {
42
+ // Réseau de chaleur "R1" connu, "R2" inconnu
43
+ if (matcher?.identifiant_reseau === 'R1') return { contenu_co2_acv: 0.15 };
44
+ return null;
45
+ })
46
+ }));
47
+
48
+ const {
49
+ default: calc_conso,
50
+ classe_bilan_dpe,
51
+ classe_emission_ges,
52
+ getCoefKey,
53
+ coef_ep
54
+ } = await import('./conso.js');
55
+
56
+ /**
57
+ * Classe énergétique (bilan DPE)
58
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf
59
+ */
60
+ describe('classe_bilan_dpe - seuils de classe énergétique', () => {
61
+ test('consommation nulle (null) : aucune classe', () => {
62
+ expect(classe_bilan_dpe(null, 1, 1, 100)).toBeNull();
63
+ });
64
+
65
+ // ca_id=1 => seuils personnalisés {A:10,B:20,C:30,D:40,E:300,F:400}
66
+ test.each([
67
+ [5, 'A'],
68
+ [15, 'B'],
69
+ [25, 'C'],
70
+ [35, 'D'],
71
+ [250, 'E'],
72
+ [350, 'F'],
73
+ [450, 'G']
74
+ ])('seuils personnalisés : conso %s => classe %s', (conso, classe) => {
75
+ expect(classe_bilan_dpe(conso, 1, 1, 100)).toBe(classe);
76
+ });
77
+
78
+ // ca_id=2 => seuils absents => valeurs par défaut
79
+ test('valeurs par défaut A-D quand la table ne fournit pas de seuil', () => {
80
+ expect(classe_bilan_dpe(50, 1, 2, 100)).toBe('A'); // < 70
81
+ expect(classe_bilan_dpe(100, 1, 2, 100)).toBe('B'); // < 110
82
+ });
83
+
84
+ test('zone climatique standard : seuils E/F par défaut 330/420', () => {
85
+ // zc_id=1 (h1a) non concernée par le cas montagne => 330/420
86
+ expect(classe_bilan_dpe(300, 1, 2, 100)).toBe('E');
87
+ expect(classe_bilan_dpe(400, 1, 2, 100)).toBe('F');
88
+ expect(classe_bilan_dpe(500, 1, 2, 100)).toBe('G');
89
+ });
90
+
91
+ test('zone montagne (h1b, > 800m) : seuils E/F relevés à 390/500', () => {
92
+ // zc_id=2 (h1b) + ca 'supérieur à 800m' => 390/500
93
+ expect(classe_bilan_dpe(350, 2, 2, 100)).toBe('E');
94
+ expect(classe_bilan_dpe(450, 2, 2, 100)).toBe('F');
95
+ expect(classe_bilan_dpe(600, 2, 2, 100)).toBe('G');
96
+ });
97
+ });
98
+
99
+ /**
100
+ * Classe climat (émissions GES)
101
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf
102
+ */
103
+ describe('classe_emission_ges - seuils de classe climat', () => {
104
+ test('émission nulle (null) : aucune classe', () => {
105
+ expect(classe_emission_ges(null, 1, 1, 100)).toBeNull();
106
+ });
107
+
108
+ test.each([
109
+ [0.5, 'A'],
110
+ [1.5, 'B'],
111
+ [2.5, 'C'],
112
+ [3.5, 'D'],
113
+ [55, 'E'],
114
+ [70, 'F'],
115
+ [95, 'G']
116
+ ])('seuils personnalisés : émission %s => classe %s', (emission, classe) => {
117
+ expect(classe_emission_ges(emission, 1, 1, 100)).toBe(classe);
118
+ });
119
+
120
+ test('valeurs par défaut A-D quand la table ne fournit pas de seuil', () => {
121
+ expect(classe_emission_ges(3, 1, 2, 100)).toBe('A'); // < 6
122
+ expect(classe_emission_ges(10, 1, 2, 100)).toBe('B'); // < 11
123
+ });
124
+
125
+ test('zone climatique standard : seuils E/F par défaut 70/100', () => {
126
+ expect(classe_emission_ges(60, 1, 2, 100)).toBe('E');
127
+ expect(classe_emission_ges(80, 1, 2, 100)).toBe('F');
128
+ expect(classe_emission_ges(120, 1, 2, 100)).toBe('G');
129
+ });
130
+
131
+ test('zone montagne (h1b, > 800m) : seuils E/F relevés à 80/110', () => {
132
+ expect(classe_emission_ges(70, 2, 2, 100)).toBe('E');
133
+ expect(classe_emission_ges(90, 2, 2, 100)).toBe('F');
134
+ expect(classe_emission_ges(120, 2, 2, 100)).toBe('G');
135
+ });
136
+ });
137
+
138
+ /**
139
+ * Construction de la clef de la table `coef_ges` pour l'électricité (dépend de l'usage).
140
+ */
141
+ describe('getCoefKey - clef du coefficient GES', () => {
142
+ test('énergie non électrique : le libellé est renvoyé tel quel', () => {
143
+ expect(getCoefKey('gaz naturel', 'ch')).toBe('gaz naturel');
144
+ });
145
+
146
+ test("électricité : suffixe d'usage ajouté", () => {
147
+ expect(getCoefKey('électricité', 'ch')).toBe('électricité ch');
148
+ expect(getCoefKey('électricité', 'ecs')).toBe('électricité ecs');
149
+ });
150
+
151
+ test('électricité avec usage inconnu : erreur signalée mais clef construite', () => {
152
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
153
+ expect(getCoefKey('électricité', 'xxx')).toBe('électricité xxx');
154
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('xxx'));
155
+ errorSpy.mockRestore();
156
+ });
157
+ });
158
+
159
+ /** Fabrique un générateur de chauffage. */
160
+ function genCh(energieId, di, deExtra = {}) {
161
+ return {
162
+ donnee_entree: { enum_type_energie_id: energieId, ...deExtra },
163
+ donnee_intermediaire: di
164
+ };
165
+ }
166
+
167
+ /** Fabrique une installation de chauffage. */
168
+ function installCh(deExtra, gens) {
169
+ return {
170
+ donnee_entree: {
171
+ enum_type_installation_id: '1',
172
+ enum_methode_calcul_conso_id: '1',
173
+ ...deExtra
174
+ },
175
+ generateur_chauffage_collection: { generateur_chauffage: gens }
176
+ };
177
+ }
178
+
179
+ /** Fabrique un générateur ECS. */
180
+ function genEcs(energieId, di, deExtra = {}) {
181
+ return {
182
+ donnee_entree: { enum_type_energie_id: energieId, ...deExtra },
183
+ donnee_intermediaire: di
184
+ };
185
+ }
186
+
187
+ /** Fabrique une installation ECS. */
188
+ function installEcs(deExtra, gens) {
189
+ return {
190
+ donnee_entree: { ...deExtra },
191
+ generateur_ecs_collection: { generateur_ecs: gens }
192
+ };
193
+ }
194
+
195
+ const DATE_DPE = '2024-01-01';
196
+
197
+ describe('calc_conso - agrégation des consommations', () => {
198
+ let errorSpy;
199
+
200
+ beforeEach(() => {
201
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
202
+ });
203
+
204
+ afterEach(() => {
205
+ errorSpy.mockRestore();
206
+ });
207
+
208
+ test('chauffage électrique individuel : EF, EP (×1,9), GES et coût déterministes', () => {
10
209
  const ch = [
11
- {
12
- donnee_entree: {
13
- description:
14
- 'Convecteur électrique NFC, NF** et NF*** avec programmateur pièce par pièce (système individuel)',
15
- reference: '2024_07_11_09_30_44_722898200737262',
16
- surface_chauffee: 22.76,
17
- rdim: 1,
18
- nombre_niveau_installation_ch: 1,
19
- enum_cfg_installation_ch_id: '1',
20
- enum_type_installation_id: '1',
21
- enum_methode_calcul_conso_id: '1'
210
+ installCh({ cle_repartition_ch: 1 }, [
211
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
212
+ ])
213
+ ];
214
+
215
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
216
+
217
+ // EF : passe-plat (coefficient nul)
218
+ expect(res.ef_conso.conso_ch).toBe(1000);
219
+ // EP : coefficient électricité 1,9
220
+ expect(res.ep_conso.ep_conso_ch).toBeCloseTo(1900, 9);
221
+ // GES : coef_ges['électricité ch'] = 0,079
222
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(79, 9);
223
+ // Coût : cout_electricite(1000) = 149 + 0,14066 * 1000
224
+ expect(res.cout.cout_ch).toBeCloseTo(149 + 0.14066 * 1000, 9);
225
+ // Classes calculées et injectées
226
+ expect(res.ep_conso.classe_bilan_dpe).toBeDefined();
227
+ expect(res.emission_ges.classe_emission_ges).toBeDefined();
228
+ // Éclairage = calc_conso_eclairage(zc) * Sh = 1 * 100 (passe-plat EF)
229
+ expect(res.ef_conso.conso_eclairage).toBe(100);
230
+ });
231
+
232
+ test('installation collective (méthode 2) : clé de répartition appliquée à conso et distribution', () => {
233
+ const ch = [
234
+ installCh(
235
+ {
236
+ cle_repartition_ch: 2,
237
+ enum_type_installation_id: '2',
238
+ enum_methode_calcul_conso_id: '2'
22
239
  },
23
- emetteur_chauffage_collection: {
24
- emetteur_chauffage: [
25
- {
26
- donnee_entree: {
27
- description: '',
28
- reference: 'Emetteur:2024_07_11_09_30_44_722898200737262#1',
29
- surface_chauffee: 22.76,
30
- tv_rendement_emission_id: 1,
31
- tv_rendement_distribution_ch_id: 1,
32
- tv_rendement_regulation_id: 1,
33
- enum_type_emission_distribution_id: '1',
34
- tv_intermittence_id: 138,
35
- reseau_distribution_isole: 0,
36
- enum_equipement_intermittence_id: '4',
37
- enum_type_regulation_id: '2',
38
- enum_periode_installation_emetteur_id: '1',
39
- enum_type_chauffage_id: '1',
40
- enum_temp_distribution_ch_id: '1',
41
- enum_lien_generateur_emetteur_id: '1'
42
- },
43
- donnee_intermediaire: {
44
- rendement_distribution: 1,
45
- rendement_emission: 0.95,
46
- rendement_regulation: 0.99,
47
- i0: 0.86
48
- }
49
- }
50
- ]
240
+ [
241
+ genCh('1', {
242
+ conso_ch: 1000,
243
+ conso_ch_depensier: 1200,
244
+ conso_auxiliaire_distribution_ch: 100
245
+ })
246
+ ]
247
+ )
248
+ ];
249
+
250
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
251
+
252
+ // conso_ch = 1000 * clé (2)
253
+ expect(res.ef_conso.conso_ch).toBe(2000);
254
+ // distribution = 100 * clé (2) (méthode 2 + clé présente)
255
+ expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(200);
256
+ });
257
+
258
+ test('installation avec clé absente (non collective) : répartition = prorata chauffage', () => {
259
+ const ch = [
260
+ installCh(
261
+ {
262
+ cle_repartition_ch: 0,
263
+ enum_type_installation_id: '1',
264
+ enum_methode_calcul_conso_id: '3'
51
265
  },
52
- generateur_chauffage_collection: {
53
- generateur_chauffage: [
54
- {
55
- donnee_entree: {
56
- description: 'Electrique - Convecteur électrique NFC, NF** et NF***',
57
- reference: 'Generateur:2024_07_11_09_30_44_722898200737262#1',
58
- reference_generateur_mixte: '',
59
- ref_produit_generateur_ch: 'Sans Objet',
60
- enum_type_generateur_ch_id: '98',
61
- enum_usage_generateur_id: '1',
62
- enum_type_energie_id: '1',
63
- position_volume_chauffe: 1,
64
- tv_rendement_generation_id: 29,
65
- identifiant_reseau_chaleur: '',
66
- enum_methode_saisie_carac_sys_id: '1',
67
- enum_lien_generateur_emetteur_id: '1'
68
- },
69
- donnee_intermediaire: {
70
- rendement_generation: 1,
71
- conso_ch: 6322.706407855126,
72
- conso_ch_depensier: 7679.56883918009,
73
- rg: 1,
74
- rg_dep: 1,
75
- conso_auxiliaire_generation_ch: 0,
76
- conso_auxiliaire_generation_ch_depensier: 0
77
- }
78
- }
79
- ]
80
- }
266
+ [
267
+ genCh('1', {
268
+ conso_ch: 1000,
269
+ conso_ch_depensier: 1200,
270
+ conso_auxiliaire_distribution_ch: 50
271
+ })
272
+ ]
273
+ )
274
+ ];
275
+
276
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
277
+
278
+ // clé 0 => prorataChauffage (1) ; distribution non multipliée (méthode 3)
279
+ expect(res.ef_conso.conso_ch).toBe(1000);
280
+ expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(50);
281
+ });
282
+
283
+ test('clé de répartition nulle (prorata 0) : consommation de chauffage annulée', () => {
284
+ // cle===1 => value.cle = prorataChauffage(0) ; type collectif => répartition = 0
285
+ const ch = [
286
+ installCh({ cle_repartition_ch: 1, enum_type_installation_id: '2' }, [
287
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
288
+ ])
289
+ ];
290
+
291
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 0, DATE_DPE, coef_ep);
292
+ expect(res.ef_conso.conso_ch).toBe(0);
293
+ });
294
+
295
+ test('réseau de chaleur connu : le coefficient GES provient de la table réseau', () => {
296
+ const ch = [
297
+ installCh({ cle_repartition_ch: 1 }, [
298
+ genCh(
299
+ '3',
300
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
301
+ { identifiant_reseau_chaleur: 'R1', date_arrete_reseau_chaleur: '2030-01-01' }
302
+ )
303
+ ])
304
+ ];
305
+
306
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
307
+ // contenu_co2_acv = 0,15 (tv mocké) => émission = 1000 * 0,15
308
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(150, 9);
309
+ });
310
+
311
+ test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée', () => {
312
+ const ch = [
313
+ installCh({ cle_repartition_ch: 1 }, [
314
+ genCh(
315
+ '3',
316
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
317
+ { identifiant_reseau_chaleur: 'R2' }
318
+ )
319
+ ])
320
+ ];
321
+
322
+ calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
323
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('R2'));
324
+ });
325
+
326
+ test('réseau de chaleur sans identifiant : coefficient GES forfaitaire 0,385', () => {
327
+ const ch = [
328
+ installCh({ cle_repartition_ch: 1 }, [
329
+ genCh('3', { conso_ch: 1000, conso_ch_depensier: 1200 })
330
+ ])
331
+ ];
332
+
333
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
334
+ // 1000 * 0,385
335
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
336
+ });
337
+
338
+ test("date de DPE invalide (levée d'exception) : année de repli 2022", () => {
339
+ // date_arrete_reseau_chaleur = Symbol => new Date(...) lève => catch => 2022
340
+ const ch = [
341
+ installCh({ cle_repartition_ch: 1 }, [
342
+ genCh(
343
+ '1',
344
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
345
+ { date_arrete_reseau_chaleur: Symbol('invalide') }
346
+ )
347
+ ])
348
+ ];
349
+
350
+ // Ne doit pas lever
351
+ expect(() => calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep)).not.toThrow();
352
+ });
353
+
354
+ test('énergie inconnue : coefficient GES par défaut de 1', () => {
355
+ const ch = [
356
+ installCh({ cle_repartition_ch: 1 }, [
357
+ genCh('4', { conso_ch: 1000, conso_ch_depensier: 1200 })
358
+ ])
359
+ ];
360
+
361
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
362
+ // coef_ges['inconnu'] absent => getGesCoeffForGenerateur renvoie 1 => émission = 1000
363
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(1000, 9);
364
+ });
365
+
366
+ test('coût du gaz naturel : trois paliers tarifaires couverts', () => {
367
+ // Trois générateurs gaz avec des consommations dans chaque palier tarifaire
368
+ const ch = [
369
+ installCh({ cle_repartition_ch: 1 }, [
370
+ genCh('2', { conso_ch: 3000, conso_ch_depensier: 3000 }), // < 5009
371
+ genCh('2', { conso_ch: 30000, conso_ch_depensier: 30000 }), // < 50055
372
+ genCh('2', { conso_ch: 60000, conso_ch_depensier: 60000 }) // else
373
+ ])
374
+ ];
375
+
376
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
377
+ // Somme des trois paliers : 0,11121*3000 + (230+0,06533*30000) + (415+0,06164*60000)
378
+ const attendu = 0.11121 * 3000 + (230 + 0.06533 * 30000) + (415 + 0.06164 * 60000);
379
+ expect(res.cout.cout_ch).toBeCloseTo(attendu, 6);
380
+ });
381
+
382
+ test("coût de l'électricité : cinq paliers tarifaires couverts", () => {
383
+ const ch = [
384
+ installCh({ cle_repartition_ch: 1 }, [
385
+ genCh('1', { conso_ch: 500, conso_ch_depensier: 500 }), // < 1000
386
+ genCh('1', { conso_ch: 1500, conso_ch_depensier: 1500 }), // < 2500
387
+ genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 }), // < 5000
388
+ genCh('1', { conso_ch: 10000, conso_ch_depensier: 10000 }), // < 15000
389
+ genCh('1', { conso_ch: 20000, conso_ch_depensier: 20000 }) // else
390
+ ])
391
+ ];
392
+
393
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
394
+ const attendu =
395
+ 0.29007 * 500 +
396
+ (149 + 0.14066 * 1500) +
397
+ (122 + 0.15176 * 3000) +
398
+ (94 + 0.15735 * 10000) +
399
+ (56 + 0.15989 * 20000);
400
+ expect(res.cout.cout_ch).toBeCloseTo(attendu, 6);
401
+ });
402
+
403
+ test('ECS collective (prorataECS=1) : clé de répartition = clé × rdim', () => {
404
+ const ecs = [
405
+ installEcs({ cle_repartition_ecs: 2, rdim: 3 }, [
406
+ genEcs('1', { conso_ecs: 100, conso_ecs_depensier: 120 })
407
+ ])
408
+ ];
409
+
410
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE, coef_ep);
411
+ // clé = 2 * 3 = 6 => conso_ecs = 100 * 6
412
+ expect(res.ef_conso.conso_ecs).toBe(600);
413
+ });
414
+
415
+ test('ECS avec prorataECS différent de 1 : clé issue du prorata', () => {
416
+ // Quand prorataECS !== 1, la boucle ECS ne renseigne pas donnee_utilisateur :
417
+ // on la fournit en amont (coefficient GES déjà connu) pour refléter les données réelles.
418
+ const gen = genEcs('1', { conso_ecs: 100, conso_ecs_depensier: 120 });
419
+ gen.donnee_utilisateur = { coeffEmissionGes: 0.065 };
420
+ const ecs = [installEcs({}, [gen])];
421
+
422
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 0.5, 1, DATE_DPE, coef_ep);
423
+ // clé absente => prorataECS (0,5) => conso_ecs = 100 * 0,5
424
+ expect(res.ef_conso.conso_ecs).toBe(50);
425
+ });
426
+
427
+ test('ECS sans clé ni rdim, énergie non renseignée : valeurs de repli (1 et électricité)', () => {
428
+ // cle_repartition_ecs et rdim absents => 1 ; enum_type_energie_id absent => électricité (id 1)
429
+ const ecs = [installEcs({}, [genEcs(undefined, { conso_ecs: 100, conso_ecs_depensier: 120 })])];
430
+
431
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE, coef_ep);
432
+ // clé = 1 * 1 => conso_ecs = 100 ; EP électricité => 100 * 1,9
433
+ expect(res.ef_conso.conso_ecs).toBe(100);
434
+ expect(res.ep_conso.ep_conso_ecs).toBeCloseTo(190, 9);
435
+ });
436
+
437
+ test('ventilation et froid : auxiliaires et froid agrégés par énergie', () => {
438
+ const vt = [
439
+ {
440
+ donnee_entree: { cle_repartition_ventilation: 2 },
441
+ donnee_intermediaire: { conso_auxiliaire_ventilation: 20 }
442
+ },
443
+ // conso absente => 0 ; pas de clé de répartition
444
+ { donnee_entree: {}, donnee_intermediaire: {} }
445
+ ];
446
+ const fr = [
447
+ {
448
+ donnee_entree: { enum_type_energie_id: '1' },
449
+ donnee_intermediaire: { conso_fr: 200, conso_fr_depensier: 250 }
81
450
  }
82
451
  ];
83
- const ecs = [];
84
- const fr = [];
85
452
 
86
- const result = calc_conso(Sh, zc_id, ca_id, vt, ch, ecs, fr);
453
+ const res = calc_conso(100, 1, 1, vt, [], [], fr, 1, 1, DATE_DPE, coef_ep);
454
+
455
+ // ventilation : 20 * 2 (clé) + 0 = 40
456
+ expect(res.ef_conso.conso_auxiliaire_ventilation).toBe(40);
457
+ expect(res.ef_conso.conso_fr).toBe(200);
458
+ // sortie par énergie pour l'électricité
459
+ const elec = res.sortie_par_energie_collection.sortie_par_energie.find(
460
+ (e) => e.enum_type_energie_id === '1'
461
+ );
462
+ expect(elec.conso_ch).toBeDefined();
463
+ });
464
+
465
+ test('sortie par énergie : électricité ajoutée même en son absence', () => {
466
+ // Seul le gaz est présent => l'électricité (id 1) est tout de même ajoutée
467
+ const ch = [
468
+ installCh({ cle_repartition_ch: 1 }, [
469
+ genCh('2', { conso_ch: 1000, conso_ch_depensier: 1200 })
470
+ ])
471
+ ];
87
472
 
88
- expect(result.ef_conso.conso_ch).toBe(6322.706407855126);
89
- expect(result.ef_conso.conso_ch_depensier).toBe(7679.56883918009);
473
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
474
+ const ids = res.sortie_par_energie_collection.sortie_par_energie.map(
475
+ (e) => e.enum_type_energie_id
476
+ );
477
+ expect(ids).toContain('2');
478
+ expect(ids).toContain('1');
90
479
  });
91
480
  });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { AddAdditionnalUeValuesTables } from './add-additionnal-ue-values-tables.js';
3
+ import { UPB_ADDITIONAL_VALUES } from '../../tv/infrastructure/assets/additional-ue-values.js';
4
+
5
+ /**
6
+ * `execute` n'utilise ni le fileStore ni l'appConfig : on les passe en doubles
7
+ * vides pour isoler la logique d'aplatissement des valeurs Ue additionnelles
8
+ * dans la table `ue`.
9
+ */
10
+ describe('AddAdditionnalUeValuesTables', () => {
11
+ const buildService = () => new AddAdditionnalUeValuesTables({}, {});
12
+
13
+ test('ajoute toutes les valeurs additionnelles dans la table ue', () => {
14
+ const nbAttendu = UPB_ADDITIONAL_VALUES.reduce((acc, entree) => acc + entree.values.length, 0);
15
+ const tableValues = { ue: [] };
16
+
17
+ const resultat = buildService().execute(tableValues);
18
+
19
+ expect(resultat).toBe(tableValues);
20
+ expect(tableValues.ue).toHaveLength(nbAttendu);
21
+ });
22
+
23
+ test('préserve les valeurs déjà présentes dans la table ue', () => {
24
+ const ligneExistante = { existant: true };
25
+ const tableValues = { ue: [ligneExistante] };
26
+ const nbAjoutees = UPB_ADDITIONAL_VALUES.reduce((acc, entree) => acc + entree.values.length, 0);
27
+
28
+ buildService().execute(tableValues);
29
+
30
+ expect(tableValues.ue[0]).toBe(ligneExistante);
31
+ expect(tableValues.ue).toHaveLength(nbAjoutees + 1);
32
+ });
33
+
34
+ test('chaque ligne ajoutée fusionne les métadonnées d’adjacence et les valeurs', () => {
35
+ const tableValues = { ue: [] };
36
+
37
+ buildService().execute(tableValues);
38
+
39
+ const premiereEntree = UPB_ADDITIONAL_VALUES[0];
40
+ const premiereValeur = premiereEntree.values[0];
41
+
42
+ expect(tableValues.ue[0]).toEqual({
43
+ type_adjacence_plancher: premiereEntree.type_adjacence_plancher,
44
+ enum_type_adjacence_id: premiereEntree.enum_type_adjacence_id,
45
+ type_adjacence: premiereEntree.type_adjacence,
46
+ enum_periode_construction_id: premiereEntree.enum_periode_construction_id,
47
+ periode_construction: premiereEntree.periode_construction,
48
+ '2s_p': premiereValeur['2s_p'],
49
+ upb: premiereValeur.upb,
50
+ ue: premiereValeur.ue
51
+ });
52
+ });
53
+ });
@@ -44,4 +44,29 @@ describe('SynchronizeSolicitationsTables unit tests', () => {
44
44
  });
45
45
  });
46
46
  });
47
+
48
+ test("regroupe sans clé quand la ligne n'a ni ilpa ni classe_altitude", () => {
49
+ const fileStore = new FileStore();
50
+ const appConfig = new ApplicationConfig();
51
+ const synchronizeSolicitationsTables = new SynchronizeSolicitationsTables(fileStore, appConfig);
52
+
53
+ // Feuille ne comportant que `mois` et une valeur : `groupKey` doit rester
54
+ // vide (branche `: ''`) et le chemin construit est `feuille.mois.clef`.
55
+ vi.spyOn(fileStore, 'readLocalOdsFileAndConvertToJson').mockResolvedValue({
56
+ text_ext: [{ mois: 'Janvier', h1a: '12.5' }]
57
+ });
58
+ vi.spyOn(ApplicationConfig.prototype, 'solicitationsExtFilePath', 'get').mockReturnValue(
59
+ 'src/file.ods'
60
+ );
61
+
62
+ return synchronizeSolicitationsTables.execute().then((output) => {
63
+ expect(output).toEqual({
64
+ text_ext: {
65
+ Janvier: {
66
+ h1a: 12.5
67
+ }
68
+ }
69
+ });
70
+ });
71
+ });
47
72
  });