@open3cl/engine 1.11.1 → 1.11.3

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 (48) hide show
  1. package/11_ecs.spec.js +17 -1
  2. package/11_rdim_ecs_individuelle_unique.js +78 -0
  3. package/11_rdim_ecs_individuelle_unique.spec.js +136 -0
  4. package/13.2_generateur_combustion.spec.js +24 -1
  5. package/13.2_generateur_combustion_bouilleur.spec.js +8 -0
  6. package/13.2_generateur_combustion_ch.spec.js +41 -0
  7. package/14_generateur_ecs.spec.js +72 -0
  8. package/15_conso_aux.spec.js +20 -0
  9. package/3.1_b.spec.js +23 -0
  10. package/3.2.1_mur.spec.js +99 -0
  11. package/3.2.3_plancher_haut.spec.js +29 -0
  12. package/3.3_baie_vitree.spec.js +10 -0
  13. package/3.4_pont_thermique.spec.js +27 -0
  14. package/4_ventilation.spec.js +12 -0
  15. package/7_inertie.spec.js +10 -0
  16. package/9_besoin_ch.spec.js +4 -0
  17. package/9_conso_ch.js +1 -1
  18. package/9_emetteur_ch.spec.js +3 -1
  19. package/9_generateur_ch.spec.js +11 -0
  20. package/README.md +13 -13
  21. package/apport_et_besoin.spec.js +5 -1
  22. package/core/assets/domain/add-additionnal-ue-values-tables.js +0 -19
  23. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +3 -4
  24. package/core/assets/domain/synchronize-dpe-ges-limit-values-tables.spec.js +25 -0
  25. package/core/assets/domain/synchronize-valeur-tables.spec.js +43 -0
  26. package/core/util/logger/log-service.spec.js +11 -0
  27. package/dpe-sanitizer.service.spec.js +9 -0
  28. package/engine.js +10 -2
  29. package/engine.spec.js +66 -5
  30. package/features/dpe/domain/models/baie-ets.model.ts +8 -11
  31. package/features/dpe/domain/models/baie-vitree.model.ts +24 -96
  32. package/features/dpe/domain/models/climatisation.model.ts +13 -23
  33. package/features/dpe/domain/models/dpe-xsd.generated.ts +2797 -0
  34. package/features/dpe/domain/models/dpe.model.ts +102 -171
  35. package/features/dpe/domain/models/ets.model.ts +10 -18
  36. package/features/dpe/domain/models/installation-chauffage.model.ts +49 -102
  37. package/features/dpe/domain/models/installation-ecs.model.ts +27 -77
  38. package/features/dpe/domain/models/mur.model.ts +10 -38
  39. package/features/dpe/domain/models/plancher.model.ts +22 -63
  40. package/features/dpe/domain/models/pont-thermique.model.ts +10 -20
  41. package/features/dpe/domain/models/porte.model.ts +10 -30
  42. package/features/dpe/domain/models/production-elec-enr.model.ts +13 -27
  43. package/features/dpe/domain/models/sortie.model.ts +32 -176
  44. package/features/dpe/domain/models/ventilation.model.ts +15 -25
  45. package/features/normalizer/domain/dpe-normalizer.service.spec.js +5 -3
  46. package/package.json +27 -45
  47. package/utils.js +1 -2
  48. package/utils.spec.js +16 -0
package/11_ecs.spec.js CHANGED
@@ -13,7 +13,8 @@ vi.mock('./enums.js', () => ({
13
13
  type_installation: {
14
14
  1: 'installation individuelle',
15
15
  2: 'installation collective',
16
- 3: 'installation collective multi-bâtiment'
16
+ 3: 'installation collective multi-bâtiment',
17
+ 4: 'installation de type inconnu'
17
18
  }
18
19
  }
19
20
  }));
@@ -566,6 +567,21 @@ describe('calc_ecs - rendement de distribution', () => {
566
567
  });
567
568
  });
568
569
 
