from __future__ import annotations from dataclasses import dataclass from typing import Literal, Optional from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.property.site_notes import SiteNotes SourcePath = Literal["site_notes", "epc_with_overlay"] @dataclass(frozen=True) class PropertyIdentity: """Identifies a single Property within a portfolio. Keyed by `(portfolio_id, uprn)` or `(portfolio_id, landlord_property_id)` — a UPRN is permanent but each portfolio gets its own Property against it (CONTEXT.md: UPRN). """ portfolio_id: int postcode: str address: str uprn: Optional[int] = None landlord_property_id: Optional[str] = None @dataclass class Property: """The Ara modelling aggregate root for a single dwelling (ADR-0002). Holds identity plus the source data the pipeline reasons about. Enrichments (geospatial, solar) and modelling outputs (baseline performance, plans) are added by later slices — this is the minimal-and-growing shape for First Run. """ identity: PropertyIdentity epc: Optional[EpcPropertyData] = None site_notes: Optional[SiteNotes] = None @property def source_path(self) -> SourcePath: """Which of the two disjoint source paths models this Property (ADR-0001). Site Notes alone, or the public EPC (with Landlord Overrides, once that slice lands). When both exist the newer wins (Recency Tie-Break); on an equal date the survey wins, as it reflects on-site observation. """ if self.site_notes is not None and self.epc is not None: epc_date = self.epc.registration_date or self.epc.inspection_date if self.site_notes.surveyed_at >= epc_date: return "site_notes" return "epc_with_overlay" if self.site_notes is not None: return "site_notes" if self.epc is not None: return "epc_with_overlay" raise ValueError( "Property has neither Site Notes nor an EPC; no source path to model from" ) @property def effective_epc(self) -> EpcPropertyData: """The EpcPropertyData the modelling pipeline scores against. Path 1: the Site Notes' surveyed data. Path 2: the public EPC (Landlord Overrides overlay is a later slice — returned as-is for now). """ if self.source_path == "site_notes": assert self.site_notes is not None return self.site_notes.to_epc_property_data() assert self.epc is not None return self.epc