"""IngestionOrchestrator fetches the batch (no DB unit open), then writes it in one Unit of Work and commits once (ADR-0012). Tested against fakes — no IO.""" from __future__ import annotations from typing import Any, Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData 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, PropertyIdentity from orchestration.ingestion_orchestrator import IngestionOrchestrator from repositories.geospatial.geospatial_repository import GeospatialRepository from tests.orchestration.fakes import ( FakeEpcRepo, FakePropertyRepo, FakeSolarRepo, FakeSpatialRepo, FakeUnitOfWork, ) class _FakeEpcFetcher: def __init__(self, epc: Optional[EpcPropertyData]) -> None: self.epc = epc self.uprns: list[int] = [] def get_by_uprn(self, uprn: int) -> Optional[EpcPropertyData]: self.uprns.append(uprn) return self.epc class _FakeGeospatialRepo(GeospatialRepository): def __init__( self, coordinates: Optional[Coordinates], restrictions: PlanningRestrictions = PlanningRestrictions(), ) -> None: self._reference: Optional[SpatialReference] = ( SpatialReference(coordinates=coordinates, restrictions=restrictions) if coordinates is not None else None ) def coordinates_for(self, uprn: int) -> Optional[Coordinates]: return self._reference.coordinates if self._reference is not None else None def spatial_for(self, uprn: int) -> Optional[SpatialReference]: return self._reference class _FakeSolarFetcher: def __init__(self, insights: dict[str, Any]) -> None: self.insights = insights self.calls: list[tuple[float, float]] = [] def get_building_insights( self, longitude: float, latitude: float ) -> dict[str, Any]: self.calls.append((longitude, latitude)) return self.insights def _property(uprn: Optional[int]) -> Property: return Property( identity=PropertyIdentity( portfolio_id=1, postcode="A0 0AA", address="1 Some Street", uprn=uprn ) ) def test_ingestion_persists_epc_and_threads_coords_into_solar() -> None: # Arrange epc = object.__new__(EpcPropertyData) insights = {"name": "buildings/X"} epc_repo = FakeEpcRepo() solar_repo = FakeSolarRepo() solar_fetcher = _FakeSolarFetcher(insights) uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(uprn=12345)}), epc=epc_repo, solar=solar_repo, ) orchestrator = IngestionOrchestrator( unit_of_work=lambda: uow, epc_fetcher=_FakeEpcFetcher(epc), geospatial_repo=_FakeGeospatialRepo( Coordinates(longitude=-0.1278, latitude=51.5074) ), solar_fetcher=solar_fetcher, ) # Act orchestrator.run([10]) # Assert — EPC persisted, coords threaded from the repo into the solar # fetcher, solar persisted, batch committed once. assert epc_repo.saved == [(epc, 10)] assert solar_fetcher.calls == [(-0.1278, 51.5074)] assert solar_repo.saved == [(12345, -0.1278, 51.5074, insights)] assert uow.commits == 1 def test_ingestion_caches_the_spatial_reference_by_uprn() -> None: # Arrange — the geospatial repo resolves a listed-building UPRN. epc = object.__new__(EpcPropertyData) reference = SpatialReference( coordinates=Coordinates(longitude=-0.1278, latitude=51.5074), restrictions=PlanningRestrictions(is_listed=True), ) spatial_repo = FakeSpatialRepo() uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(uprn=12345)}), epc=FakeEpcRepo(), spatial=spatial_repo, ) orchestrator = IngestionOrchestrator( unit_of_work=lambda: uow, epc_fetcher=_FakeEpcFetcher(epc), geospatial_repo=_FakeGeospatialRepo( reference.coordinates, restrictions=reference.restrictions ), solar_fetcher=_FakeSolarFetcher({}), ) # Act orchestrator.run([10]) # Assert — the resolved reference is cached against the UPRN so Modelling # reads the protections back (ADR-0020), and the batch commits once. assert spatial_repo.saved == [(12345, reference)] assert uow.commits == 1 def test_ingestion_skips_property_without_uprn() -> None: # Arrange epc_repo = FakeEpcRepo() solar_repo = FakeSolarRepo() solar_fetcher = _FakeSolarFetcher({}) uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(uprn=None)}), epc=epc_repo, solar=solar_repo, ) orchestrator = IngestionOrchestrator( unit_of_work=lambda: uow, epc_fetcher=_FakeEpcFetcher(object.__new__(EpcPropertyData)), geospatial_repo=_FakeGeospatialRepo(None), solar_fetcher=solar_fetcher, ) # Act orchestrator.run([10]) # Assert — nothing fetched or persisted for a UPRN-less property. assert epc_repo.saved == [] assert solar_repo.saved == [] assert solar_fetcher.calls == [] def test_ingestion_persists_epc_but_skips_solar_when_no_coordinates() -> None: # Arrange epc = object.__new__(EpcPropertyData) epc_repo = FakeEpcRepo() solar_repo = FakeSolarRepo() solar_fetcher = _FakeSolarFetcher({}) uow = FakeUnitOfWork( property=FakePropertyRepo({10: _property(uprn=12345)}), epc=epc_repo, solar=solar_repo, ) orchestrator = IngestionOrchestrator( unit_of_work=lambda: uow, epc_fetcher=_FakeEpcFetcher(epc), geospatial_repo=_FakeGeospatialRepo(None), solar_fetcher=solar_fetcher, ) # Act orchestrator.run([10]) # Assert assert epc_repo.saved == [(epc, 10)] assert solar_repo.saved == [] assert solar_fetcher.calls == []