fix(reporting): interim guard — don't count already-at-target homes as upgraded

The modelling engine emits costed plans for homes already at a scenario's
target EPC band (e.g. ~£300–600 of low-energy lighting / system tune-ups
on effective-C homes in a "Reach EPC C" scenario). Counting them inflated
"Homes upgraded" and its costs, and stopped the KPI reconciling with the
below-target band distribution (portfolio 796 / scenario 1268: 9492
upgraded vs 9039 homes below C).

Add an interim guard to the scenario metrics upgrade aggregate (count +
cost): for Increasing-EPC scenarios, drop any home whose effective band
already meets the target (effective band <= goal_value; lexical, A best).
Unknown/NULL effective band is kept. Verified against the DB: scenario
1268 goes 9492 → 8871 upgraded, −£282k of works on compliant homes.

This is a reporting-only band-aid; the root cause is in the engine —
tracked in Hestia-Homes/Model#1652. The default/recommended view has no
single target band, so the guard doesn't apply there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-20 09:30:53 +01:00
parent 32da5bf079
commit 41b89c0d8f

View file

@ -7,6 +7,7 @@ import {
carbonSql,
billsSql,
effectiveSapSql,
effectiveEpcBandSql,
isNewApproachSql,
lodgedSapSql,
} from "@/lib/services/epcSources";
@ -199,6 +200,30 @@ export async function GET(
? EPC_MIN_SAP[scenario.goal_value]
: null;
// The scenario's target band, for Increasing-EPC scenarios only (other goals
// have no band target). Used by the interim already-at-target guard below.
const targetBand =
scenario.goal === "Increasing EPC" ? scenario.goal_value : null;
/**
* INTERIM guard for a backend bug (Hestia-Homes/Model#1652): the modelling engine
* emits costed plans for homes that are ALREADY at the scenario's target band
* e.g. ~£300600 of low-energy lighting / system tune-ups on effective-C
* homes in a "Reach EPC C" scenario. Those aren't upgrades toward the target,
* so counting them inflates "Homes upgraded" (and its costs) and stops the KPI
* reconciling with the below-target band distribution. Until the engine stops
* recommending works on already-compliant homes, exclude any home whose
* EFFECTIVE band already meets the target from the upgrade aggregate. Bands
* compare lexically (A best), so "still needs work" = effective band > target.
* Unknown/NULL effective band is kept (can't prove compliant). Only bites for
* Increasing-EPC scenarios; NULL targetBand no-op.
*/
const stillNeedsUpgradeSql = sql`(
${targetBand}::text IS NULL
OR (${effectiveEpcBandSql}) IS NULL
OR (${effectiveEpcBandSql})::text > ${targetBand}::text
)`;
/* ----------------------------------------------------------
The three data queries below are independent they only need `minSap`
(from the scenario definition above), not each other's results so they
@ -307,6 +332,9 @@ export async function GET(
-- (target-level post_sap on already-compliant homes) not real upgrades.
-- COALESCE keeps rows we can't compare. See ADR-0002.
AND COALESCE(lp.post_sap_points >= (${effectiveSapSql}), true)
-- INTERIM: drop homes already at the scenario target band (backend bug
-- see stillNeedsUpgradeSql above, Hestia-Homes/Model#1652).
AND ${stillNeedsUpgradeSql}
AND (
${useLodgedBaseline} = false
OR ${minSap}::float IS NULL