From 65db15d3c7796c7a69786c83eac3422ff44a7238 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 23 Jun 2026 20:15:37 +0000 Subject: [PATCH] fix(plans): show effective (modelling) baseline as plan "current" SAP/EPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plans are scored against the effective baseline on property_baseline_performance, which re-baselining can move away from the lodged EPC shown everywhere else. Using lodged as a plan's "current" misrepresented the uplift — e.g. lodged C/71 vs effective D/64 with a post-retrofit C/69.6 read as a downgrade (71 → 69.6) instead of the real D → C improvement. Add resolveEffectiveHeadline + getPlanPropertyMeta and switch both plan pages to it. Legacy properties and backfill gaps fall back to the lodged/row values. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[propertyId]/plans/[planId]/page.tsx | 4 +-- .../[propertyId]/plans/page.tsx | 4 +-- .../building-passport/[propertyId]/utils.ts | 22 ++++++++++++++ src/lib/services/epcSources.ts | 30 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx index 320f6685..c3cc05d2 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx @@ -1,6 +1,6 @@ import RecommendationContainer from "@/app/components/building-passport/RecommendationContainer"; import { - getPropertyMeta, + getPlanPropertyMeta, getRecommendations, getPlanMeta, getInstalledMeasuresByUprn, @@ -59,7 +59,7 @@ export default async function PlanDetail(props: { params: Promise<{ slug: string; propertyId: string; planId: string }>; }) { const params = await props.params; - const propertyMeta = await getPropertyMeta(params.propertyId); + const propertyMeta = await getPlanPropertyMeta(params.propertyId); const [recommendations, planMeta, installedMeasures, scenarioData] = await Promise.all([ getRecommendations(params.planId), diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx index 735c5638..47a1d5ea 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx @@ -1,4 +1,4 @@ -import { getPlans, getPropertyMeta } from "../utils"; +import { getPlans, getPlanPropertyMeta } from "../utils"; import { sapToEpc } from "@/app/utils"; import PlanCard from "./PlanCard"; import PlanHeroCard from "./PlanHeroCard"; @@ -9,7 +9,7 @@ export default async function RecommendationPlans(props: { }) { const params = await props.params; const [propertyMeta, plans] = await Promise.all([ - getPropertyMeta(params.propertyId), + getPlanPropertyMeta(params.propertyId), getPlans(params.propertyId), ]); diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 89875a9b..022d76a9 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -24,6 +24,7 @@ import { ConditionReport, resolveConditionReport, resolvePropertyMeta, + resolveEffectiveHeadline, } from "@/lib/services/epcSources"; import { getRating, serializeBigInt } from "@/app/utils"; import { eq, desc, and } from "drizzle-orm"; @@ -254,6 +255,27 @@ export async function getPropertyMeta( return JSON.parse(JSON.stringify(meta, serializeBigInt)) as PropertyMeta; } +/** + * Property meta for the plan pages. Identical to `getPropertyMeta` except the + * "current" SAP/EPC is the effective (modelling) baseline the plans were scored + * against, not the lodged EPC — see `resolveEffectiveHeadline`. Legacy + * properties and backfill gaps fall back to the lodged/row values. + */ +export async function getPlanPropertyMeta( + propertyId: string +): Promise { + const meta = await getPropertyMeta(propertyId); + const effective = await resolveEffectiveHeadline( + BigInt(propertyId), + meta.updatedAt + ); + return { + ...meta, + currentSapPoints: effective.currentSapPoints ?? meta.currentSapPoints, + currentEpcRating: effective.currentEpcRating ?? meta.currentEpcRating, + }; +} + export async function getConditionReport( propertyId: string ): Promise { diff --git a/src/lib/services/epcSources.ts b/src/lib/services/epcSources.ts index 62db4a70..42d50410 100644 --- a/src/lib/services/epcSources.ts +++ b/src/lib/services/epcSources.ts @@ -428,6 +428,36 @@ export async function resolvePropertyHeadline( }; } +/** + * Headline SAP score + EPC band for the *plan* pages. Plans are scored against + * the modelling baseline (`effective_*` on property_baseline_performance), which + * re-baselining can move away from the lodged (gov-registry) EPC we show + * everywhere else. Using the lodged figure as a plan's "current" would + * misrepresent the uplift — e.g. lodged C/71 vs an effective baseline of D/64 + * with a post-retrofit C/69.6 reads as a *downgrade* (71 → 69.6) instead of the + * real D → C improvement. So plan pages take the effective baseline as "current". + * + * Returns nulls for legacy properties (no effective/lodged split — keep the + * property-row values) and when no baseline row exists yet (backfill gap — fall + * back to the lodged headline). + */ +export async function resolveEffectiveHeadline( + 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: { effectiveSapScore: true, effectiveEpcBand: true }, + where: eq(propertyBaselinePerformance.propertyId, propertyId), + }); + return { + currentSapPoints: baseline?.effectiveSapScore ?? null, + currentEpcRating: baseline?.effectiveEpcBand ?? null, + }; +} + /** The minimal EPC meta the `/api/property-meta` route embeds as `detailsEpc`. */ export interface DetailsEpcMeta { currentEnergyDemand: number | null;