Model/repositories/epc/epc_repository.py
Khalim Conn-Kowlessar fd43cf2d23 feat(epc-prediction): slice-5c predicted-EPC persistence slot
Add a `source` discriminator (lodged | predicted) to the EPC store so a Property
holds a lodged EPC and a predicted one (EPC Prediction gap-fill) at once
(ADR-0031). EpcRepository.save gains source="lodged"; idempotent delete is now
per-source (a predicted save no longer wipes lodged, and vice versa);
get_for_property/get_for_properties filter lodged; new get_predicted_for_property
/ get_predicted_for_properties read predicted. PropertyPostgresRepository.get +
get_many hydrate Property.predicted_epc, so the predicted picture reaches the
modelling read (both load via get_many). FakeEpcRepo mirrors the dual slot.

EpcPropertyModel gains `source` (default "lodged"); the test DB builds from the
SQLModel mirror so this is exercised without the prod migration. The matching
Drizzle change (column + per-(property_id,source) uniqueness) is the team's to
action before merge — docs/MIGRATION_NOTE_predicted_epc_source.md.

3 store tests (coexist, idempotent predicted re-save leaves lodged, lodged-only
has no predicted) + property-repo wiring; 85 pass across affected suites; new
code pyright-clean (2 pre-existing wwhrs errors in epc_property_table untouched).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 03:50:19 +00:00

61 lines
2.1 KiB
Python

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Literal, Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData
# Provenance of a persisted EPC picture (ADR-0031): a real "lodged" EPC, or a
# "predicted" one synthesised by EPC Prediction. A property can hold one of each.
EpcSource = Literal["lodged", "predicted"]
class EpcRepository(ABC):
"""Persists and loads the structured EPC Property Data slice.
`save` writes the `EpcPropertyData` to the `epc_property` parent row and its
child tables; `get` reconstructs the persisted projection back into an
`EpcPropertyData`. Round-trip fidelity over that projection is pinned by the
Slice-1 round-trip test (Hestia-Homes/Model#1129). Each EPC carries a
`source` so a lodged and a predicted picture coexist per property (ADR-0031).
"""
@abstractmethod
def save(
self,
data: EpcPropertyData,
property_id: int | None = None,
portfolio_id: int | None = None,
source: EpcSource = "lodged",
) -> int: ...
@abstractmethod
def get(self, epc_property_id: int) -> EpcPropertyData: ...
@abstractmethod
def get_for_property(self, property_id: int) -> Optional[EpcPropertyData]:
"""The property's LODGED EPC (the predicted slot is read separately)."""
...
@abstractmethod
def get_predicted_for_property(
self, property_id: int
) -> Optional[EpcPropertyData]:
"""The property's PREDICTED EPC (EPC Prediction gap-fill), or None."""
...
@abstractmethod
def get_for_properties(
self, property_ids: list[int]
) -> dict[int, EpcPropertyData]:
"""Bulk-hydrate a batch's LODGED EPCs, keyed by property_id (only those
with one are present). A handful of per-table queries, not N per property."""
...
@abstractmethod
def get_predicted_for_properties(
self, property_ids: list[int]
) -> dict[int, EpcPropertyData]:
"""Bulk-hydrate a batch's PREDICTED EPCs (ADR-0031), keyed by property_id
(only those with one are present)."""
...