Drop phantom alternative-wall building parts lodged inline (reinstates dropped fix)

Task b9fcd354-2a33-4fc4-a64e-388c060a055c (portfolio 824 / scenario 1278)
still failed 5 of 128 batches with NotNullViolation on
epc_building_part.construction_age_band even after the SAP-Schema-15.0
mapper landed (PR #1531). Cert 8169-6926-5310-0447-8996 (UPRN
100010344955, SAP-Schema-15.0) lodges a bare alternative-wall record
(wall_area/wall_construction/wall_insulation_type, no identifier, age
band, or floor dimensions) as a second sap_building_parts element rather
than under a distinct sap_alternative_wall_1 key. RdSapSchema17_1 (which
the 15.0/16.x reduced-field mappers delegate to) has no alt-wall slot to
route it to, so it persisted as a phantom building part with a null age
band.

_drop_building_parts_without_age_band already solved exactly this shape
of problem for lodging artifacts (ae81a81e), but that commit sits on
fix/drop-building-part-without-age-band, an unmerged branch that has
since drifted far behind main. Reinstating the same filter (fresh, not
cherry-picked) fixes both the original artifact case and this SAP-15
alt-wall case, since RdSapSchema17_1 has nowhere else to put it either
way.

Verified against all 174 properties across the task's originally-failing
batches: zero remaining null-age-band building parts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-10 11:34:17 +00:00
parent 066cac8cb1
commit 2851a0151b
3 changed files with 374 additions and 1 deletions

View file

@ -3057,7 +3057,10 @@ class EpcPropertyDataMapper:
return _clear_basement_flag_when_system_built(
_with_renewable_heat_incentive(
_with_recorded_performance(mapped, data), data
_with_recorded_performance(
_drop_building_parts_without_age_band(mapped), data
),
data,
)
)
@ -3067,6 +3070,45 @@ class EpcPropertyDataMapper:
# ---------------------------------------------------------------------------
def _drop_building_parts_without_age_band(
epc: EpcPropertyData,
) -> EpcPropertyData:
"""Drop building parts that carry no `construction_age_band`.
Some gov-API certs lodge a leading building part with floor geometry but no
construction descriptors (no age band, identifier or wall/roof construction)
a glazed-perimeter "conservatory shell" / lodging artifact, e.g. cert
3235-4627-4400-0610-7292's part 0 `{floor_area, room_height, glazed_perimeter,
double_glazed}`. A part with no age band cannot have its RdSAP fabric U-values
derived (the engine reads `parts[0].construction_age_band` as the primary era),
and it violates the epc_building_part NOT-NULL column on persist.
Also catches 2011-era `SAP-Schema-15.0`/16.x reduced-field certs (see
`from_sap_schema_15_0`) that lodge an alternative-wall record (bare
`wall_area`/`wall_construction`/`wall_insulation_type`, no identifier or
floor dimensions) inline as a second `sap_building_parts` element instead of
under a distinct `sap_alternative_wall_1` key e.g. cert
8169-6926-5310-0447-8996 (UPRN 100010344955), part 1 `{wall_area: 18.74,
wall_construction: 5, wall_insulation_type: 4}`. `RdSapSchema17_1` has no
alternative-wall slot to route it to, so it is dropped like any other
band-less part rather than persisted as a phantom building part.
Drops such parts only when at least one banded part remains, so a fully
band-less cert (e.g. full SAP, measured-U) is left untouched. Returns the
same object when nothing changes."""
parts = epc.sap_building_parts
banded = [
p
for p in parts
# construction_age_band is typed str but the gov API genuinely omits it
# on these artifact parts, so the runtime None check is real.
if p.construction_age_band is not None # pyright: ignore[reportUnnecessaryComparison]
]
if banded and len(banded) != len(parts):
return replace(epc, sap_building_parts=banded)
return epc
# RdSAP 10 Table 27 (p.52) living-area fraction by habitable-room count.
# Mirrored here read-only to back-solve a room count from full SAP's measured
# living_area (single home is the calculator; this is the inverse lookup).

View file

@ -65,3 +65,24 @@ class TestFromSapSchema15_0:
result = Sap10Calculator().calculate(epc)
# Lodged 73; engine produces 72 (Elmhurst validation not yet run).
assert result.sap_score == 72
def test_drops_a_lone_alternative_wall_lodged_as_a_phantom_building_part(
self,
) -> None:
# Task b9fcd354-2a33-4fc4-a64e-388c060a055c (portfolio 824 / scenario
# 1278) still failed to persist UPRN 100010344955 even once the schema
# itself dispatches: cert 8169-6926-5310-0447-8996 lodges a bare
# alternative-wall record (wall_area/wall_construction/
# wall_insulation_type, no identifier/age band/floor dimensions) as a
# second `sap_building_parts` element rather than under a distinct
# `sap_alternative_wall_1` key. `RdSapSchema17_1` has no slot to route
# it to, so it must be dropped like any other band-less part
# (`_drop_building_parts_without_age_band`) instead of persisted as a
# phantom building part that violates the NOT-NULL
# `construction_age_band` column.
epc = EpcPropertyDataMapper.from_api_response(
load("sap_15_0_uprn_100010344955.json")
)
assert len(epc.sap_building_parts) == 1
assert epc.sap_building_parts[0].construction_age_band == "D"

View file

