mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
feat(portfolio): resolve Main Fuel column + filter via override precedence
Slice 4. The new EPC graph exposes no main-fuel string (documented gap), so for new-approach properties the landlord override is the only source. resolveMainFuel returns the raw value (override → legacy EPC → 'Unknown'); the cell's resolveEnumLabel folds it to a chip. MAINFUEL_OPTIONS.dbValues extended with the override vocabulary so overridden fuels are both displayable and filterable — guarded by a coverage test over MainFuelValues. resolvedMainFuelSql mirrors it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
558e20f2fe
commit
fc2f7d5b30
5 changed files with 114 additions and 14 deletions
|
|
@ -25,7 +25,6 @@ import {
|
|||
totalFloorAreaSql,
|
||||
lodgementDateSql,
|
||||
isExpiredSql,
|
||||
mainfuelSql,
|
||||
effectiveSapSql,
|
||||
effectiveEpcBandSql,
|
||||
lodgedEpcBandSql,
|
||||
|
|
@ -37,6 +36,8 @@ import {
|
|||
resolvedBuiltFormSql,
|
||||
constructionAgeOverrideJoin,
|
||||
resolvedConstructionAgeSql,
|
||||
mainFuelOverrideJoin,
|
||||
resolvedMainFuelSql,
|
||||
} from "@/lib/services/epcSources";
|
||||
import {
|
||||
FilterGroups,
|
||||
|
|
@ -607,9 +608,10 @@ function buildConditionSql(filter: PropertyFilter): ReturnType<typeof sql> | nul
|
|||
tenure: sql`p.tenure`,
|
||||
yearBuilt: resolvedConstructionAgeSql,
|
||||
provenance: provenanceSignalSql,
|
||||
// GAP: new-approach properties have no main-fuel string → NULL, so they
|
||||
// only match the "no value" filter branch. See epcSources.ts.
|
||||
mainfuel: mainfuelSql(sql`epc`),
|
||||
// Override-resolved (ADR-0012): the new EPC graph has no main fuel, so a
|
||||
// new-approach property matches a fuel filter only via a landlord override;
|
||||
// otherwise it resolves to 'Unknown'.
|
||||
mainfuel: resolvedMainFuelSql(sql`epc`),
|
||||
};
|
||||
const col = colMap[filter.field];
|
||||
|
||||
|
|
@ -734,7 +736,8 @@ export async function getPropertiesCount(
|
|||
${newApproachJoins}
|
||||
${propertyTypeOverrideJoin}
|
||||
${builtFormOverrideJoin}
|
||||
${constructionAgeOverrideJoin}`
|
||||
${constructionAgeOverrideJoin}
|
||||
${mainFuelOverrideJoin}`
|
||||
: sql``;
|
||||
const planJoin = needsPlanJoin
|
||||
? sql`LEFT JOIN LATERAL (
|
||||
|
|
@ -816,7 +819,9 @@ export async function getProperties(
|
|||
${isExpiredSql(sql`epc`)} AS "epcIsExpired",
|
||||
${totalFloorAreaSql(sql`epc`)} AS "totalFloorArea",
|
||||
${carbonSql(sql`epc`)} AS "co2Emissions",
|
||||
${mainfuelSql(sql`epc`)} AS mainfuel,
|
||||
-- Main Fuel: override → EPC-derived → 'Unknown'. The new EPC graph has no
|
||||
-- main fuel, so overrides are the only new-approach source (ADR-0012).
|
||||
${resolvedMainFuelSql(sql`epc`)} AS mainfuel,
|
||||
p.lexiscore AS lexiscore
|
||||
FROM property p
|
||||
-- LATERAL one-row lookups keep this query at "N properties × index probe"
|
||||
|
|
@ -855,6 +860,7 @@ export async function getProperties(
|
|||
${propertyTypeOverrideJoin}
|
||||
${builtFormOverrideJoin}
|
||||
${constructionAgeOverrideJoin}
|
||||
${mainFuelOverrideJoin}
|
||||
WHERE p.portfolio_id = ${portfolioId}
|
||||
-- Unmatched (no-UPRN) properties live in the "Needs attention" tab until
|
||||
-- resolved, so they're excluded from the main table.
|
||||
|
|
|
|||
|
|
@ -119,19 +119,24 @@ export const YEAR_BUILT_OPTIONS: EnumOption[] = [
|
|||
"2007–2011", "2012–2022", "2023 onwards", "Unknown",
|
||||
].map((label) => ({ label, dbValues: [label] }));
|
||||
|
||||
// dbValues fold two vocabularies onto one user-facing label: the raw EPC
|
||||
// mainfuel strings (legacy) AND the landlord main_fuel override values
|
||||
// ("mains gas", "LPG (bulk)", … — see MainFuelValues). resolvedMainFuelSql emits
|
||||
// whichever applies; both must resolve to the same chip (ADR-0012). A
|
||||
// descriptorResolution.test asserts every override value is covered here.
|
||||
export const MAINFUEL_OPTIONS: EnumOption[] = [
|
||||
{ label: "Mains Gas", dbValues: ["Gas mains gas", "Mains gas community", "Mains gas not community", "Mains gas this is for backwards compatibility only and should not be used"] },
|
||||
{ label: "LPG", dbValues: ["Bottled lpg", "Lpg community", "Lpg not community", "Lpg this is for backwards compatibility only and should not be used"] },
|
||||
{ label: "Oil", dbValues: ["Oil heating oil", "Oil community", "Oil not community", "Oil this is for backwards compatibility only and should not be used"] },
|
||||
{ label: "Electricity", dbValues: ["Electricity electricity unspecified tariff", "Electricity community", "Electricity not community", "Electricity this is for backwards compatibility only and should not be used"] },
|
||||
{ label: "Biomass", dbValues: ["Bulk wood pellets", "Wood chips", "Wood logs", "Biomass community", "Biomass this is for backwards compatibility only and should not be used"] },
|
||||
{ label: "Coal", dbValues: ["Anthracite", "House coal not community", "House coal this is for backwards compatibility only and should not be used", "Smokeless coal", "Coal community"] },
|
||||
{ label: "Dual Fuel (Mineral + Wood)", dbValues: ["Dual fuel mineral wood"] },
|
||||
{ label: "Mains Gas", dbValues: ["Gas mains gas", "Mains gas community", "Mains gas not community", "Mains gas this is for backwards compatibility only and should not be used", "mains gas", "mains gas (community)"] },
|
||||
{ label: "LPG", dbValues: ["Bottled lpg", "Lpg community", "Lpg not community", "Lpg this is for backwards compatibility only and should not be used", "LPG (bulk)", "bottled LPG", "LPG special condition"] },
|
||||
{ label: "Oil", dbValues: ["Oil heating oil", "Oil community", "Oil not community", "Oil this is for backwards compatibility only and should not be used", "oil"] },
|
||||
{ label: "Electricity", dbValues: ["Electricity electricity unspecified tariff", "Electricity community", "Electricity not community", "Electricity this is for backwards compatibility only and should not be used", "electricity", "electricity (community)"] },
|
||||
{ label: "Biomass", dbValues: ["Bulk wood pellets", "Wood chips", "Wood logs", "Biomass community", "Biomass this is for backwards compatibility only and should not be used", "biomass (community)", "wood logs"] },
|
||||
{ label: "Coal", dbValues: ["Anthracite", "House coal not community", "House coal this is for backwards compatibility only and should not be used", "Smokeless coal", "Coal community", "house coal", "smokeless coal"] },
|
||||
{ label: "Dual Fuel (Mineral + Wood)", dbValues: ["Dual fuel mineral wood", "dual fuel (mineral and wood)"] },
|
||||
{ label: "Biogas", dbValues: ["Biogas not community"] },
|
||||
{ label: "Biodiesel", dbValues: ["Heat from boilers using biodiesel from any biomass source community"] },
|
||||
{ label: "B30d (Biodiesel blend)", dbValues: ["B30d community"] },
|
||||
{ label: "B30k (Biodiesel blend)", dbValues: ["B30k not community"] },
|
||||
{ label: "Community Heat Network", dbValues: ["From heat network data community"] },
|
||||
{ label: "No Heating System", dbValues: ["To be used only when there is no heatinghotwater system", "To be used only when there is no heatinghotwater system or data is from a community network"] },
|
||||
{ label: "Unknown / No Data", dbValues: ["UNKNOWN", "NO DATA!"] },
|
||||
{ label: "Unknown / No Data", dbValues: ["UNKNOWN", "NO DATA!", "Unknown"] },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
resolveBuiltForm,
|
||||
resolveConstructionAge,
|
||||
resolveMainFuel,
|
||||
resolvePropertyType,
|
||||
} from "./descriptorResolution";
|
||||
import { MAINFUEL_OPTIONS } from "@/app/utils/propertyFilters";
|
||||
import { MainFuelValues } from "@/app/db/schema/landlord_overrides";
|
||||
|
||||
describe("resolvePropertyType", () => {
|
||||
it("prefers a landlord override over the EPC-derived value", () => {
|
||||
|
|
@ -279,6 +282,55 @@ describe("resolveConstructionAge", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("resolveMainFuel", () => {
|
||||
it("prefers a landlord override (the only main-fuel source for new-approach)", () => {
|
||||
expect(
|
||||
resolveMainFuel({
|
||||
override: "mains gas",
|
||||
isNewApproach: true,
|
||||
legacyMainFuel: null,
|
||||
}),
|
||||
).toBe("mains gas");
|
||||
});
|
||||
|
||||
it("has no EPC main fuel for new-approach, so resolves to 'Unknown' without an override", () => {
|
||||
expect(
|
||||
resolveMainFuel({
|
||||
override: null,
|
||||
isNewApproach: true,
|
||||
legacyMainFuel: "Gas mains gas", // legacy value must NOT surface for new-approach
|
||||
}),
|
||||
).toBe("Unknown");
|
||||
});
|
||||
|
||||
it("reads the legacy EPC main fuel for a legacy property", () => {
|
||||
expect(
|
||||
resolveMainFuel({
|
||||
override: null,
|
||||
isNewApproach: false,
|
||||
legacyMainFuel: "Gas mains gas",
|
||||
}),
|
||||
).toBe("Gas mains gas");
|
||||
});
|
||||
|
||||
it("resolves to 'Unknown' when nothing is available (never null)", () => {
|
||||
expect(
|
||||
resolveMainFuel({ override: null, isNewApproach: false, legacyMainFuel: null }),
|
||||
).toBe("Unknown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MAINFUEL_OPTIONS coverage", () => {
|
||||
// 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", () => {
|
||||
const covered = new Set(MAINFUEL_OPTIONS.flatMap((o) => o.dbValues));
|
||||
for (const value of MainFuelValues) {
|
||||
expect(covered).toContain(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** Legacy-property helper: only year_built is set, cutoff is false. */
|
||||
function legacyAge(year: string): string {
|
||||
return resolveConstructionAge({
|
||||
|
|
|
|||
|
|
@ -113,6 +113,26 @@ function bandLetterToLabel(letter: string): string {
|
|||
* a year below the earliest start is pre-1900 → band A. Mirrors the legacy branch
|
||||
* of resolvedConstructionAgeSql.
|
||||
*/
|
||||
export interface MainFuelInputs {
|
||||
/** property_overrides.override_value for component 'main_fuel', building_part 0. */
|
||||
override: string | null;
|
||||
isNewApproach: boolean;
|
||||
/** property_details_epc.mainfuel — the raw legacy EPC string. */
|
||||
legacyMainFuel: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main fuel raw value (folded to a label by the cell's resolveEnumLabel). The
|
||||
* new EPC graph exposes no main-fuel string (documented gap in mainfuelSql), so
|
||||
* for new-approach properties the landlord override is the only source; legacy
|
||||
* reads the EPC string. Never null → 'Unknown'. Mirrors resolvedMainFuelSql.
|
||||
*/
|
||||
export function resolveMainFuel(i: MainFuelInputs): string {
|
||||
if (i.override) return i.override;
|
||||
const derived = i.isNewApproach ? null : i.legacyMainFuel;
|
||||
return derived ?? "Unknown";
|
||||
}
|
||||
|
||||
function bucketLegacyYearToBand(yearBuilt: string | null): string | null {
|
||||
if (yearBuilt == null || !/^[0-9]+$/.test(yearBuilt)) return null;
|
||||
const year = parseInt(yearBuilt, 10);
|
||||
|
|
|
|||
|
|
@ -319,6 +319,23 @@ export const builtFormOverrideJoin = sql`
|
|||
/** built_form: override → EPC-derived → 'Unknown'. Mirrors resolveBuiltForm. */
|
||||
export const resolvedBuiltFormSql = sql`COALESCE(pobf.override_value, ${builtFormTypeSql}, 'Unknown')`;
|
||||
|
||||
/** LEFT JOIN exposing the main_fuel override snapshot (alias `pmf`). */
|
||||
export const mainFuelOverrideJoin = sql`
|
||||
LEFT JOIN property_overrides pmf
|
||||
ON pmf.property_id = p.id
|
||||
AND pmf.override_component = 'main_fuel'
|
||||
AND pmf.building_part = 0
|
||||
`;
|
||||
|
||||
/**
|
||||
* main fuel (raw value; folded to a label by the cell's resolveEnumLabel):
|
||||
* override → EPC-derived → 'Unknown'. The new EPC graph has no main fuel, so
|
||||
* mainfuelSql is NULL for new-approach — the override is the only new-approach
|
||||
* source. Takes the legacy `property_details_epc` alias. Mirrors resolveMainFuel.
|
||||
*/
|
||||
export const resolvedMainFuelSql = (e: Alias) =>
|
||||
sql`COALESCE(pmf.override_value, ${mainfuelSql(e)}, 'Unknown')`;
|
||||
|
||||
/**
|
||||
* The main building part's construction age band for one epc_property alias, as
|
||||
* a correlated scalar subquery — properties can also have extension parts, so a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue