From f1132f62056e1116c34cce505b242a585927fb82 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 23 Oct 2025 18:22:31 +0000 Subject: [PATCH] validated postcodes --- src/app/api/postcode/OsPlacesToFlat.ts | 85 +++++++++++++++++-- .../remote-assessment/AddressSearch.tsx | 12 ++- .../RemoteAssessmentClient.tsx | 16 ++-- .../remote-assessment/ScenarioSetup.tsx | 8 ++ .../[slug]/remote-assessment/page.tsx | 1 + 5 files changed, 108 insertions(+), 14 deletions(-) diff --git a/src/app/api/postcode/OsPlacesToFlat.ts b/src/app/api/postcode/OsPlacesToFlat.ts index 0fcf8606..d750f7ef 100644 --- a/src/app/api/postcode/OsPlacesToFlat.ts +++ b/src/app/api/postcode/OsPlacesToFlat.ts @@ -1,5 +1,64 @@ -// lib/osPlaces/mapper.ts -export type FlatAddress = { uprn: string; address: string }; +export type FlatAddress = { + uprn: string; + address: string; + propertyType?: PropertyType; + builtForm?: BuiltForm; +}; + +export type PropertyType = "House" | "Flat" | "Maisonette" | "Bungalow"; +export type BuiltForm = + | "Detached" + | "Semi-Detached" + | "Mid-Terrace" + | "End-Terrace" + | "Enclosed End-Terrace" + | "Enclosed Mid-Terrace"; + +export function mapOsClassToProperty(classification?: string): { + propertyType?: PropertyType; + builtForm?: BuiltForm; +} { + if (!classification) return {}; + + const code = classification.toUpperCase(); + + if (code.startsWith("RD02")) { + return { propertyType: "House", builtForm: "Detached" }; + } + if (code.startsWith("RD03")) { + return { propertyType: "House", builtForm: "Semi-Detached" }; + } + if (code.startsWith("RD04")) { + return { propertyType: "House", builtForm: "Mid-Terrace" }; + } + if (code.startsWith("RD06")) { + return { propertyType: "Flat" }; + } + + return {}; +} + +export function inferPropertyFromAddress(address: string): { + propertyType?: PropertyType; + builtForm?: BuiltForm; +} { + const addr = address.toLowerCase(); + + // Detect explicit mentions + if (addr.includes("bungalow")) return { propertyType: "Bungalow" }; + if (addr.includes("maisonette")) return { propertyType: "Maisonette" }; + if (addr.includes("flat") || addr.includes("apartment")) + return { propertyType: "Flat" }; + + // If it says "terrace" in address, but no RD code, assume terraced house + if (addr.includes("terrace") || addr.includes("terraced")) + return { propertyType: "House", builtForm: "Mid-Terrace" }; + + // Minimal fallback if text only shows 'house' or street number + if (addr.match(/\d+\s+[a-z]/)) return { propertyType: "House" }; + + return {}; +} export function mapOsPlacesToFlat( results?: Array<{ DPA?: any; LPI?: any }> @@ -9,13 +68,25 @@ export function mapOsPlacesToFlat( const items = results .map((r) => r.DPA ?? r.LPI) .filter(Boolean) - .map((rec: any) => ({ - uprn: String(rec.UPRN ?? ""), - address: String(rec.ADDRESS ?? ""), - })) + .map((rec: any) => { + const classification = rec.CLASSIFICATION_CODE as string | undefined; + const base = mapOsClassToProperty(classification); + const textBased = inferPropertyFromAddress(String(rec.ADDRESS ?? "")); + + // Merge — classification has priority, but text fills in missing gaps + const propertyType = base.propertyType ?? textBased.propertyType; + const builtForm = base.builtForm ?? textBased.builtForm; + + return { + uprn: String(rec.UPRN ?? ""), + address: String(rec.ADDRESS ?? ""), + propertyType, + builtForm, + }; + }) .filter((x) => x.uprn && x.address); - // de-dupe by UPRN (prefers first occurrence) + // De-duplicate by UPRN const seen = new Set(); const deduped: FlatAddress[] = []; for (const it of items) { diff --git a/src/app/portfolio/[slug]/remote-assessment/AddressSearch.tsx b/src/app/portfolio/[slug]/remote-assessment/AddressSearch.tsx index 26d44ce6..ef3b8b90 100644 --- a/src/app/portfolio/[slug]/remote-assessment/AddressSearch.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/AddressSearch.tsx @@ -50,12 +50,18 @@ export default function AddressSearch({ if (!validation.data || validation.data.status !== 200) return; + const validatedPostcode = + validation.data.result?.postcode?.toUpperCase() ?? postcode.toUpperCase(); + + // Use the validated postcode for fetching addresses + onPostcodeSelect(validatedPostcode); + setLoadingAddresses(true); setAddressError(null); try { const res = await fetch( - `/api/postcode/${encodeURIComponent(postcode)}/addresses` + `/api/postcode/${encodeURIComponent(validatedPostcode)}/addresses` ); const json = await res.json(); @@ -66,6 +72,8 @@ export default function AddressSearch({ const mapped = json.results.map((r: any) => ({ address: r.address, uprn: r.uprn, + propertyType: r.propertyType, + builtForm: r.builtForm, })); setAddresses(mapped); setShowDropdown(true); @@ -83,7 +91,7 @@ export default function AddressSearch({ const selected = addresses.find((a) => a.address === value) || null; setSelectedAddress(selected); setShowDropdown(false); - onAddressSelect(selected); // ✅ This now matches the correct type + onAddressSelect(selected); } function handleChangeAddress() { diff --git a/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx b/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx index 72ccdd13..8b6b436b 100644 --- a/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/RemoteAssessmentClient.tsx @@ -7,6 +7,7 @@ import { ScenarioSelect } from "@/app/db/schema/recommendations"; import { measuresList } from "@/app/db/schema/recommendations"; import useCreateRemoteAssessment from "./useCreateRemoteAssessment"; import { RemoteAssessmentFormValues } from "@/app/portfolio/[slug]/components/FormSchema"; +import BackToPortfolioButton from "@/app/components/building-passport/BackToPortfolioButton"; export default function RemoteAssessmentClient({ portfolioId, @@ -18,6 +19,8 @@ export default function RemoteAssessmentClient({ const [selectedAddress, setSelectedAddress] = useState<{ address: string; uprn: string; + propertyType?: string; + builtForm?: string; } | null>(null); const [selectedPostcode, setSelectedPostcode] = useState(""); @@ -28,8 +31,8 @@ export default function RemoteAssessmentClient({ addressLineOne: selectedAddress?.address || "", postcode: selectedPostcode, valuation: null, - propertyType: null, - builtForm: null, + propertyType: selectedAddress?.propertyType || null, + builtForm: selectedAddress?.builtForm || null, measures: measuresList, }); @@ -39,9 +42,10 @@ export default function RemoteAssessmentClient({ return (
-

- Remote Assessment -

+
+ +

Remote Assessment

+
diff --git a/src/app/portfolio/[slug]/remote-assessment/ScenarioSetup.tsx b/src/app/portfolio/[slug]/remote-assessment/ScenarioSetup.tsx index ca0179eb..72eec373 100644 --- a/src/app/portfolio/[slug]/remote-assessment/ScenarioSetup.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/ScenarioSetup.tsx @@ -44,6 +44,8 @@ export default function ScenarioSetup({ selectedAddress, selectedPostcode, selectedUprn, + selectedPropertyType, + selectedBuiltForm, isSubmitting, onSubmitRemoteAssessment, }: { @@ -53,6 +55,8 @@ export default function ScenarioSetup({ selectedAddress: string | null; selectedPostcode: string; selectedUprn: number | null; + selectedPropertyType: string | null; + selectedBuiltForm: string | null; isSubmitting: boolean; onSubmitRemoteAssessment: (values: RemoteAssessmentFormValues) => void; }) { @@ -106,6 +110,8 @@ export default function ScenarioSetup({ addressLineOne: selectedAddress || "", postcode: selectedPostcode || "", uprn: selectedUprn || undefined, + propertyType: selectedPropertyType || null, + builtForm: selectedBuiltForm || null, }); } else { form.reset({ @@ -118,6 +124,8 @@ export default function ScenarioSetup({ addressLineOne: selectedAddress || "", postcode: selectedPostcode || "", uprn: selectedUprn || undefined, + propertyType: selectedPropertyType || null, + builtForm: selectedBuiltForm || null, }); } } diff --git a/src/app/portfolio/[slug]/remote-assessment/page.tsx b/src/app/portfolio/[slug]/remote-assessment/page.tsx index d1b4c622..0c2fa4a9 100644 --- a/src/app/portfolio/[slug]/remote-assessment/page.tsx +++ b/src/app/portfolio/[slug]/remote-assessment/page.tsx @@ -3,6 +3,7 @@ import { getPortfolio, getPortfolioScenarios, } from "@/app/portfolio/[slug]/utils"; +import BackButton from "@/app/portfolio/[slug]/remote-assessment/BackButton"; export default async function RemoteAssessmentPage(props: { params: Promise<{ slug: string }>;