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