fix(property-meta): resolve in-process, drop self-fetch to process.env.URL

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-17 14:40:17 +00:00
parent e1d56cc773
commit fec714a406
3 changed files with 69 additions and 66 deletions

View file

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

View file

@ -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<PlanRelation[]> {
export async function getPropertyMeta(
propertyId: string
): Promise<PropertyMeta> {
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(

View file

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