From fec714a4064a52bd99af12a308dde6834ff779ae Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 17 Jun 2026 14:40:17 +0000 Subject: [PATCH] fix(property-meta): resolve in-process, drop self-fetch to process.env.URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPropertyMeta self-fetched process.env.URL + /api/property-meta. That URL can resolve to a different deployment (e.g. production), so the building-passport pages were reading property meta from OLD code — the backfilled headline SAP/EPC never arrived and the current rating rendered blank, regardless of caching. Extract resolvePropertyMeta() and call it directly from both the route and getPropertyMeta, so the page runs identical in-process logic. JSON round-trip through serializeBigInt preserves the previous shape (bigints/dates as strings). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/property-meta/[propertyId]/route.ts | 60 +++---------------- .../building-passport/[propertyId]/utils.ts | 24 ++++---- src/lib/services/epcSources.ts | 51 ++++++++++++++++ 3 files changed, 69 insertions(+), 66 deletions(-) diff --git a/src/app/api/property-meta/[propertyId]/route.ts b/src/app/api/property-meta/[propertyId]/route.ts index 73281f6b..84ccc2c9 100644 --- a/src/app/api/property-meta/[propertyId]/route.ts +++ b/src/app/api/property-meta/[propertyId]/route.ts @@ -1,66 +1,20 @@ -import { db } from "@/app/db/db"; -import { property } from "@/app/db/schema/property"; import { serializeBigInt } from "@/app/utils"; -import { - resolveDetailsEpcMeta, - resolvePropertyHeadline, -} from "@/lib/services/epcSources"; -import { eq } from "drizzle-orm"; +import { resolvePropertyMeta } from "@/lib/services/epcSources"; import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest, props: { params: Promise<{ propertyId: string }> }) { const params = await props.params; const propertyId = params.propertyId; - const propertyMeta = await db.query.property.findFirst({ - columns: { - id: true, - uprn: true, - address: true, - postcode: true, - hasPreConditionReport: true, - hasRecommendations: true, - createdAt: true, - propertyType: true, - builtForm: true, - localAuthority: true, - constituency: true, - numberOfRooms: true, - yearBuilt: true, - tenure: true, - currentEpcRating: true, - currentSapPoints: true, - originalSapPoints: true, - updatedAt: true, - currentValuation: true, - }, - where: eq(property.id, BigInt(propertyId)), - }); + // `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 propertyMeta = await resolvePropertyMeta(propertyId); if (!propertyMeta) { return new NextResponse(null, { status: 404 }); } - // `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, - currentSapPoints: - headline.currentSapPoints ?? propertyMeta.currentSapPoints, - currentEpcRating: - headline.currentEpcRating ?? propertyMeta.currentEpcRating, - detailsEpc, - }, - serializeBigInt, - ), - ); + return new NextResponse(JSON.stringify(propertyMeta, serializeBigInt)); } diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts index 6728e09b..c2533cea 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/utils.ts @@ -23,8 +23,9 @@ import { import { ConditionReport, resolveConditionReport, + resolvePropertyMeta, } from "@/lib/services/epcSources"; -import { getRating } from "@/app/utils"; +import { getRating, serializeBigInt } from "@/app/utils"; import { eq, desc, and } from "drizzle-orm"; import { energyAssessment, @@ -240,20 +241,17 @@ export async function getPlans(propertyId: string): Promise { export async function getPropertyMeta( propertyId: string ): Promise { - const url = process.env.URL + `/api/property-meta/${propertyId}`; - - // Don't cache: property meta (incl. headline SAP/EPC, which is now backfilled - // from the baseline table) changes as the modelling pipeline writes data, and - // a stale Data Cache entry was causing the current rating to render blank. - const response = await fetch(url, { - method: "GET", - headers: { "Content-Type": "application/json" }, - cache: "no-store", - }); - if (!response.ok) { + // Resolve in-process instead of self-fetching /api/property-meta. The HTTP + // round-trip went to process.env.URL, which can point at a different + // deployment (e.g. production) and was serving stale/old-code responses — so + // the backfilled headline SAP/EPC never reached the page. We JSON round-trip + // through serializeBigInt to keep the exact shape the fetch used to return + // (bigints as strings, dates as ISO strings). + const meta = await resolvePropertyMeta(propertyId); + if (!meta) { throw new Error("Network response was not ok"); } - return response.json(); + return JSON.parse(JSON.stringify(meta, serializeBigInt)) as PropertyMeta; } export async function getConditionReport( diff --git a/src/lib/services/epcSources.ts b/src/lib/services/epcSources.ts index efec6984..0b376957 100644 --- a/src/lib/services/epcSources.ts +++ b/src/lib/services/epcSources.ts @@ -464,3 +464,54 @@ export async function resolveDetailsEpcMeta( estimated: legacy?.estimated ?? false, }; } + +/** + * Resolve the full property-meta object (the property row + branched + * `detailsEpc` + headline SAP/EPC). Shared by the `/api/property-meta` route and + * the building-passport `getPropertyMeta` so both run identical, in-process + * logic — no self-fetch to `process.env.URL` (which could hit a different + * deployment) and no Data Cache. Returns null when the property doesn't exist. + * BigInt fields are left raw; the caller serialises with `serializeBigInt`. + */ +export async function resolvePropertyMeta(propertyId: string) { + const propertyMeta = await db.query.property.findFirst({ + columns: { + id: true, + uprn: true, + address: true, + postcode: true, + hasPreConditionReport: true, + hasRecommendations: true, + createdAt: true, + propertyType: true, + builtForm: true, + localAuthority: true, + constituency: true, + numberOfRooms: true, + yearBuilt: true, + tenure: true, + currentEpcRating: true, + currentSapPoints: true, + originalSapPoints: true, + updatedAt: true, + currentValuation: true, + }, + where: eq(property.id, BigInt(propertyId)), + }); + + if (!propertyMeta) return null; + + const [detailsEpc, headline] = await Promise.all([ + resolveDetailsEpcMeta(propertyMeta.id, propertyMeta.updatedAt), + resolvePropertyHeadline(propertyMeta.id, propertyMeta.updatedAt), + ]); + + return { + ...propertyMeta, + currentSapPoints: + headline.currentSapPoints ?? propertyMeta.currentSapPoints, + currentEpcRating: + headline.currentEpcRating ?? propertyMeta.currentEpcRating, + detailsEpc, + }; +}