@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
@@ -234,6 +234,16 @@ describe('tv_uw - coefficient de transmission de la fenêtre', () => {
234
234
  expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Pas de valeur forfaitaire uw'));
235
235
  });
236
236
 
237
+ test('Ug exactement tabulé sans ligne uw : uw indéfini et erreur émise', () => {
238
+ // delta_ug = 0 (bornes identiques) mais aucune ligne uw pour ce Ug
239
+ rows.uw = null;
240
+ const bv = baseBv();
241
+ calc_bv(bv, 1);
242
+
243
+ expect(bv.donnee_intermediaire.uw).toBeUndefined();
244
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Pas de valeur forfaitaire uw'));
245
+ });
246
+
237
247
  test('interpolation entre deux Ug tabulés (delta_ug ≠ 0)', () => {
238
248
  rows.ug = { ug: '3', tv_ug_id: '5' };
239
249
  getRange.mockReturnValue([2.8, 4]);
@@ -810,6 +810,33 @@ describe('calc_pont_thermique - récupération du mur via la baie vitrée (compa
810
810
  expect(errorSpy).toHaveBeenCalled();
811
811
  });
812
812
 
813
+ test('hors compatibilité bug : la baie vitrée n’est pas consultée pour retrouver le mur', () => {
814
+ state.bug = false;
815
+ tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
816
+ const mur = [
817
+ {
818
+ donnee_entree: { reference: 'M1', enum_type_adjacence_id: '1', type_isolation: 'iti' },
819
+ donnee_utilisateur: {}
820
+ }
821
+ ];
822
+ // Même configuration que le cas bug_for_bug : reference_paroi = index du mur (0)
823
+ const bv = [{ donnee_entree: { reference: 'BV1', reference_paroi: '0' } }];
824
+ const pt = pontThermique({
825
+ type_liaison: 'refend / mur',
826
+ reference_1: 'BV1',
827
+ description: 'refend'
828
+ });
829
+
830
+ calc_pont_thermique(pt, '1', logement({ mur, bv }));
831
+
832
+ // Mur non retrouvé via la baie : isolation ITI par défaut, aucun message de récupération
833
+ expect(tv).toHaveBeenCalledWith('pont_thermique', {
834
+ enum_type_liaison_id: '12',
835
+ isolation_mur: '^iti'
836
+ });
837
+ expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Aucun mur trouvé'));
838
+ });
839
+
813
840
  test('reference_paroi ne correspondant pas à l’index du mur : aucun mur retrouvé (isolation ITI par défaut)', () => {
814
841
  state.bug = true;
815
842
  tv.mockReturnValue({ k: '0.5', tv_pont_thermique_id: '42' });
@@ -146,6 +146,18 @@ describe('calc_ventilation - orchestration', () => {
146
146
  );
147
147
  });
148
148
 
149
+ test('surface ventilée égale à Sh : conservée sans application de clé de répartition', () => {
150
+ const vt = ventilation({
151
+ surface_ventile: 100,
152
+ cle_repartition_ventilation: 2,
153
+ q4pa_conv_saisi: 1.3
154
+ });
155
+ calc_ventilation(vt, CG, 'th', 80, 100, [], [], [], []);
156
+
157
+ // surface_ventile = Sh = 100 (clé ignorée) => Hvent = 0,34 * 1,2 * 100
158
+ expect(vt.donnee_intermediaire.hvent).toBeCloseTo(40.8, 9);
159
+ });
160
+
149
161
  test('surface ventilée différente de Sh sans clé de répartition : divisée par 1', () => {
150
162
  // Branche `de.cle_repartition_ventilation || 1` : surface_ventile conservée telle quelle
151
163
  const vt = ventilation({ surface_ventile: 50, q4pa_conv_saisi: 1.3 });
package/7_inertie.spec.js CHANGED
@@ -54,6 +54,16 @@ describe('Inertie unit tests', () => {
54
54
  }
55
55
  );
56
56
 
57
+ test('plancher haut non lourd (type 9) isolé par ITE ou non isolé : inertie 0', () => {
58
+ expect(
59
+ inertie.calculateInertiePhLourd({ enum_type_isolation_id: 2, enum_type_plancher_haut_id: 9 })
60
+ ).toBe(0);
61
+ });
62
+
63
+ test('type de plancher haut inconnu avec isolation ITE : considéré lourd, inertie 1', () => {
64
+ expect(inertie.calculateInertiePhLourd({ enum_type_isolation_id: 4 })).toBe(1);
65
+ });
66
+
57
67
  test.each([
58
68
  [1, 1, 0, 2, 0],
59
69
  [1, 1, 0, 3, 0],
@@ -93,6 +93,10 @@ describe('calc_Fj - fraction des besoins de chauffage couverte par les apports',
93
93
  expect(lourde).not.toBeCloseTo(legere, 9);
94
94
  });
95
95
 
96
+ test('une inertie inconnue ne définit pas alpha : le résultat n’est pas un nombre', () => {
97
+ expect(calc_Fj(100, 2000, 5000, 1000, 'inconnue')).toBeNaN();
98
+ });
99
+
96
100
  test('les inerties "très lourde" et "lourde" partagent le même alpha', () => {
97
101
  expect(calc_Fj(100, 2000, 5000, 1000, 'très lourde')).toBeCloseTo(
98
102
  calc_Fj(100, 2000, 5000, 1000, 'lourde'),
package/9_conso_ch.js CHANGED
@@ -289,7 +289,7 @@ function calc_ch_base_appoint(
289
289
 
290
290
  di.conso_ch = 0;
291
291
  for (const mois of mois_liste) {
292
- let bch_base_j = 0;
292
+ let bch_base_j;
293
293
 
294
294
  if (pe > 0) {
295
295
  // Nombre d’heures de chauffage sur le mois j
@@ -23,7 +23,9 @@ vi.mock('./utils.js', () => ({
23
23
  }));
24
24
 
25
25
  vi.mock('./core/tv/infrastructure/tvs.store.js', () => ({
26
- TvsStore: vi.fn(() => mockTvsStore)
26
+ TvsStore: vi.fn(function () {
27
+ return mockTvsStore;
28
+ })
27
29
  }));
28
30
 
29
31
  const { rendement_emission, calc_emetteur_ch, isIntermittenceCollectiveSaisie } =
@@ -342,6 +342,17 @@ describe('checkForGeneratorType - classification du générateur', () => {
342
342
  expect(de.enum_type_generateur_ch_id).toBe('119');
343
343
  });
344
344
 
345
+ test('cas 119 par défaut avec ligne de rendement sans type de générateur : aucune réécriture', () => {
346
+ vi.mocked(requestInputID).mockReturnValue('119');
347
+ vi.mocked(tv).mockReturnValue({ enum_type_generateur_ch_id: undefined });
348
+ const de = { tv_rendement_generation_id: 3, enum_type_generateur_ch_id: '119' };
349
+ const du = {};
350
+ checkForGeneratorType({}, de, {}, du);
351
+ expect(de.enum_type_generateur_ch_id).toBe('119');
352
+ expect(du.isPacGenerator).toBe(false);
353
+ expect(du.isCombustionGenerator).toBe(false);
354
+ });
355
+
345
356
  /**
346
357
  * Chaudière fioul par défaut : chaudière standard datant de la construction du bâtiment.
347
358
  * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.2.1.1
package/README.md CHANGED
@@ -383,7 +383,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
383
383
 
384
384
  <!-- CORPUS:START -->
385
385
 
386
- > **Version `1.10.0`** · branche `main` · généré le 2026-10-29
386
+ > **Version `1.11.2`** · à publier · branche `main` · généré le 2026-10-30
387
387
  > Seuil de tolérance **5%**
388
388
 
389
389
  <table>
@@ -416,24 +416,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
416
416
 
417
417
  <table>
418
418
  <tr>
419
- <td align="center"><strong>5,64 ms</strong><br/><sub>moyenne</sub></td>
420
- <td align="center"><strong>4,87 ms</strong><br/><sub>médiane</sub></td>
419
+ <td align="center"><strong>5,99 ms</strong><br/><sub>moyenne</sub></td>
420
+ <td align="center"><strong>5,23 ms</strong><br/><sub>médiane</sub></td>
421
421
  <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
422
- <td align="center"><strong>957,0 ms</strong><br/><sub>max</sub></td>
422
+ <td align="center"><strong>641,5 ms</strong><br/><sub>max</sub></td>
423
423
  </tr>
424
424
  </table>
425
425
 
426
426
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
427
427
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
428
- | **Appartement · chauffage individuel (2025)** | 4,00 ms | 3,58 ms | 1,42 ms | 34,6 ms | 6,99 ms | 9,30 ms |
429
- | **Appartement · chauffage collectif (2025)** | 4,46 ms | 4,04 ms | 1,62 ms | 42,4 ms | 7,33 ms | 9,33 ms |
430
- | **Logement individuel (2025)** | 5,20 ms | 4,57 ms | 1,74 ms | 56,0 ms | 9,72 ms | 13,5 ms |
431
- | **Maison individuelle (2025)** | 6,01 ms | 5,41 ms | 2,04 ms | 51,8 ms | 10,7 ms | 14,3 ms |
432
- | **Maison individuelle (2026)** | 6,16 ms | 5,47 ms | 2,09 ms | 46,7 ms | 11,1 ms | 15,1 ms |
433
- | **Immeuble · chauffage collectif** | 5,81 ms | 4,91 ms | 0,14 ms | 126,7 ms | 10,9 ms | 17,7 ms |
434
- | **Immeuble · chauffage individuel** | 7,07 ms | 5,99 ms | 0,11 ms | 123,4 ms | 14,5 ms | 22,6 ms |
435
- | **Immeuble · chauffage mixte** | 5,73 ms | 4,63 ms | 0,20 ms | 957,0 ms | 11,9 ms | 19,7 ms |
436
- | **Individuel généré depuis l'immeuble (2026)** | 6,33 ms | 5,26 ms | 1,79 ms | 79,2 ms | 12,0 ms | 19,1 ms |
428
+ | **Appartement · chauffage individuel (2025)** | 4,09 ms | 3,70 ms | 1,56 ms | 47,5 ms | 7,05 ms | 9,52 ms |
429
+ | **Appartement · chauffage collectif (2025)** | 4,98 ms | 4,64 ms | 1,66 ms | 41,1 ms | 8,18 ms | 10,7 ms |
430
+ | **Logement individuel (2025)** | 5,36 ms | 4,70 ms | 1,75 ms | 61,9 ms | 10,0 ms | 14,0 ms |
431
+ | **Maison individuelle (2025)** | 6,37 ms | 5,70 ms | 1,93 ms | 58,0 ms | 11,5 ms | 15,4 ms |
432
+ | **Maison individuelle (2026)** | 6,43 ms | 5,76 ms | 2,16 ms | 48,3 ms | 11,7 ms | 15,7 ms |
433
+ | **Immeuble · chauffage collectif** | 5,75 ms | 4,90 ms | 0,11 ms | 102,7 ms | 10,6 ms | 17,6 ms |
434
+ | **Immeuble · chauffage individuel** | 7,49 ms | 6,40 ms | 0,11 ms | 127,0 ms | 15,3 ms | 23,9 ms |
435
+ | **Immeuble · chauffage mixte** | 6,57 ms | 5,44 ms | 0,19 ms | 641,5 ms | 13,8 ms | 23,4 ms |
436
+ | **Individuel généré depuis l'immeuble (2026)** | 6,88 ms | 5,83 ms | 1,86 ms | 79,6 ms | 12,9 ms | 21,8 ms |
437
437
 
438
438
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
439
439
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
@@ -22,7 +22,11 @@ vi.mock('./6.2_surface_sud_equivalente.js', () => ({ calc_sse: vi.fn() }));
22
22
  vi.mock('./10_besoin_fr.js', () => ({ default: vi.fn() }));
23
23
  vi.mock('./11_besoin_ecs.js', () => ({ default: vi.fn() }));
24
24
  vi.mock('./6.1_apport_gratuit.js', () => ({ calc_ai: vi.fn(), calc_as: vi.fn() }));
25
- vi.mock('./11_nadeq.js', () => ({ Nadeq: vi.fn(() => ({ calculateNadeq })) }));
25
+ vi.mock('./11_nadeq.js', () => ({
26
+ Nadeq: vi.fn(function () {
27
+ return { calculateNadeq };
28
+ })
29
+ }));
26
30
 
27
31
  const { default: calc_apport_et_besoin } = await import('./apport_et_besoin.js');
28
32
  const { calc_sse } = await import('./6.2_surface_sud_equivalente.js');
@@ -4,25 +4,6 @@ import { UPB_ADDITIONAL_VALUES } from '../../tv/infrastructure/assets/additional
4
4
  * Add additional values for ue
5
5
  */
6
6
  export class AddAdditionnalUeValuesTables {
7
- /**
8
- * @type {FileStore}
9
- */
10
- #fileStore;
11
-
12
- /**
13
- * @type {ApplicationConfig}
14
- */
15
- #appConfig;
16
-
17
- /**
18
- * @param fileStore {FileStore}
19
- * @param appConfig {ApplicationConfig}
20
- */
21
- constructor(fileStore, appConfig) {
22
- this.#fileStore = fileStore;
23
- this.#appConfig = appConfig;
24
- }
25
-
26
7
  /**
27
8
  * Ajout de valeurs supplémentaires pour le calcul du facteur Ue pour les déperditions plancher_bas
28
9
  * @param tableValues
@@ -3,12 +3,11 @@ import { AddAdditionnalUeValuesTables } from './add-additionnal-ue-values-tables
3
3
  import { UPB_ADDITIONAL_VALUES } from '../../tv/infrastructure/assets/additional-ue-values.js';
4
4
 
5
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`.
6
+ * Le service n'a aucune dépendance : on vérifie la logique d'aplatissement des
7
+ * valeurs Ue additionnelles dans la table `ue`.
9
8
  */
10
9
  describe('AddAdditionnalUeValuesTables', () => {
11
- const buildService = () => new AddAdditionnalUeValuesTables({}, {});
10
+ const buildService = () => new AddAdditionnalUeValuesTables();
12
11
 
13
12
  test('ajoute toutes les valeurs additionnelles dans la table ue', () => {
14
13
  const nbAttendu = UPB_ADDITIONAL_VALUES.reduce((acc, entree) => acc + entree.values.length, 0);
@@ -69,4 +69,29 @@ describe('SynchronizeDpeGesLimitValuesTables unit tests', () => {
69
69
  });
70
70
  });
71
71
  });
72
+ test('regroupe plusieurs surfaces sous une même classe d’altitude', () => {
73
+ const fileStore = new FileStore();
74
+ const appConfig = new ApplicationConfig();
75
+ const synchronizeDpeGesLimitValuesTables = new SynchronizeDpeGesLimitValuesTables(
76
+ fileStore,
77
+ appConfig
78
+ );
79
+
80
+ vi.spyOn(fileStore, 'readLocalOdsFileAndConvertToJson').mockResolvedValue({
81
+ dpe_class_limit: [
82
+ { classe_altitude: '400-800m', surface: '10', A: '124' },
83
+ { classe_altitude: '400-800m', surface: '20', A: '100' }
84
+ ]
85
+ });
86
+ vi.spyOn(ApplicationConfig.prototype, 'solicitationsExtFilePath', 'get').mockReturnValue(
87
+ 'src/file.ods'
88
+ );
89
+
90
+ return synchronizeDpeGesLimitValuesTables.execute().then((output) => {
91
+ // Le groupe d'altitude n'est créé qu'une fois : la 1ʳᵉ surface n'est pas écrasée
92
+ expect(output).toStrictEqual({
93
+ dpe_class_limit: { '400-800m': { 10: { A: 124 }, 20: { A: 100 } } }
94
+ });
95
+ });
96
+ });
72
97
  });
@@ -99,4 +99,47 @@ describe('SynchronizeValeurTables unit tests', () => {
99
99
  expect(contenu).toContain('valeurs-ue-completees');
100
100
  });
101
101
  });
102
+ test('force isolation_surfaces à "0" uniquement lorsque la valeur est absente dans q4pa_conv', () => {
103
+ const fileStore = new FileStore();
104
+ const appConfig = new ApplicationConfig();
105
+ const synchronizeC1Tables = new SynchronizeC1Tables(fileStore, appConfig);
106
+ const synchronizeSolicitationTables = new SynchronizeSolicitationsTables(fileStore, appConfig);
107
+ const synchronizeDpeGesLimitValuesTables = new SynchronizeDpeGesLimitValuesTables(
108
+ fileStore,
109
+ appConfig
110
+ );
111
+ const addAdditionnalUeValuesTables = new AddAdditionnalUeValuesTables(fileStore, appConfig);
112
+ const synchronizeValeurTables = new SynchronizeValeurTables(
113
+ fileStore,
114
+ appConfig,
115
+ synchronizeSolicitationTables,
116
+ synchronizeDpeGesLimitValuesTables,
117
+ addAdditionnalUeValuesTables,
118
+ synchronizeC1Tables
119
+ );
120
+
121
+ vi.spyOn(ApplicationConfig.prototype, 'ademeValeurTablesFileUrl', 'get').mockReturnValue(
122
+ 'http://localhost/file.xlsx'
123
+ );
124
+ vi.spyOn(ApplicationConfig.prototype, 'assetsOutputFolder', 'get').mockReturnValue(
125
+ 'src/assets'
126
+ );
127
+
128
+ // Le parsing Excel ignore les valeurs à 0 : la 1ʳᵉ ligne n'a pas de isolation_surfaces,
129
+ // la 2ᵉ en possède déjà une qui doit être conservée.
130
+ vi.spyOn(fileStore, 'downloadXlsxFileAndConvertToJson').mockResolvedValue({
131
+ q4pa_conv: [{ q4pa_conv: 'a' }, { q4pa_conv: 'b', isolation_surfaces: 'oui' }]
132
+ });
133
+ vi.spyOn(fileStore, 'writeFileToLocalSystem').mockResolvedValue(null);
134
+ vi.spyOn(synchronizeC1Tables, 'execute').mockResolvedValue({});
135
+ vi.spyOn(synchronizeSolicitationTables, 'execute').mockResolvedValue({});
136
+ vi.spyOn(synchronizeDpeGesLimitValuesTables, 'execute').mockResolvedValue({});
137
+ vi.spyOn(addAdditionnalUeValuesTables, 'execute').mockResolvedValue({});
138
+
139
+ return synchronizeValeurTables.execute().then(() => {
140
+ const contenu = vi.mocked(fileStore.writeFileToLocalSystem).mock.calls[0][1];
141
+ const tvs = JSON.parse(contenu.match(/tvs = (\{[\s\S]*\});\n export/)[1]);
142
+ expect(tvs.q4pa_conv.map((l) => l.isolation_surfaces)).toEqual(['0', 'oui']);
143
+ });
144
+ });
102
145
  });
@@ -58,6 +58,17 @@ describe('Service de logs (log-service)', () => {
58
58
  expect(console.error).toBe(consoleOrigine.error);
59
59
  });
60
60
 
61
+ test('setLoggerOn sans option conserve console.error neutralisé', () => {
62
+ setLoggerOff(true);
63
+ const erreurNeutralisee = console.error;
64
+ setLoggerOn();
65
+
66
+ expect(logger.silent).toBe(false);
67
+ expect(console.log).toBe(consoleOrigine.log);
68
+ expect(console.error).toBe(erreurNeutralisee);
69
+ expect(console.error).not.toBe(consoleOrigine.error);
70
+ });
71
+
61
72
  test('les méthodes console neutralisées sont des fonctions no-op', () => {
62
73
  setLoggerOff();
63
74
 
@@ -77,6 +77,15 @@ describe('DpeSanitizerService.execute - collections manquantes', () => {
77
77
  });
78
78
  });
79
79
 
80
+ describe('DpeSanitizerService - transformation des clés', () => {
81
+ test('conserve les clés inchangées (transformation identité)', () => {
82
+ vi.mocked(has).mockReturnValue(true);
83
+ service.execute({});
84
+ const keyTransform = vi.mocked(ObjectUtil.deepObjectTransform).mock.calls[0][1];
85
+ expect(keyTransform('generateur_ecs')).toBe('generateur_ecs');
86
+ });
87
+ });
88
+
80
89
  /**
81
90
  * Récupère la fonction de transformation des valeurs passée en 3e argument de
82
91
  * `deepObjectTransform` après un appel à `execute`.
package/engine.js CHANGED
@@ -34,9 +34,12 @@ import getFicheTechnique from './ficheTechnique.js';
34
34
  import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
35
35
  import DpeSanitizerService from './dpe-sanitizer.service.js';
36
36
  import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
37
- import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
37
+ import {
38
+ reconstituerRdimEcsIndividuelleUnique,
39
+ reconstituerRdimEcsIndividuellesMultiples
40
+ } from './11_rdim_ecs_individuelle_unique.js';
38
41
 
39
- const LIB_VERSION = '1.11.1';
42
+ const LIB_VERSION = '1.11.3';
40
43
 
41
44
  const dpeSanitizerService = new DpeSanitizerService();
42
45
 
@@ -305,6 +308,11 @@ export function calcul_3cl(inputDpe, options) {
305
308
  * @see https://github.com/Open3CL/engine/issues/288
306
309
  */
307
310
  reconstituerRdimEcsIndividuelleUnique(th, ecs, cg.surface_habitable_immeuble);
311
+ /**
312
+ * Immeuble avec plusieurs installations ECS individuelles décrites chacune pour un logement type
313
+ * (Σ Sh_i × nombre_logement_i ≈ SH, sans rdim) : rdim_i = nombre_logement_i.
314
+ */
315
+ reconstituerRdimEcsIndividuellesMultiples(th, ecs, cg.surface_habitable_immeuble);
308
316
  const Nb_lgt = cg.nombre_appartement || 1;
309
317
 
310
318
  /**
package/engine.spec.js CHANGED
@@ -75,7 +75,8 @@ vi.mock('./17.2_rdim_dpe_genere_immeuble.js', () => ({
75
75
  reconstituerRdimDpeGenereImmeuble: vi.fn()
76
76
  }));
77
77
  vi.mock('./11_rdim_ecs_individuelle_unique.js', () => ({
78
- reconstituerRdimEcsIndividuelleUnique: vi.fn()
78
+ reconstituerRdimEcsIndividuelleUnique: vi.fn(),
79
+ reconstituerRdimEcsIndividuellesMultiples: vi.fn()
79
80
  }));
80
81
 
81
82
  // Les instances Inertie / ProductionENR sont créées au chargement du module.
@@ -83,9 +84,15 @@ vi.mock('./11_rdim_ecs_individuelle_unique.js', () => ({
83
84
  const calculateInertie = vi.fn();
84
85
  const calculateEnr = vi.fn();
85
86
  const ratioProrataPvLogement = vi.fn();
86
- vi.mock('./7_inertie.js', () => ({ Inertie: vi.fn(() => ({ calculateInertie })) }));
87
+ vi.mock('./7_inertie.js', () => ({
88
+ Inertie: vi.fn(function () {
89
+ return { calculateInertie };
90
+ })
91
+ }));
87
92
  vi.mock('./16.2_production_enr.js', () => ({
88
- ProductionENR: vi.fn(() => ({ calculateEnr })),
93
+ ProductionENR: vi.fn(function () {
94
+ return { calculateEnr };
95
+ }),
89
96
  ratioProrataPvLogement: (...args) => ratioProrataPvLogement(...args)
90
97
  }));
91
98
 
@@ -93,7 +100,9 @@ vi.mock('./16.2_production_enr.js', () => ({
93
100
  // renvoie l'entrée telle quelle.
94
101
  const sanitizeExecute = vi.fn((dpe) => dpe);
95
102
  vi.mock('./dpe-sanitizer.service.js', () => ({
96
- default: vi.fn(() => ({ execute: sanitizeExecute }))
103
+ default: vi.fn(function () {
104
+ return { execute: sanitizeExecute };
105
+ })
97
106
  }));
98
107
 
99
108
  const { getVersion, get_conso_coeff_1_7_2027, get_classe_ges_dpe, calcul_3cl, calcul_3cl_xml } =
@@ -121,7 +130,7 @@ const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
121
130
  const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
122
131
  const getFicheTechnique = (await import('./ficheTechnique.js')).default;
123
132
  const { reconstituerRdimDpeGenereImmeuble } = await import('./17.2_rdim_dpe_genere_immeuble.js');
124
- const { reconstituerRdimEcsIndividuelleUnique } =
133
+ const { reconstituerRdimEcsIndividuelleUnique, reconstituerRdimEcsIndividuellesMultiples } =
125
134
  await import('./11_rdim_ecs_individuelle_unique.js');
126
135
 
127
136
  /** Objet conso renvoyé par le mock de `calc_conso` (mêmes réf pour les 3 appels). */
@@ -650,6 +659,20 @@ describe('calcul_3cl - collections vidables (poursuite du calcul)', () => {
650
659
  });
651
660
 
652
661
  describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
662
+ test('méthode application inconnue : surface habitable indéterminée transmise à la déperdition', () => {
663
+ enums.methode_application_dpe_log = { 1: 'méthode farfelue' };
664
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
665
+ const dpe = makeDpe();
666
+
667
+ calcul_3cl(dpe, { sanitize: false });
668
+
669
+ // th null => aucune surface retenue (ni logement ni immeuble)
670
+ expect(calc_deperdition.mock.calls[0][2]).toBeNull();
671
+ expect(calc_deperdition.mock.calls[0][5]).toBeUndefined();
672
+ expect(errSpy).toHaveBeenCalled();
673
+ errSpy.mockRestore();
674
+ });
675
+
653
676
  test('immeuble : Sh = surface immeuble ; ECS collective individualisée non divisée', () => {
654
677
  enums.methode_application_dpe_log = { 1: 'dpe immeuble collectif' };
655
678
  const dpe = makeDpe();
@@ -1304,6 +1327,26 @@ describe('calcul_3cl - rdim de l’installation ECS individuelle unique d’un i
1304
1327
  });
1305
1328
  });
1306
1329
 
1330
+ describe('calcul_3cl - rdim des installations ECS individuelles multiples d’un immeuble', () => {
1331
+ test('reconstitution appelée avec le type de bâtiment, les installations ECS et SH, avant le calcul ECS', () => {
1332
+ const dpe = makeDpe();
1333
+ dpe.logement.caracteristique_generale.surface_habitable_immeuble = 566.16;
1334
+ vi.mocked(reconstituerRdimEcsIndividuellesMultiples).mockImplementationOnce(() => {
1335
+ expect(calc_ecs).not.toHaveBeenCalled();
1336
+ return false;
1337
+ });
1338
+
1339
+ calcul_3cl(dpe, { sanitize: false });
1340
+
1341
+ expect(reconstituerRdimEcsIndividuellesMultiples).toHaveBeenCalledWith(
1342
+ 'maison',
1343
+ dpe.logement.installation_ecs_collection.installation_ecs,
1344
+ 566.16
1345
+ );
1346
+ expect(calc_ecs).toHaveBeenCalled();
1347
+ });
1348
+ });
1349
+
1307
1350
  describe('get_conso_coeff_1_7_2027', () => {
1308
1351
  test('projette la conso EP 2027 pour une maison', () => {
1309
1352
  vi.mocked(classe_bilan_dpe).mockReturnValue('C');
@@ -1340,4 +1383,22 @@ describe('get_conso_coeff_1_7_2027', () => {
1340
1383
  expect(res.ep_conso_5_usages_2027).toBeCloseTo(ep, 6);
1341
1384
  expect(classe_bilan_dpe).toHaveBeenCalledWith(Math.floor(ep / 200), 'zc1', 'ca1', 200);
1342
1385
  });
1386
+
1387
+ test('méthode application inconnue : surface indéterminée, aucune surface ne lui est substituée', () => {
1388
+ enums.methode_application_dpe_log = { 1: 'méthode farfelue' };
1389
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
1390
+ vi.mocked(classe_bilan_dpe).mockReturnValue('G');
1391
+ const dpe = makeDpe();
1392
+ dpe.logement.sortie = {
1393
+ ep_conso: { ep_conso_5_usages: 10000 },
1394
+ ef_conso: { conso_5_usages: 6000 }
1395
+ };
1396
+
1397
+ get_conso_coeff_1_7_2027(dpe);
1398
+
1399
+ // th null => ni surface logement ni surface immeuble
1400
+ expect(classe_bilan_dpe.mock.calls[0][3]).toBeUndefined();
1401
+ expect(errSpy).toHaveBeenCalled();
1402
+ errSpy.mockRestore();
1403
+ });
1343
1404
  });
@@ -1,12 +1,9 @@
1
- import { DE } from './dpe.model';
1
+ // Alias des types générés depuis le XSD DPE (voir dpe-xsd.generated.ts). Les extensions propres
2
+ // à Open3CL se déclarent ici (hors fichier généré) pour survivre à la régénération.
3
+ import type {
4
+ XsdLogementEnveloppeEtsBaieEts,
5
+ XsdLogementEnveloppeEtsBaieEtsDonneeEntree
6
+ } from './dpe-xsd.generated';
2
7
 
3
- export interface BaieEts {
4
- donnee_entree?: BaieEtsDE;
5
- }
6
-
7
- export interface BaieEtsDE extends DE {
8
- enum_orientation_id?: number;
9
- enum_inclinaison_vitrage_id?: number; // ENUM inclinaison_vitrage
10
- surface_totale_baie?: number;
11
- nb_baie?: number;
12
- }
8
+ export type BaieEts = XsdLogementEnveloppeEtsBaieEts;
9
+ export type BaieEtsDE = XsdLogementEnveloppeEtsBaieEtsDonneeEntree;
@@ -1,97 +1,25 @@
1
- import { DE } from './dpe.model';
1
+ // Alias des types générés depuis le XSD DPE (voir dpe-xsd.generated.ts). Les extensions propres
2
+ // à Open3CL se déclarent ici (hors fichier généré) pour survivre à la régénération.
3
+ import type {
4
+ XsdLogementEnveloppeBaieVitree,
5
+ XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetre,
6
+ XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetreDonneeEntree,
7
+ XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetreDonneeIntermediaire,
8
+ XsdLogementEnveloppeBaieVitreeDonneeEntree,
9
+ XsdLogementEnveloppeBaieVitreeDonneeEntreeMasqueLointainNonHomogene,
10
+ XsdLogementEnveloppeBaieVitreeDonneeIntermediaire
11
+ } from './dpe-xsd.generated';
2
12
 
3
- export interface BaieVitree {
4
- donnee_entree?: BaieVitreeDE;
5
- donnee_intermediaire?: BaieVitreeDI;
6
- baie_vitree_double_fenetre?: BaieVitreeDoubleFenetre;
7
- }
8
-
9
- export interface BaieVitreeDE extends DE {
10
- reference_paroi?: string;
11
- reference_lnc?: string;
12
- tv_coef_reduction_deperdition_id?: string; // TV
13
- surface_aiu?: number;
14
- surface_aue?: number;
15
- enum_cfg_isolation_lnc_id?: string; // ENUM cfg_isolation_lnc
16
- enum_type_adjacence_id?: string; // ENUM type_adjacence
17
- surface_totale_baie?: number;
18
- nb_baie?: number;
19
- tv_ug_id?: string; // TV
20
- enum_type_vitrage_id?: string; // ENUM type_vitrage
21
- enum_inclinaison_vitrage_id?: string; // ENUM inclinaison_vitrage
22
- enum_type_gaz_lame_id?: string; // ENUM type_gaz_lame
23
- epaisseur_lame?: number;
24
- vitrage_vir?: boolean;
25
- enum_methode_saisie_perf_vitrage_id?: string; // ENUM methode_saisie_perf_vitrage
26
- ug_saisi?: number;
27
- tv_uw_id?: string;
28
- enum_type_materiaux_menuiserie_id?: string; // ENUM type_materiaux_menuiserie
29
- enum_type_baie_id?: string; // ENUM type_baie
30
- uw_saisi?: number;
31
- double_fenetre?: number;
32
- uw_1?: number;
33
- sw_1?: number;
34
- uw_2?: number;
35
- sw_2?: number;
36
- tv_deltar_id?: string;
37
- tv_ujn_id?: string;
38
- enum_type_fermeture_id?: string;
39
- presence_protection_solaire_hors_fermeture?: boolean;
40
- ujn_saisi?: number;
41
- presence_retour_isolation?: boolean;
42
- presence_joint?: boolean;
43
- largeur_dormant?: number;
44
- tv_sw_id?: string;
45
- sw_saisi?: number;
46
- enum_type_pose_id?: string; // ENUM type_pose
47
- enum_orientation_id?: string;
48
- tv_coef_masque_proche_id?: number;
49
- tv_coef_masque_lointain_homogene_id?: number;
50
- masque_lointain_non_homogene_collection: {
51
- masque_lointain_non_homogene: MasqueLointainNonHomogene;
52
- }[];
53
- }
54
-
55
- export interface BaieVitreeDI {
56
- b: number;
57
- ug?: number;
58
- uw: number;
59
- ujn?: number;
60
- u_menuiserie: number;
61
- sw: number;
62
- fe1: number;
63
- fe2: number;
64
- }
65
-
66
- export interface BaieVitreeDoubleFenetre {
67
- donnee_entree?: BaieVitreeDoubleFenetreDE;
68
- donnee_intermediaire?: BaieVitreeDoubleFenetreDI;
69
- }
70
-
71
- export interface BaieVitreeDoubleFenetreDE {
72
- tv_ug_id?: string;
73
- enum_type_vitrage_id: string;
74
- enum_inclinaison_vitrage_id: string;
75
- enum_type_gaz_lame_id?: string;
76
- epaisseur_lame?: number;
77
- vitrage_vir?: boolean;
78
- enum_methode_saisie_perf_vitrage_id: string;
79
- ug_saisi?: number;
80
- tv_uw_id?: string;
81
- enum_type_materiaux_menuiserie_id: string;
82
- enum_type_baie_id: string;
83
- uw_saisi?: number;
84
- tv_sw_id?: string;
85
- sw_saisi?: number;
86
- enum_type_pose_id: string;
87
- }
88
-
89
- export interface BaieVitreeDoubleFenetreDI {
90
- ug?: number;
91
- uw: number;
92
- sw: number;
93
- }
94
-
95
- export interface MasqueLointainNonHomogene {
96
- tv_coef_masque_lointain_non_homogene_id?: string;
97
- }
13
+ /** + `donnee_utilisateur` Open3CL hors XSD. */
14
+ export type BaieVitree = XsdLogementEnveloppeBaieVitree & {
15
+ donnee_utilisateur?: Record<string, any>;
16
+ };
17
+ export type BaieVitreeDE = XsdLogementEnveloppeBaieVitreeDonneeEntree;
18
+ export type BaieVitreeDI = XsdLogementEnveloppeBaieVitreeDonneeIntermediaire;
19
+ export type BaieVitreeDoubleFenetre = XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetre;
20
+ export type BaieVitreeDoubleFenetreDE =
21
+ XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetreDonneeEntree;
22
+ export type BaieVitreeDoubleFenetreDI =
23
+ XsdLogementEnveloppeBaieVitreeBaieVitreeDoubleFenetreDonneeIntermediaire;
24
+ export type MasqueLointainNonHomogene =
25
+ XsdLogementEnveloppeBaieVitreeDonneeEntreeMasqueLointainNonHomogene;