570
+ test('type d’installation ni individuelle ni collective : aucun critère de configuration', () => {
571
+ const de = { enum_type_installation_id: '4' };
572
+ const ecs = makeEcs(de, [
573
+ {
574
+ donnee_entree: { position_volume_chauffe: 1 },
575
+ donnee_intermediaire: { conso_ecs: 0, conso_ecs_depensier: 0 }
576
+ }
577
+ ]);
578
+ calc_ecs({}, ecs, 100, 200, 1, 'ca1', 'h1a', 'maison', false, null, null, false);
579
+
580
+ // Le matcher reste vide : ni configuration_logement ni type_reseau_collectif
581
+ expect(tv).toHaveBeenCalledWith('rendement_distribution_ecs', {});
582
+ expect(requestInput).not.toHaveBeenCalled();
583
+ });
584
+
569
585
  test('aucune ligne forfaitaire trouvée : rendement de distribution non renseigné', () => {
570
586
  const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
571
587
  vi.mocked(tv).mockReturnValue(undefined);
@@ -60,3 +60,81 @@ export function reconstituerRdimEcsIndividuelleUnique(th, instal_ecs, surfaceHab
60
60
  instal_ecs[0].donnee_entree.rdim_reconstitue = true;
61
61
  return true;
62
62
  }
63
+
64
+ /**
65
+ * DPE immeuble avec plusieurs installations ECS individuelles, chacune décrite pour un logement type.
66
+ *
67
+ * Même convention d'export que l'installation unique (3.0.x, 2.0.x, 1.1.x) : pour chaque
68
+ * installation i, `surface_habitable` = Sh_i d'un logement type et `nombre_logement` = N_i, sans
69
+ * `rdim`, avec Σ Sh_i × N_i = SH. Tribu traite chaque installation indépendamment avec Rdim_i = N_i :
70
+ * Becs_i = Becs_immeuble × Sh_i × Rdim_i / SH / Rdim_i, puis Cecs = Σ Cecs_i × Rdim_i. L'export ADEME
71
+ * le confirme : besoin_ecs_i = Becs_immeuble × Sh_i / SH et conso_ecs = Σ conso_ecs_i × N_i.
72
+ *
73
+ * Sans rdim, Σ Sh_i ≠ SH : Open3CL se replie sur Becs_immeuble / nombre_appartement par
74
+ * installation et ne remultiplie pas la conso par N_i.
75
+ *
76
+ * Gardes :
77
+ * - Σ Sh_i × N_i ≈ SH (TOLERANCE_SURFACE_LOGEMENT_TYPE) : exclut les exports où Sh_i est déjà
78
+ * la surface totale desservie (Σ Sh_i = SH, ex. N_i fractionnaires) ;
79
+ * - au moins un N_i > 1 : sinon rdim = 1 partout, sans effet (LICIEL, nombre_logement = 1).
80
+ *
81
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.4 et § 17.1.3.2
82
+ * @see Tribu : Calcul_batiment.cs l. 581-588 (boucle par installation, Ratio = Sh·Rdim/SH,
83
+ * Becs / Rdim), l. 1660-1662 (Cecs_i / Rdim puis × Rdim) ; Calcul_installation_ECS.cs l. 75
84
+ * @see Guide Tribu TE-211208 « Particularités d'une répétition de systèmes d'ECS individuel dans un
85
+ * immeuble collectif » (Sh_ecs = surface d'un logement, Rdim = nombre d'installations identiques)
86
+ */
87
+
88
+ /**
89
+ * Rdim implicites des installations ECS individuelles multiples d'un immeuble (dans l'ordre des
90
+ * installations), ou null si non concernées.
91
+ *
92
+ * @param th {string} type de bâtiment (maison, appartement, immeuble)
93
+ * @param instal_ecs {Array} installation_ecs
94
+ * @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
95
+ * @return {number[]|null}
96
+ */
97
+ export function rdimsImplicitesEcsIndividuellesMultiples(th, instal_ecs, surfaceHabitableImmeuble) {
98
+ if (th !== 'immeuble' || !Array.isArray(instal_ecs) || instal_ecs.length < 2) return null;
99
+
100
+ const des = instal_ecs.map((inst) => inst?.donnee_entree);
101
+ const concernees = des.every(
102
+ (de) => de && String(de.enum_type_installation_id) === '1' && !de.rdim
103
+ );
104
+ if (!concernees) return null;
105
+
106
+ const SH = Number(surfaceHabitableImmeuble);
107
+ if (!(SH > 0)) return null;
108
+
109
+ const surfaces = des.map((de) => Number(de.surface_habitable));
110
+ const rdims = des.map((de) => Number(de.nombre_logement) || 1);
111
+ if (!surfaces.every((sh) => sh > 0) || !rdims.every((n) => n > 0)) return null;
112
+ if (!rdims.some((n) => n > 1)) return null;
113
+
114
+ const sommeShN = surfaces.reduce((acc, sh, i) => acc + sh * rdims[i], 0);
115
+ return Math.abs(sommeShN - SH) <= TOLERANCE_SURFACE_LOGEMENT_TYPE * SH ? rdims : null;
116
+ }
117
+
118
+ /**
119
+ * Reconstitue le rdim des installations ECS individuelles multiples d'un immeuble décrites pour un
120
+ * logement type. Chaque installation reçoit `rdim` = nombre_logement (1 si absent) et le flag
121
+ * `rdim_reconstitue`.
122
+ *
123
+ * @param th {string} type de bâtiment
124
+ * @param instal_ecs {Array} installation_ecs
125
+ * @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
126
+ * @return {boolean} true si les rdim ont été reconstitués
127
+ */
128
+ export function reconstituerRdimEcsIndividuellesMultiples(
129
+ th,
130
+ instal_ecs,
131
+ surfaceHabitableImmeuble
132
+ ) {
133
+ const rdims = rdimsImplicitesEcsIndividuellesMultiples(th, instal_ecs, surfaceHabitableImmeuble);
134
+ if (rdims === null) return false;
135
+ instal_ecs.forEach((inst, i) => {
136
+ inst.donnee_entree.rdim = rdims[i];
137
+ inst.donnee_entree.rdim_reconstitue = true;
138
+ });
139
+ return true;
140
+ }
@@ -1,7 +1,9 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
  import {
3
3
  rdimImpliciteEcsIndividuelleUnique,
4
+ rdimsImplicitesEcsIndividuellesMultiples,
4
5
  reconstituerRdimEcsIndividuelleUnique,
6
+ reconstituerRdimEcsIndividuellesMultiples,
5
7
  TOLERANCE_SURFACE_LOGEMENT_TYPE
6
8
  } from './11_rdim_ecs_individuelle_unique.js';
7
9
 
@@ -111,3 +113,137 @@ describe("Rdim de l'installation ECS individuelle unique d'un immeuble", () => {
111
113
  });
112
114
  });
