mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-19 17:02:59 +00:00
fix(postcode-search): classify RH (HMO) family as addable residential
7 Leyton Avenue (UPRN 100031252876) came back from OS Places as RH03
("HMO Not Further Divided") and rendered as "Non-residential", so it
couldn't be added from the postcode-search "add properties" journey.
classifyOsCode decided "residential?" purely on an RD prefix, dropping the
whole RH (HMO) family — even though RD07 (the RD-family HMO code) was
already mapped to House. Extend the residential check to RH and map
RH01/RH02/RH03 to House (built form unknowable), consistent with RD07.
Updates ADR-0007's enumerated mapping table to stay truthful.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8bbcd77974
commit
fac425762e
3 changed files with 48 additions and 3 deletions
|
|
@ -35,9 +35,13 @@ modelling exists.
|
|||
distinguishable.
|
||||
- **Deterministic mapping** (fixed table, tested): RD02→House+Detached,
|
||||
RD03→House+Semi-Detached, RD04→House (Mid/End unknowable — no built form),
|
||||
RD06→Flat, RD07→House. Every other code maps nothing; `Unknown` is never
|
||||
RD06→Flat, RD07→House, RH01/RH02/RH03→House (the dedicated HMO family, built
|
||||
form unknowable). Every other code maps nothing; `Unknown` is never
|
||||
written (absence, not noise) — preserving the finaliser's invariant. The
|
||||
backend's predict-EPC service consumes these for archetype matching.
|
||||
- **Addable codes**: the RD (dwelling) family except RD10 (institutions), plus
|
||||
the RH (HMO) family. All other codes — commercial, institutional, land — are
|
||||
not addable and render "Non-residential".
|
||||
- **Read-through cache** on the existing `postcode_search` jsonb table:
|
||||
serve when fresher than 30 days, else refresh from OS Places and upsert.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ describe("classifyOsCode", () => {
|
|||
expect(classifyOsCode("RD07")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
|
||||
});
|
||||
|
||||
it("maps the RH (HMO) family to House, like RD07", () => {
|
||||
// e.g. UPRN 100031252876, 7 Leyton Avenue — RH03 must not read as non-residential.
|
||||
expect(classifyOsCode("RH01")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
|
||||
expect(classifyOsCode("RH02")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
|
||||
expect(classifyOsCode("RH03")).toEqual({ selectable: true, propertyType: "House", builtForm: null });
|
||||
});
|
||||
|
||||
it("keeps other residential codes selectable but writes no facts — never Unknown", () => {
|
||||
for (const code of ["RD", "RD01", "RD08"]) {
|
||||
expect(classifyOsCode(code)).toEqual({ selectable: true, propertyType: null, builtForm: null });
|
||||
|
|
@ -63,6 +70,21 @@ describe("toAddressCandidates", () => {
|
|||
expect(out[1]).toMatchObject({ uprn: "101", selectable: false });
|
||||
});
|
||||
|
||||
it("classifies an RH03 HMO as a selectable House, not Non-residential", () => {
|
||||
// Regression: 7 Leyton Avenue (UPRN 100031252876) came back RH03 ("HMO Not
|
||||
// Further Divided") on a street of RD02/RD03 houses and rendered as
|
||||
// "Non-residential", so it couldn't be added.
|
||||
const hmo = [
|
||||
{ DPA: { UPRN: "100031252876", ADDRESS: "7, LEYTON AVENUE, SUTTON-IN-ASHFIELD, NG17 3BD",
|
||||
POSTCODE: "NG17 3BD", CLASSIFICATION_CODE: "RH03", LAT: 53.1352313, LNG: -1.2522665 } },
|
||||
];
|
||||
const out = toAddressCandidates(hmo as never, "NG17 3BD");
|
||||
expect(out[0]).toMatchObject({
|
||||
uprn: "100031252876", address: "7, Leyton Avenue, Sutton-In-Ashfield",
|
||||
classificationCode: "RH03", selectable: true, propertyType: "House", builtForm: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("orders addresses numerically, so Flat 2 comes before Flat 10", () => {
|
||||
const flats = [
|
||||
{ DPA: { UPRN: "1", ADDRESS: "FLAT 10, BIRCH COURT, M20 4TF", CLASSIFICATION_CODE: "RD06" } },
|
||||
|
|
@ -102,6 +124,18 @@ describe("buildWritePlan", () => {
|
|||
{ component: "built_form_type", value: "Detached", originalDescription: "RD02", buildingPart: 0 },
|
||||
]);
|
||||
});
|
||||
it("writes a House property_type override for an RH03 HMO, keyed to the OS code", () => {
|
||||
const plan = buildWritePlan(
|
||||
{ uprn: "100031252876", address: "7, Leyton Avenue, Sutton-In-Ashfield", postcode: "NG17 3BD",
|
||||
classificationCode: "RH03", lat: 53.1352313, lng: -1.2522665, selectable: true,
|
||||
propertyType: "House", builtForm: null },
|
||||
geo,
|
||||
);
|
||||
// HMO built form is unknowable, so only the property_type fact is written.
|
||||
expect(plan.overrides).toEqual([
|
||||
{ component: "property_type", value: "House", originalDescription: "RH03", buildingPart: 0 },
|
||||
]);
|
||||
});
|
||||
it("writes no overrides when the code maps nothing — never Unknown", () => {
|
||||
const plan = buildWritePlan(
|
||||
{ uprn: "200", address: "Birch Court", postcode: "M20 4UD", classificationCode: "RD",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ const OS_CODE_FACTS: Record<string, { propertyType: string; builtForm: string |
|
|||
RD03: { propertyType: "House", builtForm: "Semi-Detached" },
|
||||
RD04: { propertyType: "House", builtForm: null }, // terraced — mid/end unknowable
|
||||
RD06: { propertyType: "Flat", builtForm: null },
|
||||
RD07: { propertyType: "House", builtForm: null }, // HMO
|
||||
RD07: { propertyType: "House", builtForm: null }, // HMO (RD-family code)
|
||||
// RH is the dedicated HMO family; treat as House like RD07, built form unknowable.
|
||||
RH01: { propertyType: "House", builtForm: null }, // HMO parent
|
||||
RH02: { propertyType: "House", builtForm: null }, // HMO bedsit / non-self-contained
|
||||
RH03: { propertyType: "House", builtForm: null }, // HMO not further divided
|
||||
};
|
||||
|
||||
const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
|
@ -181,7 +185,10 @@ export function cacheAgeDays(fetchedAt: Date, now: Date): number {
|
|||
export function classifyOsCode(code: string | null): OsClassification {
|
||||
if (!code) return { selectable: false, propertyType: null, builtForm: null };
|
||||
const c = code.trim().toUpperCase();
|
||||
const residential = c === "RD" || (c.startsWith("RD") && c !== "RD10");
|
||||
// Addable dwellings: the RD (dwelling) family except RD10 (institutions),
|
||||
// plus the RH (HMO) family. Everything else — commercial, institutional,
|
||||
// land — is not addable.
|
||||
const residential = (c.startsWith("RD") && c !== "RD10") || c.startsWith("RH");
|
||||
if (!residential) return { selectable: false, propertyType: null, builtForm: null };
|
||||
const facts = OS_CODE_FACTS[c];
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue