Model/tests/orchestration/test_ingestion_orchestrator.py
2026-06-12 12:52:36 +00:00

186 lines
5.9 KiB
Python

"""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 == []