mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
fix(reporting): temp guards for sub-baseline scenario post_sap
The modelling writes a target-level post_sap (e.g. ~C) even for homes already above the scenario target, so plans can carry cost while modelling post_sap BELOW the effective baseline. That skewed three reporting surfaces. Three TEMP (demo) guards, all keyed on the effective baseline (ADR-0002); revert once the Model team fixes the sub-baseline plans: 1. EPC band chart: post-scenario SAP clamped to GREATEST(baseline, post) so already-compliant properties aren't shown "improving" down a band (portfolio 796: EPC B 4,244 -> 1,660 wrongly, now 4,479). 2. n_units_upgraded + cost: exclude plans whose post_sap is below the effective baseline (not real upgrades) -- 796: 10,283 -> 9,765, -£1.28M. 3. total_sap_uplift / £-per-SAP: baseline is the effective SAP, not the null-for-new-approach current_sap_points, and counts genuine gains only -- uplift 0 -> 89,724, so £/SAP £0 -> £536. Also fixes the no-plan branch to use the effective baseline instead of the null current_sap_points (Unknown-band leakage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bb2b093e5
commit
7de48448c0
2 changed files with 60 additions and 12 deletions
|
|
@ -3,7 +3,12 @@ import { sql } from "drizzle-orm";
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sapToEpc } from "@/app/utils";
|
||||
import type { PortfolioGoalType } from "@/app/db/schema/portfolio";
|
||||
import { newApproachJoins, carbonSql, billsSql } from "@/lib/services/epcSources";
|
||||
import {
|
||||
newApproachJoins,
|
||||
carbonSql,
|
||||
billsSql,
|
||||
effectiveSapSql,
|
||||
} from "@/lib/services/epcSources";
|
||||
|
||||
/* =======================
|
||||
Types
|
||||
|
|
@ -125,14 +130,18 @@ export async function GET(
|
|||
SUM(post_energy_bill)::float AS total_bills,
|
||||
SUM(
|
||||
CASE
|
||||
-- TEMP (demo): baseline is the effective SAP, not p.current_sap_points
|
||||
-- (NULL for new-approach → uplift summed to 0 → £/SAP showed 0). Count
|
||||
-- genuine gains only (post > baseline); sub-baseline plans excluded.
|
||||
WHEN cost_of_works > 0
|
||||
AND post_sap_points IS NOT NULL
|
||||
THEN post_sap_points - p.current_sap_points
|
||||
AND post_sap_points > (${effectiveSapSql})
|
||||
THEN post_sap_points - (${effectiveSapSql})
|
||||
ELSE 0
|
||||
END
|
||||
)::float AS total_sap_uplift
|
||||
FROM latest_plans lp
|
||||
JOIN property p ON p.id = lp.property_id
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
|
||||
-- Conditional filter: only restrict by original_sap_points when the toggle is on
|
||||
-- AND the scenario has an EPC target. Written as an OR chain so Postgres evaluates
|
||||
-- it as a single WHERE clause — avoiding the need to dynamically build the query
|
||||
|
|
@ -177,8 +186,13 @@ export async function GET(
|
|||
)::float AS total_funding
|
||||
FROM latest_plans lp
|
||||
JOIN property p ON p.id = lp.property_id
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
|
||||
LEFT JOIN funding_package fp ON fp.plan_id = lp.id
|
||||
WHERE lp.cost_of_works > 0
|
||||
-- TEMP (demo): exclude plans whose post SAP is below the effective baseline
|
||||
-- (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)
|
||||
AND (
|
||||
${useOriginalBaseline} = false
|
||||
OR ${minSap}::float IS NULL
|
||||
|
|
@ -257,10 +271,18 @@ export async function GET(
|
|||
const epcRows = await db.execute(sql`
|
||||
SELECT
|
||||
CASE
|
||||
WHEN lp.id IS NOT NULL THEN lp.post_sap_points
|
||||
ELSE p.current_sap_points
|
||||
-- A retrofit scenario can't make a property worse. The engine writes a
|
||||
-- target-level post_sap (e.g. ~C) even for properties already above the
|
||||
-- target, so post_sap can sit BELOW the baseline — which made the chart
|
||||
-- show e.g. B properties "improving" down to C. Clamp to the baseline.
|
||||
WHEN lp.id IS NOT NULL THEN GREATEST(${effectiveSapSql}, lp.post_sap_points)
|
||||
-- No qualifying plan → unchanged property. Use the effective
|
||||
-- (re-baselined) baseline to match the "before" distribution; NOT
|
||||
-- p.current_sap_points (NULL for new-approach → "Unknown"). See ADR-0002.
|
||||
ELSE ${effectiveSapSql}
|
||||
END AS effective_sap
|
||||
FROM property p
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM plan
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import { sql } from "drizzle-orm";
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sapToEpc } from "@/app/utils";
|
||||
import type { PortfolioGoalType } from "@/app/db/schema/portfolio";
|
||||
import { newApproachJoins, carbonSql, billsSql } from "@/lib/services/epcSources";
|
||||
import {
|
||||
newApproachJoins,
|
||||
carbonSql,
|
||||
billsSql,
|
||||
effectiveSapSql,
|
||||
} from "@/lib/services/epcSources";
|
||||
|
||||
/* =======================
|
||||
Types
|
||||
|
|
@ -91,14 +96,19 @@ export async function GET(
|
|||
SUM(post_energy_bill)::float AS total_bills,
|
||||
SUM(
|
||||
CASE
|
||||
-- TEMP (demo): baseline is the effective SAP, not p.current_sap_points
|
||||
-- (NULL for new-approach → uplift summed to 0 → £/SAP showed 0). Count
|
||||
-- genuine gains only (post > baseline); sub-baseline "downgrade" plans
|
||||
-- are excluded rather than dragging the uplift negative. See ADR-0002.
|
||||
WHEN cost_of_works > 0
|
||||
AND post_sap_points IS NOT NULL
|
||||
THEN post_sap_points - p.current_sap_points
|
||||
AND post_sap_points > (${effectiveSapSql})
|
||||
THEN post_sap_points - (${effectiveSapSql})
|
||||
ELSE 0
|
||||
END
|
||||
)::float AS total_sap_uplift
|
||||
FROM latest_plans lp
|
||||
JOIN property p ON p.id = lp.property_id;
|
||||
JOIN property p ON p.id = lp.property_id
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id;
|
||||
`);
|
||||
|
||||
const scenarioAgg = scenarioMetricsResult.rows[0] as ScenarioAggregates;
|
||||
|
|
@ -124,8 +134,15 @@ export async function GET(
|
|||
COALESCE(fp.total_uplift, 0)
|
||||
)::float AS total_funding
|
||||
FROM latest_plans lp
|
||||
JOIN property p ON p.id = lp.property_id
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
|
||||
LEFT JOIN funding_package fp ON fp.plan_id = lp.id
|
||||
WHERE lp.cost_of_works > 0;
|
||||
WHERE lp.cost_of_works > 0
|
||||
-- TEMP (demo): a plan whose post SAP is below the effective baseline isn't
|
||||
-- a real upgrade (target-level post_sap on already-compliant homes), so
|
||||
-- exclude it from the count + cost. COALESCE keeps rows we can't compare
|
||||
-- (NULL post or NULL baseline). See ADR-0002.
|
||||
AND COALESCE(lp.post_sap_points >= (${effectiveSapSql}), true);
|
||||
`);
|
||||
|
||||
const upgraded = upgradedResult.rows[0] as UpgradedAggregates;
|
||||
|
|
@ -187,10 +204,19 @@ export async function GET(
|
|||
const epcRows = await db.execute(sql`
|
||||
SELECT
|
||||
CASE
|
||||
WHEN lp.id IS NOT NULL THEN lp.post_sap_points
|
||||
ELSE p.current_sap_points
|
||||
-- A retrofit scenario can't make a property worse. The engine writes a
|
||||
-- target-level post_sap (e.g. ~C) even for properties already above the
|
||||
-- target, so post_sap can sit BELOW the baseline — which made the chart
|
||||
-- show e.g. B properties "improving" down to C. Clamp to the baseline so
|
||||
-- the post-scenario band is never worse than before.
|
||||
WHEN lp.id IS NOT NULL THEN GREATEST(${effectiveSapSql}, lp.post_sap_points)
|
||||
-- No plan → unchanged property. Use the effective (re-baselined) baseline
|
||||
-- so this matches the "before" distribution — NOT p.current_sap_points,
|
||||
-- which is NULL for new-approach properties. See ADR-0002.
|
||||
ELSE ${effectiveSapSql}
|
||||
END AS effective_sap
|
||||
FROM property p
|
||||
LEFT JOIN property_baseline_performance bp ON bp.property_id = p.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT *
|
||||
FROM plan
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue