Persist the lodged wall_u_value override 🟩

SapBuildingPart.wall_u_value (the gov-EPC lodged wall U, authoritative over the
derived default) had no column and dropped on save, so the reloaded dwelling fell
back to the age-band default U — -2.32 SAP on the 2 affected RdSAP corpus certs.
Surfaced by the new DB-round-trip gate. The prior _UNPERSISTED_ALLOWLIST
justification ("deliberate — full-SAP only; RdSAP re-model uses default U-values")
was stale: the cascade DOES honour the lodged U on the API path.

Add wall_u_value to EpcBuildingPartModel, write in from_domain, reconstruct in
_to_building_part, drop the allowlist entry. Verified: certs 39036600 / 38137278
round-trip 72.43 / 69.11 exactly. Needs an added FE column
(epc_building_part.wall_u_value, double precision NULL) on assessment-model#443.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-23 09:23:26 +00:00
parent 846a3238b3
commit 50493f07c6
3 changed files with 7 additions and 1 deletions

View file

@ -680,6 +680,11 @@ class EpcBuildingPartModel(SQLModel, table=True):
# to the gov-API code-6 heuristic", `False` means "explicitly system-built,
# do NOT apply the heuristic". Collapsing False to None inverts the answer.
wall_is_basement: Optional[bool] = Field(default=None)
# #1665: lodged wall U (gov-EPC `wall_u_value`), authoritative over the
# derived default when present; dropped it fell back to the age-band default
# (-2.32 SAP). The prior allowlist "RdSAP uses defaults" was stale — the
# cascade does honour the lodged U on the API path.
wall_u_value: Optional[float] = Field(default=None)
room_in_roof_floor_area: Optional[float] = Field(default=None)
room_in_roof_construction_age_band: Optional[str] = Field(default=None)
alt_wall_1_area: Optional[float] = Field(default=None)
@ -740,6 +745,7 @@ class EpcBuildingPartModel(SQLModel, table=True):
roof_insulation_thickness=part.roof_insulation_thickness,
rafter_insulation_thickness=part.rafter_insulation_thickness,
wall_is_basement=part.wall_is_basement,
wall_u_value=part.wall_u_value,
room_in_roof_floor_area=float(rir.floor_area) if rir else None,
room_in_roof_construction_age_band=(
rir.construction_age_band if rir else None

View file

@ -1185,6 +1185,7 @@ class EpcPostgresRepository(EpcRepository):
roof_insulation_thickness=bp.roof_insulation_thickness,
rafter_insulation_thickness=bp.rafter_insulation_thickness,
wall_is_basement=bp.wall_is_basement,
wall_u_value=bp.wall_u_value,
# Reconstruct from the epc_room_in_roof child table (full geometry
# incl. detailed_surfaces); the flat room_in_roof_* columns on
# epc_building_part are superseded and dropped in a follow-up (#1664).

View file

@ -59,7 +59,6 @@ _UNPERSISTED_ALLOWLIST: dict[str, str] = {
# `wall_is_basement` is deliberately NOT in this list — it is a
# disambiguation flag selecting which RdSAP default applies (§5.17/Table 23),
# not a measured U-value, and it IS persisted.
"SapBuildingPart.wall_u_value": "deliberate — full-SAP only; RdSAP re-model uses default U-values (#1656)",
"SapBuildingPart.roof_u_value": "deliberate — full-SAP only; RdSAP re-model uses default U-values (#1656)",
"SapBuildingPart.floor_u_value": "deliberate — full-SAP only; RdSAP re-model uses default U-values (#1656)",
}