mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Land an override on the EPC's part at its position when the label is absent 🟩
A Landlord Override's building_part is a positional index (0=main, 1=extension 1…, ADR-0004), but the gov-API EPC can label that slot differently (e.g. lodge the 2nd part as 'other', not 'extension_1'). The previous fix skipped such orphaned overrides, silently discarding the landlord's correction. Now the override falls back onto the EPC's part at that position (via _resolve_part), so the correction lands; only a position the EPC models no part at is skipped (no geometry to model a wholly-absent part). Replaces the skip-only behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3f6f17bb85
commit
9956df07ff
2 changed files with 73 additions and 17 deletions
|
|
@ -9,10 +9,12 @@ then discarded). See ADR-0016.
|
|||
|
||||
import copy
|
||||
from dataclasses import fields
|
||||
from typing import Optional, Sequence
|
||||
from typing import Final, Optional, Sequence
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BuildingPartIdentifier,
|
||||
EpcPropertyData,
|
||||
SapBuildingPart,
|
||||
SapVentilation,
|
||||
SapWindow,
|
||||
WindowTransmissionDetails,
|
||||
|
|
@ -29,6 +31,39 @@ from domain.modelling.simulation import (
|
|||
)
|
||||
|
||||
|
||||
# A Landlord Override's building part is a POSITIONAL index (0=main, 1=extension
|
||||
# 1…, ADR-0004), translated to a `BuildingPartIdentifier` upstream. This recovers
|
||||
# that position so an override can fall back onto the part the EPC actually models
|
||||
# at that slot when the gov-API labelled it differently (e.g. lodged a 2nd part as
|
||||
# `other` rather than `extension_1`).
|
||||
_POSITION_BY_IDENTIFIER: Final[dict[BuildingPartIdentifier, int]] = {
|
||||
BuildingPartIdentifier.MAIN: 0,
|
||||
BuildingPartIdentifier.EXTENSION_1: 1,
|
||||
BuildingPartIdentifier.EXTENSION_2: 2,
|
||||
BuildingPartIdentifier.EXTENSION_3: 3,
|
||||
BuildingPartIdentifier.EXTENSION_4: 4,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_part(
|
||||
epc: EpcPropertyData,
|
||||
parts_by_id: dict[BuildingPartIdentifier, SapBuildingPart],
|
||||
identifier: BuildingPartIdentifier,
|
||||
) -> Optional[SapBuildingPart]:
|
||||
"""The building part an overlay targets: the EPC's part with that identifier
|
||||
when present, else the part at the override's positional index (so a
|
||||
correction for `extension_1` still lands on the EPC's 2nd part even when the
|
||||
gov-API lodged it under a different label). ``None`` when the EPC models no
|
||||
part at that position."""
|
||||
part = parts_by_id.get(identifier)
|
||||
if part is not None:
|
||||
return part
|
||||
position = _POSITION_BY_IDENTIFIER.get(identifier)
|
||||
if position is None or position >= len(epc.sap_building_parts):
|
||||
return None
|
||||
return epc.sap_building_parts[position]
|
||||
|
||||
|
||||
def apply_simulations(
|
||||
baseline: EpcPropertyData, simulations: Sequence[EpcSimulation]
|
||||
) -> EpcPropertyData:
|
||||
|
|
@ -41,11 +76,10 @@ def apply_simulations(
|
|||
|
||||
for simulation in simulations:
|
||||
for identifier, overlay in simulation.building_parts.items():
|
||||
# A Landlord Override can target a building part the lodged (or
|
||||
# predicted) EPC never carried — e.g. an `extension_1` override on a
|
||||
# property whose EPC has only `main`. Skip the orphaned part rather
|
||||
# than crashing the whole property's modelling on a `KeyError`.
|
||||
part = parts_by_id.get(identifier)
|
||||
part = _resolve_part(result, parts_by_id, identifier)
|
||||
# No part at this position — the EPC models fewer parts than the
|
||||
# override's index. We have no geometry to model the missing part, so
|
||||
# skip it rather than crash the whole property's modelling.
|
||||
if part is None:
|
||||
continue
|
||||
for overlay_field in fields(overlay):
|
||||
|
|
|
|||
|
|
@ -55,12 +55,37 @@ def test_apply_writes_targeted_building_part_and_leaves_others_untouched() -> No
|
|||
)
|
||||
|
||||
|
||||
def test_override_targeting_a_part_absent_from_the_epc_is_skipped() -> None:
|
||||
# A Landlord Override can reference a building part the lodged (or predicted)
|
||||
# EPC never carried — e.g. an `extension_1` override on a property whose EPC
|
||||
# has only `main`. The orphaned part is skipped, not crashed on, so the rest
|
||||
# of the overlay still folds and the property still models.
|
||||
# Arrange — build_epc() has MAIN + EXTENSION_1 but no EXTENSION_2.
|
||||
def test_override_for_an_absent_semantic_part_lands_on_the_part_at_that_position() -> (
|
||||
None
|
||||
):
|
||||
# `building_part` is a POSITIONAL index (0=main, 1=extension 1…, ADR-0004). The
|
||||
# gov-API EPC can label its parts differently (e.g. a 2nd part lodged as `other`
|
||||
# rather than `extension_1`). An `extension_1` override must still land on the
|
||||
# part at position 1 — the landlord's correction is applied, not dropped.
|
||||
# Arrange — build_epc() is [MAIN, EXTENSION_1]; relabel the 2nd part to OTHER so
|
||||
# the EXTENSION_1 identifier is absent but position 1 still exists.
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
baseline.sap_building_parts[1].identifier = BuildingPartIdentifier.OTHER
|
||||
simulation = EpcSimulation(
|
||||
building_parts={
|
||||
BuildingPartIdentifier.EXTENSION_1: BuildingPartOverlay(
|
||||
wall_insulation_type=3
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
||||
|
||||
# Assert — the override folded onto the part at position 1 (the OTHER part).
|
||||
assert _part(result, BuildingPartIdentifier.OTHER).wall_insulation_type == 3
|
||||
|
||||
|
||||
def test_override_with_no_part_at_that_position_is_skipped() -> None:
|
||||
# When there is genuinely no part at the override's position (the EPC models
|
||||
# fewer parts than the index), the override is skipped rather than crashing —
|
||||
# we cannot model an extension we have no geometry for.
|
||||
# Arrange — build_epc() has 2 parts (positions 0, 1); position 2 is absent.
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
simulation = EpcSimulation(
|
||||
building_parts={
|
||||
|
|
@ -74,12 +99,9 @@ def test_override_targeting_a_part_absent_from_the_epc_is_skipped() -> None:
|
|||
# Act
|
||||
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
||||
|
||||
# Assert — the present part got its overlay; the absent one was not added.
|
||||
# Assert — the present part got its overlay; nothing was added for position 2.
|
||||
assert _part(result, BuildingPartIdentifier.MAIN).wall_insulation_type == 1
|
||||
assert all(
|
||||
part.identifier is not BuildingPartIdentifier.EXTENSION_2
|
||||
for part in result.sap_building_parts
|
||||
)
|
||||
assert len(result.sap_building_parts) == len(baseline.sap_building_parts)
|
||||
|
||||
|
||||
def test_flat_roof_construction_type_folds_onto_the_part() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue