fix(plans): show effective (modelling) baseline as plan "current" SAP/EPC

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-23 20:15:37 +00:00
parent a89e6bfe30
commit 65db15d3c7
4 changed files with 56 additions and 4 deletions

View file

@ -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),

View file

@ -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),
]);

View file

@ -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<PropertyMeta> {
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<ConditionReport> {

View file

@ -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;