@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
@@ -1,102 +1,488 @@
1
- import calc_chauffage from './9_chauffage.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 inertie_id = 4; // Inertie du batiment
10
- const map_id = 2; // Méthode d'application du DPE (enum_methode_application_dpe_log_id)
11
- const bch = 7998.446638037776; // Besoin en chauffage
12
- const bch_dep = 9714.925476692655; // Besoin en chauffage dépensier
13
- const GV = 150.18558182166174; // Déperdition de l'enveloppe
14
- const hsp = 2.5700000000000003; // Hauteur sous plafond
15
- const ac = 1; // Période de construction
16
-
17
- const ch = {
18
- donnee_entree: {
19
- description:
20
- 'Convecteur électrique NFC, NF** et NF*** avec programmateur pièce par pièce (système individuel)',
21
- reference: '2024_07_11_09_30_44_722898200737262',
22
- surface_chauffee: 22.76,
23
- rdim: 1,
24
- nombre_niveau_installation_ch: 1,
25
- enum_cfg_installation_ch_id: '1',
26
- enum_type_installation_id: '1',
27
- enum_methode_calcul_conso_id: '1'
28
- },
29
- emetteur_chauffage_collection: {
30
- emetteur_chauffage: [
31
- {
32
- donnee_entree: {
33
- description: '',
34
- reference: 'Emetteur:2024_07_11_09_30_44_722898200737262#1',
35
- surface_chauffee: 22.76,
36
- tv_rendement_emission_id: 1,
37
- tv_rendement_distribution_ch_id: 1,
38
- tv_rendement_regulation_id: 1,
39
- enum_type_emission_distribution_id: '1',
40
- tv_intermittence_id: 138,
41
- reseau_distribution_isole: 0,
42
- enum_equipement_intermittence_id: '4',
43
- enum_type_regulation_id: '2',
44
- enum_periode_installation_emetteur_id: '1',
45
- enum_type_chauffage_id: '1',
46
- enum_temp_distribution_ch_id: '1',
47
- enum_lien_generateur_emetteur_id: '1'
48
- }
49
- }
50
- ]
51
- },
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
- }
70
- ]
71
- }
72
- };
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `9_chauffage.js` :
5
+ * - `utils` : `requestInput`, `tv`, `tvColumnIDs` et la table `Tbase` ;
6
+ * - `9_emetteur_ch` : `calc_emetteur_ch` (renseignement des émetteurs) ;
7
+ * - `9_generateur_ch` : `calc_generateur_ch` (consommations, piloté par un
8
+ * espion qui écrit les données intermédiaires), `checkForGeneratorType`
9
+ * (classification combustion/PAC) et `hasConsoForAuxDistribution` ;
10
+ * - `13.2_generateur_combustion` / `13.2_generateur_combustion_ch` : calculs de
11
+ * combustion (no-op espionnés).
12
+ *
13
+ * On vérifie la logique d'orchestration propre au module : mise à l'échelle du
14
+ * besoin, facteur de couverture solaire, prorata entre générateurs (cascade,
15
+ * hybride) et répartition des auxiliaires.
16
+ */
17
+ vi.mock('./utils.js', () => ({
18
+ requestInput: vi.fn(),
19
+ tv: vi.fn(),
20
+ tvColumnIDs: vi.fn(),
21
+ Tbase: {
22
+ ca1: { h1: -9.5, h2: -6, h3: -3 },
23
+ ca2: { h1: -11 }
24
+ }
25
+ }));
26
+
27
+ vi.mock('./9_emetteur_ch.js', () => ({
28
+ calc_emetteur_ch: vi.fn()
29
+ }));
30
+
31
+ vi.mock('./9_generateur_ch.js', () => ({
32
+ calc_generateur_ch: vi.fn(),
33
+ checkForGeneratorType: vi.fn(),
34
+ hasConsoForAuxDistribution: vi.fn()
35
+ }));
36
+
37
+ vi.mock('./13.2_generateur_combustion.js', () => ({
38
+ tv_generateur_combustion: vi.fn()
39
+ }));
40
+
41
+ vi.mock('./13.2_generateur_combustion_ch.js', () => ({
42
+ tv_temp_fonc_30_100: vi.fn()
43
+ }));
44
+
45
+ vi.mock('./enums.js', () => ({
46
+ default: {
47
+ classe_altitude: { 1: 'ca1', 2: 'ca2' },
48
+ zone_climatique: { 1: 'h1a', 2: 'h2b', 3: 'h3c' }
49
+ }
50
+ }));
51
+
52
+ const { default: calc_chauffage, tauxChargeForGenerator } = await import('./9_chauffage.js');
53
+ const { requestInput, tv, tvColumnIDs } = await import('./utils.js');
54
+ const { calc_emetteur_ch } = await import('./9_emetteur_ch.js');
55
+ const { calc_generateur_ch, checkForGeneratorType, hasConsoForAuxDistribution } = await import(
56
+ './9_generateur_ch.js'
57
+ );
58
+ const { tv_generateur_combustion } = await import('./13.2_generateur_combustion.js');
59
+ const { tv_temp_fonc_30_100 } = await import('./13.2_generateur_combustion_ch.js');
60
+
61
+ /** calc_generateur_ch mocké : écrit des consommations déterministes. */
62
+ function stubGenerateur({ conso = 100, consoDep = 120, auxDistribution } = {}) {
63
+ vi.mocked(calc_generateur_ch).mockImplementation((_dpe, gen) => {
64
+ gen.donnee_intermediaire = gen.donnee_intermediaire || {};
65
+ gen.donnee_intermediaire.conso_ch = conso;
66
+ gen.donnee_intermediaire.conso_ch_depensier = consoDep;
67
+ if (auxDistribution !== undefined) {
68
+ gen.donnee_intermediaire.conso_auxiliaire_distribution_ch = auxDistribution;
69
+ }
70
+ });
71
+ }
72
+
73
+ /** Émetteur minimal. */
74
+ function emetteur(lien = '1') {
75
+ return { donnee_entree: { enum_lien_generateur_emetteur_id: lien } };
76
+ }
73
77
 
