mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
201 lines
7.8 KiB
Python
201 lines
7.8 KiB
Python
"""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"
|