113
115
  });
116
+
117
+ /**
118
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.4 et § 17.1.3.2
119
+ * @see Tribu : Calcul_batiment.cs l. 581-588 et l. 1660-1662 ; Calcul_installation_ECS.cs l. 75
120
+ */
121
+ describe("Rdim des installations ECS individuelles multiples d'un immeuble", () => {
122
+ /** Installation ECS individuelle décrite pour un logement type */
123
+ const inst = (surface_habitable, nombre_logement, de = {}) => ({
124
+ donnee_entree: { enum_type_installation_id: '1', surface_habitable, nombre_logement, ...de }
125
+ });
126
+ /** Cas type 2244E3039157K : 48,82 × 2 + 78,09 × 6 = 566,18 ≈ SH = 566,16 */
127
+ const ecs = () => [inst(48.82, 2), inst(78.09, 6)];
128
+
129
+ describe('rdimsImplicitesEcsIndividuellesMultiples', () => {
130
+ test('installations individuelles, Σ Sh × N = SH, sans rdim : rdim_i = nombre_logement_i', () => {
131
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), 566.16)).toEqual([2, 6]);
132
+ expect(
133
+ rdimsImplicitesEcsIndividuellesMultiples(
134
+ 'immeuble',
135
+ [
136
+ inst('48.82', '2', { enum_type_installation_id: 1 }),
137
+ inst('78.09', '6', { enum_type_installation_id: 1 })
138
+ ],
139
+ '566.16'
140
+ )
141
+ ).toEqual([2, 6]);
142
+ });
143
+
144
+ test('nombre_logement absent sur une installation : rdim = 1 pour celle-ci', () => {
145
+ // 100 × 4 + 50 × 1 = 450
146
+ expect(
147
+ rdimsImplicitesEcsIndividuellesMultiples(
148
+ 'immeuble',
149
+ [inst(100, 4), inst(50, undefined)],
150
+ 450
151
+ )
152
+ ).toEqual([4, 1]);
153
+ });
154
+
155
+ test('tolérance de 5 % sur Σ Sh × N ≈ SH', () => {
156
+ // Σ = 100 × 4 + 50 × 2 = 500 ; |500 − SH| ≤ 0,05 × SH ⇔ 476,19 ≤ SH ≤ 526,31
157
+ const deux = () => [inst(100, 4), inst(50, 2)];
158
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 526)).toEqual([4, 2]);
159
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 477)).toEqual([4, 2]);
160
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 527)).toBeNull();
161
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 476)).toBeNull();
162
+ });
163
+
164
+ test('surfaces exportées comme surface totale desservie (Σ Sh = SH) : non concerné', () => {
165
+ // Cas type 2224E1374434V : 74,4 × 3 + 31 × 1 = 254,2 ≠ SH = 105,4 = Σ Sh
166
+ expect(
167
+ rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(74.4, 3), inst(31, 1)], 105.4)
168
+ ).toBeNull();
169
+ });
170
+
171
+ test('tous les nombre_logement à 1 (LICIEL) : non concerné', () => {
172
+ expect(
173
+ rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(60, 1), inst(40, 1)], 100)
174
+ ).toBeNull();
175
+ });
176
+
177
+ test('un rdim exporté sur une installation : on conserve la saisie', () => {
178
+ expect(
179
+ rdimsImplicitesEcsIndividuellesMultiples(
180
+ 'immeuble',
181
+ [inst(48.82, 2, { rdim: 2 }), inst(78.09, 6)],
182
+ 566.16
183
+ )
184
+ ).toBeNull();
185
+ });
186
+
187
+ test('une installation collective : non concerné', () => {
188
+ expect(
189
+ rdimsImplicitesEcsIndividuellesMultiples(
190
+ 'immeuble',
191
+ [inst(48.82, 2), inst(78.09, 6, { enum_type_installation_id: '2' })],
192
+ 566.16
193
+ )
194
+ ).toBeNull();
195
+ });
196
+
197
+ test('maison ou appartement : non concerné', () => {
198
+ expect(rdimsImplicitesEcsIndividuellesMultiples('maison', ecs(), 566.16)).toBeNull();
199
+ expect(rdimsImplicitesEcsIndividuellesMultiples('appartement', ecs(), 566.16)).toBeNull();
200
+ });
201
+
202
+ test('installation unique : non concerné (cf. rdimImpliciteEcsIndividuelleUnique)', () => {
203
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(64, 10)], 640)).toBeNull();
204
+ });
205
+
206
+ test('données manquantes ou invalides : non concerné', () => {
207
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', undefined, 566.16)).toBeNull();
208
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', [], 566.16)).toBeNull();
209
+ expect(
210
+ rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(48.82, 2), {}], 566.16)
211
+ ).toBeNull();
212
+ expect(
213
+ rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(48.82, 2), null], 566.16)
214
+ ).toBeNull();
215
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), undefined)).toBeNull();
216
+ expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), 0)).toBeNull();
217
+ expect(
218
+ rdimsImplicitesEcsIndividuellesMultiples(
219
+ 'immeuble',
220
+ [inst(48.82, 2), inst(undefined, 6)],
221
+ 566.16
222
+ )
223
+ ).toBeNull();
224
+ expect(
225
+ rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(100, 4), inst(50, -2)], 300)
226
+ ).toBeNull();
227
+ });
228
+ });
229
+
230
+ describe('reconstituerRdimEcsIndividuellesMultiples', () => {
231
+ test('installations concernées : rdim et flag positionnés sur chacune', () => {
232
+ const installations = ecs();
233
+ expect(reconstituerRdimEcsIndividuellesMultiples('immeuble', installations, 566.16)).toBe(
234
+ true
235
+ );
236
+ expect(installations[0].donnee_entree).toMatchObject({ rdim: 2, rdim_reconstitue: true });
237
+ expect(installations[1].donnee_entree).toMatchObject({ rdim: 6, rdim_reconstitue: true });
238
+ });
239
+
240
+ test('installations non concernées : inchangées', () => {
241
+ const installations = [inst(60, 1), inst(40, 1)];
242
+ expect(reconstituerRdimEcsIndividuellesMultiples('immeuble', installations, 100)).toBe(false);
243
+ installations.forEach((i) => {
244
+ expect(i.donnee_entree.rdim).toBeUndefined();
245
+ expect(i.donnee_entree.rdim_reconstitue).toBeUndefined();
246
+ });
247
+ });
248
+ });
249
+ });
@@ -28,7 +28,8 @@ vi.mock('./enums.js', () => ({
28
28
  type_generateur_ch: {
29
29
  89: 'chaudière gaz standard 2001-2015',
30
30
  X: 'chaudière fioul',
31
- Y: 'chaudière gaz'
31
+ Y: 'chaudière gaz',
32
+ Z: 'générateur sans équivalent ECS'
32
33
  },
33
34
  type_generateur_ecs: {
34
35
  50: 'chaudière gaz standard',
@@ -563,6 +564,28 @@ describe('tv_generateur_combustion - génération mixte (checkEcsVsChauffageForM
563
564
  errSpy.mockRestore();
564
565
  });
565
566
 
567
+ test('type CH sans équivalent ECS : type de générateur ECS conservé sans message d’alignement', () => {
568
+ state.bug = true;
569
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
570
+ tv.mockReturnValue({ ...ROW_DEFAUT });
571
+ const di = { pn: 20000 };
572
+ // CH mixte 'Z' : aucun type ECS portant le même libellé -> pas d'alignement possible
573
+ const de = {
574
+ enum_type_generateur_ecs_id: 'A',
575
+ enum_usage_generateur_id: '3',
576
+ tv_generateur_combustion_id: '42',
577
+ ratio_virtualisation: 1,
578
+ presence_ventouse: 0
579
+ };
580
+ tv_generateur_combustion(dpeAvecChMixte('Z'), di, de, 'ecs', 200, -9, 1);
581
+ expect(tv).toHaveBeenCalledWith(
582
+ 'generateur_combustion',
583
+ expect.objectContaining({ enum_type_generateur_ecs_id: 'A' })
584
+ );
585
+ expect(errSpy).not.toHaveBeenCalled();
586
+ errSpy.mockRestore();
587
+ });
588
+
566
589
  test('types ECS et CH identiques : type de générateur ECS conservé', () => {
567
590
  state.bug = true;
568
591
  tv.mockReturnValue({ ...ROW_DEFAUT });
@@ -58,6 +58,14 @@ describe('updateGenerateurBouilleurs - substitution par une chaudière bois équ
58
58
  expect(de.enum_type_generateur_ch_id).toBe(attendu);
59
59
  });
60
60
 
61
+ test('année numérique antérieure à 1948 : aucune période correspondante, valeur par défaut conservée', () => {
62
+ getFicheTechnique.mockReturnValue({ valeur: '1900' });
63
+ const de = { enum_type_generateur_ch_id: '48' };
64
+ updateGenerateurBouilleurs({}, de, 'ch');
65
+ // Aucun seuil atteint : on garde la chaudière bois par défaut 2004-2012 (values[2004])
66
+ expect(de.enum_type_generateur_ch_id).toBe('58');
67
+ });
68
+
61
69
  test('type ECS (granulés 115) : utilise la table ECS des chaudières bois', () => {
62
70
  getFicheTechnique.mockReturnValue({ valeur: '2016' });
63
71
  const de = { enum_type_generateur_ecs_id: '115' };
@@ -173,6 +173,27 @@ describe('tv_temp_fonc_30_100 - températures de fonctionnement', () => {
173
173
  expect(di.temp_fonc_100).toBe(75);
174
174
  });
175
175
 
176
+ test('une température à 100 % plus basse issue d’un second émetteur ne remplace pas la valeur retenue', () => {
177
+ tv.mockImplementation((table, matcher) => {
178
+ if (table !== 'temp_fonc_100') return null;
179
+ // Le premier émetteur renvoie 75, le second 60 (plus basse)
180
+ return matcher.enum_temp_distribution_ch_id === '3'
181
+ ? { tv_temp_fonc_100_id: 'a', temp_fonc_100: '75' }
182
+ : { tv_temp_fonc_100_id: 'b', temp_fonc_100: '60' };
183
+ });
184
+ const di = {};
185
+ const de = { enum_type_generateur_ch_id: '89' };
186
+ tv_temp_fonc_30_100(
187
+ di,
188
+ de,
189
+ {},
190
+ [emetteur(), emetteur({ enum_temp_distribution_ch_id: '4' })],
191
+ 1975
192
+ );
193
+ expect(di.temp_fonc_100).toBe(75);
194
+ expect(de.tv_temp_fonc_100_id).toBe('a');
195
+ });
196
+
176
197
  test('aucune ligne forfaitaire trouvée : erreurs signalées pour temp_fonc_30 et temp_fonc_100', () => {
177
198
  const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
178
199
  tv.mockReturnValue(null);
@@ -709,6 +730,26 @@ describe('calc_generateur_combustion_ch - rendement de génération', () => {
709
730
  warnSpy.mockRestore();
710
731
  });
711
732
 
733
+ test('bug_for_bug_compat : qp0 déjà exprimé en W (>= 1) conservé sans correction', () => {
734
+ state.bug = true;
735
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
736
+ const di = { pn: 20000, rpn: 0.9, rpint: 0.85, qp0: 200, temp_fonc_30: 40, temp_fonc_100: 70 };
737
+ const de = {
738
+ enum_type_generateur_ch_id: '89',
739
+ enum_type_energie_id: '2',
740
+ type_energie: 'gaz naturel',
741
+ presence_regulation_combustion: false,
742
+ description: 't'
743
+ };
744
+ calc_generateur_combustion_ch({}, di, de, { cdimref: 0.8, cdimrefDep: 0.6 });
745
+ // qp0 >= 1 -> pas de multiplication par 1000, pas d'avertissement
746
+ expect(di.qp0).toBe(200);
747
+ expect(warnSpy).not.toHaveBeenCalled();
748
+ // même résultat que la correction d'un qp0 de 0,2 kW (référence de régression ci-dessus)
749
+ expect(di.rg).toBeCloseTo(0.8557877790761849, 9);
750
+ warnSpy.mockRestore();
751
+ });
752
+
712
753
  test('la présence de régulation modifie le rendement (tf30 vs tf100)', () => {
713
754
  const base = () => ({
714
755
  pn: 25000,
@@ -559,6 +559,23 @@ describe('calc_gen_ecs - consommation par générateur', () => {
559
559
  expect(g.donnee_intermediaire.Qgw).toBe(0);
560
560
  });
561
561
 
562
+ test('installation collective : volume de la fiche non numérique ignoré, volume saisi conservé', () => {
563
+ // « ba » (texte avant le premier « l ») n'est pas un nombre -> VsFiche = NaN, rejeté
564
+ vi.mocked(getFicheTechnique).mockReturnValue({ valeur: 'ballon' });
565
+ ecs_de.enum_type_installation_id = '2';
566
+ ecs_de.ratio_virtualisation = 1;
567
+ const g = gen({
568
+ type_energie: 'gaz',
569
+ enum_type_generateur_ecs_id: 'autre',
570
+ type_stockage_ecs: 'ballon',
571
+ volume_stockage: 150
572
+ });
573
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'immeuble');
574
+
575
+ // Vs = volume saisi (150), identique au cas d'une fiche « 150 L »
576
+ expect(g.donnee_intermediaire.Qgw).toBeCloseTo(1064620.987190969, 9);
577
+ });
578
+
562
579
  test('bug_for_bug_compat : volume proratisé recalé sur la description', () => {
563
580
  state.bug = true;
564
581
  vi.mocked(getVolumeStockageFromDescription).mockReturnValue(100);
@@ -852,6 +869,61 @@ describe('calc_gen_ecs - consommation par générateur', () => {
852
869
  expect(g.donnee_entree.enum_type_generateur_ecs_id).toBe('84');
853
870
  });
854
871
 
872
+ test('type 84 combustion : ligne combustion sans type ECS, type 84 conservé', () => {
873
+ vi.mocked(getChaudiereFioulDefautId).mockReturnValue(undefined);
874
+ vi.mocked(tv).mockReturnValue({ tv_generateur_combustion_id: '42' });
875
+ const g = gen({
876
+ type_energie: 'gaz',
877
+ enum_type_generateur_ecs_id: '84',
878
+ tv_generateur_combustion_id: '42',
879
+ enum_methode_saisie_carac_sys_id: '1'
880
+ });
881
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'maison');
882
+
883
+ // Aucune liste de types ECS exploitable dans la ligne : pas de redressement
884
+ expect(g.donnee_entree.enum_type_generateur_ecs_id).toBe('84');
885
+ });
886
+
887
+ test('type 84 sans combustion ni pompe à chaleur : ni calcul SCOP ni redressement du type', () => {
888
+ const g = gen({
889
+ type_energie: 'gaz',
890
+ enum_type_generateur_ecs_id: '84',
891
+ enum_methode_saisie_carac_sys_id: '1'
892
+ });
893
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'maison');
894
+
895
+ expect(scopOrCop).not.toHaveBeenCalled();
896
+ expect(tv_generateur_combustion).not.toHaveBeenCalled();
897
+ expect(g.donnee_entree.enum_type_generateur_ecs_id).toBe('84');
898
+ });
899
+
900
+ test('bug_for_bug_compat : qp0 déjà en W (>= 1) conservé sans correction', () => {
901
+ state.bug = true;
902
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
903
+ vi.mocked(tvColumnIDs).mockImplementation((table) =>
904
+ table === 'generateur_combustion' ? ['chaud'] : []
905
+ );
906
+ vi.mocked(tv_generateur_combustion).mockImplementation((dpe, di) => {
907
+ di.rpn = 0.9;
908
+ di.qp0 = 100;
909
+ di.pn = 20000;
910
+ di.pveilleuse = 1;
911
+ di.pveil = 0;
912
+ });
913
+ const g = gen({
914
+ type_energie: 'gaz',
915
+ enum_type_generateur_ecs_id: 'chaud',
916
+ enum_methode_saisie_carac_sys_id: '1'
917
+ });
918
+ calc_gen_ecs({}, g, ecs_di, ecs_de, 0, '1', '1', 'maison');
919
+
920
+ expect(g.donnee_intermediaire.qp0).toBe(100);
921
+ expect(warnSpy).not.toHaveBeenCalled();
922
+ // même rendement que pour un qp0 de 0,1 kW corrigé en 100 W (cf. test de correction)
923
+ expect(g.donnee_intermediaire.rendement_generation).toBeCloseTo(0.3446955189582535, 9);
924
+ warnSpy.mockRestore();
925
+ });
926
+
855
927
  test('type 84 pompe à chaleur : déduit via tv_scop_id', () => {
856
928
  vi.mocked(scopOrCop).mockImplementation((di) => {
857
929
  di.scop = 3;
@@ -355,6 +355,26 @@ describe('conso_aux_distribution_ch - auxiliaires de distribution', () => {
355
355
  expect(di.conso_auxiliaire_distribution_ch).toBeCloseTo(372.082418731648, 9);
356
356
  });
357
357
 
358
+ test('émetteur hors listes (type 1, « autre ») : perte de charge par défaut deltaPem=35, Fcot=0,802', () => {
359
+ const conso = (type) => {
360
+ const em = [
361
+ {
362
+ donnee_entree: {
363
+ enum_type_emission_distribution_id: type,
364
+ enum_temp_distribution_ch_id: '3'
365
+ }
366
+ }
367
+ ];
368
+ const di = {};
369
+ conso_aux_distribution_ch(em, {}, di, {}, 100, 1, 1, '0', 100000);
370
+ return di.conso_auxiliaire_distribution_ch;
371
+ };
372
+ // Type 24 (monotube) : deltaPem=30, même Fcot (0,802) -> seul deltaPem diffère
373
+ expect(conso('1')).toBeGreaterThan(conso('24'));
374
+ // Type 10 (radiateurs autres) : deltaPem=10
375
+ expect(conso('24')).toBeGreaterThan(conso('10'));
376
+ });
377
+
358
378
  /**
359
379
  * Seuls les émetteurs reliés au générateur (même enum_lien_generateur_emetteur_id) dimensionnent
360
380
  * son circulateur. Autotest CSTB IC5-0-3 : chaudière (lien 1) sur plancher chauffant + convecteurs
package/3.1_b.spec.js CHANGED
@@ -365,4 +365,27 @@ describe('b - calcul du coefficient de réduction des déperditions', () => {
365
365
  });
366
366
  expect(di.b).toBe(0.7);
367
367
  });
368
+
369
+ test('adjacence hors liste aiu/aue (type 10 absent, type inconnu) : seul le type est apparié', () => {
370
+ tv.mockReturnValue({ b: '0.5', tv_coef_reduction_deperdition_id: '7' });
371
+ const di = {};
372
+ const de = { enum_type_adjacence_id: '1' };
373
+ b(di, de, {}, 1);
374
+ expect(tv).toHaveBeenCalledWith(TABLE, { enum_type_adjacence_id: '1' });
375
+ expect(di.b).toBe(0.5);
376
+ });
377
+
378
+ test('rapport inférieur à la plus petite borne : seule la borne max est renseignée', () => {
379
+ tv.mockReturnValue({ b: '0.9', tv_coef_reduction_deperdition_id: '3' });
380
+ const de = {
381
+ enum_type_adjacence_id: '8',
382
+ enum_cfg_isolation_lnc_id: '2',
383
+ surface_aiu: 1,
384
+ surface_aue: 10
385
+ };
386
+ b({}, de, {}, 1);
387
+ const matcher = tv.mock.calls[0][1];
388
+ expect(matcher.aiu_aue_max).toBe('≤ 0,25');
389
+ expect(matcher.aiu_aue_min).toBeUndefined();
390
+ });
368
391
  });
package/3.2.1_mur.spec.js CHANGED
@@ -226,6 +226,63 @@ describe('calc_mur - calcul de Umur0 par table forfaitaire (tv_umur0)', () => {
226
226
  error.mockRestore();
227
227
  });
228
228
 
229
+ test('bug_for_bug : épaisseur absente et ligne tv_umur0_id sans épaisseur : matcher inchangé', () => {
230
+ bugForBugCompat = true;
231
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
232
+ tv.mockImplementation((table, matcher) => {
233
+ if (table === 'umur0' && matcher.tv_umur0_id) return { tv_umur0_id: '1' };
234
+ if (table === 'umur0') return { ...ROW_UMUR0 };
235
+ return null;
236
+ });
237
+ const mur = {
238
+ donnee_entree: {
239
+ methode_saisie_u: 'non isolé',
240
+ methode_saisie_u0:
241
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
242
+ enum_materiaux_structure_mur_id: '5',
243
+ description: 'mur sans épaisseur',
244
+ tv_umur0_id: 1,
245
+ enum_type_doublage_id: '2'
246
+ }
247
+ };
248
+
249
+ calc_mur(mur, 'h1a', '1', '0');
250
+
251
+ // Aucune épaisseur à reprendre : pas d'avertissement et épaisseur nulle conservée
252
+ expect(error).not.toHaveBeenCalled();
253
+ expect(tv).toHaveBeenLastCalledWith('umur0', {
254
+ enum_materiaux_structure_mur_id: '5',
255
+ epaisseur_structure: 0
256
+ });
257
+ error.mockRestore();
258
+ });
259
+
260
+ test('bug_for_bug : ligne tv_umur0_id introuvable : matcher inchangé', () => {
261
+ bugForBugCompat = true;
262
+ tv.mockImplementation((table, matcher) => {
263
+ if (table === 'umur0' && matcher.tv_umur0_id) return undefined;
264
+ if (table === 'umur0') return { ...ROW_UMUR0 };
265
+ return null;
266
+ });
267
+ const mur = {
268
+ donnee_entree: {
269
+ methode_saisie_u: 'non isolé',
270
+ methode_saisie_u0:
271
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
272
+ enum_materiaux_structure_mur_id: '5',
273
+ tv_umur0_id: 1,
274
+ enum_type_doublage_id: '2'
275
+ }
276
+ };
277
+
278
+ calc_mur(mur, 'h1a', '1', '0');
279
+
280
+ expect(tv).toHaveBeenLastCalledWith('umur0', {
281
+ enum_materiaux_structure_mur_id: '5',
282
+ epaisseur_structure: 0
283
+ });
284
+ });
285
+
229
286
  test('bug_for_bug : épaisseur > 80 recalculée en la divisant par 10 (valeur en millimètres)', () => {
230
287
  bugForBugCompat = true;
231
288
  const error = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -423,6 +480,48 @@ describe('calc_mur - prise en compte du doublage', () => {
423
480
  }
424
481
  );
425
482
 
483
+ test('bug_for_bug : doublage de type 1 sans description : type de doublage inchangé', () => {
484
+ bugForBugCompat = true;
485
+ const mur = {
486
+ donnee_entree: {
487
+ methode_saisie_u: 'non isolé',
488
+ methode_saisie_u0:
489
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
490
+ enum_materiaux_structure_mur_id: '5',
491
+ epaisseur_structure: 30,
492
+ enum_type_doublage_id: '1'
493
+ }
494
+ };
495
+
496
+ calc_mur(mur, 'h1a', '1', '0');
497
+
498
+ // Type 1 (pas de doublage) : Umur0 de la table non modifié
499
+ expect(mur.donnee_intermediaire.umur0).toBe(2);
500
+ });
501
+
502
+ test('bug_for_bug : type de doublage > 2 inconnu (6) : aucune résistance de doublage ajoutée', () => {
503
+ bugForBugCompat = true;
504
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
505
+ const mur = {
506
+ donnee_entree: {
507
+ methode_saisie_u: 'non isolé',
508
+ methode_saisie_u0:
509
+ 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire',
510
+ enum_materiaux_structure_mur_id: '5',
511
+ epaisseur_structure: 30,
512
+ enum_type_doublage_id: '6'
513
+ }
514
+ };
515
+
516
+ calc_mur(mur, 'h1a', '1', '0');
517
+
518
+ // Le type 6 est hors énumération type_doublage (1 à 5) : aucune résistance de doublage n'est
519
+ // définie, ni dans le calcul ni dans la vérification de minoration. Comportement actuel figé :
520
+ // Umur0 devient NaN (donnée d'entrée invalide, non protégée par le moteur).
521
+ expect(mur.donnee_intermediaire.umur0).toBeNaN();
522
+ error.mockRestore();
523
+ });
524
+
426
525
  test('bug_for_bug : DPE minorant Umur0 à 2,5 avant doublage : avertissement (Umur0 avant)', () => {
427
526
  bugForBugCompat = true;
428
527
  const error = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -342,6 +342,35 @@ describe('calc_ph - compatibilité bug for bug (récupération depuis tv_uph_id)
342
342
  error.mockRestore();
343
343
  });
344
344
 
345
+ test('ligne tv_uph_id introuvable : type_toiture et effet_joule calculés conservés', () => {
346
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
347
+ tv.mockImplementation((table, matcher) => {
348
+ if (table === 'uph0') return { ...ROW_UPH0 };
349
+ if (table === 'uph' && matcher.tv_uph_id) return undefined;
350
+ if (table === 'uph') return { ...ROW_UPH };
351
+ return null;
352
+ });
353
+
354
+ const ph = {
355
+ donnee_entree: {
356
+ methode_saisie_u: 'isolation inconnue (table forfaitaire)',
357
+ methode_saisie_u0: 'type de paroi inconnu (valeur par défaut)',
358
+ type_adjacence: 'extérieur',
359
+ type_plancher_haut: 'terrasse',
360
+ tv_uph_id: 7,
361
+ description: 'PH test'
362
+ }
363
+ };
364
+
365
+ calc_ph(ph, 'h1a', '1', '0');
366
+
367
+ // Aucune correction issue du DPE : pas d'avertissement, matcher final en 'terrasse'
368
+ expect(error).not.toHaveBeenCalled();
369
+ expect(tv).toHaveBeenCalledWith('uph', expect.objectContaining({ type_toiture: 'terrasse' }));
370
+ expect(ph.donnee_intermediaire.uph).toBe(0.3);
371
+ error.mockRestore();
372
+ });
373
+
345
374
  test('effet_joule du DPE différent : la valeur du DPE est conservée dans le matcher', () => {
346
375
  const error = vi.spyOn(console, 'error').mockImplementation(() => {});
347
376
  tv.mockImplementation((table, matcher) => {