74
- calc_chauffage(ch, ca_id, zc_id, inertie_id, map_id, bch, bch_dep, GV, Sh, hsp, ac);
78
+ /** Générateur minimal. */
79
+ function generateur(de = {}, di = {}) {
80
+ return {
81
+ donnee_entree: {
82
+ enum_type_generateur_ch_id: '98',
83
+ enum_lien_generateur_emetteur_id: '1',
84
+ ...de
85
+ },
86
+ donnee_intermediaire: di
87
+ };
88
+ }
89
+
90
+ /** Installation de chauffage minimale. */
91
+ function installation(gens, ems, deOverrides = {}) {
92
+ return {
93
+ donnee_entree: {
94
+ surface_chauffee: 100,
95
+ enum_cfg_installation_ch_id: '1',
96
+ ...deOverrides
97
+ },
98
+ emetteur_chauffage_collection: { emetteur_chauffage: ems },
99
+ generateur_chauffage_collection: { generateur_chauffage: gens }
100
+ };
101
+ }
102
+
103
+ /** DPE minimal (enveloppe non exploitée ici, calc_generateur_ch mocké). */
104
+ const dpe = { logement: { enveloppe: {} } };
105
+
106
+ /** Appel standard de calc_chauffage. */
107
+ function appel(ch, overrides = {}) {
108
+ const args = {
109
+ ca_id: 1,
110
+ zc_id: 1,
111
+ inertie_id: 3,
112
+ map_id: 2,
113
+ bch: 1000,
114
+ bch_dep: 1200,
115
+ GV: 150,
116
+ Sh: 100,
117
+ hsp: 2.5,
118
+ ac: 1,
119
+ ilpa: 0,
120
+ besoin_ch_mois: {},
121
+ ...overrides
122
+ };
123
+ calc_chauffage(
124
+ dpe,
125
+ ch,
126
+ args.ca_id,
127
+ args.zc_id,
128
+ args.inertie_id,
129
+ args.map_id,
130
+ args.bch,
131
+ args.bch_dep,
132
+ args.GV,
133
+ args.Sh,
134
+ args.hsp,
135
+ args.ac,
136
+ args.ilpa,
137
+ args.besoin_ch_mois
138
+ );
139
+ return args;
140
+ }
141
+
142
+ beforeEach(() => {
143
+ vi.mocked(requestInput).mockReset();
144
+ vi.mocked(tv).mockReset();
145
+ vi.mocked(tvColumnIDs).mockReset();
146
+ vi.mocked(calc_emetteur_ch).mockReset();
147
+ vi.mocked(calc_generateur_ch).mockReset();
148
+ vi.mocked(checkForGeneratorType).mockReset();
149
+ vi.mocked(hasConsoForAuxDistribution).mockReset();
150
+ vi.mocked(tv_generateur_combustion).mockReset();
151
+ vi.mocked(tv_temp_fonc_30_100).mockReset();
152
+
153
+ // valeurs par défaut : générateur ni combustion ni PAC, pas d'auxiliaire
154
+ vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => {
155
+ du.isCombustionGenerator = false;
156
+ });
157
+ vi.mocked(hasConsoForAuxDistribution).mockReturnValue(false);
158
+ vi.mocked(requestInput).mockReturnValue('installation de chauffage simple');
159
+ vi.mocked(tv).mockReturnValue({ facteur_couverture_solaire: '0.4' });
160
+ stubGenerateur();
161
+ });
162
+
163
+ /**
164
+ * 9. Consommation de chauffage — orchestration
165
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9
166
+ */
167
+ describe('calc_chauffage - cas nominal (1 générateur, 1 émetteur)', () => {
168
+ test('agrège les consommations et renseigne les données intermédiaires', () => {
169
+ const ch = installation([generateur()], [emetteur('1')]);
170
+ appel(ch);
75
171
 
76
172
  expect(ch.donnee_intermediaire).toStrictEqual({
77
- besoin_ch: 7998.446638037776,
78
- besoin_ch_depensier: 9714.925476692655,
79
- conso_ch: 6322.706769289927,
80
- conso_ch_depensier: 7679.569278179351
173
+ besoin_ch: 1000,
174
+ besoin_ch_depensier: 1200,
175
+ conso_ch: 100,
176
+ conso_ch_depensier: 120
81
177
  });
178
+ expect(ch.donnee_utilisateur.Pnominal).toBe(0);
179
+ expect(calc_emetteur_ch).toHaveBeenCalledTimes(1);
180
+ expect(checkForGeneratorType).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ test('renseigne les valeurs par défaut du générateur (fch, ratios, surface)', () => {
184
+ const ch = installation([generateur()], [emetteur('1')]);
185
+ appel(ch);
186
+
187
+ const genDe = ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree;
188
+ // Fch issu de la table de couverture solaire (0.4)
189
+ expect(genDe.fch).toBe(0.4);
190
+ expect(genDe.ratio_virtualisation).toBe(1);
191
+ expect(genDe.cle_repartition_ch).toBe(1);
192
+ expect(genDe.nombre_niveau_installation_ch).toBe(1);
193
+ expect(genDe.surface_chauffee).toBe(100);
194
+ });
195
+
196
+ test('conserve les valeurs déjà présentes sur le générateur', () => {
197
+ const ch = installation([generateur()], [emetteur('1')], {});
198
+ // valeurs propres sur l'installation
199
+ ch.donnee_entree.ratio_virtualisation = 2;
200
+ ch.donnee_entree.cle_repartition_ch = 3;
201
+ ch.donnee_entree.nombre_niveau_installation_ch = 4;
202
+ appel(ch);
203
+
204
+ const genDe = ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree;
205
+ expect(genDe.ratio_virtualisation).toBe(2);
206
+ expect(genDe.cle_repartition_ch).toBe(3);
207
+ expect(genDe.nombre_niveau_installation_ch).toBe(4);
208
+ });
209
+
210
+ test('prorata unitaire : le besoin complet est transmis au générateur', () => {
211
+ const ch = installation([generateur()], [emetteur('1')]);
212
+ appel(ch, { bch: 800 });
213
+ // 6e argument de calc_generateur_ch = bch * prorata (= 800 * 1)
214
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(800);
215
+ });
216
+ });
217
+
218
+ describe('calc_chauffage - mise à l’échelle du besoin selon la surface', () => {
219
+ test('besoin ajusté au prorata surface_chauffee / Sh quand elles diffèrent', () => {
220
+ const ch = installation([generateur()], [emetteur('1')]);
221
+ ch.donnee_entree.surface_chauffee = 50;
222
+ appel(ch, { bch: 1000, Sh: 100 });
223
+ // bch mis à l'échelle : 1000 * 50 / 100 = 500 transmis au générateur
224
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(500);
225
+ });
226
+
227
+ test('surface d’installation absente : la surface habitable Sh sert de repli', () => {
228
+ const ch = installation([generateur()], [emetteur('1')], { surface_chauffee: 0 });
229
+ appel(ch, { Sh: 120 });
230
+ // de.surface_chauffee falsy => genChDe.surface_chauffee = Sh
82
231
  expect(
83
- ch.emetteur_chauffage_collection.emetteur_chauffage[0].donnee_intermediaire
84
- ).toStrictEqual({
85
- i0: 0.86,
86
- rendement_distribution: 1,
87
- rendement_emission: 0.95,
88
- rendement_regulation: 0.99
232
+ ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree.surface_chauffee
233
+ ).toBe(120);
234
+ });
235
+ });
236
+
237
+ describe('calc_chauffage - facteur de couverture solaire (Fch)', () => {
238
+ test('facteur saisi directement pris tel quel', () => {
239
+ const ch = installation([generateur()], [emetteur('1')]);
240
+ ch.donnee_entree.fch_saisi = 0.7;
241
+ appel(ch);
242
+ expect(ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree.fch).toBe(0.7);
243
+ expect(tv).not.toHaveBeenCalled();
244
+ });
245
+
246
+ test('facteur lu en table quand non saisi', () => {
247
+ const ch = installation([generateur()], [emetteur('1')]);
248
+ vi.mocked(tv).mockReturnValue({ facteur_couverture_solaire: '0.55' });
249
+ appel(ch);
250
+ expect(ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree.fch).toBe(0.55);
251
+ });
252
+
253
+ test('aucune ligne trouvée : Fch nul, repli sur 0.5 et message d’erreur', () => {
254
+ const err = vi.spyOn(console, 'error').mockImplementation(() => {});
255
+ const ch = installation([generateur()], [emetteur('1')]);
256
+ vi.mocked(tv).mockReturnValue(null);
257
+ appel(ch);
258
+ // Fch = null => genChDe.fch = null || 0.5
259
+ expect(ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree.fch).toBe(0.5);
260
+ expect(err).toHaveBeenCalled();
261
+ err.mockRestore();
262
+ });
263
+ });
264
+
265
+ describe('calc_chauffage - générateurs à combustion', () => {
266
+ beforeEach(() => {
267
+ vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => {
268
+ du.isCombustionGenerator = true;
89
269
  });
270
+ vi.mocked(tvColumnIDs).mockReturnValue(['5']);
271
+ });
272
+
273
+ test('méthode de saisie ≠ 5 : recalcul des températures de fonctionnement', () => {
274
+ const ch = installation(
275
+ [generateur({ enum_type_generateur_ch_id: '5', enum_methode_saisie_carac_sys_id: '1' })],
276
+ [emetteur('1')]
277
+ );
278
+ appel(ch);
279
+ expect(tv_generateur_combustion).toHaveBeenCalled();
280
+ expect(tv_temp_fonc_30_100).toHaveBeenCalled();
281
+ });
282
+
283
+ test('méthode 5 avec températures déjà connues : pas de recalcul', () => {
284
+ const ch = installation(
285
+ [
286
+ generateur(
287
+ { enum_type_generateur_ch_id: '5', enum_methode_saisie_carac_sys_id: '5' },
288
+ { temp_fonc_30: 30, temp_fonc_100: 45 }
289
+ )
290
+ ],
291
+ [emetteur('1')]
292
+ );
293
+ appel(ch);
294
+ expect(tv_temp_fonc_30_100).not.toHaveBeenCalled();
295
+ });
296
+
297
+ test('méthode 5 mais temp_fonc_30 manquante : recalcul', () => {
298
+ const ch = installation(
299
+ [
300
+ generateur(
301
+ { enum_type_generateur_ch_id: '5', enum_methode_saisie_carac_sys_id: '5' },
302
+ { temp_fonc_100: 45 }
303
+ )
304
+ ],
305
+ [emetteur('1')]
306
+ );
307
+ appel(ch);
308
+ expect(tv_temp_fonc_30_100).toHaveBeenCalled();
309
+ });
310
+
311
+ test('méthode 5 mais temp_fonc_100 manquante : recalcul', () => {
312
+ const ch = installation(
313
+ [
314
+ generateur(
315
+ { enum_type_generateur_ch_id: '5', enum_methode_saisie_carac_sys_id: '5' },
316
+ { temp_fonc_30: 30 }
317
+ )
318
+ ],
319
+ [emetteur('1')]
320
+ );
321
+ appel(ch);
322
+ expect(tv_temp_fonc_30_100).toHaveBeenCalled();
323
+ });
324
+
325
+ test('type de générateur hors table temp_fonc_30 : pas de recalcul de températures', () => {
326
+ const ch = installation(
327
+ [generateur({ enum_type_generateur_ch_id: '99', enum_methode_saisie_carac_sys_id: '1' })],
328
+ [emetteur('1')]
329
+ );
330
+ appel(ch);
331
+ expect(tv_generateur_combustion).toHaveBeenCalled();
332
+ expect(tv_temp_fonc_30_100).not.toHaveBeenCalled();
333
+ });
334
+
335
+ test('deux générateurs à combustion en cascade : prorata au prorata des puissances', () => {
336
+ const gen1 = generateur({ enum_type_generateur_ch_id: '5' }, { pn: 3000 });
337
+ const gen2 = generateur({ enum_type_generateur_ch_id: '5' }, { pn: 1000 });
338
+ const ch = installation([gen1, gen2], [emetteur('1')], {
339
+ enum_cfg_installation_ch_id: '1'
340
+ });
341
+ appel(ch, { bch: 1000 });
342
+
343
+ // Pnominal = 4000 ; prorata gen1 = 3000/4000, gen2 = 1000/4000
344
+ expect(ch.donnee_utilisateur.Pnominal).toBe(4000);
345
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(750, 9);
346
+ expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBeCloseTo(250, 9);
347
+ });
348
+ });
349
+
350
+ describe('calc_chauffage - prorata entre générateurs', () => {
351
+ test('configuration ≠ 1 : prorata neutre (1 / (0 || 1))', () => {
352
+ const ch = installation([generateur()], [emetteur('1')], {
353
+ enum_cfg_installation_ch_id: '2'
354
+ });
355
+ appel(ch, { bch: 900 });
356
+ // nbCascadeForSameEmetteur = 0 => prorata = 1 / (0 || 1) = 1
357
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(900);
358
+ });
359
+
360
+ test('plusieurs émetteurs pour le même lien : pas de cascade (nbEmetteur ≠ 1)', () => {
361
+ const ch = installation(
362
+ [generateur({ enum_lien_generateur_emetteur_id: '1' })],
363
+ [emetteur('1'), emetteur('1')],
364
+ { enum_cfg_installation_ch_id: '1' }
365
+ );
366
+ appel(ch, { bch: 900 });
367
+ // nbEmetteur = 2 => nbCascadeForSameEmetteur = 0 => prorata = 1
368
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(900);
369
+ });
370
+
371
+ test('générateur PAC hybride avec scop : part PAC (0.8 en zone H1)', () => {
372
+ const gen = generateur({ enum_type_generateur_ch_id: '150' }, { scop: 3.5 });
373
+ const ch = installation([gen], [emetteur('1')]);
374
+ appel(ch, { bch: 1000, zc_id: 1 });
375
+ // hybrideProrata h1 pac = 0.8
376
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(800, 9);
377
+ });
378
+
379
+ test('générateur PAC hybride sans scop/cop : part chaudière (0.2 en zone H1)', () => {
380
+ const gen = generateur({ enum_type_generateur_ch_id: '150' }, {});
381
+ const ch = installation([gen], [emetteur('1')]);
382
+ appel(ch, { bch: 1000, zc_id: 1 });
383
+ // hybrideProrata h1 chaudiere = 0.2
384
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(200, 9);
385
+ });
386
+ });
387
+
388
+ describe('calc_chauffage - auxiliaires de distribution répartis', () => {
389
+ test('répartition de la consommation d’auxiliaires sur les générateurs éligibles', () => {
390
+ vi.mocked(hasConsoForAuxDistribution).mockReturnValue(true);
391
+ stubGenerateur({ auxDistribution: 400 });
392
+
393
+ const ch = installation(
394
+ [
395
+ generateur({ enum_type_generateur_ch_id: '110' }),
396
+ generateur({ enum_type_generateur_ch_id: '110' })
397
+ ],
398
+ [emetteur('1')],
399
+ { enum_cfg_installation_ch_id: '2' }
400
+ );
401
+ appel(ch);
402
+
403
+ // 2 générateurs éligibles => 400 / 2 = 200 chacun
404
+ const gens = ch.generateur_chauffage_collection.generateur_chauffage;
405
+ expect(gens[0].donnee_intermediaire.conso_auxiliaire_distribution_ch).toBe(200);
406
+ expect(gens[1].donnee_intermediaire.conso_auxiliaire_distribution_ch).toBe(200);
407
+ });
408
+
409
+ test('aucun auxiliaire de distribution : pas de division', () => {
410
+ const ch = installation([generateur()], [emetteur('1')]);
411
+ appel(ch);
412
+ // conso_auxiliaire_distribution_ch non défini par le stub => aucune répartition
90
413
  expect(
91
414
  ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_intermediaire
92
- ).toStrictEqual({
93
- conso_auxiliaire_generation_ch: 0,
94
- conso_auxiliaire_generation_ch_depensier: 0,
95
- conso_ch: 6322.706769289927,
96
- conso_ch_depensier: 7679.569278179351,
97
- rendement_generation: 1,
98
- rg: 1,
99
- rg_dep: 1
100
- });
415
+ .conso_auxiliaire_distribution_ch
416
+ ).toBeUndefined();
417
+ });
418
+ });
419
+
420
+ describe('calc_chauffage - initialisation défensive des données du générateur', () => {
421
+ test('générateur sans donnee_utilisateur ni donnee_intermediaire initiales', () => {
422
+ const gen = {
423
+ donnee_entree: { enum_type_generateur_ch_id: '98', enum_lien_generateur_emetteur_id: '1' }
424
+ };
425
+ const ch = installation([gen], [emetteur('1')]);
426
+ appel(ch);
427
+ expect(gen.donnee_utilisateur.nbGenerateurCascade).toBe(1);
428
+ expect(gen.donnee_intermediaire.conso_ch).toBe(100);
429
+ });
430
+ });
431
+
432
+ /**
433
+ * 13.2.1.2 Taux de charge des générateurs à combustion (cdimref)
434
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.2
435
+ */
436
+ describe('tauxChargeForGenerator - taux de charge des générateurs à combustion', () => {
437
+ /** Installation avec un générateur à combustion. */
438
+ function installCombustion(pnominal = 5000, rdim) {
439
+ return {
440
+ donnee_entree: { rdim },
441
+ donnee_utilisateur: { Pnominal: pnominal },
442
+ generateur_chauffage_collection: {
443
+ generateur_chauffage: [{ donnee_utilisateur: { isCombustionGenerator: true } }]
444
+ }
445
+ };
446
+ }
447
+
448
+ test('maison : GV utilisé directement pour le calcul du taux de charge', () => {
449
+ const install = installCombustion(5000);
450
+ tauxChargeForGenerator([install], 200, 1, 1, 'maison');
451
+
452
+ const gen = install.generateur_chauffage_collection.generateur_chauffage[0];
453
+ // cdimref = Pn / (GV * (19 - tbase)) ; tbase = -9.5 (ca1/h1)
454
+ expect(gen.donnee_utilisateur.cdimref).toBeCloseTo(5000 / (200 * (19 + 9.5)), 9);
455
+ expect(gen.donnee_utilisateur.cdimrefDep).toBeCloseTo(5000 / (200 * (21 + 9.5)), 9);
456
+ });
457
+
458
+ test('immeuble : GV corrigé par le ratio de dimensionnement rdim', () => {
459
+ const install = installCombustion(5000, 2);
460
+ tauxChargeForGenerator([install], 200, 1, 1, 'immeuble');
461
+
462
+ const gen = install.generateur_chauffage_collection.generateur_chauffage[0];
463
+ const gvRatio = 200 * (1 / 2);
464
+ expect(gen.donnee_utilisateur.cdimref).toBeCloseTo(5000 / (gvRatio * (19 + 9.5)), 9);
465
+ });
466
+
467
+ test('immeuble sans rdim : ratio de dimensionnement par défaut (1)', () => {
468
+ const install = installCombustion(5000);
469
+ tauxChargeForGenerator([install], 200, 1, 1, 'immeuble');
470
+
471
+ const gen = install.generateur_chauffage_collection.generateur_chauffage[0];
472
+ // rdim absent => GV_ratio = GV
473
+ expect(gen.donnee_utilisateur.cdimref).toBeCloseTo(5000 / (200 * (19 + 9.5)), 9);
474
+ });
475
+
476
+ test('installation sans générateur à combustion : ignorée', () => {
477
+ const installSans = {
478
+ donnee_entree: {},
479
+ donnee_utilisateur: {},
480
+ generateur_chauffage_collection: {
481
+ generateur_chauffage: [{ donnee_utilisateur: { isCombustionGenerator: false } }]
482
+ }
483
+ };
484
+ // aucune erreur, aucune donnée de taux de charge écrite
485
+ expect(() => tauxChargeForGenerator([installSans], 200, 1, 1, 'maison')).not.toThrow();
486
+ expect(installSans.donnee_utilisateur.genCombustion).toBeUndefined();
101
487
  });
102
488
  });