import { describe, expect, it } from "vitest"; import { resolveBuiltForm, resolveConstructionAge, resolveMainFuel, resolveOverrideDescriptor, resolvePropertyType, } from "./descriptorResolution"; import { MAINFUEL_OPTIONS, RDSAP_MAIN_FUEL_CODE_LABELS, EPR_MAIN_FUEL_STRING_LABELS, WALL_TYPE_OPTIONS, ROOF_TYPE_OPTIONS, HEATING_SYSTEM_OPTIONS, DESCRIPTOR_FILTER_CONFIG, classifyDescriptor, } from "@/app/utils/propertyFilters"; import { MainFuelValues, WallTypeValues, RoofTypeValues, MainHeatingSystemValues, } from "@/app/db/schema/landlord_overrides"; describe("resolvePropertyType", () => { it("prefers a landlord override over the EPC-derived value", () => { expect( resolvePropertyType({ override: "Maisonette", isNewApproach: true, lodgedPropertyType: "2", // EPC code for Flat lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("Maisonette"); }); it("maps an RdSAP property-type code to its label when there is no override", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: "0", // RdSAP code for House lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("House"); }); it("falls back to the predicted EPC when there is no lodged value", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: null, lodgedDwellingType: null, predictedPropertyType: "2", // RdSAP code for Flat predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("Flat"); }); it("prefers the lodged EPC over the predicted EPC when both exist", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: "0", // House lodgedDwellingType: null, predictedPropertyType: "2", // Flat predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("House"); }); it("falls back to dwelling_type when the lodged property_type is absent", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: null, lodgedDwellingType: "Flat", predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("Flat"); }); it("reads the legacy property-row value for a legacy property", () => { expect( resolvePropertyType({ override: null, isNewApproach: false, lodgedPropertyType: "0", // must be ignored for legacy lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: "Bungalow", }), ).toBe("Bungalow"); }); it("never reads the EPC graph nor the legacy row across the cutoff (new-approach ignores the legacy row)", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: null, lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: "Bungalow", // legacy row must NOT surface for a new-approach property }), ).toBe("Unknown"); }); // Spec locks for decisions taken in ADR-0012 — an override always wins (even an // explicit "Unknown"), and an unrecognised RdSAP code is passed through as-is. it("lets an explicit 'Unknown' override win over a known EPC value", () => { expect( resolvePropertyType({ override: "Unknown", isNewApproach: true, lodgedPropertyType: "0", // EPC knows "House" — the override still wins lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("Unknown"); }); it("passes an unrecognised RdSAP code through unchanged", () => { expect( resolvePropertyType({ override: null, isNewApproach: true, lodgedPropertyType: "9", // not in PROPERTY_TYPE_LABELS lodgedDwellingType: null, predictedPropertyType: null, predictedDwellingType: null, legacyPropertyType: null, }), ).toBe("9"); }); }); describe("resolveBuiltForm", () => { it("prefers a landlord override over the EPC-derived value", () => { expect( resolveBuiltForm({ override: "Detached", isNewApproach: true, lodgedBuiltForm: "4", // EPC code for Mid-Terrace predictedBuiltForm: null, legacyBuiltForm: null, }), ).toBe("Detached"); }); it("maps an RdSAP built-form code to its label when there is no override", () => { expect( resolveBuiltForm({ override: null, isNewApproach: true, lodgedBuiltForm: "2", // RdSAP code for Semi-Detached predictedBuiltForm: null, legacyBuiltForm: null, }), ).toBe("Semi-Detached"); }); it("falls back to the predicted EPC when there is no lodged value", () => { expect( resolveBuiltForm({ override: null, isNewApproach: true, lodgedBuiltForm: null, predictedBuiltForm: "1", // Detached legacyBuiltForm: null, }), ).toBe("Detached"); }); it("reads the legacy property-row value for a legacy property", () => { expect( resolveBuiltForm({ override: null, isNewApproach: false, lodgedBuiltForm: "1", // must be ignored for legacy predictedBuiltForm: null, legacyBuiltForm: "End-Terrace", }), ).toBe("End-Terrace"); }); it("resolves to 'Unknown' when nothing is available (never null)", () => { expect( resolveBuiltForm({ override: null, isNewApproach: true, lodgedBuiltForm: null, predictedBuiltForm: null, legacyBuiltForm: "Detached", // legacy row must NOT surface for new-approach }), ).toBe("Unknown"); }); }); describe("resolveConstructionAge", () => { it("maps a landlord override band letter to its band label", () => { expect( resolveConstructionAge({ override: "B", isNewApproach: true, lodgedAgeBand: "F", // EPC says a different band — override still wins predictedAgeBand: null, legacyYearBuilt: null, }), ).toBe("1900–1929"); }); it("maps the lodged EPC band letter to its label when there is no override", () => { expect( resolveConstructionAge({ override: null, isNewApproach: true, lodgedAgeBand: "C", predictedAgeBand: null, legacyYearBuilt: null, }), ).toBe("1930–1949"); }); it("falls back to the predicted EPC band when there is no lodged band", () => { expect( resolveConstructionAge({ override: null, isNewApproach: true, lodgedAgeBand: null, predictedAgeBand: "M", legacyYearBuilt: null, }), ).toBe("2023 onwards"); }); it("buckets a legacy year_built into its band (mid-range)", () => { expect(legacyAge("1985")).toBe("1983–1990"); }); it("buckets a legacy year before 1900 into band A", () => { expect(legacyAge("1899")).toBe("before 1900"); }); it("buckets the first year of a band correctly (boundary)", () => { expect(legacyAge("1900")).toBe("1900–1929"); }); it("buckets a recent legacy year into the open-ended final band", () => { expect(legacyAge("2025")).toBe("2023 onwards"); }); it("resolves a non-numeric legacy year_built to 'Unknown'", () => { expect(legacyAge("N/A")).toBe("Unknown"); }); it("resolves to 'Unknown' when nothing is available; ignores the legacy row for new-approach", () => { expect( resolveConstructionAge({ override: null, isNewApproach: true, lodgedAgeBand: null, predictedAgeBand: null, legacyYearBuilt: "1985", // must NOT surface for a new-approach property }), ).toBe("Unknown"); }); it("lets an explicit 'Unknown' override win", () => { expect( resolveConstructionAge({ override: "Unknown", isNewApproach: true, lodgedAgeBand: "C", predictedAgeBand: null, legacyYearBuilt: null, }), ).toBe("Unknown"); }); }); describe("resolveMainFuel", () => { const base = { override: null, isNewApproach: true, lodgedMainFuel: null, predictedMainFuel: null, legacyMainFuel: null, }; it("prefers a landlord override over the EPC-derived value", () => { expect( resolveMainFuel({ ...base, override: "mains gas", lodgedMainFuel: "26" }), ).toBe("mains gas"); }); it("reads the new-approach EPC main-fuel code (lodged) as a raw value", () => { expect(resolveMainFuel({ ...base, lodgedMainFuel: "26" })).toBe("26"); }); it("falls back to the predicted EPC main fuel when there is no lodged one", () => { expect(resolveMainFuel({ ...base, predictedMainFuel: "29" })).toBe("29"); }); it("prefers the lodged EPC main fuel over the predicted one", () => { expect( resolveMainFuel({ ...base, lodgedMainFuel: "26", predictedMainFuel: "29" }), ).toBe("26"); }); it("passes an EPR string main fuel through unchanged", () => { expect(resolveMainFuel({ ...base, lodgedMainFuel: "Mains gas" })).toBe("Mains gas"); }); it("resolves to 'Unknown' for a new-approach property with no EPC main fuel; ignores the legacy row", () => { expect( resolveMainFuel({ ...base, legacyMainFuel: "Gas mains gas" }), ).toBe("Unknown"); }); it("reads the legacy EPC main fuel for a legacy property", () => { expect( resolveMainFuel({ ...base, isNewApproach: false, lodgedMainFuel: "26", // must be ignored for legacy legacyMainFuel: "Gas mains gas", }), ).toBe("Gas mains gas"); }); it("resolves to 'Unknown' when nothing is available (never null)", () => { expect(resolveMainFuel({ ...base, isNewApproach: false })).toBe("Unknown"); }); }); describe("MAINFUEL_OPTIONS coverage", () => { const covered = new Set(MAINFUEL_OPTIONS.flatMap((o) => o.dbValues)); const labels = new Set(MAINFUEL_OPTIONS.map((o) => o.label)); // Every landlord main-fuel override value must fold into a filter option, or an // overridden fuel would display but be unfilterable (ADR-0012). it("covers every landlord main-fuel override value", () => { for (const value of MainFuelValues) { expect(covered).toContain(value); } }); // Every RdSAP main_fuel code the new EPC graph can store must fold into a // filter option under a real label (a typo'd label would be silently dropped // by the MAINFUEL_OPTIONS merge — assert both the code AND its label). it("folds every RdSAP main_fuel code under a real label", () => { for (const [code, label] of Object.entries(RDSAP_MAIN_FUEL_CODE_LABELS)) { expect(labels).toContain(label); expect(covered).toContain(code); } }); // Every EPR free-text main_fuel string must fold in too. it("folds every EPR main-fuel string under a real label", () => { for (const [str, label] of Object.entries(EPR_MAIN_FUEL_STRING_LABELS)) { expect(labels).toContain(label); expect(covered).toContain(str); } }); }); describe("resolveOverrideDescriptor (wall / roof / heating free-text)", () => { const base = { override: null, isNewApproach: true, lodgedDescription: null, predictedDescription: null, legacyDescription: null, }; it("prefers a landlord override over the EPC description", () => { expect( resolveOverrideDescriptor({ ...base, override: "Cavity wall, filled cavity", lodgedDescription: "Solid brick, as built, no insulation (assumed)", }), ).toBe("Cavity wall, filled cavity"); }); it("uses the lodged EPC description when there is no override", () => { expect( resolveOverrideDescriptor({ ...base, lodgedDescription: "Pitched, 250 mm loft insulation", }), ).toBe("Pitched, 250 mm loft insulation"); }); it("falls back to the predicted EPC description when there is no lodged one", () => { expect( resolveOverrideDescriptor({ ...base, predictedDescription: "Flat, insulated (assumed)" }), ).toBe("Flat, insulated (assumed)"); }); it("reads the legacy description for a legacy property", () => { expect( resolveOverrideDescriptor({ ...base, isNewApproach: false, lodgedDescription: "ignored for legacy", legacyDescription: "Boiler and radiators, mains gas", }), ).toBe("Boiler and radiators, mains gas"); }); it("resolves to 'Unknown' when nothing is available; ignores legacy for new-approach", () => { expect( resolveOverrideDescriptor({ ...base, legacyDescription: "must not surface" }), ).toBe("Unknown"); }); }); describe("classifyDescriptor — wall / roof / heating buckets", () => { const wall = (v: string) => classifyDescriptor(v, WALL_TYPE_OPTIONS, DESCRIPTOR_FILTER_CONFIG.wallType.match); const roof = (v: string) => classifyDescriptor(v, ROOF_TYPE_OPTIONS, DESCRIPTOR_FILTER_CONFIG.roofType.match); const heat = (v: string) => classifyDescriptor(v, HEATING_SYSTEM_OPTIONS, DESCRIPTOR_FILTER_CONFIG.heatingSystem.match); // Every landlord override value must fall into a real (non-Unknown) bucket, or // an overridden component would display but be unfilterable (ADR-0012). The // literal "Unknown" is the sole exception — it maps to the Unknown bucket. const assertVocabCovered = ( values: readonly string[], classify: (v: string) => string, labels: Set, ) => { for (const value of values) { const bucket = classify(value); expect(labels).toContain(bucket); if (value !== "Unknown") expect(bucket).not.toBe("Unknown"); } }; it("buckets every WallType override value", () => { assertVocabCovered(WallTypeValues, wall, new Set(WALL_TYPE_OPTIONS.map((b) => b.label))); }); it("buckets every RoofType override value", () => { assertVocabCovered(RoofTypeValues, roof, new Set(ROOF_TYPE_OPTIONS.map((b) => b.label))); }); it("buckets every MainHeatingSystem override value", () => { assertVocabCovered( MainHeatingSystemValues, heat, new Set(HEATING_SYSTEM_OPTIONS.map((b) => b.label)), ); }); // Representative EPC / legacy descriptions (from production) bucket as expected. it("buckets representative EPC/legacy WALL descriptions", () => { expect(wall("Cavity wall, filled cavity")).toBe("Cavity"); expect(wall("Solid brick, as built, no insulation (assumed)")).toBe("Solid Brick"); expect(wall("Timber frame, as built, insulated (assumed)")).toBe("Timber Frame"); expect(wall("Sandstone or limestone, as built, no insulation (assumed)")).toBe("Stone"); expect(wall("Granite or whinstone, as built, no insulation (assumed)")).toBe("Stone"); expect(wall("System built, with external insulation")).toBe("System Built"); // A bare U-value is not a construction type → Unknown. expect(wall("Average thermal transmittance 0.21 W/m²K")).toBe("Unknown"); }); it("buckets representative EPC/legacy ROOF descriptions", () => { expect(roof("Pitched, 200 mm loft insulation")).toBe("Pitched"); expect(roof("Flat, no insulation (assumed)")).toBe("Flat"); expect(roof("Roof room(s), insulated (assumed)")).toBe("Room-in-Roof"); expect(roof("(another dwelling above)")).toBe("Premises Above"); expect(roof("(other premises above)")).toBe("Premises Above"); expect(roof("Another Premises Above")).toBe("Premises Above"); expect(roof("Average thermal transmittance 0.11 W/m²K")).toBe("Unknown"); }); it("buckets representative EPC/legacy HEATING descriptions", () => { expect(heat("Boiler and radiators, mains gas")).toBe("Gas Boiler"); expect(heat("Room heaters, mains gas")).toBe("Gas Room Heater"); expect(heat("Community scheme")).toBe("Community"); expect(heat("Community scheme, biomass")).toBe("Community"); expect(heat("Electric storage heaters")).toBe("Electric Storage"); expect(heat("Room heaters, electric")).toBe("Direct Electric"); expect(heat("Air source heat pump, radiators, electric")).toBe("Heat Pump"); // "oil" is a substring of "boiler" — a gas boiler must NOT bucket as Oil. expect(heat("Boiler and radiators, oil")).toBe("Oil"); expect(heat("Boiler and radiators, dual fuel (mineral and wood)")).toBe("Solid Fuel"); expect(heat("Boiler and radiators, lpg")).toBe("LPG"); }); // The non-gas wet-boiler archetypes: their fuel leads the string rather than // following a comma, so they are the case the ", oil" needle cannot catch. // Asserted by bucket rather than just "not Unknown", because the failure mode // that matters is a *wrong* bucket — reorder the needles and "Oil boiler, // combi" silently becomes a Gas Boiler, which the coverage test would pass. it("buckets the non-gas wet-boiler override archetypes by their fuel", () => { expect(heat("Oil boiler, regular")).toBe("Oil"); expect(heat("Oil boiler, combi")).toBe("Oil"); expect(heat("Solid fuel boiler")).toBe("Solid Fuel"); // Guard the collision the "oil boiler" needle could have caused: Solid Fuel // is checked first, and a gas boiler still has no "oil" fuel token. expect(heat("Gas boiler, combi")).toBe("Gas Boiler"); }); }); /** Legacy-property helper: only year_built is set, cutoff is false. */ function legacyAge(year: string): string { return resolveConstructionAge({ override: null, isNewApproach: false, lodgedAgeBand: null, predictedAgeBand: null, legacyYearBuilt: year, }); }