@ -0,0 +1,310 @@
{
"uprn": 100010344955,
"roofs": [
{
"description": "Pitched, 100 mm loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
{
"description": "Timber frame, as built, partial insulation (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"floors": [
{
"description": "Suspended, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"windows": [
{
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"lighting": {
"description": "Low energy lighting in 50% of fixed outlets",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "BB11 4DP",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"post_town": "BURNLEY",
"built_form": 2,
"created_at": "2011-06-13 13:23:32",
"glazed_area": 1,
"region_code": 19,
"report_type": 2,
"sap_heating": {
"cylinder_size": 2,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"secondary_fuel_type": 26,
"main_heating_details": [
{
"main_fuel_type": 26,
"boiler_flue_type": 1,
"heat_emitter_type": 1,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 111,
"main_heating_data_source": 2
}
],
"secondary_heating_type": 601,
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 38
},
"sap_version": 9.9,
"schema_type": "SAP-Schema-15.0",
"uprn_source": "Energy Assessor",
"country_code": "EAW",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"dwelling_type": "Semi-detached house",
"language_code": 1,
"property_type": 0,
"address_line_1": "9, Kinross Street",
"schema_version": "LIG-15.0",
"assessment_type": "RdSAP",
"completion_date": "2011-06-13",
"inspection_date": "2011-06-13",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 63,
"transaction_type": 5,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2011-06-13",
"restricted_access": 0,
"sap_energy_source": {
"main_gas": "Y",
"meter_type": 2,
"photovoltaic_supply": {
"percent_roof_area": 0
},
"wind_turbines_count": 0,
"wind_turbines_terrain_type": 2
},
"secondary_heating": {
"description": "Room heaters, mains gas",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"floor_insulation": 0,
"total_floor_area": 32.93,
"floor_construction": 2,
"heat_loss_perimeter": 16.46
},
{
"floor": 1,
"room_height": 2.4,
"total_floor_area": 29.57,
"heat_loss_perimeter": 15.76
}
],
"wall_insulation_type": 4,
"construction_age_band": "D",
"roof_insulation_location": 2,
"roof_insulation_thickness": "100mm"
},
{
"wall_area": 18.74,
"wall_location": 0,
"wall_construction": 5,
"wall_insulation_type": 4
}
],
"low_energy_lighting": 50,
"solar_water_heating": "N",
"bedf_revision_number": 310,
"habitable_room_count": 3,
"heating_cost_current": 648,
"co2_emissions_current": 4.5,
"energy_rating_current": 50,
"lighting_cost_current": 51,
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"multiple_glazing_type": 1,
"open_fireplaces_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": 426,
"hot_water_cost_current": 177,
"mechanical_ventilation": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 17,
"indicative_cost": "\u00a3100 - \u00a3300",
"improvement_type": "A",
"improvement_details": {
"improvement_number": 5
},
"improvement_category": 1,
"energy_performance_rating": 51,
"environmental_impact_rating": 47
},
{
"sequence": 2,
"typical_saving": 110,
"indicative_cost": "\u00a3100 - \u00a3300",
"improvement_type": "B",
"improvement_details": {
"improvement_number": 6
},
"improvement_category": 1,
"energy_performance_rating": 57,
"environmental_impact_rating": 54
},
{
"sequence": 3,
"typical_saving": 14,
"indicative_cost": "\u00a310",
"improvement_type": "E",
"improvement_details": {
"improvement_number": 35
},
"improvement_category": 1,
"energy_performance_rating": 58,
"environmental_impact_rating": 55
},
{
"sequence": 4,
"typical_saving": 30,
"indicative_cost": "\u00a3200 - \u00a3400",
"improvement_type": "F",
"improvement_details": {
"improvement_number": 4
},
"improvement_category": 1,
"energy_performance_rating": 60,
"environmental_impact_rating": 57
},
{
"sequence": 5,
"typical_saving": 40,
"indicative_cost": "\u00a3350 - \u00a3450",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 14
},
"improvement_category": 1,
"energy_performance_rating": 62,
"environmental_impact_rating": 60
},
{
"sequence": 6,
"typical_saving": 109,
"indicative_cost": "\u00a31,500 - \u00a33,500",
"improvement_type": "I",
"improvement_details": {
"improvement_number": 20
},
"improvement_category": 2,
"energy_performance_rating": 68,
"environmental_impact_rating": 68
},
{
"sequence": 7,
"typical_saving": 31,
"indicative_cost": "\u00a34,000 - \u00a36,000",
"improvement_type": "N",
"improvement_details": {
"improvement_number": 19
},
"improvement_category": 3,
"energy_performance_rating": 70,
"environmental_impact_rating": 71
},
{
"sequence": 8,
"typical_saving": 207,
"indicative_cost": "\u00a311,000 - \u00a320,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 3,
"energy_performance_rating": 82,
"environmental_impact_rating": 82
},
{
"sequence": 9,
"typical_saving": 17,
"indicative_cost": "\u00a31,500 - \u00a34,000",
"improvement_type": "V",
"improvement_details": {
"improvement_number": 44
},
"improvement_category": 3,
"energy_performance_rating": 83,
"environmental_impact_rating": 83
}
],
"co2_emissions_potential": 2.6,
"energy_rating_potential": 68,
"lighting_cost_potential": 34,
"hot_water_cost_potential": 97,
"renewable_heat_incentive": {
"water_heating": 3180,
"space_heating_existing_dwelling": 9251,
"space_heating_with_loft_insulation": 9108,
"space_heating_with_cavity_insulation": 7244,
"space_heating_with_loft_and_cavity_insulation": 7092
},
"seller_commission_report": "Y",
"energy_consumption_current": 370,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "19.04r16",
"energy_consumption_potential": 213,
"environmental_impact_current": 46,
"fixed_lighting_outlets_count": 8,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 68,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "D",
"co2_emissions_current_per_floor_area": 71,
"low_energy_fixed_lighting_outlets_count": 4
}