fix(reporting): read scenario measures via denormalised recommendation.plan_id

The scenario measures modal came up empty because both measures routes
gated on EXISTS against plan_recommendations — a retired join table with
no rows for new-approach plans (25.7M legacy rows, none for e.g.
portfolio 812's plans), so the query returned zero measures.

Read the denormalised model instead: drive from the latest plan per
property (default or scenario) and JOIN recommendation by the indexed
property_id, scoped to the plan via recommendation.plan_id. Portfolio 812
default now returns 5 measures (solar_pv 54 homes/£266k, …) where it
returned 0. Also removes the stale commented query block that referenced
the retired plan_recommendations / recommendation_materials tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 11:42:19 +00:00
parent eef9fdc171
commit 2f0f937fb6
2 changed files with 34 additions and 88 deletions

View file

@ -20,62 +20,11 @@ export async function GET(
const pid = BigInt(portfolioId);
const sid = BigInt(scenarioId);
// TEMP: Remove batteries as underspecified
// const result = await db.execute(sql`
// WITH latest_plans AS (
// SELECT DISTINCT ON (property_id)
// *
// FROM plan
// WHERE portfolio_id = ${pid}
// AND scenario_id = ${sid}
// ORDER BY property_id, created_at DESC
// ),
// recommendation_flags AS (
// SELECT
// r.id AS recommendation_id,
// r.measure_type AS measure_type,
// r.property_id AS property_id,
// r.estimated_cost AS estimated_cost,
// BOOL_OR(m.includes_battery) AS includes_battery
// FROM latest_plans lp
// JOIN plan_recommendations pr
// ON pr.plan_id = lp.id
// JOIN recommendation r
// ON r.id = pr.recommendation_id
// LEFT JOIN recommendation_materials rm
// ON rm.recommendation_id = r.id
// LEFT JOIN material m
// ON m.id = rm.material_id
// AND m.is_active = true
// WHERE r.default = true
// AND r.already_installed = false
// GROUP BY
// r.id,
// r.measure_type,
// r.property_id,
// r.estimated_cost
// )
// SELECT
// measure_type,
// COALESCE(includes_battery, false) AS includes_battery,
// COUNT(DISTINCT property_id)::int AS homes_count,
// SUM(estimated_cost)::float AS total_cost,
// AVG(estimated_cost)::float AS average_cost
// FROM recommendation_flags
// GROUP BY
// measure_type,
// includes_battery
// ORDER BY total_cost DESC;
// `);
// Latest plan per property for this scenario, then its recommendations read
// through the DENORMALISED link: join by the indexed recommendation.property_id
// and scope to the plan via recommendation.plan_id. The plan_recommendations
// join table is retired (no rows for new-approach plans), so the old EXISTS
// against it returned zero measures. See the handover / ADR notes.
const result = await db.execute(sql`
SELECT
r.measure_type,
@ -83,23 +32,19 @@ export async function GET(
COUNT(DISTINCT r.property_id)::int AS homes_count,
SUM(r.estimated_cost)::float AS total_cost,
AVG(r.estimated_cost)::float AS average_cost
FROM recommendation r
WHERE r.default = true
FROM (
SELECT DISTINCT ON (property_id)
id, property_id
FROM plan
WHERE portfolio_id = ${pid}
AND scenario_id = ${sid}
ORDER BY property_id, created_at DESC
) lp
JOIN recommendation r
ON r.property_id = lp.property_id
AND r.plan_id = lp.id
AND r.default = true
AND r.already_installed = false
AND EXISTS (
SELECT 1
FROM (
SELECT DISTINCT ON (p.property_id)
p.id
FROM plan p
WHERE p.portfolio_id = ${pid}
AND p.scenario_id = ${sid}
ORDER BY p.property_id, p.created_at DESC
) lp
JOIN plan_recommendations pr
ON pr.plan_id = lp.id
WHERE pr.recommendation_id = r.id
)
GROUP BY
r.measure_type,
r.type

View file

@ -19,6 +19,11 @@ export async function GET(
const pid = BigInt(portfolioId);
// Latest default plan per property, then its recommendations read through the
// DENORMALISED link: join by the indexed recommendation.property_id and scope
// to the plan via recommendation.plan_id. The plan_recommendations join table
// is retired (no rows for new-approach plans), so the old EXISTS against it
// returned zero measures.
const result = await db.execute(sql`
SELECT
r.measure_type,
@ -26,23 +31,19 @@ export async function GET(
COUNT(DISTINCT r.property_id)::int AS homes_count,
SUM(r.estimated_cost)::float AS total_cost,
AVG(r.estimated_cost)::float AS average_cost
FROM recommendation r
WHERE r.default = true
FROM (
SELECT DISTINCT ON (property_id)
id, property_id
FROM plan
WHERE portfolio_id = ${pid}
AND is_default = true
ORDER BY property_id, created_at DESC
) lp
JOIN recommendation r
ON r.property_id = lp.property_id
AND r.plan_id = lp.id
AND r.default = true
AND r.already_installed = false
AND EXISTS (
SELECT 1
FROM (
SELECT DISTINCT ON (p.property_id)
p.id
FROM plan p
WHERE p.portfolio_id = ${pid}
AND p.is_default = true
ORDER BY p.property_id, p.created_at DESC
) lp
JOIN plan_recommendations pr
ON pr.plan_id = lp.id
WHERE pr.recommendation_id = r.id
)
GROUP BY
r.measure_type,
r.type