validated postcodes

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-23 18:22:31 +00:00
parent 1e5ccac346
commit f1132f6205
5 changed files with 108 additions and 14 deletions

View file

@ -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<string>();
const deduped: FlatAddress[] = [];
for (const it of items) {

View file

@ -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() {

View file

@ -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<string>("");
@ -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 (
<div className="max-w-4xl mx-auto p-8 space-y-12">
<h1 className="text-3xl font-bold text-brandblue mb-8">
Remote Assessment
</h1>
<div className="flex items-center gap-3 mb-8">
<BackToPortfolioButton portfolioId={portfolioId} />
<h1 className="text-3xl font-bold text-brandblue">Remote Assessment</h1>
</div>
<AddressSearch
onAddressSelect={setSelectedAddress}
@ -63,6 +67,8 @@ export default function RemoteAssessmentClient({
selectedAddress={selectedAddress?.address ?? ""}
selectedPostcode={selectedPostcode}
selectedUprn={Number(selectedAddress?.uprn) ?? null}
selectedPropertyType={selectedAddress?.propertyType ?? null}
selectedBuiltForm={selectedAddress?.builtForm ?? null}
isSubmitting={isUploading}
onSubmitRemoteAssessment={onSubmitRemoteAssessment}
/>

View file

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

View file

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