Model/tests/repositories/epc/test_epc_predicted_slot.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

82 lines
2.9 KiB
Python

"""EPC store gains a `source` discriminator so a lodged EPC and a predicted EPC
(EPC Prediction gap-fill) coexist per property — ADR-0031 slice-5c."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from sqlalchemy import Engine
from sqlmodel import Session
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from repositories.epc.epc_postgres_repository import EpcPostgresRepository
_JSON_SAMPLES = Path(__file__).resolve().parents[3] / "backend/epc_api/json_samples"
def _epc() -> 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 test_lodged_and_predicted_epcs_coexist_per_property(db_engine: Engine) -> None:
# Arrange — the same property gets a lodged EPC and a predicted EPC.
epc = _epc()
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
repo.save(epc, property_id=42)
repo.save(epc, property_id=42, source="predicted")
session.commit()
# Act
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
lodged = repo.get_for_property(42)
predicted = repo.get_predicted_for_property(42)
# Assert — both slots are populated independently; neither overwrote the other.
assert lodged == epc
assert predicted == epc
def test_re_saving_a_predicted_epc_leaves_the_lodged_one_intact(
db_engine: Engine,
) -> None:
# Arrange — a lodged EPC, then a predicted one saved twice (idempotent re-run).
epc = _epc()
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
repo.save(epc, property_id=7)
repo.save(epc, property_id=7, source="predicted")
repo.save(epc, property_id=7, source="predicted")
session.commit()
# Act
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
lodged = repo.get_for_property(7)
predicted_bulk = repo.get_predicted_for_properties([7])
# Assert — the lodged slot survived the predicted re-saves; bulk read finds it.
assert lodged == epc
assert predicted_bulk == {7: epc}
def test_a_lodged_only_property_has_no_predicted_epc(db_engine: Engine) -> None:
# Arrange — only a lodged EPC is saved.
epc = _epc()
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
repo.save(epc, property_id=9)
session.commit()
# Act / Assert — the predicted slot is empty; the lodged one is unaffected.
with Session(db_engine) as session:
repo = EpcPostgresRepository(session)
assert repo.get_predicted_for_property(9) is None
assert repo.get_for_property(9) == epc