@open3cl/engine 1.6.1 → 1.7.0

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 (109) 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.js +121 -1
  18. package/15_conso_aux.spec.js +550 -0
  19. package/16.2_production_enr.spec.js +188 -0
  20. package/16_conso_eclairage.spec.js +62 -0
  21. package/2021_04_13_confort_ete.spec.js +159 -0
  22. package/2021_04_13_qualite_isolation.js +1 -1
  23. package/2021_04_13_qualite_isolation.spec.js +286 -0
  24. package/3.1_b.js +1 -1
  25. package/3.1_b.spec.js +367 -0
  26. package/3.2.1_mur.spec.js +812 -39
  27. package/3.2.2_plancher_bas.spec.js +643 -78
  28. package/3.2.3_plancher_haut.spec.js +452 -0
  29. package/3.3.1.4_porte.spec.js +114 -0
  30. package/3.3_baie_vitree.spec.js +449 -295
  31. package/3.4_pont_thermique.spec.js +890 -0
  32. package/3_deperdition.spec.js +397 -0
  33. package/4_ventilation.js +1 -1
  34. package/4_ventilation.spec.js +406 -0
  35. package/5_conso_ventilation.spec.js +186 -0
  36. package/6.2_surface_sud_equivalente.js +2 -1
  37. package/6.2_surface_sud_equivalente.spec.js +138 -0
  38. package/8_intermittence.spec.js +33 -0
  39. package/9_besoin_ch.js +2 -2
  40. package/9_besoin_ch.spec.js +381 -0
  41. package/9_chauffage.js +4 -0
  42. package/9_chauffage.spec.js +478 -92
  43. package/9_conso_ch.spec.js +502 -248
  44. package/9_emetteur_ch.spec.js +339 -0
  45. package/9_generateur_ch.spec.js +467 -81
  46. package/apport_et_besoin.spec.js +126 -0
  47. package/conso.js +21 -7
  48. package/conso.spec.js +491 -82
  49. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  50. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  51. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  52. package/core/conf/infrastructure/application.config.spec.js +35 -0
  53. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  54. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  55. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  56. package/core/util/infrastructure/object-util.spec.js +20 -0
  57. package/core/util/logger/log-service.js +16 -7
  58. package/core/util/logger/log-service.spec.js +101 -0
  59. package/dpe-sanitizer.service.spec.js +138 -0
  60. package/engine.js +20 -1
  61. package/engine.spec.js +949 -0
  62. package/enums.spec.js +46 -0
  63. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  64. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  65. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  66. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  67. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  68. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  69. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  70. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  71. package/features/dpe/infrastructure/tv.store.js +28 -9
  72. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  73. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  74. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  75. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  76. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  77. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  78. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  79. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  80. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  81. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  82. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  83. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  84. package/features/engine/domain/constants.spec.js +14 -0
  85. package/features/engine/domain/contexte.builder.spec.js +21 -0
  86. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  87. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  88. package/features/engine/domain/engine.service.spec.js +227 -4
  89. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  90. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  91. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  92. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  93. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  94. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  95. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  96. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  97. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  98. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  99. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  100. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  101. package/ficheTechnique.spec.js +26 -0
  102. package/index.spec.js +54 -0
  103. package/package.json +5 -2
  104. package/preprocessor/index.js +20 -0
  105. package/preprocessor/index.spec.js +15 -0
  106. package/tv.spec.js +38 -0
  107. package/utils.js +43 -1
  108. package/utils.spec.js +518 -2
  109. 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}`);
@@ -218,6 +218,11 @@ export default function calc_conso(
218
218
  return acc.concat(generateur_chauffage);
219
219
  }, []);
220
220
 
221
+ ecs = Array.isArray(ecs) ? ecs : [];
222
+
223
+ const conso_aux_distribution_ecs_total = ecs.reduce((acc, ecs) => {
224
+ return acc + (ecs.donnee_intermediaire.conso_auxiliaire_distribution_ecs || 0);
225
+ }, 0);
221
226
  const gen_ecs = ecs.reduce((acc, ecs) => {
222
227
  const generateur_ecs = ecs.generateur_ecs_collection.generateur_ecs;
223
228
  if (prorataECS === 1) {
@@ -243,7 +248,8 @@ export default function calc_conso(
243
248
  'conso',
244
249
  null,
245
250
  prorataECS,
246
- prorataChauffage
251
+ prorataChauffage,
252
+ conso_aux_distribution_ecs_total
247
253
  ),
248
254
  ep_conso: calc_conso_pond(
249
255
  Sh,
@@ -255,7 +261,8 @@ export default function calc_conso(
255
261
  'ep_conso',
256
262
  coeffEp,
257
263
  prorataECS,
258
- prorataChauffage
264
+ prorataChauffage,
265
+ conso_aux_distribution_ecs_total
259
266
  ),
260
267
  emission_ges: calc_conso_pond(
261
268
  Sh,
@@ -267,7 +274,8 @@ export default function calc_conso(
267
274
  'emission_ges',
268
275
  coef_ges,
269
276
  prorataECS,
270
- prorataChauffage
277
+ prorataChauffage,
278
+ conso_aux_distribution_ecs_total
271
279
  ),
272
280
  cout: calc_conso_pond(
273
281
  Sh,
@@ -279,7 +287,8 @@ export default function calc_conso(
279
287
  'cout',
280
288
  coef_cout,
281
289
  prorataECS,
282
- prorataChauffage
290
+ prorataChauffage,
291
+ conso_aux_distribution_ecs_total
283
292
  )
284
293
  };
285
294
  ret.ep_conso.classe_bilan_dpe = classe_bilan_dpe(
@@ -464,7 +473,8 @@ function calc_conso_pond(
464
473
  prefix,
465
474
  coef,
466
475
  prorataECS,
467
- prorataChauffage
476
+ prorataChauffage,
477
+ conso_aux_distribution_ecs_total
468
478
  ) {
469
479
  const ret = {};
470
480
  ret.auxiliaire_ventilation = vt_list.reduce((acc, vt) => {
@@ -528,7 +538,11 @@ function calc_conso_pond(
528
538
  return acc + getConso(coef, 'électricité auxiliaire', conso);
529
539
  }, 0);
530
540
 
531
- ret.auxiliaire_distribution_ecs = 0;
541
+ ret.auxiliaire_distribution_ecs = getConso(
542
+ coef,
543
+ 'électricité auxiliaire',
544
+ conso_aux_distribution_ecs_total || 0
545
+ );
532
546
 
533
547
  ret.ecs = getEcsConso(gen_ecs, 'conso_ecs', coef, prorataECS, prefix);
534
548
 
package/conso.spec.js CHANGED
@@ -1,91 +1,500 @@
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
+ donnee_intermediaire: {},
192
+ generateur_ecs_collection: { generateur_ecs: gens }
193
+ };
194
+ }
195
+
196
+ const DATE_DPE = '2024-01-01';
197
+
198
+ describe('calc_conso - agrégation des consommations', () => {
199
+ let errorSpy;
200
+
201
+ beforeEach(() => {
202
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
203
+ });
204
+
205
+ afterEach(() => {
206
+ errorSpy.mockRestore();
207
+ });
208
+
209
+ test('chauffage électrique individuel : EF, EP (×1,9), GES et coût déterministes', () => {
10
210
  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'
211
+ installCh({ cle_repartition_ch: 1 }, [
212
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
213
+ ])
214
+ ];
215
+
216
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
217
+
218
+ // EF : passe-plat (coefficient nul)
219
+ expect(res.ef_conso.conso_ch).toBe(1000);
220
+ // EP : coefficient électricité 1,9
221
+ expect(res.ep_conso.ep_conso_ch).toBeCloseTo(1900, 9);
222
+ // GES : coef_ges['électricité ch'] = 0,079
223
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(79, 9);
224
+ // Coût : cout_electricite(1000) = 149 + 0,14066 * 1000
225
+ expect(res.cout.cout_ch).toBeCloseTo(149 + 0.14066 * 1000, 9);
226
+ // Classes calculées et injectées
227
+ expect(res.ep_conso.classe_bilan_dpe).toBeDefined();
228
+ expect(res.emission_ges.classe_emission_ges).toBeDefined();
229
+ // Éclairage = calc_conso_eclairage(zc) * Sh = 1 * 100 (passe-plat EF)
230
+ expect(res.ef_conso.conso_eclairage).toBe(100);
231
+ });
232
+
233
+ test('installation collective (méthode 2) : clé de répartition appliquée à conso et distribution', () => {
234
+ const ch = [
235
+ installCh(
236
+ {
237
+ cle_repartition_ch: 2,
238
+ enum_type_installation_id: '2',
239
+ enum_methode_calcul_conso_id: '2'
22
240
  },
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
- ]
241
+ [
242
+ genCh('1', {
243
+ conso_ch: 1000,
244
+ conso_ch_depensier: 1200,
245
+ conso_auxiliaire_distribution_ch: 100
246
+ })
247
+ ]
248
+ )
249
+ ];
250
+
251
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
252
+
253
+ // conso_ch = 1000 * clé (2)
254
+ expect(res.ef_conso.conso_ch).toBe(2000);
255
+ // distribution = 100 * clé (2) (méthode 2 + clé présente)
256
+ expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(200);
257
+ });
258
+
259
+ test('installation avec clé absente (non collective) : répartition = prorata chauffage', () => {
260
+ const ch = [
261
+ installCh(
262
+ {
263
+ cle_repartition_ch: 0,
264
+ enum_type_installation_id: '1',
265
+ enum_methode_calcul_conso_id: '3'
51
266
  },
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
- }
267
+ [
268
+ genCh('1', {
269
+ conso_ch: 1000,
270
+ conso_ch_depensier: 1200,
271
+ conso_auxiliaire_distribution_ch: 50
272
+ })
273
+ ]
274
+ )
275
+ ];
276
+
277
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
278
+
279
+ // clé 0 => prorataChauffage (1) ; distribution non multipliée (méthode 3)
280
+ expect(res.ef_conso.conso_ch).toBe(1000);
281
+ expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(50);
282
+ });
283
+
284
+ test('clé de répartition nulle (prorata 0) : consommation de chauffage annulée', () => {
285
+ // cle===1 => value.cle = prorataChauffage(0) ; type collectif => répartition = 0
286
+ const ch = [
287
+ installCh({ cle_repartition_ch: 1, enum_type_installation_id: '2' }, [
288
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
289
+ ])
290
+ ];
291
+
292
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 0, DATE_DPE, coef_ep);
293
+ expect(res.ef_conso.conso_ch).toBe(0);
294
+ });
295
+
296
+ test('réseau de chaleur connu : le coefficient GES provient de la table réseau', () => {
297
+ const ch = [
298
+ installCh({ cle_repartition_ch: 1 }, [
299
+ genCh(
300
+ '3',
301
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
302
+ { identifiant_reseau_chaleur: 'R1', date_arrete_reseau_chaleur: '2030-01-01' }
303
+ )
304
+ ])
305
+ ];
306
+
307
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
308
+ // contenu_co2_acv = 0,15 (tv mocké) => émission = 1000 * 0,15
309
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(150, 9);
310
+ });
311
+
312
+ test('réseau de chaleur inconnu (identifiant sans ligne) : erreur signalée', () => {
313
+ const ch = [
314
+ installCh({ cle_repartition_ch: 1 }, [
315
+ genCh(
316
+ '3',
317
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
318
+ { identifiant_reseau_chaleur: 'R2' }
319
+ )
320
+ ])
321
+ ];
322
+
323
+ calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
324
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('R2'));
325
+ });
326
+
327
+ test('réseau de chaleur sans identifiant : coefficient GES forfaitaire 0,385', () => {
328
+ const ch = [
329
+ installCh({ cle_repartition_ch: 1 }, [
330
+ genCh('3', { conso_ch: 1000, conso_ch_depensier: 1200 })
331
+ ])
332
+ ];
333
+
334
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
335
+ // 1000 * 0,385
336
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(385, 9);
337
+ });
338
+
339
+ test("date de DPE invalide (levée d'exception) : année de repli 2022", () => {
340
+ // date_arrete_reseau_chaleur = Symbol => new Date(...) lève => catch => 2022
341
+ const ch = [
342
+ installCh({ cle_repartition_ch: 1 }, [
343
+ genCh(
344
+ '1',
345
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
346
+ { date_arrete_reseau_chaleur: Symbol('invalide') }
347
+ )
348
+ ])
349
+ ];
350
+
351
+ // Ne doit pas lever
352
+ expect(() => calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep)).not.toThrow();
353
+ });
354
+
355
+ test('énergie inconnue : coefficient GES par défaut de 1', () => {
356
+ const ch = [
357
+ installCh({ cle_repartition_ch: 1 }, [
358
+ genCh('4', { conso_ch: 1000, conso_ch_depensier: 1200 })
359
+ ])
360
+ ];
361
+
362
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
363
+ // coef_ges['inconnu'] absent => getGesCoeffForGenerateur renvoie 1 => émission = 1000
364
+ expect(res.emission_ges.emission_ges_ch).toBeCloseTo(1000, 9);
365
+ });
366
+
367
+ test('coût du gaz naturel : trois paliers tarifaires couverts', () => {
368
+ // Trois générateurs gaz avec des consommations dans chaque palier tarifaire
369
+ const ch = [
370
+ installCh({ cle_repartition_ch: 1 }, [
371
+ genCh('2', { conso_ch: 3000, conso_ch_depensier: 3000 }), // < 5009
372
+ genCh('2', { conso_ch: 30000, conso_ch_depensier: 30000 }), // < 50055
373
+ genCh('2', { conso_ch: 60000, conso_ch_depensier: 60000 }) // else
374
+ ])
375
+ ];
376
+
377
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
378
+ // Somme des trois paliers : 0,11121*3000 + (230+0,06533*30000) + (415+0,06164*60000)
379
+ const attendu = 0.11121 * 3000 + (230 + 0.06533 * 30000) + (415 + 0.06164 * 60000);
380
+ expect(res.cout.cout_ch).toBeCloseTo(attendu, 6);
381
+ });
382
+
383
+ test("coût de l'électricité : cinq paliers tarifaires couverts", () => {
384
+ const ch = [
385
+ installCh({ cle_repartition_ch: 1 }, [
386
+ genCh('1', { conso_ch: 500, conso_ch_depensier: 500 }), // < 1000
387
+ genCh('1', { conso_ch: 1500, conso_ch_depensier: 1500 }), // < 2500
388
+ genCh('1', { conso_ch: 3000, conso_ch_depensier: 3000 }), // < 5000
389
+ genCh('1', { conso_ch: 10000, conso_ch_depensier: 10000 }), // < 15000
390
+ genCh('1', { conso_ch: 20000, conso_ch_depensier: 20000 }) // else
391
+ ])
392
+ ];
393
+
394
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
395
+ const attendu =
396
+ 0.29007 * 500 +
397
+ (149 + 0.14066 * 1500) +
398
+ (122 + 0.15176 * 3000) +
399
+ (94 + 0.15735 * 10000) +
400
+ (56 + 0.15989 * 20000);
401
+ expect(res.cout.cout_ch).toBeCloseTo(attendu, 6);
402
+ });
403
+
404
+ test('ECS collective (prorataECS=1) : clé de répartition = clé × rdim', () => {
405
+ const ecs = [
406
+ installEcs({ cle_repartition_ecs: 2, rdim: 3 }, [
407
+ genEcs('1', { conso_ecs: 100, conso_ecs_depensier: 120 })
408
+ ])
409
+ ];
410
+
411
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE, coef_ep);
412
+ // clé = 2 * 3 = 6 => conso_ecs = 100 * 6
413
+ expect(res.ef_conso.conso_ecs).toBe(600);
414
+ });
415
+
416
+ test('ECS avec prorataECS différent de 1 : clé issue du prorata', () => {
417
+ // Quand prorataECS !== 1, la boucle ECS ne renseigne pas donnee_utilisateur :
418
+ // on la fournit en amont (coefficient GES déjà connu) pour refléter les données réelles.
419
+ const gen = genEcs('1', { conso_ecs: 100, conso_ecs_depensier: 120 });
420
+ gen.donnee_utilisateur = { coeffEmissionGes: 0.065 };
421
+ const ecs = [installEcs({}, [gen])];
422
+
423
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 0.5, 1, DATE_DPE, coef_ep);
424
+ // clé absente => prorataECS (0,5) => conso_ecs = 100 * 0,5
425
+ expect(res.ef_conso.conso_ecs).toBe(50);
426
+ });
427
+
428
+ test('ECS sans clé ni rdim, énergie non renseignée : valeurs de repli (1 et électricité)', () => {
429
+ // cle_repartition_ecs et rdim absents => 1 ; enum_type_energie_id absent => électricité (id 1)
430
+ const ecs = [installEcs({}, [genEcs(undefined, { conso_ecs: 100, conso_ecs_depensier: 120 })])];
431
+
432
+ const res = calc_conso(100, 1, 1, [], [], ecs, [], 1, 1, DATE_DPE, coef_ep);
433
+ // clé = 1 * 1 => conso_ecs = 100 ; EP électricité => 100 * 1,9
434
+ expect(res.ef_conso.conso_ecs).toBe(100);
435
+ expect(res.ep_conso.ep_conso_ecs).toBeCloseTo(190, 9);
436
+ });
437
+
438
+ test.each([
439
+ ['absente', undefined],
440
+ ['nulle', null],
441
+ ['non tabulaire', { installation_ecs: [] }]
442
+ ])('collection ECS %s : traitée comme une liste vide', (_libelle, ecs) => {
443
+ const chFixture = () => [
444
+ installCh({ cle_repartition_ch: 1 }, [
445
+ genCh('1', { conso_ch: 1000, conso_ch_depensier: 1200 })
446
+ ])
447
+ ];
448
+
449
+ const res = calc_conso(100, 1, 1, [], chFixture(), ecs, [], 1, 1, DATE_DPE, coef_ep);
450
+
451
+ // Identique à un appel avec une collection ECS vide : aucune conso ni auxiliaire d'ECS
452
+ expect(res).toEqual(calc_conso(100, 1, 1, [], chFixture(), [], [], 1, 1, DATE_DPE, coef_ep));
453
+ expect(res.ef_conso.conso_ecs).toBe(0);
454
+ expect(res.ef_conso.conso_auxiliaire_distribution_ecs).toBe(0);
455
+ });
456
+
457
+ test('ventilation et froid : auxiliaires et froid agrégés par énergie', () => {
458
+ const vt = [
459
+ {
460
+ donnee_entree: { cle_repartition_ventilation: 2 },
461
+ donnee_intermediaire: { conso_auxiliaire_ventilation: 20 }
462
+ },
463
+ // conso absente => 0 ; pas de clé de répartition
464
+ { donnee_entree: {}, donnee_intermediaire: {} }
465
+ ];
466
+ const fr = [
467
+ {
468
+ donnee_entree: { enum_type_energie_id: '1' },
469
+ donnee_intermediaire: { conso_fr: 200, conso_fr_depensier: 250 }
81
470
  }
82
471
  ];
83
- const ecs = [];
84
- const fr = [];
85
472
 
86
- const result = calc_conso(Sh, zc_id, ca_id, vt, ch, ecs, fr);
473
+ const res = calc_conso(100, 1, 1, vt, [], [], fr, 1, 1, DATE_DPE, coef_ep);
474
+
475
+ // ventilation : 20 * 2 (clé) + 0 = 40
476
+ expect(res.ef_conso.conso_auxiliaire_ventilation).toBe(40);
477
+ expect(res.ef_conso.conso_fr).toBe(200);
478
+ // sortie par énergie pour l'électricité
479
+ const elec = res.sortie_par_energie_collection.sortie_par_energie.find(
480
+ (e) => e.enum_type_energie_id === '1'
481
+ );
482
+ expect(elec.conso_ch).toBeDefined();
483
+ });
484
+
485
+ test('sortie par énergie : électricité ajoutée même en son absence', () => {
486
+ // Seul le gaz est présent => l'électricité (id 1) est tout de même ajoutée
487
+ const ch = [
488
+ installCh({ cle_repartition_ch: 1 }, [
489
+ genCh('2', { conso_ch: 1000, conso_ch_depensier: 1200 })
490
+ ])
491
+ ];
87
492
 
88
- expect(result.ef_conso.conso_ch).toBe(6322.706407855126);
89
- expect(result.ef_conso.conso_ch_depensier).toBe(7679.56883918009);
493
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
494
+ const ids = res.sortie_par_energie_collection.sortie_par_energie.map(
495
+ (e) => e.enum_type_energie_id
496
+ );
497
+ expect(ids).toContain('2');
498
+ expect(ids).toContain('1');
90
499
  });
91
500
  });