"""Property aggregate — source-path precedence and Effective EPC resolution. The two disjoint source paths (ADR-0001): a Property is modelled either from its Site Notes alone, or from the public EPC (with Landlord Overrides, once that slice lands). When both exist, the newer wins (Recency Tie-Break). """ from __future__ import annotations import json from datetime import date from pathlib import Path from typing import Any from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.mapper import EpcPropertyDataMapper from domain.property.properties import Properties from domain.property.property import Property, PropertyIdentity from domain.property.site_notes import SiteNotes _JSON_SAMPLES = Path(__file__).resolve().parents[3] / "backend/epc_api/json_samples" def _epc(inspection: str = "2023-12-01") -> EpcPropertyData: raw: dict[str, Any] = json.loads( (_JSON_SAMPLES / "RdSAP-Schema-21.0.0" / "epc.json").read_text() ) return EpcPropertyDataMapper.from_api_response(raw) def _identity() -> PropertyIdentity: return PropertyIdentity( portfolio_id=1, postcode="A0 0AA", address="1 Some Street", uprn=12345 ) def test_source_path_is_epc_with_overlay_when_only_epc_present() -> None: # Arrange prop = Property(identity=_identity(), epc=_epc()) # Act path = prop.source_path # Assert assert path == "epc_with_overlay" def test_source_path_is_site_notes_when_only_site_notes_present() -> None: # Arrange prop = Property( identity=_identity(), site_notes=SiteNotes(surveyed_at=date(2024, 6, 1), epc=_epc()), ) # Act path = prop.source_path # Assert assert path == "site_notes" def test_recency_tie_break_newer_site_notes_win_over_older_epc() -> None: # Arrange — EPC inspected 2023-12-01; survey is newer prop = Property( identity=_identity(), epc=_epc(), site_notes=SiteNotes(surveyed_at=date(2025, 1, 1), epc=_epc()), ) # Act / Assert assert prop.source_path == "site_notes" def test_recency_tie_break_older_site_notes_lose_to_newer_epc() -> None: # Arrange — survey predates the EPC's inspection date prop = Property( identity=_identity(), epc=_epc(), site_notes=SiteNotes(surveyed_at=date(2020, 1, 1), epc=_epc()), ) # Act / Assert assert prop.source_path == "epc_with_overlay" def test_effective_epc_follows_the_selected_source_path() -> None: # Arrange survey_epc = _epc() public_epc = _epc() site_notes_property = Property( identity=_identity(), site_notes=SiteNotes(surveyed_at=date(2025, 1, 1), epc=survey_epc), ) epc_property = Property(identity=_identity(), epc=public_epc) # Act / Assert assert site_notes_property.effective_epc is survey_epc assert epc_property.effective_epc is public_epc def test_property_with_no_source_raises() -> None: # Arrange prop = Property(identity=_identity()) # Act / Assert try: _ = prop.source_path except ValueError: pass else: # pragma: no cover raise AssertionError("expected ValueError when no source is present") def test_properties_collection_iterates_and_filters() -> None: # Arrange with_epc = Property(identity=_identity(), epc=_epc()) without = Property(identity=_identity()) properties = Properties([with_epc, without]) # Act with_source = properties.filter(lambda p: p.epc is not None) # Assert assert len(properties) == 2 assert list(properties) == [with_epc, without] assert len(with_source) == 1 assert list(with_source) == [with_epc]