Fail unmodellable properties with a specific, debuggable error 🟩

_predict_epc returned None for three unrelated causes — unresolved
property_type, an empty same-type cohort, and a degenerate (no MAIN part)
prediction — which the handler collapsed into one generic "not predictable"
string. The SubTask output could not say which cause fired or which data to
fix.

Raise a specific PropertyNotModellableError subclass per cause, each carrying
the property's identity (property_id, uprn, postcode, portfolio_id) and
cause-specific context. The unresolved-property-type message points at the
likely missing/contradictory Landlord Override. All subclass ValueError, so the
per-property failure boundary keeps catching them and records str(exc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-24 14:13:28 +00:00
parent dd06b19c77
commit 04ee16488e
2 changed files with 41 additions and 14 deletions

View file

@ -67,6 +67,11 @@ from infrastructure.solar.google_solar_api_client import (
BuildingInsightsNotFoundError,
GoogleSolarApiClient,
)
from applications.modelling_e2e.errors import (
DegeneratePredictionError,
NoSameTypeComparablesError,
UnresolvedPropertyTypeError,
)
from applications.modelling_e2e.modelling_e2e_trigger_body import (
ModellingE2ETriggerBody,
)
@ -181,16 +186,19 @@ def _predict_epc(
cohort_for: Callable[[str], list[ComparableProperty]],
broaden: Callable[[PredictionTarget], list[ComparableProperty]],
predictor: EpcPrediction,
) -> Optional[EpcPropertyData]:
) -> EpcPropertyData:
"""Synthesise an EpcPropertyData for an EPC-less property from its postcode
cohort (EPC Prediction Path 3, ADR-0031), or None when ineligible.
cohort (EPC Prediction Path 3, ADR-0031).
When the property's own postcode holds no same-type comparables (a sparse
postcode e.g. the only flat among houses), the cohort is broadened to the
real unit postcodes physically nearest it (``broaden``) before giving up.
Returns None when property_type is unresolvable (hard cohort filter cannot
fire) or when even the broadened cohort is empty after filtering.
Raises a specific ``PropertyNotModellableError`` subclass naming the cause
and carrying the property's identity — when it cannot predict: property_type
unresolved, an empty same-type cohort, or a degenerate (no MAIN part)
prediction. The per-property handler records ``str(exc)`` in the SubTask
output, so the cause is debuggable from the output alone.
"""
attributes = attributes_reader.attributes_for(property_id)
identity = PropertyIdentity(
@ -198,18 +206,41 @@ def _predict_epc(
)
target = build_prediction_target(identity, coordinates, attributes)
if target is None:
return None
raise UnresolvedPropertyTypeError(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
property_type=attributes.property_type,
built_form=attributes.built_form,
)
comparables = select_comparables(target, cohort_for(target.postcode))
broadened = False
if not comparables.members:
broadened = True
comparables = select_comparables(target, broaden(target))
if not comparables.members:
return None
raise NoSameTypeComparablesError(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
property_type=target.property_type,
broadened=broadened,
)
predicted = predictor.predict(target, comparables)
if not any(
part.identifier is BuildingPartIdentifier.MAIN
for part in predicted.sap_building_parts
):
return None
raise DegeneratePredictionError(
property_id=property_id,
uprn=uprn,
postcode=postcode,
portfolio_id=portfolio_id,
property_type=target.property_type,
cohort_size=len(comparables.members),
)
return predicted
@ -344,12 +375,6 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator,
broaden=_broaden,
predictor=predictor,
)
if predicted_epc is None:
raise ValueError(
f"no EPC for UPRN {uprn} and not predictable "
f"(unresolved property_type, or no same-type "
f"comparables in or near '{postcode}')"
)
effective_epc = Property(
identity=PropertyIdentity(
portfolio_id=portfolio_id,

View file

@ -15,7 +15,9 @@ from applications.modelling_e2e.errors import (
NoSameTypeComparablesError,
UnresolvedPropertyTypeError,
)
from applications.modelling_e2e.handler import _predict_epc
from applications.modelling_e2e.handler import (
_predict_epc, # pyright: ignore[reportPrivateUsage]
)
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,