"""END-TO-END showcase: an EPC-less Property flows through Ingestion, gets a predicted EPC synthesised from its postcode cohort, is persisted to the predicted slot, and comes back out of the Property repository resolving as the Effective EPC (ADR-0031). This is the full production path with ONLY the external HTTP clients faked (the EPC API, the geospatial S3 reader, the Solar API) — everything else is the real thing: the real Postgres Unit of Work, the real EPC + Property repositories against the test database, the real `EpcComparablePropertiesRepository`, and the real `EpcPrediction`. It is the canonical "see the whole flow" reference; the narrower unit tests live in: - tests/orchestration/test_ingestion_prediction.py (orchestrator: gate / persist) - tests/repositories/epc/test_epc_predicted_slot.py (the lodged|predicted slot) - tests/domain/property/test_property.py (the "predicted" source path) - tests/domain/epc_prediction/test_prediction_target.py (the eligibility gate) """ from __future__ import annotations import json from pathlib import Path from typing import Any, Optional from sqlalchemy import Engine from sqlmodel import Session from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.mapper import EpcPropertyDataMapper from datatypes.epc.search.epc_search_result import EpcSearchResult from domain.epc_prediction.epc_prediction import EpcPrediction from domain.geospatial.coordinates import Coordinates from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.geospatial.spatial_reference import SpatialReference from domain.property.property import Property from infrastructure.postgres.property_override_table import PropertyOverrideRow from infrastructure.postgres.property_table import PropertyRow from orchestration.ingestion_orchestrator import IngestionOrchestrator from repositories.comparable_properties.epc_comparable_properties_repository import ( EpcComparablePropertiesRepository, ) from repositories.epc.epc_postgres_repository import EpcPostgresRepository from repositories.geospatial.geospatial_repository import GeospatialRepository from repositories.postgres_unit_of_work import PostgresUnitOfWork from repositories.property.override_backed_prediction_attributes_reader import ( OverrideBackedPredictionAttributesReader, ) from repositories.property.property_overrides_postgres_reader import ( PropertyOverridesPostgresReader, ) from repositories.property.property_postgres_repository import ( PropertyPostgresRepository, ) from repositories.spatial.spatial_postgres_repository import SpatialPostgresRepository _JSON_SAMPLES = Path(__file__).resolve().parents[2] / "backend/epc_api/json_samples" _POSTCODE = "LS6 1AA" 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) # --- fakes for the THREE external HTTP boundaries (everything else is real) ---- class _FakeCohortEpcClient: """Stands in for the live EPC API: the postcode's lodged certs + their data.""" def __init__(self, results: list[EpcSearchResult]) -> None: self._results = results def search_by_postcode(self, postcode: str) -> list[EpcSearchResult]: return self._results def get_by_certificate_number(self, cert_num: str) -> EpcPropertyData: return _epc() class _FakeGeospatialRepo(GeospatialRepository): """Stands in for the S3 Open-UPRN reader: UPRN → coordinates.""" def __init__(self, coords: dict[int, Coordinates]) -> None: self._coords = coords def coordinates_for(self, uprn: int) -> Optional[Coordinates]: return self._coords.get(uprn) def spatial_for(self, uprn: int) -> Optional[SpatialReference]: coordinates = self._coords.get(uprn) if coordinates is None: return None return SpatialReference( coordinates=coordinates, restrictions=PlanningRestrictions() ) class _NoEpcFetcher: """The target Property is EPC-less — the EPC API finds nothing for its UPRN.""" def get_by_uprn(self, uprn: int) -> Optional[EpcPropertyData]: return None class _NoSolarFetcher: def get_building_insights( self, longitude: float, latitude: float ) -> dict[str, Any]: return {} def _cohort_results() -> list[EpcSearchResult]: return [ EpcSearchResult( certificate_number=f"CERT-{i}", address_line_1=f"{i} Neighbour Road", address_line_2=None, address_line_3=None, address_line_4=None, postcode=_POSTCODE, post_town="LEEDS", uprn=20000 + i, current_energy_efficiency_band="D", registration_date=f"2023-0{i + 1}-01", ) for i in range(3) ] def test_epc_less_property_is_predicted_persisted_and_resolved_end_to_end( db_engine: Engine, ) -> None: # Arrange — an EPC-less Property exists in the database (postcode + UPRN known, # no EPC lodged), plus its postcode cohort behind the faked EPC API, plus the # landlord overrides the finaliser resolved for it (House / Semi-Detached) that # the real read adapter will translate into the gov-code space ("0" / "2"). with Session(db_engine) as session: row = PropertyRow( portfolio_id=1, postcode=_POSTCODE, address="1 Target Street", uprn=10000 ) session.add(row) session.commit() property_id = row.id assert property_id is not None session.add( PropertyOverrideRow( property_id=property_id, portfolio_id=1, building_part=0, override_component="property_type", override_value="House", original_spreadsheet_description="3-bed semi", ) ) session.add( PropertyOverrideRow( property_id=property_id, portfolio_id=1, building_part=0, override_component="built_form_type", override_value="Semi-Detached", original_spreadsheet_description="3-bed semi", ) ) session.commit() cohort_coords = {20000 + i: Coordinates(longitude=-1.55, latitude=53.81) for i in range(3)} comparables_repo = EpcComparablePropertiesRepository( _FakeCohortEpcClient(_cohort_results()), _FakeGeospatialRepo(cohort_coords) ) orchestrator = IngestionOrchestrator( unit_of_work=lambda: PostgresUnitOfWork(lambda: Session(db_engine)), epc_fetcher=_NoEpcFetcher(), geospatial_repo=_FakeGeospatialRepo({10000: Coordinates(longitude=-1.55, latitude=53.81)}), solar_fetcher=_NoSolarFetcher(), comparables_repo=comparables_repo, prediction_attributes_reader=OverrideBackedPredictionAttributesReader( PropertyOverridesPostgresReader(lambda: Session(db_engine)) ), epc_prediction=EpcPrediction(), ) # Act — run Ingestion: no lodged EPC found → predict from the cohort → persist. orchestrator.run([property_id]) # Assert — reloading the Property through the real repository, its Effective # EPC is the predicted picture, flagged by the "predicted" source path. with Session(db_engine) as session: epc_repo = EpcPostgresRepository(session) prop: Property = PropertyPostgresRepository( session, epc_repo, SpatialPostgresRepository(session) ).get(property_id) assert prop.epc is None # no lodged EPC assert prop.predicted_epc is not None # a predicted one was persisted assert prop.source_path == "predicted" assert prop.effective_epc is prop.predicted_epc assert prop.effective_epc.property_type == "0"