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 c3cc05d2..320f6685 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 { - getPlanPropertyMeta, + getPropertyMeta, 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 getPlanPropertyMeta(params.propertyId); + const propertyMeta = await getPropertyMeta(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 47a1d5ea..735c5638 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, getPlanPropertyMeta } from "../utils"; +import { getPlans, getPropertyMeta } 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([ - getPlanPropertyMeta(params.propertyId), + getPropertyMeta(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 022d76a9..89875a9b 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -24,7 +24,6 @@ import { ConditionReport, resolveConditionReport, resolvePropertyMeta, - resolveEffectiveHeadline, } from "@/lib/services/epcSources"; import { getRating, serializeBigInt } from "@/app/utils"; import { eq, desc, and } from "drizzle-orm"; @@ -255,27 +254,6 @@ 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 42d50410..cbec24b5 100644 --- a/src/lib/services/epcSources.ts +++ b/src/lib/services/epcSources.ts @@ -320,9 +320,11 @@ async function resolveNewConditionReport( // GAP: no energy tariff in the new graph — omitted. energyTariff: null, currentEnergyDemand: sumKwh, + // Effective (re-baselined) baseline so the assessment view agrees with the + // headline SAP/EPC and the plan maths — see resolvePropertyHeadline. primaryEnergyConsumption: - baseline?.lodgedPrimaryEnergyIntensityKwhPerM2Yr ?? null, - co2Emissions: baseline?.lodgedCo2EmissionsTPerYr ?? null, + baseline?.effectivePrimaryEnergyIntensityKwhPerM2Yr ?? null, + co2Emissions: baseline?.effectiveCo2EmissionsTPerYr ?? null, heatingEnergyCostCurrent: baseline?.heatingCostGbp ?? null, hotWaterEnergyCostCurrent: baseline?.hotWaterCostGbp ?? null, lightingEnergyCostCurrent: baseline?.lightingCostGbp ?? null, @@ -405,45 +407,23 @@ export async function resolveConditionReport( /** * 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. + * so we fall back to the modelling 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. + * Uses `effective_*` — the re-baselined figure the modelling (and therefore the + * plans) was scored against. The per-property building-passport views (overview, + * assessment, plans) must agree with the plan maths: re-baselining can move the + * effective score away from the lodged (gov-registry) EPC, so showing lodged as + * "current" misrepresents a plan's uplift — e.g. lodged C/78 vs an effective + * baseline of C/71 with a post-retrofit C/71.4 reads as flat instead of the real + * improvement. The set-based reporting fragments (`sapSql`, `epcBandSql`, + * `carbonSql`, …) deliberately stay on `lodged_*` to keep portfolio/reporting + * totals matched to the gov register. */ 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, - }; -} - -/** - * 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 }; @@ -500,7 +480,7 @@ export async function resolveDetailsEpcMeta( return { currentEnergyDemand, - co2Emissions: baseline?.lodgedCo2EmissionsTPerYr ?? null, + co2Emissions: baseline?.effectiveCo2EmissionsTPerYr ?? null, estimated: !lodged && !!predicted, }; }