From 5d87481b6bddcdac450591c426ec4d98a4e253da Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 17 Jun 2026 14:20:49 +0000 Subject: [PATCH] fix(plans): source headline SAP/EPC from baseline for new properties The plans page computed the plan uplift as postSapPoints - currentSapPoints, but current_sap_points is unwritten on the property row for new-approach properties (null -> 0), so the uplift showed the full post score (e.g. +71.39) and no current rating. property-meta now backfills currentSapPoints / currentEpcRating from property_baseline_performance (lodged_sap_score / lodged_epc_band) for new-approach properties, fixing every propertyMeta consumer (plans page, hero cards, building passport). e.g. 709634: current 60 (D), uplift now +11.39. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/property-meta/[propertyId]/route.ts | 32 +++++++++++++------ src/lib/services/epcSources.ts | 26 +++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/app/api/property-meta/[propertyId]/route.ts b/src/app/api/property-meta/[propertyId]/route.ts index f526544d..73281f6b 100644 --- a/src/app/api/property-meta/[propertyId]/route.ts +++ b/src/app/api/property-meta/[propertyId]/route.ts @@ -1,7 +1,10 @@ import { db } from "@/app/db/db"; import { property } from "@/app/db/schema/property"; import { serializeBigInt } from "@/app/utils"; -import { resolveDetailsEpcMeta } from "@/lib/services/epcSources"; +import { + resolveDetailsEpcMeta, + resolvePropertyHeadline, +} from "@/lib/services/epcSources"; import { eq } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; @@ -38,15 +41,26 @@ export async function GET(request: NextRequest, props: { params: Promise<{ prope return new NextResponse(null, { status: 404 }); } - // `detailsEpc` is resolved via the cutoff: legacy properties read - // property_details_epc; new-approach ones read epc_property + - // property_baseline_performance. See src/lib/services/epcSources.ts. - const detailsEpc = await resolveDetailsEpcMeta( - propertyMeta.id, - propertyMeta.updatedAt, - ); + // `detailsEpc` and the headline SAP/EPC are resolved via the cutoff: legacy + // properties read property_details_epc / the property row; new-approach ones + // read property_baseline_performance (the property-row headline columns aren't + // written yet). See src/lib/services/epcSources.ts. + const [detailsEpc, headline] = await Promise.all([ + resolveDetailsEpcMeta(propertyMeta.id, propertyMeta.updatedAt), + resolvePropertyHeadline(propertyMeta.id, propertyMeta.updatedAt), + ]); return new NextResponse( - JSON.stringify({ ...propertyMeta, detailsEpc }, serializeBigInt), + JSON.stringify( + { + ...propertyMeta, + currentSapPoints: + headline.currentSapPoints ?? propertyMeta.currentSapPoints, + currentEpcRating: + headline.currentEpcRating ?? propertyMeta.currentEpcRating, + detailsEpc, + }, + serializeBigInt, + ), ); } diff --git a/src/lib/services/epcSources.ts b/src/lib/services/epcSources.ts index c7377f34..efec6984 100644 --- a/src/lib/services/epcSources.ts +++ b/src/lib/services/epcSources.ts @@ -381,6 +381,32 @@ export async function resolveConditionReport( return legacyConditionReport((legacy ?? {}) as Record); } +/** + * Headline SAP score + EPC band for the property meta. New-approach properties + * don't have `current_sap_points` / `current_epc_rating` written on the row yet, + * so we fall back to the lodged baseline (property_baseline_performance). Returns + * nulls for legacy properties, signalling the caller to keep the row values. + * + * Uses lodged_* (gov-registry actual) to match the rest of the EPC reads; swap to + * effective_* for the modelling-adjusted baseline. + */ +export async function resolvePropertyHeadline( + propertyId: bigint, + updatedAt: Date | string | null | undefined, +): Promise<{ currentSapPoints: number | null; currentEpcRating: string | null }> { + if (!isNewApproach(updatedAt)) { + return { currentSapPoints: null, currentEpcRating: null }; + } + const baseline = await db.query.propertyBaselinePerformance.findFirst({ + columns: { lodgedSapScore: true, lodgedEpcBand: true }, + where: eq(propertyBaselinePerformance.propertyId, propertyId), + }); + return { + currentSapPoints: baseline?.lodgedSapScore ?? null, + currentEpcRating: baseline?.lodgedEpcBand ?? null, + }; +} + /** The minimal EPC meta the `/api/property-meta` route embeds as `detailsEpc`. */ export interface DetailsEpcMeta { currentEnergyDemand: number | null;