@open3cl/engine 1.6.0 → 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 (108) 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/README.md +18 -9
  45. package/apport_et_besoin.spec.js +126 -0
  46. package/conso.js +1 -1
  47. package/conso.spec.js +471 -82
  48. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  49. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  50. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  51. package/core/conf/infrastructure/application.config.spec.js +35 -0
  52. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  53. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  54. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  55. package/core/util/infrastructure/object-util.spec.js +20 -0
  56. package/core/util/logger/log-service.js +16 -7
  57. package/core/util/logger/log-service.spec.js +101 -0
  58. package/dpe-sanitizer.service.spec.js +138 -0
  59. package/engine.js +7 -5
  60. package/engine.spec.js +854 -0
  61. package/enums.spec.js +46 -0
  62. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  63. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  64. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  65. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  66. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  67. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  68. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  69. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  70. package/features/dpe/infrastructure/tv.store.js +28 -9
  71. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  72. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  73. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  74. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  75. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  76. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  77. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  78. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  79. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  80. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  81. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  82. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  83. package/features/engine/domain/constants.spec.js +14 -0
  84. package/features/engine/domain/contexte.builder.spec.js +21 -0
  85. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  86. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  87. package/features/engine/domain/engine.service.spec.js +227 -4
  88. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  89. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  90. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  91. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  92. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  93. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  94. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  95. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  96. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  97. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  98. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  99. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  100. package/ficheTechnique.spec.js +26 -0
  101. package/index.spec.js +54 -0
  102. package/package.json +5 -2
  103. package/preprocessor/index.js +20 -0
  104. package/preprocessor/index.spec.js +15 -0
  105. package/tv.spec.js +38 -0
  106. package/utils.js +43 -1
  107. package/utils.spec.js +518 -2
  108. package/output.js +0 -1
@@ -0,0 +1,131 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler `scopOrCop` :
5
+ * - `tv` : accès à la table `scop` (on contrôle la ligne retournée) ;
6
+ * - `requestInputID` : simple passe-plat renvoyant l'identifiant demandé et le recopiant.
7
+ * Le comportement de ces utilitaires n'est pas testé ici, seulement la logique du module.
8
+ */
9
+ vi.mock('./utils.js', () => ({
10
+ tv: vi.fn(),
11
+ requestInputID: vi.fn((de, du, field) => {
12
+ const enumName = `enum_${field}_id`;
13
+ du[enumName] = de[enumName];
14
+ return de[enumName];
15
+ })
16
+ }));
17
+
18
+ const { scopOrCop } = await import('./12.4_pac.js');
19
+ const { tv, requestInputID } = await import('./utils.js');
20
+
21
+ beforeEach(() => {
22
+ vi.mocked(tv).mockReset();
23
+ vi.mocked(requestInputID).mockClear();
24
+ });
25
+
26
+ /**
27
+ * 12.4 - Détermination du SCOP / COP des pompes à chaleur.
28
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §12.4
29
+ */
30
+ describe('scopOrCop - méthode de saisie "caractéristiques saisies" (id 6)', () => {
31
+ test('SCOP renseigné : rg et rg_dep prennent la valeur du SCOP sans accès à la table', () => {
32
+ const di = { scop: 3.5 };
33
+ const de = { enum_methode_saisie_carac_sys_id: '6' };
34
+ scopOrCop(di, de, {}, 'zc', null, 'ch');
35
+ expect(di.rg).toBe(3.5);
36
+ expect(di.rg_dep).toBe(3.5);
37
+ expect(tv).not.toHaveBeenCalled();
38
+ });
39
+
40
+ test('SCOP absent : repli sur le COP', () => {
41
+ const di = { cop: 2.8 };
42
+ const de = { enum_methode_saisie_carac_sys_id: '6' };
43
+ scopOrCop(di, de, {}, 'zc', null, 'ch');
44
+ expect(di.rg).toBe(2.8);
45
+ expect(di.rg_dep).toBe(2.8);
46
+ });
47
+ });
48
+
49
+ describe('scopOrCop - valeur forfaitaire (table scop)', () => {
50
+ test('matcher sans identifiant d’émission : zone climatique + type de générateur', () => {
51
+ tv.mockReturnValue({ scop_ou_cop: 'scop', scop: '4.2', tv_scop_id: '15' });
52
+ const di = {};
53
+ const de = { enum_type_generateur_ch_id: '160' };
54
+
55
+ scopOrCop(di, de, {}, 'h1a', null, 'ch');
56
+
57
+ expect(tv).toHaveBeenCalledWith(
58
+ 'scop',
59
+ {
60
+ enum_zone_climatique_id: 'h1a',
61
+ enum_type_generateur_ch_id: '160'
62
+ },
63
+ de
64
+ );
65
+ expect(di.scop).toBe(4.2);
66
+ expect(di.rg).toBe(4.2);
67
+ expect(di.rg_dep).toBe(4.2);
68
+ expect(de.tv_scop_id).toBe(15);
69
+ });
70
+
71
+ test('identifiant d’émission fourni : ajouté au matcher', () => {
72
+ tv.mockReturnValue({ scop_ou_cop: 'cop', scop: '3.1', tv_scop_id: '20' });
73
+ const di = {};
74
+ const de = { enum_type_generateur_ecs_id: '10' };
75
+
76
+ scopOrCop(di, de, {}, 'h2b', '5', 'ecs');
77
+
78
+ expect(tv).toHaveBeenCalledWith(
79
+ 'scop',
80
+ {
81
+ enum_zone_climatique_id: 'h2b',
82
+ enum_type_generateur_ecs_id: '10',
83
+ enum_type_emission_distribution_id: '5'
84
+ },
85
+ de
86
+ );
87
+ // scop_ou_cop vaut 'cop' : la valeur est stockée sous di.cop
88
+ expect(di.cop).toBe(3.1);
89
+ expect(di.rg).toBe(3.1);
90
+ });
91
+
92
+ test('tv_scop_id du DPE différent de celui de la table : avertissement mais valeur de la table utilisée', () => {
93
+ tv.mockReturnValue({ scop_ou_cop: 'scop', scop: '4.2', tv_scop_id: '15' });
94
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
95
+ // tv_scop_id renseigné (18) et différent de celui de la ligne (15)
96
+ const di = {};
97
+ const de = { enum_type_generateur_ch_id: '160', tv_scop_id: 18 };
98
+
99
+ scopOrCop(di, de, {}, 'h1a', null, 'ch');
100
+
101
+ expect(errorSpy).toHaveBeenCalledTimes(1);
102
+ // la valeur de la table prime : tv_scop_id est corrigé
103
+ expect(de.tv_scop_id).toBe(15);
104
+ expect(di.rg).toBe(4.2);
105
+ errorSpy.mockRestore();
106
+ });
107
+
108
+ test('tv_scop_id du DPE identique à celui de la table : pas d’avertissement', () => {
109
+ tv.mockReturnValue({ scop_ou_cop: 'scop', scop: '4.2', tv_scop_id: '15' });
110
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
111
+ const di = {};
112
+ const de = { enum_type_generateur_ch_id: '160', tv_scop_id: 15 };
113
+
114
+ scopOrCop(di, de, {}, 'h1a', null, 'ch');
115
+
116
+ expect(errorSpy).not.toHaveBeenCalled();
117
+ expect(de.tv_scop_id).toBe(15);
118
+ errorSpy.mockRestore();
119
+ });
120
+
121
+ test('aucune ligne trouvée : ni rg ni rg_dep ne sont définis', () => {
122
+ tv.mockReturnValue(null);
123
+ const di = {};
124
+ const de = { enum_type_generateur_ch_id: '160' };
125
+
126
+ scopOrCop(di, de, {}, 'h1a', null, 'ch');
127
+
128
+ expect(di.rg).toBeUndefined();
129
+ expect(di.rg_dep).toBeUndefined();
130
+ });
131
+ });
@@ -190,6 +190,8 @@ export function tv_generateur_combustion(dpe, di, de, type, GV, tbase, methodeSa
190
190
  if (methodeSaisie === 1 || !di.pveilleuse) {
191
191
  di.pveil = Number(row.pveil) || 0;
192
192
  } else {
193
+ // La branche else n'est atteinte que si di.pveilleuse est vrai, donc le repli `|| 0` est mort.
194
+ /* c8 ignore next */
193
195
  di.pveil = di.pveilleuse || 0;
194
196
  }
195
197
  }
@@ -0,0 +1,537 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ /**
4
+ * Dépendances mockées pour isoler la logique du module :
5
+ * - `tv` : accès à la table `generateur_combustion` (ligne contrôlée) ;
6
+ * - `tvColumnLines` : liste des critères de puissance (vidée pour neutraliser le critère Pn) ;
7
+ * - `convertExpression` : non utilisée dans le chemin testé (critères vides) ;
8
+ * - `bug_for_bug_compat` : désactivé pour isoler le comportement nominal ;
9
+ * - `enums` : mapping minimal ;
10
+ * - les modules de substitution de générateurs (bouilleur / chaudière / pac) : simples espions ;
11
+ * - `getFicheTechnique` : présence d'un ventilateur.
12
+ * La bibliothèque `mathjs` (evaluate) n'est pas mockée : c'est une fonction pure déterministe.
13
+ */
14
+ const state = vi.hoisted(() => ({ bug: false }));
15
+
16
+ vi.mock('./utils.js', () => ({
17
+ get bug_for_bug_compat() {
18
+ return state.bug;
19
+ },
20
+ tv: vi.fn(),
21
+ tvColumnLines: vi.fn(() => []),
22
+ convertExpression: vi.fn((x) => x)
23
+ }));
24
+
25
+ vi.mock('./enums.js', () => ({
26
+ default: {
27
+ type_generateur_ch: {
28
+ 89: 'chaudière gaz standard 2001-2015',
29
+ X: 'chaudière fioul',
30
+ Y: 'chaudière gaz'
31
+ },
32
+ type_generateur_ecs: {
33
+ 50: 'chaudière gaz standard',
34
+ A: 'chaudière gaz',
35
+ B: 'chaudière fioul',
36
+ 84: "système collectif par défaut en abscence d'information"
37
+ }
38
+ }
39
+ }));
40
+
41
+ vi.mock('./13.2_generateur_combustion_bouilleur.js', () => ({
42
+ updateGenerateurBouilleurs: vi.fn()
43
+ }));
44
+ vi.mock('./13.2_generateur_combustion_chaudiere.js', () => ({
45
+ updateGenerateurChaudieres: vi.fn()
46
+ }));
47
+ vi.mock('./13.2_generateur_pac.js', () => ({
48
+ updateGenerateurPacs: vi.fn()
49
+ }));
50
+ vi.mock('./ficheTechnique.js', () => ({
51
+ default: vi.fn()
52
+ }));
53
+
54
+ const { tv_generateur_combustion, updateGenerateurCombustion } = await import(
55
+ './13.2_generateur_combustion.js'
56
+ );
57
+ const { tv, tvColumnLines } = await import('./utils.js');
58
+ const { updateGenerateurBouilleurs } = await import('./13.2_generateur_combustion_bouilleur.js');
59
+ const { updateGenerateurChaudieres } = await import('./13.2_generateur_combustion_chaudiere.js');
60
+ const { updateGenerateurPacs } = await import('./13.2_generateur_pac.js');
61
+ const { default: getFicheTechnique } = await import('./ficheTechnique.js');
62
+
63
+ beforeEach(() => {
64
+ vi.mocked(tv).mockReset();
65
+ vi.mocked(tvColumnLines).mockReset();
66
+ vi.mocked(tvColumnLines).mockReturnValue([]);
67
+ vi.mocked(updateGenerateurBouilleurs).mockReset();
68
+ vi.mocked(updateGenerateurChaudieres).mockReset();
69
+ vi.mocked(updateGenerateurPacs).mockReset();
70
+ vi.mocked(getFicheTechnique).mockReset();
71
+ state.bug = false;
72
+ });
73
+
74
+ const ROW_DEFAUT = {
75
+ tv_generateur_combustion_id: '42',
76
+ rpn: '90',
77
+ rpint: '85',
78
+ qp0_perc: '5',
79
+ pveil: '10'
80
+ };
81
+
82
+ /**
83
+ * 13.2 - Récupération des caractéristiques forfaitaires d'un générateur à combustion.
84
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2
85
+ */
86
+ describe('tv_generateur_combustion - caractéristiques du générateur', () => {
87
+ test('puissance nominale calculée à partir de GV et Tbase lorsqu’elle est absente', () => {
88
+ tv.mockReturnValue({ ...ROW_DEFAUT });
89
+ const di = {};
90
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
91
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
92
+ // Pn = (1.2 * GV * (19 - Tbase)) / 0.95^3 -- valeur de référence de régression
93
+ expect(di.pn).toBeCloseTo(7837.877241580406, 9);
94
+ expect(de.tv_generateur_combustion_id).toBe(42);
95
+ });
96
+
97
+ test('puissance nominale déjà renseignée : non recalculée', () => {
98
+ tv.mockReturnValue({ ...ROW_DEFAUT });
99
+ const di = { pn: 12345 };
100
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
101
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
102
+ expect(di.pn).toBe(12345);
103
+ });
104
+
105
+ test('méthode forfaitaire (1) en chauffage : rpn, rpint, qp0 et pveil renseignés', () => {
106
+ tv.mockReturnValue({ ...ROW_DEFAUT });
107
+ const di = { pn: 20000 };
108
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
109
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
110
+ expect(di.rpn).toBeCloseTo(0.9, 9);
111
+ expect(di.rpint).toBeCloseTo(0.85, 9);
112
+ // qp0_perc constant '5' (ni Pn ni %) => 5 * 1000 * ratio
113
+ expect(di.qp0).toBe(5000);
114
+ expect(di.pveil).toBe(10);
115
+ });
116
+
117
+ test('type ECS : le rendement intermédiaire rpint n’est pas calculé', () => {
118
+ tv.mockReturnValue({ ...ROW_DEFAUT });
119
+ const di = { pn: 20000 };
120
+ const de = { enum_type_generateur_ecs_id: '50', ratio_virtualisation: 1, presence_ventouse: 0 };
121
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
122
+ expect(di.rpn).toBeCloseTo(0.9, 9);
123
+ expect(di.rpint).toBeUndefined();
124
+ });
125
+
126
+ test('qp0_perc exprimé en fonction de Pn : qp0 proportionnel à la puissance nominale', () => {
127
+ tv.mockReturnValue({ ...ROW_DEFAUT, qp0_perc: 'Pn' });
128
+ const di = { pn: 20000 };
129
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
130
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
131
+ // qp0_calc = Pn(kW) = 20 ; inclut 'Pn' => qp0 = 20 * 1000 * ratio = pn
132
+ expect(di.qp0).toBe(20000);
133
+ });
134
+
135
+ test('présence d’une ventouse : sélection du couple (E, F) dans la formule de rendement', () => {
136
+ const diSansVentouse = { pn: 20000 };
137
+ tv.mockReturnValue({ ...ROW_DEFAUT, rpn: 'E*40' });
138
+ tv_generateur_combustion(
139
+ {},
140
+ diSansVentouse,
141
+ { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 },
142
+ 'ch',
143
+ 200,
144
+ -9,
145
+ 1
146
+ );
147
+ // E = 2.5 (sans ventouse) => rpn = 2.5*40 / 100 = 1
148
+ expect(diSansVentouse.rpn).toBeCloseTo(1, 9);
149
+
150
+ const diVentouse = { pn: 20000 };
151
+ tv.mockReturnValue({ ...ROW_DEFAUT, rpn: 'E*40' });
152
+ tv_generateur_combustion(
153
+ {},
154
+ diVentouse,
155
+ { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 1 },
156
+ 'ch',
157
+ 200,
158
+ -9,
159
+ 1
160
+ );
161
+ // E = 1.75 (avec ventouse) => rpn = 1.75*40 / 100 = 0.7
162
+ expect(diVentouse.rpn).toBeCloseTo(0.7, 9);
163
+ });
164
+
165
+ test('méthode 4 : rpn, rpint et qp0 saisis sont conservés (non recalculés)', () => {
166
+ tv.mockReturnValue({ ...ROW_DEFAUT });
167
+ const di = { pn: 20000, rpn: 0.5, rpint: 0.4, qp0: 999 };
168
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
169
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 4);
170
+ expect(di.rpn).toBe(0.5);
171
+ expect(di.rpint).toBe(0.4);
172
+ expect(di.qp0).toBe(999);
173
+ });
174
+
175
+ test('puissance de veilleuse saisie prise en compte hors méthode forfaitaire', () => {
176
+ tv.mockReturnValue({ ...ROW_DEFAUT });
177
+ const di = { pn: 20000, pveilleuse: 15 };
178
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
179
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 2);
180
+ expect(di.pveil).toBe(15);
181
+ });
182
+
183
+ test('aucune ligne forfaitaire trouvée : arrêt sans identifiant de générateur', () => {
184
+ tv.mockReturnValue(undefined);
185
+ const di = { pn: 20000 };
186
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
187
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
188
+ expect(de.tv_generateur_combustion_id).toBeUndefined();
189
+ });
190
+
191
+ test('ratio de virtualisation absent : valeur de repli 1', () => {
192
+ tv.mockReturnValue({ ...ROW_DEFAUT, qp0_perc: 'Pn' });
193
+ const di = { pn: 20000 };
194
+ // pas de ratio_virtualisation -> `|| 1`
195
+ const de = { enum_type_generateur_ch_id: '89', presence_ventouse: 0 };
196
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
197
+ // qp0_perc = 'Pn' => qp0 = Pn(kW) * 1000 * ratio(=1) = pn
198
+ expect(di.qp0).toBe(20000);
199
+ });
200
+
201
+ test('qp0_perc exprimé en pourcentage : qp0 proportionnel à la puissance nominale', () => {
202
+ tv.mockReturnValue({ ...ROW_DEFAUT, qp0_perc: '2%' });
203
+ const di = { pn: 20000 };
204
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
205
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
206
+ // '2%' => 0.02 (mathjs) ; contient '%' mais pas 'Pn' => qp0 = 0.02 * pn
207
+ expect(di.qp0).toBeCloseTo(400, 9);
208
+ });
209
+
210
+ test('qp0_perc absent : qp0 nul', () => {
211
+ const row = { ...ROW_DEFAUT };
212
+ delete row.qp0_perc;
213
+ tv.mockReturnValue(row);
214
+ const di = { pn: 20000 };
215
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
216
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
217
+ expect(di.qp0).toBe(0);
218
+ });
219
+
220
+ test('pveil forfaitaire absent : valeur de repli 0', () => {
221
+ const row = { ...ROW_DEFAUT };
222
+ delete row.pveil;
223
+ tv.mockReturnValue(row);
224
+ const di = { pn: 20000 };
225
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
226
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
227
+ expect(di.pveil).toBe(0);
228
+ });
229
+
230
+ test('critère de puissance : le critère satisfait est renseigné dans le matcher', () => {
231
+ tvColumnLines.mockReturnValue(['Pn ≤ 50', 'Pn > 50']);
232
+ tv.mockReturnValue({ ...ROW_DEFAUT });
233
+ const di = { pn: 20000 };
234
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
235
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
236
+ // Pn = 20000 / (1 * 1000) = 20 ≤ 50 -> premier critère retenu, '≤' restauré
237
+ expect(tv).toHaveBeenCalledWith(
238
+ 'generateur_combustion',
239
+ expect.objectContaining({ critere_pn: 'Pn ≤ 50' })
240
+ );
241
+ });
242
+
243
+ test('critère de puissance : aucun critère satisfait -> avertissement et critère nul', () => {
244
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
245
+ tvColumnLines.mockReturnValue(['Pn > 100']);
246
+ tv.mockReturnValue({ ...ROW_DEFAUT });
247
+ const di = { pn: 20000 };
248
+ const de = { enum_type_generateur_ch_id: '89', ratio_virtualisation: 1, presence_ventouse: 0 };
249
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
250
+ // Pn = 20 n'est pas > 100 -> aucun critère satisfait (ret reste undefined)
251
+ expect(tv).toHaveBeenCalledWith(
252
+ 'generateur_combustion',
253
+ expect.objectContaining({ critere_pn: undefined })
254
+ );
255
+ expect(warnSpy).toHaveBeenCalled();
256
+ warnSpy.mockRestore();
257
+ });
258
+ });
259
+
260
+ /**
261
+ * 13.2 - Comportements spécifiques activés par bug_for_bug_compat (redressement de données DPE).
262
+ */
263
+ describe('tv_generateur_combustion - compatibilité bug_for_bug_compat', () => {
264
+ test('générateur ECS collectif par défaut (84) : caractéristiques issues de tv_generateur_combustion_id', () => {
265
+ state.bug = true;
266
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
267
+ tv.mockReturnValue({ ...ROW_DEFAUT });
268
+ const di = { pn: 20000 };
269
+ const de = {
270
+ enum_type_generateur_ecs_id: '84',
271
+ tv_generateur_combustion_id: '42',
272
+ ratio_virtualisation: 1,
273
+ presence_ventouse: 0
274
+ };
275
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
276
+ // La ligne est récupérée directement via l'identifiant du générateur à combustion
277
+ expect(tv).toHaveBeenCalledWith('generateur_combustion', {
278
+ tv_generateur_combustion_id: '42'
279
+ });
280
+ expect(de.tv_generateur_combustion_id).toBe(42);
281
+ expect(warnSpy).toHaveBeenCalled();
282
+ warnSpy.mockRestore();
283
+ });
284
+
285
+ test('générateur CH collectif par défaut (119) : même récupération directe', () => {
286
+ state.bug = true;
287
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
288
+ tv.mockReturnValue({ ...ROW_DEFAUT });
289
+ const di = { pn: 20000 };
290
+ const de = {
291
+ enum_type_generateur_ch_id: '119',
292
+ tv_generateur_combustion_id: '42',
293
+ ratio_virtualisation: 1,
294
+ presence_ventouse: 0
295
+ };
296
+ tv_generateur_combustion({}, di, de, 'ch', 200, -9, 1);
297
+ expect(tv).toHaveBeenCalledWith('generateur_combustion', {
298
+ tv_generateur_combustion_id: '42'
299
+ });
300
+ vi.restoreAllMocks();
301
+ });
302
+
303
+ test('type 84 sans tv_generateur_combustion_id : retour au chemin forfaitaire nominal', () => {
304
+ state.bug = true;
305
+ tv.mockReturnValue({ ...ROW_DEFAUT });
306
+ const di = { pn: 20000 };
307
+ const de = { enum_type_generateur_ecs_id: '84', ratio_virtualisation: 1, presence_ventouse: 0 };
308
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
309
+ // Pas d'identifiant -> matcher forfaitaire construit avec le type de générateur
310
+ expect(tv).toHaveBeenCalledWith(
311
+ 'generateur_combustion',
312
+ expect.objectContaining({ enum_type_generateur_ecs_id: '84' })
313
+ );
314
+ });
315
+
316
+ test('ECS : identifiant utilisé différent de celui du DPE non compatible -> erreur signalée', () => {
317
+ state.bug = true;
318
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
319
+ tv.mockImplementation((table, matcher) => {
320
+ // La ligne du DPE (id 99) ne référence pas le type de générateur ECS courant
321
+ if (matcher.tv_generateur_combustion_id === '99') {
322
+ return { tv_generateur_combustion_id: '99', enum_type_generateur_ecs_id: '60|61' };
323
+ }
324
+ return { ...ROW_DEFAUT };
325
+ });
326
+ const di = { pn: 20000 };
327
+ const de = {
328
+ enum_type_generateur_ecs_id: '50',
329
+ tv_generateur_combustion_id: '99',
330
+ ratio_virtualisation: 1,
331
+ presence_ventouse: 0
332
+ };
333
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
334
+ expect(errSpy).toHaveBeenCalled();
335
+ errSpy.mockRestore();
336
+ });
337
+
338
+ test('ECS : identifiant du DPE cohérent avec le type de générateur -> aucune erreur', () => {
339
+ state.bug = true;
340
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
341
+ tv.mockImplementation((table, matcher) => {
342
+ if (matcher.tv_generateur_combustion_id === '99') {
343
+ return { tv_generateur_combustion_id: '99', enum_type_generateur_ecs_id: '50|60' };
344
+ }
345
+ return { ...ROW_DEFAUT };
346
+ });
347
+ const di = { pn: 20000 };
348
+ const de = {
349
+ enum_type_generateur_ecs_id: '50',
350
+ tv_generateur_combustion_id: '99',
351
+ ratio_virtualisation: 1,
352
+ presence_ventouse: 0
353
+ };
354
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
355
+ // '50' figure dans '50|60' -> pas d'incohérence signalée
356
+ expect(errSpy).not.toHaveBeenCalled();
357
+ errSpy.mockRestore();
358
+ });
359
+
360
+ test('ECS : ligne DPE introuvable pour l’identifiant saisi -> aucune erreur', () => {
361
+ state.bug = true;
362
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
363
+ tv.mockImplementation((table, matcher) => {
364
+ if (matcher.tv_generateur_combustion_id === '99') return undefined;
365
+ return { ...ROW_DEFAUT };
366
+ });
367
+ const di = { pn: 20000 };
368
+ const de = {
369
+ enum_type_generateur_ecs_id: '50',
370
+ tv_generateur_combustion_id: '99',
371
+ ratio_virtualisation: 1,
372
+ presence_ventouse: 0
373
+ };
374
+ tv_generateur_combustion({}, di, de, 'ecs', 200, -9, 1);
375
+ expect(errSpy).not.toHaveBeenCalled();
376
+ errSpy.mockRestore();
377
+ });
378
+ });
379
+
380
+ /**
381
+ * 13.2 - Génération mixte Chauffage + ECS : redressement du type de générateur ECS.
382
+ */
383
+ describe('tv_generateur_combustion - génération mixte (checkEcsVsChauffageForMixteGeneration)', () => {
384
+ /** Fabrique un dpe contenant un générateur de chauffage mixte (usage 3). */
385
+ function dpeAvecChMixte(chId) {
386
+ return {
387
+ logement: {
388
+ installation_chauffage_collection: {
389
+ installation_chauffage: [
390
+ {
391
+ generateur_chauffage_collection: {
392
+ generateur_chauffage: [
393
+ {
394
+ donnee_entree: {
395
+ enum_usage_generateur_id: '3',
396
+ enum_type_generateur_ch_id: chId
397
+ }
398
+ }
399
+ ]
400
+ }
401
+ }
402
+ ]
403
+ }
404
+ }
405
+ };
406
+ }
407
+
408
+ test('types ECS et CH différents : le type de générateur ECS est aligné sur celui du chauffage', () => {
409
+ state.bug = true;
410
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
411
+ tv.mockReturnValue({ ...ROW_DEFAUT });
412
+ const di = { pn: 20000 };
413
+ // ECS 'A' = chaudière gaz ; CH mixte 'X' = chaudière fioul -> alignement sur 'B' (chaudière fioul ECS)
414
+ const de = {
415
+ enum_type_generateur_ecs_id: 'A',
416
+ enum_usage_generateur_id: '3',
417
+ tv_generateur_combustion_id: '42',
418
+ ratio_virtualisation: 1,
419
+ presence_ventouse: 0
420
+ };
421
+ tv_generateur_combustion(dpeAvecChMixte('X'), di, de, 'ecs', 200, -9, 1);
422
+ expect(tv).toHaveBeenCalledWith(
423
+ 'generateur_combustion',
424
+ expect.objectContaining({ enum_type_generateur_ecs_id: 'B' })
425
+ );
426
+ expect(errSpy).toHaveBeenCalled();
427
+ errSpy.mockRestore();
428
+ });
429
+
430
+ test('types ECS et CH identiques : type de générateur ECS conservé', () => {
431
+ state.bug = true;
432
+ tv.mockReturnValue({ ...ROW_DEFAUT });
433
+ const di = { pn: 20000 };
434
+ // ECS 'A' = chaudière gaz ; CH mixte 'Y' = chaudière gaz -> pas de changement
435
+ const de = {
436
+ enum_type_generateur_ecs_id: 'A',
437
+ enum_usage_generateur_id: '3',
438
+ tv_generateur_combustion_id: '42',
439
+ ratio_virtualisation: 1,
440
+ presence_ventouse: 0
441
+ };
442
+ tv_generateur_combustion(dpeAvecChMixte('Y'), di, de, 'ecs', 200, -9, 1);
443
+ expect(tv).toHaveBeenCalledWith(
444
+ 'generateur_combustion',
445
+ expect.objectContaining({ enum_type_generateur_ecs_id: 'A' })
446
+ );
447
+ });
448
+
449
+ test('aucun générateur de chauffage mixte : type de générateur ECS conservé', () => {
450
+ state.bug = true;
451
+ tv.mockReturnValue({ ...ROW_DEFAUT });
452
+ const di = { pn: 20000 };
453
+ const dpe = {
454
+ logement: {
455
+ installation_chauffage_collection: {
456
+ installation_chauffage: [
457
+ {
458
+ generateur_chauffage_collection: {
459
+ // usage 1 (non mixte) -> filtré
460
+ generateur_chauffage: [
461
+ {
462
+ donnee_entree: {
463
+ enum_usage_generateur_id: '1',
464
+ enum_type_generateur_ch_id: 'X'
465
+ }
466
+ }
467
+ ]
468
+ }
469
+ }
470
+ ]
471
+ }
472
+ }
473
+ };
474
+ const de = {
475
+ enum_type_generateur_ecs_id: 'A',
476
+ enum_usage_generateur_id: '3',
477
+ tv_generateur_combustion_id: '42',
478
+ ratio_virtualisation: 1,
479
+ presence_ventouse: 0
480
+ };
481
+ tv_generateur_combustion(dpe, di, de, 'ecs', 200, -9, 1);
482
+ expect(tv).toHaveBeenCalledWith(
483
+ 'generateur_combustion',
484
+ expect.objectContaining({ enum_type_generateur_ecs_id: 'A' })
485
+ );
486
+ });
487
+
488
+ test('générateur ECS par défaut (previous dans la liste d’exclusion) : aucun redressement', () => {
489
+ state.bug = true;
490
+ tv.mockReturnValue({ ...ROW_DEFAUT });
491
+ const di = { pn: 20000 };
492
+ const de = {
493
+ enum_type_generateur_ecs_id: 'A',
494
+ previous_enum_type_generateur_ecs_id: '78',
495
+ enum_usage_generateur_id: '3',
496
+ tv_generateur_combustion_id: '42',
497
+ ratio_virtualisation: 1,
498
+ presence_ventouse: 0
499
+ };
500
+ tv_generateur_combustion(dpeAvecChMixte('X'), di, de, 'ecs', 200, -9, 1);
501
+ // previous = '78' (autre système à combustion gaz) -> pas assez précis, on conserve 'A'
502
+ expect(tv).toHaveBeenCalledWith(
503
+ 'generateur_combustion',
504
+ expect.objectContaining({ enum_type_generateur_ecs_id: 'A' })
505
+ );
506
+ });
507
+ });
508
+
509
+ /**
510
+ * 13.2 - Orchestration des substitutions de générateurs et enrichissement fiches techniques.
511
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2
512
+ */
513
+ describe('updateGenerateurCombustion - orchestration des substitutions', () => {
514
+ test('délègue aux trois routines de substitution avec les mêmes arguments', () => {
515
+ getFicheTechnique.mockReturnValue(undefined);
516
+ const dpe = {};
517
+ const de = { description: 'gen' };
518
+ updateGenerateurCombustion(dpe, de, 'ch');
519
+ expect(updateGenerateurBouilleurs).toHaveBeenCalledWith(dpe, de, 'ch');
520
+ expect(updateGenerateurChaudieres).toHaveBeenCalledWith(dpe, de, 'ch');
521
+ expect(updateGenerateurPacs).toHaveBeenCalledWith(dpe, de, 'ch');
522
+ });
523
+
524
+ test('présence d’un ventilateur signalée dans les fiches techniques : presenceVentilateur = 1', () => {
525
+ getFicheTechnique.mockReturnValue({ valeur: 'oui' });
526
+ const de = { description: 'gen' };
527
+ updateGenerateurCombustion({}, de, 'ch');
528
+ expect(de.presenceVentilateur).toBe(1);
529
+ });
530
+
531
+ test('absence de ventilateur : presenceVentilateur non renseigné', () => {
532
+ getFicheTechnique.mockReturnValue({ valeur: 'non' });
533
+ const de = { description: 'gen' };
534
+ updateGenerateurCombustion({}, de, 'ch');
535
+ expect(de.presenceVentilateur).toBeUndefined();
536
+ });
537
+ });
@@ -142,7 +142,10 @@ function updateGenerateurBouilleur(dpe, ids, de, type) {
142
142
  * 2012.
143
143
  * - Les poêles à bois bouilleur installées avant 2012 sont traités comme des chaudières bois installées entre 1978 et 1994.
144
144
  */
145
- let newGenerateurId = generateurId === 13 ? values[1978] : values[2004];
145
+ let newGenerateurId = values[2004];
146
+ // branche morte : generateurId provient de Object.keys (chaîne), jamais === 13 (nombre)
147
+ /* c8 ignore next */
148
+ if (generateurId === 13) newGenerateurId = values[1978];
146
149
 
147
150
  if (ficheTechnique) {
148
151
  if (ficheTechnique.toString().toLowerCase() === 'avant 1948') {