"""``_predict_epc`` conditions its prediction on an expired Historic EPC and labels the result "expired" (ADR-0054). The gov EPC API only serves certificates registered since 2012, so a dwelling whose only certificate predates that is EPC-less to Ingestion and gets a predicted picture. Persisting that picture as plain "predicted" loses the fact that a real — if stale — certificate exists, which is exactly the distinction portfolio reporting draws between "no EPC at all" and "only an expired one". `IngestionOrchestrator` already does this; `modelling_e2e` is the pipeline that actually runs, and predicts through its own `_predict_epc`. These tests pin the source label there. """ from __future__ import annotations import dataclasses import json from datetime import date from pathlib import Path from typing import Any, Callable, Optional, cast from applications.modelling_e2e.handler import ( _predict_epc, # pyright: ignore[reportPrivateUsage] ) from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.historic_epc import HistoricEpc from datatypes.epc.domain.mapper import EpcPropertyDataMapper from domain.epc_prediction.comparable_properties import ComparableProperty from domain.epc_prediction.epc_prediction import EpcPrediction from domain.epc_prediction.prediction_target import ( PredictionTarget, PredictionTargetAttributes, ) from repositories.property.override_backed_prediction_attributes_reader import ( OverrideBackedPredictionAttributesReader, ) _JSON_SAMPLES = Path(__file__).resolve().parents[3] / "backend/epc_api/json_samples" def _epc() -> EpcPropertyData: raw: dict[str, Any] = json.loads( (_JSON_SAMPLES / "RdSAP-Schema-21.0.0" / "epc.json").read_text() ) return EpcPropertyDataMapper.from_api_response(raw) def _cohort() -> list[ComparableProperty]: # Three same-type neighbours (property_type "0"), distinct addresses so the # dedupe keeps all three. return [ ComparableProperty( epc=_epc(), certificate_number=f"CERT-{i}", address=f"{i} Some Street", registration_date=date(2023, 1, i + 1), ) for i in range(3) ] def _historic(**overrides: str) -> HistoricEpc: """A pre-2012 certificate. Every field defaults to "" — unset fields resolve to None in `conditioning_from_historic`, so a test conditions on exactly the attributes it names and nothing else.""" fields = {f.name: "" for f in dataclasses.fields(HistoricEpc)} fields.update(overrides) return HistoricEpc(**fields) class _HistoricReaderReturning: """The historic-EPC backup, holding one record — or none, on a miss.""" def __init__(self, record: Optional[HistoricEpc]) -> None: self._record = record self.lookups: list[tuple[str, str]] = [] def record_for_uprn(self, uprn: str, postcode: str) -> Optional[HistoricEpc]: self.lookups.append((uprn, postcode)) return self._record class _FixedAttributesReader: def __init__(self, attributes: PredictionTargetAttributes) -> None: self._attributes = attributes def attributes_for(self, property_id: int) -> PredictionTargetAttributes: return self._attributes def _reader( attributes: PredictionTargetAttributes, ) -> OverrideBackedPredictionAttributesReader: return cast( OverrideBackedPredictionAttributesReader, _FixedAttributesReader(attributes) ) def _no_broaden() -> Callable[[PredictionTarget], list[ComparableProperty]]: return lambda _target: [] def _predict( *, attributes: PredictionTargetAttributes, historic: Optional[_HistoricReaderReturning], ) -> tuple[EpcPropertyData, str]: return _predict_epc( property_id=733290, uprn=100021987320, postcode="SE4 1DX", portfolio_id=796, attributes_reader=_reader(attributes), historic_epc_reader=historic, coordinates=None, cohort_for=lambda _postcode: _cohort(), broaden=_no_broaden(), predictor=EpcPrediction(), ) def test_prediction_conditioned_by_expired_historic_epc_is_sourced_expired() -> None: # Arrange — an override resolves the type, and the backup also holds this # UPRN's expired pre-2012 certificate. historic = _HistoricReaderReturning(_historic(property_type="House")) # Act _predicted, source = _predict( attributes=PredictionTargetAttributes(property_type="0"), historic=historic ) # Assert — the expired certificate is recorded in the source, not discarded, # and it was looked up by this Property's UPRN and postcode. assert source == "expired" assert historic.lookups == [("100021987320", "SE4 1DX")] def test_prediction_without_a_historic_record_is_sourced_predicted() -> None: # Arrange — the backup is wired, but holds nothing for this UPRN. # Act _predicted, source = _predict( attributes=PredictionTargetAttributes(property_type="0"), historic=_HistoricReaderReturning(None), ) # Assert — a blind gap-fill stays plain "predicted". assert source == "predicted" def test_unwired_historic_reader_leaves_every_prediction_predicted() -> None: # Arrange — the feature is off (no HISTORIC_EPC_S3_ROOT in the environment). # Act _predicted, source = _predict( attributes=PredictionTargetAttributes(property_type="0"), historic=None ) # Assert — behaviour is exactly as before the ADR-0054 wiring. assert source == "predicted" def test_historic_epc_supplies_the_property_type_when_no_override_resolved() -> None: # Arrange — no Landlord Override resolves a property_type, which on its own # gates the Property out of prediction entirely (UnresolvedPropertyTypeError). # The expired certificate observed one, so the Property becomes predictable. # Act _predicted, source = _predict( attributes=PredictionTargetAttributes(property_type=None), historic=_HistoricReaderReturning(_historic(property_type="House")), ) # Assert — rescued by the historic observation rather than failing to model. assert source == "expired" def test_landlord_override_wins_over_the_expired_observation() -> None: # Arrange — the override and the expired cert disagree on property type. The # override speaks to *current* state, so it wins; the historic cert only # fills gaps (ADR-0054). "0" (House) is the cohort's type — were the historic # "Flat" to win, no same-type comparable would survive and this would raise. # Act _predicted, source = _predict( attributes=PredictionTargetAttributes(property_type="0"), historic=_HistoricReaderReturning(_historic(property_type="Flat")), ) # Assert — predicted against the override's cohort, still labelled expired. assert source == "expired"