Pin the resolution reached in the grill: planning status persists as a per-UPRN write-through cache in the existing `property_details_spatial` table (not FE-property columns), read back off the Property in Modelling; unknown UPRN defaults to unrestricted, matching legacy `empty_spatial_df` (superseding the earlier "conservative stance" note). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.3 KiB
Conservation / Listed / Heritage as distinct Property attributes
Wall Insulation Eligibility (ADR-0019) — and later Solar-PV and Windows generators — gate on a Property's planning status. That status is not on the EPC and not in the OS Open-UPRN parquet the geospatial Repo reads today (it only yields coordinates); legacy derives it by spatial-joining separate conservation/listed/heritage layers (OpenUprnClient.set_spatial_data → property_spatial table). We decided to model it as three distinct boolean Property attributes — in_conservation_area, is_listed, is_heritage — resolved through the geospatial layer during Ingestion and read off the Property (not the EPC), because they describe the building's location/protection, not its energy fabric.
Decision
- Keep the three flags separate, not legacy's collapsed
restricted_measuresboolean — they gate different Measure Options (conservation blocks EWI only; listed/heritage block both — ADR-0019). - Surface them via the geospatial Repo (a
GeospatialRepositorymethod returning a planning-status record, alongsidecoordinates_for), persist onto the Property in Ingestion, and pass them into the generator as an explicit input. - Build this as the final integrating slice of the solid-wall feature (build order A), after the calculator mechanics and generator are pinned — it also unlocks the PV/Windows gates.
Source (decided)
The conservation/listed/heritage flags are co-located with longitude/latitude in the same S3 partition the geospatial Repo already reads — so we extend the existing GeospatialS3Repository to surface those extra columns alongside the coordinates, rather than porting a separate spatial-join or reading the legacy property_spatial table. A further deep-dive into the exact S3 columns/shape precedes slice 3.
Persistence (decided, slice 3c)
The OS Open-UPRN reference set is tens of millions of rows — too large to host in Postgres — so it lives in S3 and is resolved on demand. The flags must nonetheless reach the front-end (the FE displays them on the property_details_spatial view so a user can see why a Property did or did not get a given measure), and the FE reads Postgres, not S3. So Ingestion follows a write-through cache: fetch the spatial reference row from S3, use the coordinates to drive the Solar fetch, and persist the whole row (coordinates + three flags) into the existing property_details_spatial table, keyed by UPRN (one shared row per UPRN, not per Property — uprn is unique; cf. legacy bulk_upsert_property_spatial's on_conflict_do_update). Modelling reads the flags back off the Property (PropertyPostgresRepository hydrates Property.planning_restrictions from that table by UPRN) — the stage boundary stays repo-mediated (ADR-0011); Modelling never touches S3.
This is the resolution of the earlier "persist onto the Property" wording: the flags are a Property attribute in the domain sense (an enrichment hydrated into the aggregate, exactly like the EPC is hydrated from epc_property), persisted in a backend-written reference table, not as columns on the FE-owned property row.
Unknown default — resolved to unrestricted (allow EWI). When S3 has no row for a UPRN (or the cache has none yet), the Property hydrates to PlanningRestrictions() — all flags False. This matches what legacy actually does (OpenUprnClient.empty_spatial_df sets all three flags False; the nearest-UPRN proxy keeps the flags and only nulls coordinates), so the Consequences note below — which read legacy as conservatively blocking on missing data — was mistaken about legacy and is superseded: we do not suppress a valid measure on absent evidence. UPRN is now a required identifier (it stitches the datasets together), so "Property with no UPRN" is an edge handled by the same unrestricted fallback rather than a designed-for case.
Consequences
- Generators that need planning status take it as an input or read it off the Property; it never lands on
EpcPropertyData. - Until this slice ships, the ADR-0019 gate defaults to "unrestricted" (offline only).
- Mirrors legacy's conservative stance where missing data implied restriction — the source slice should decide the "unknown" default explicitly (block EWI vs allow) rather than silently allowing.