diff --git a/src/3.2.2_plancher_bas.js b/src/3.2.2_plancher_bas.js index 9ccf7e5a..e2006603 100644 --- a/src/3.2.2_plancher_bas.js +++ b/src/3.2.2_plancher_bas.js @@ -55,7 +55,15 @@ function tv_upb(di, de, du, pc_id, zc, effetJoule) { const values_2s_p = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20]; -function tv_ue(di, de, du, pc_id, pb_list) { +/** + * Ue du plancher bas courant, calculé plancher par plancher (issue #46). + * 2S/P est déterminé avec la surface (surface_ue, à défaut surface_paroi_opaque) et le périmètre + * (perimetre_ue) du seul plancher courant, sans sommer les autres planchers de même adjacence. + * Les valeurs absentes du tableau sont obtenues par interpolation linéaire sur upb. + * + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 (tableau ue) + */ +function tv_ue(di, de, du, pc_id) { const type_adjacence = enums.type_adjacence[de.enum_type_adjacence_id]; let type_adjacence_plancher; let upb1, upb2; @@ -72,21 +80,9 @@ function tv_ue(di, de, du, pc_id, pb_list) { [upb1, upb2] = getRange(di.upb, [0.31, 0.34, 0.37, 0.41, 0.45, 0.83, 1.43, 3.33]); } - /** - * La surface Ue est la surface de tous les planchers bas ayant le même type d'adjacence - * Le périmètre Ue est le périmètre de tous les planchers bas ayant le même type d'adjacence - */ - const { surfaceUe, perimetreUe } = pb_list.reduce( - (acc, plancherBas) => { - if (plancherBas.donnee_entree.enum_type_adjacence_id === de.enum_type_adjacence_id) { - acc.surfaceUe += - plancherBas.donnee_entree.surface_ue || plancherBas.donnee_entree.surface_paroi_opaque; - acc.perimetreUe += plancherBas.donnee_entree.perimetre_ue || 0; - } - return acc; - }, - { surfaceUe: 0, perimetreUe: 0 } - ); + // Surface et périmètre Ue du plancher courant uniquement + const surfaceUe = de.surface_ue || de.surface_paroi_opaque; + const perimetreUe = de.perimetre_ue || 0; const matcher = { type_adjacence_plancher, @@ -134,7 +130,7 @@ function calc_upb0(di, de, du) { } } -export default function calc_pb(pb, zc, pc_id, effetJoule, pb_list) { +export default function calc_pb(pb, zc, pc_id, effetJoule) { const de = pb.donnee_entree; const du = {}; const di = {}; @@ -246,7 +242,7 @@ export default function calc_pb(pb, zc, pc_id, effetJoule, pb_list) { case 'vide sanitaire': case 'sous-sol non chauffé': case 'terre-plein': - tv_ue(di, de, du, pc_id, pb_list); + tv_ue(di, de, du, pc_id); di.upb_final = de.ue; break; default: diff --git a/src/3.2.2_plancher_bas.spec.js b/src/3.2.2_plancher_bas.spec.js index c112aa54..1b8e742a 100644 --- a/src/3.2.2_plancher_bas.spec.js +++ b/src/3.2.2_plancher_bas.spec.js @@ -561,14 +561,17 @@ describe('calc_pb - calcul de Ue (tv_ue)', () => { enum_type_plancher_bas_id: '9', type_adjacence: 'terre-plein', enum_type_adjacence_id: '5', - surface_paroi_opaque: 4 // pas de surface_ue => fallback + surface_paroi_opaque: 4, // pas de surface_ue => fallback + perimetre_ue: 2 } }; calc_pb(pb, 'h1a', '1', '0', [pb1, pb, autre]); - // surfaceUe = 20 (pb1.surface_ue) + 4 (pb.surface_paroi_opaque) = 24 ; perimetreUe = 12 + 0 = 12 - // 2S/P = round(48/12) = 4 (valeur présente dans values_2s_p) + // Calcul plancher par plancher (#46) : les autres planchers (pb1, autre) sont ignorés. + // Ce test encodait auparavant la somme par adjacence (S = 20 + 4, P = 12 + 0) ; perimetre_ue + // est désormais porté par le plancher courant pour conserver 2S/P = 4. + // surfaceUe = 4 (pb.surface_paroi_opaque) ; perimetreUe = 2 => 2S/P = round(8/2) = 4 expect(tv).toHaveBeenCalledWith('ue', { type_adjacence_plancher: 'terre plein bâtiment construit avant 2001', '2s_p': '^4$', @@ -652,4 +655,109 @@ describe('calc_pb - calcul de Ue (tv_ue)', () => { expect(pb.donnee_intermediaire.upb_final).toBe(0.19); }); + + /** + * Issue #46 : ue calculé plancher par plancher, 2S/P issu du seul plancher courant. + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 (tableau ue : « Les données ne figurant + * pas dans le tableau peuvent être obtenues par interpolation et extrapolation ») + */ + describe('calcul plancher par plancher (#46)', () => { + /** Plancher non isolé (methode 1) dont upb = upb0 = 1,45. */ + const plancher = (donnees) => ({ + donnee_entree: { + enum_methode_saisie_u_id: '1', + methode_saisie_u0: + 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire', + enum_type_plancher_bas_id: '2', + type_adjacence: 'terre-plein', + enum_type_adjacence_id: '5', + ...donnees + } + }); + + /** Table ue mockée par couple (2s_p, upb) : terre-plein avant 2001. */ + const UE = { + '^7$': { 0.85: 0.33, 1.5: 0.39 }, + '^2$': { 0.85: 0.55, 1.5: 0.8 }, + '^1$': { 0.85: 0.7, 1.5: 0.9 }, + '^4$': { 0.85: 0.45, 1.5: 0.6 } + }; + + beforeEach(() => { + getRange.mockReturnValue([0.85, 1.5]); + tv.mockImplementation((table, matcher) => { + if (table === 'upb0') return { upb0: '1.45', tv_upb0_id: '2' }; + if (table === 'ue') return { ue: String(UE[matcher['2s_p']][matcher.upb]) }; + return null; + }); + }); + + test('2187E0982591I plancher 1 : 2S/P = 7 et ue = 0,385384615 malgré un autre terre-plein', () => { + const pb1 = plancher({ surface_paroi_opaque: 44.8, perimetre_ue: 12.8 }); + const pb3 = plancher({ surface_paroi_opaque: 10.5, perimetre_ue: 10 }); + + calc_pb(pb1, 'h1a', '1', '0', [pb1, pb3]); + + // 2S/P = 2 * 44,8 / 12,8 = 7 (somme par adjacence : 2 * 55,3 / 22,8 ≈ 4,85 => 5) + expect(tv).toHaveBeenCalledWith('ue', { + type_adjacence_plancher: 'terre plein bâtiment construit avant 2001', + '2s_p': '^7$', + upb: '0.85' + }); + expect(tv).toHaveBeenCalledWith('ue', { + type_adjacence_plancher: 'terre plein bâtiment construit avant 2001', + '2s_p': '^7$', + upb: '1.5' + }); + expect(tv).not.toHaveBeenCalledWith('ue', expect.objectContaining({ '2s_p': '^5$' })); + // ue = 0,33 + (0,39 - 0,33) * (1,45 - 0,85) / (1,5 - 0,85) = 0,385384615 (valeur ADEME) + expect(pb1.donnee_entree.ue).toBeCloseTo(0.385384615, 9); + expect(pb1.donnee_intermediaire.upb_final).toBeCloseTo(0.385384615, 9); + }); + + test('deux planchers de même adjacence avec surface_ue / perimetre_ue différents : ue différents', () => { + const pbA = plancher({ surface_paroi_opaque: 50, surface_ue: 70, perimetre_ue: 20 }); + const pbB = plancher({ surface_paroi_opaque: 50, surface_ue: 10, perimetre_ue: 10 }); + const liste = [pbA, pbB]; + + liste.forEach((pb) => calc_pb(pb, 'h1a', '1', '0', liste)); + + // pbA : 2 * 70 / 20 = 7 => ue interpolé entre 0,33 et 0,39 + expect(pbA.donnee_entree.ue).toBeCloseTo(0.385384615, 9); + // pbB : 2 * 10 / 10 = 2 => 0,55 + 0,25 * 0,6 / 0,65 (valeur de référence de régression) + expect(pbB.donnee_entree.ue).toBeCloseTo(0.780769231, 9); + expect(pbA.donnee_entree.ue).not.toBeCloseTo(pbB.donnee_entree.ue, 3); + }); + + test('surface_ue absente : surface_paroi_opaque du plancher courant utilisée', () => { + const pb = plancher({ surface_paroi_opaque: 20, perimetre_ue: 10 }); + const autre = plancher({ surface_ue: 500, surface_paroi_opaque: 500, perimetre_ue: 10 }); + + calc_pb(pb, 'h1a', '1', '0', [pb, autre]); + + // 2S/P = 2 * 20 / 10 = 4 + expect(tv).toHaveBeenCalledWith('ue', expect.objectContaining({ '2s_p': '^4$' })); + // ue = 0,45 + 0,15 * 0,6 / 0,65 (valeur de référence de régression) + expect(pb.donnee_entree.ue).toBeCloseTo(0.588461538, 9); + }); + + test('surface_ue prioritaire sur surface_paroi_opaque', () => { + const pb = plancher({ surface_paroi_opaque: 20, surface_ue: 35, perimetre_ue: 10 }); + + calc_pb(pb, 'h1a', '1', '0', [pb]); + + // 2S/P = 2 * 35 / 10 = 7 + expect(tv).toHaveBeenCalledWith('ue', expect.objectContaining({ '2s_p': '^7$' })); + }); + + test('perimetre_ue absent : comportement inchangé (division par 0 => 2S/P = 1), même si un autre plancher en a un', () => { + const pb = plancher({ surface_paroi_opaque: 20 }); + const autre = plancher({ surface_paroi_opaque: 20, perimetre_ue: 10 }); + + calc_pb(pb, 'h1a', '1', '0', [pb, autre]); + + expect(tv).toHaveBeenCalledWith('ue', expect.objectContaining({ '2s_p': '^1$' })); + expect(pb.donnee_entree.ue).toBeCloseTo(0.884615385, 9); + }); + }); }); diff --git a/src/3_deperdition.js b/src/3_deperdition.js index 3ac94b76..e76aedff 100644 --- a/src/3_deperdition.js +++ b/src/3_deperdition.js @@ -230,7 +230,7 @@ export default function calc_deperdition(cg, zc, th, effetJoule, dpe, Sh) { */ const versionDpe = parseFloat(dpe.administratif?.enum_version_id); mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule, versionDpe)); - pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule, pb_list)); + pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule)); ph_list.forEach((ph) => calc_ph(ph, zc, pc, effetJoule)); bv_list.forEach((bv) => calc_bv(bv, zc)); porte_list.forEach((porte) => calc_porte(porte, zc)); diff --git a/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.js b/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.js index a6a561fe..24066f39 100755 --- a/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.js +++ b/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.js @@ -207,11 +207,7 @@ export class DeperditionEnveloppeService { const plancherBas = enveloppe.plancher_bas_collection.plancher_bas || []; plancherBas?.forEach((pb) => { - pb.donnee_intermediaire = this.#deperditionPlancherBasService.execute( - ctx, - pb.donnee_entree, - plancherBas - ); + pb.donnee_intermediaire = this.#deperditionPlancherBasService.execute(ctx, pb.donnee_entree); deperditions.deperdition_plancher_bas += pb.donnee_intermediaire.b * pb.donnee_entree.surface_paroi_opaque * diff --git a/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js b/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js index b8dccb50..e560e04a 100644 --- a/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +++ b/src/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js @@ -158,10 +158,10 @@ describe('DeperditionEnveloppeService (§3 déperditions de l’enveloppe)', () }); // Délégation aux services de paroi + // Ue calculé plancher par plancher (#46) : la liste des planchers n'est plus transmise expect(plancherBasService.execute).toHaveBeenCalledWith( ctx, - env.plancher_bas_collection.plancher_bas[0].donnee_entree, - env.plancher_bas_collection.plancher_bas + env.plancher_bas_collection.plancher_bas[0].donnee_entree ); expect(pontThermiqueService.execute).toHaveBeenCalledWith( ctx, diff --git a/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.js b/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.js index 7829078e..e7535835 100755 --- a/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.js +++ b/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.js @@ -22,13 +22,12 @@ export class DeperditionPlancherBasService extends DeperditionService { /** * @param ctx {Contexte} * @param pbDE {PlancherBasDE} - * @param plancherBas {PlancherBas[]} * @return {PlancherBasDI} */ - execute(ctx, pbDE, plancherBas) { + execute(ctx, pbDE) { const upb0 = this.#upb0(pbDE); const upb = this.#upb(pbDE, upb0, ctx); - const upb_final = this.#upbFinal(pbDE, upb, ctx, plancherBas); + const upb_final = this.#upbFinal(pbDE, upb, ctx); const b = this.b({ enumTypeAdjacenceId: pbDE.enum_type_adjacence_id, surfaceAiu: pbDE.surface_aiu, @@ -113,10 +112,9 @@ export class DeperditionPlancherBasService extends DeperditionService { * @param pbDE {PlancherBasDE} * @param upb {number} * @param ctx {Contexte} - * @param plancherBas {PlancherBas[]} * @return {number|undefined} */ - #upbFinal(pbDE, upb, ctx, plancherBas) { + #upbFinal(pbDE, upb, ctx) { if (pbDE.calcul_ue === 1) { return pbDE.ue; } @@ -131,23 +129,13 @@ export class DeperditionPlancherBasService extends DeperditionService { } /** - * La surface Ue est la surface de tous les planchers bas ayant le même type d'adjacence - * Le périmètre Ue est le périmètre de tous les planchers bas ayant le même type d'adjacence + * Calcul plancher par plancher (issue #46) : 2S/P est déterminé avec la surface (surface_ue, à + * défaut surface_paroi_opaque) et le périmètre (perimetre_ue) du seul plancher courant, sans + * sommer les autres planchers de même adjacence. + * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 (tableau ue) */ - const { surfaceUe, perimetreUe } = plancherBas - .filter( - (plancherBas) => - pbDE.enum_type_adjacence_id === plancherBas.donnee_entree.enum_type_adjacence_id - ) - .reduce( - (acc, plancherBas) => { - acc.surfaceUe += - plancherBas.donnee_entree.surface_ue || plancherBas.donnee_entree.surface_paroi_opaque; - acc.perimetreUe += plancherBas.donnee_entree.perimetre_ue || 0; - return acc; - }, - { surfaceUe: 0, perimetreUe: 0 } - ); + const surfaceUe = pbDE.surface_ue || pbDE.surface_paroi_opaque; + const perimetreUe = pbDE.perimetre_ue || 0; const available2sp = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20]; let dsp = perimetreUe ? Math.round((2 * surfaceUe) / perimetreUe) : 1; diff --git a/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js b/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js new file mode 100644 index 00000000..f828ae19 --- /dev/null +++ b/src/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js @@ -0,0 +1,250 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { DeperditionPlancherBasService } from './deperdition-plancher-bas.service.js'; + +/** + * Tests unitaires isolés de `DeperditionPlancherBasService` (nouvelle architecture). + * + * Le `TvStore` est remplacé par un double de test (spies `vi.fn`) et le coefficient b (classe de + * base) est espionné : seule la logique propre au service (Upb0, Upb, Ue, type d'isolation) est + * vérifiée, sans dépendre des tables de valeurs réelles. + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 + */ + +/** @type {{ getUpb0: import('vitest').Mock, getUpb: import('vitest').Mock, getUeByUpd: import('vitest').Mock }} */ +let tvStore; + +/** @type {DeperditionPlancherBasService} */ +let service; + +const ctx = { + zoneClimatique: { id: '1', value: 'h1a' }, + effetJoule: false, + enumPeriodeConstructionId: '1' +}; + +beforeEach(() => { + tvStore = { + getUpb0: vi.fn(() => 1.45), + getUpb: vi.fn(() => 0.4), + getUeByUpd: vi.fn(() => 0.385384615) + }; + service = new DeperditionPlancherBasService(tvStore); + vi.spyOn(service, 'b').mockReturnValue(1); +}); + +/** Plancher non isolé, U0 forfaitaire, adjacence extérieur par défaut. */ +const plancherDE = (donnees = {}) => ({ + enum_methode_saisie_u0_id: '2', + enum_methode_saisie_u_id: '1', + enum_type_plancher_bas_id: '2', + enum_type_adjacence_id: '1', + ...donnees +}); + +describe('DeperditionPlancherBasService - execute()', () => { + test('transmet les données du plancher au calcul de b', () => { + const pbDE = plancherDE({ + surface_aiu: 10, + surface_aue: 20, + enum_cfg_isolation_lnc_id: '3' + }); + + const di = service.execute(ctx, pbDE); + + expect(service.b).toHaveBeenCalledWith({ + enumTypeAdjacenceId: '1', + surfaceAiu: 10, + surfaceAue: 20, + enumCfgIsolationLncId: '3', + zoneClimatique: 'h1a' + }); + expect(di).toEqual({ upb0: 1.45, upb: 1.45, upb_final: 1.45, b: 1 }); + }); +}); + +describe('DeperditionPlancherBasService - calcul de Upb0', () => { + test.each(['1', '2'])('méthode u0 %s : valeur forfaitaire de la table upb0', (methode) => { + const di = service.execute(ctx, plancherDE({ enum_methode_saisie_u0_id: methode })); + + expect(tvStore.getUpb0).toHaveBeenCalledWith('2'); + expect(di.upb0).toBe(1.45); + }); + + test('méthode u0 5 (u saisi) : upb0 non déterminé', () => { + const di = service.execute( + ctx, + plancherDE({ enum_methode_saisie_u0_id: '5', enum_methode_saisie_u_id: '9', upb_saisi: 0.3 }) + ); + + expect(di.upb0).toBeUndefined(); + expect(di.upb).toBe(0.3); + }); + + test('u0 saisi : valeur saisie retenue', () => { + const di = service.execute( + ctx, + plancherDE({ enum_methode_saisie_u0_id: '3', upb0_saisi: 1.2 }) + ); + + expect(tvStore.getUpb0).not.toHaveBeenCalled(); + expect(di.upb0).toBe(1.2); + }); +}); + +describe('DeperditionPlancherBasService - calcul de Upb', () => { + test('non isolé : upb = min(upb0, 2)', () => { + tvStore.getUpb0.mockReturnValue(3); + + expect(service.execute(ctx, plancherDE()).upb).toBe(2); + }); + + test.each(['2', '7', '8'])( + 'méthode u %s (table forfaitaire) : min(upb nu, upb de la table)', + (methode) => { + const di = service.execute(ctx, plancherDE({ enum_methode_saisie_u_id: methode })); + + // Construction avant 1975 sans période d'isolation => période d'isolation 3 (1975-1977) + expect(tvStore.getUpb).toHaveBeenCalledWith('3', '1', false); + expect(di.upb).toBe(0.4); + } + ); + + test.each(['3', '4'])('méthode u %s : épaisseur d’isolation', (methode) => { + const di = service.execute( + ctx, + plancherDE({ enum_methode_saisie_u_id: methode, epaisseur_isolation: 10 }) + ); + + // 1 / (1 / 1,45 + 0,1 / 0,042) arrondi à 1e-5 (valeur de référence de régression) + expect(di.upb).toBe(0.32567); + }); + + test.each(['5', '6'])('méthode u %s : résistance d’isolation', (methode) => { + const di = service.execute( + ctx, + plancherDE({ enum_methode_saisie_u_id: methode, resistance_isolation: 2 }) + ); + + // 1 / (1 / 1,45 + 2) arrondi à 1e-5 (valeur de référence de régression) + expect(di.upb).toBe(0.37179); + }); + + test('saisie directe : upb saisi', () => { + const di = service.execute(ctx, plancherDE({ enum_methode_saisie_u_id: '9', upb_saisi: 0.25 })); + + expect(di.upb).toBe(0.25); + }); +}); + +describe('DeperditionPlancherBasService - calcul de upb_final (Ue)', () => { + test('calcul_ue = 1 : ue du DPE retenu', () => { + const di = service.execute( + ctx, + plancherDE({ enum_type_adjacence_id: '5', calcul_ue: 1, ue: 0.38538462 }) + ); + + expect(di.upb_final).toBe(0.38538462); + expect(tvStore.getUeByUpd).not.toHaveBeenCalled(); + }); + + test('adjacence sans Ue (extérieur) : upb_final = upb', () => { + const di = service.execute(ctx, plancherDE({ enum_type_adjacence_id: '1' })); + + expect(di.upb_final).toBe(1.45); + expect(tvStore.getUeByUpd).not.toHaveBeenCalled(); + }); + + /** + * Issue #46 : ue calculé plancher par plancher, 2S/P issu du seul plancher courant. + * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §3.2.2 (tableau ue) + */ + describe('calcul plancher par plancher (#46)', () => { + test('2187E0982591I plancher 1 : 2S/P = 2 * 44,8 / 12,8 = 7', () => { + const di = service.execute( + ctx, + plancherDE({ enum_type_adjacence_id: '5', surface_paroi_opaque: 44.8, perimetre_ue: 12.8 }) + ); + + expect(tvStore.getUeByUpd).toHaveBeenCalledWith('5', '1', 7, 1.45); + expect(di.upb_final).toBe(0.38538); + }); + + test('deux planchers de même adjacence : chacun son propre 2S/P', () => { + const pbA = plancherDE({ + enum_type_adjacence_id: '5', + surface_paroi_opaque: 50, + surface_ue: 70, + perimetre_ue: 20 + }); + const pbB = plancherDE({ + enum_type_adjacence_id: '5', + surface_paroi_opaque: 50, + surface_ue: 10, + perimetre_ue: 10 + }); + tvStore.getUeByUpd.mockImplementation((adj, pc, dsp) => (dsp === 7 ? 0.38 : 0.78)); + + const diA = service.execute(ctx, pbA); + const diB = service.execute(ctx, pbB); + + // pbA : 2 * 70 / 20 = 7 ; pbB : 2 * 10 / 10 = 2 (somme par adjacence : 2 * 80 / 30 => 5) + expect(tvStore.getUeByUpd).toHaveBeenNthCalledWith(1, '5', '1', 7, 1.45); + expect(tvStore.getUeByUpd).toHaveBeenNthCalledWith(2, '5', '1', 2, 1.45); + expect(diA.upb_final).toBe(0.38); + expect(diB.upb_final).toBe(0.78); + }); + + test('surface_ue absente : surface_paroi_opaque utilisée', () => { + service.execute( + ctx, + plancherDE({ enum_type_adjacence_id: '3', surface_paroi_opaque: 20, perimetre_ue: 10 }) + ); + + expect(tvStore.getUeByUpd).toHaveBeenCalledWith('3', '1', 4, 1.45); + }); + + test('2S/P hors tableau : valeur disponible la plus proche (11 => 10)', () => { + service.execute( + ctx, + plancherDE({ enum_type_adjacence_id: '6', surface_ue: 55, perimetre_ue: 10 }) + ); + + expect(tvStore.getUeByUpd).toHaveBeenCalledWith('6', '1', 10, 1.45); + }); + + test('perimetre_ue absent : 2S/P = 1 (comportement inchangé)', () => { + service.execute(ctx, plancherDE({ enum_type_adjacence_id: '5', surface_paroi_opaque: 20 })); + + expect(tvStore.getUeByUpd).toHaveBeenCalledWith('5', '1', 1, 1.45); + }); + }); +}); + +describe('DeperditionPlancherBasService - typeIsolation()', () => { + test.each([ + ['terre-plein avant 2001', '5', undefined, '6', 2], + ['terre-plein à partir de 2001', '5', undefined, '7', 4], + ['terre-plein, période d’isolation prioritaire', '5', '7', '1', 4], + ['autre adjacence avant 1975', '3', undefined, '2', 2], + ['autre adjacence à partir de 1975', '3', undefined, '3', 4] + ])('isolation inconnue, %s', (_, adjacence, periodeIsolation, periodeConstruction, attendu) => { + expect( + service.typeIsolation( + { enumPeriodeConstructionId: periodeConstruction }, + { + enum_type_isolation_id: '1', + enum_type_adjacence_id: adjacence, + enum_periode_isolation_id: periodeIsolation + } + ) + ).toBe(attendu); + }); + + test('isolé mais type d’isolation inconnu (9) : ITE (4)', () => { + expect(service.typeIsolation(ctx, { enum_type_isolation_id: '9' })).toBe(4); + }); + + test('type d’isolation connu : renvoyé tel quel', () => { + expect(service.typeIsolation(ctx, { enum_type_isolation_id: '3' })).toBe(3); + }); +});