mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(modelling_e2e): persist predicted EPC + baseline for predicted properties
A predicted Property (no lodged EPC) got a Plan but nothing else: the synthesised EPC was never written to epc_property, and Baseline Performance was skipped — so property 729529 (portfolio 796 / scenario 1268), predicted from its DA16 1QZ cohort, was "missed" with no predicted-EPC row and no baseline row. Persist the synthesised EPC in the predicted slot (uow.epc.save(..., source= "predicted"), ADR-0031) inside the Plan UoW, then run the Baseline orchestrator for predicted Properties too — it re-hydrates the predicted EPC and establishes the baseline from it. The earlier "lodged only" guard is dropped: by the write block the Property always has a persisted EPC (lodged or predicted); one that could be neither fetched nor predicted raised earlier. Verified against the DB by invoking the real handler for 729529: predicted epc_property rows 0->1 and property_baseline_performance rows 0->1. Baseline on the predicted picture builds cleanly (RHI present, reason pre_sap10). Tests updated: prediction + broadening paths now assert the predicted-slot epc.save and the baseline run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2ee1b35dca
commit
e80be44fd1
2 changed files with 36 additions and 14 deletions
|
|
@ -309,6 +309,7 @@ def handler(body: dict[str, Any], context: Any) -> Optional[dict[str, Any]]:
|
|||
|
||||
epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn)
|
||||
overrides = overlays_from(overrides_reader.overrides_for(property_id))
|
||||
predicted_epc: Optional[EpcPropertyData] = None
|
||||
|
||||
if epc is not None:
|
||||
logger.info(f"property={property_id} lodged EPC found")
|
||||
|
|
@ -391,6 +392,16 @@ def handler(body: dict[str, Any], context: Any) -> Optional[dict[str, Any]]:
|
|||
uow.epc.save(
|
||||
epc, property_id=property_id, portfolio_id=portfolio_id
|
||||
)
|
||||
elif predicted_epc is not None:
|
||||
# Persist the synthesised EPC in the predicted slot (ADR-0031),
|
||||
# so the Baseline stage can re-hydrate it and downstream sees
|
||||
# the picture the Plan was modelled from.
|
||||
uow.epc.save(
|
||||
predicted_epc,
|
||||
property_id=property_id,
|
||||
portfolio_id=portfolio_id,
|
||||
source="predicted",
|
||||
)
|
||||
if spatial is not None:
|
||||
uow.spatial.save(uprn, spatial)
|
||||
if (
|
||||
|
|
@ -417,12 +428,12 @@ def handler(body: dict[str, Any], context: Any) -> Optional[dict[str, Any]]:
|
|||
uow.commit()
|
||||
logger.info(f"property={property_id} plan saved")
|
||||
|
||||
# Baseline Performance is re-established from the persisted EPC, so
|
||||
# it runs after the Plan UoW commits. Only lodged Properties have
|
||||
# the lodged figures the Baseline reads; predicted ones are skipped.
|
||||
if epc is not None:
|
||||
baseline_orchestrator.run([property_id])
|
||||
logger.info(f"property={property_id} baseline saved")
|
||||
# Baseline Performance is re-established from the persisted EPC
|
||||
# (lodged or predicted), so it runs after the Plan UoW commits. By
|
||||
# here the property always has a persisted EPC — a property that
|
||||
# could be neither fetched nor predicted raised earlier.
|
||||
baseline_orchestrator.run([property_id])
|
||||
logger.info(f"property={property_id} baseline saved")
|
||||
|
||||
except Exception as error: # noqa: BLE001
|
||||
logger.error(
|
||||
|
|
|
|||
|
|
@ -312,12 +312,12 @@ def test_skipped_cohort_certs_are_surfaced_in_the_outputs() -> None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_prediction_path_saves_plan_without_epc_save(
|
||||
def test_prediction_path_saves_predicted_epc_plan_and_baseline(
|
||||
_baseline_orchestrator: MagicMock,
|
||||
) -> None:
|
||||
"""When get_by_uprn returns None the handler synthesises an EPC via
|
||||
prediction and saves the plan — but never calls epc.save, and (having no
|
||||
lodged figures) never establishes a Baseline."""
|
||||
prediction, persists it in the predicted slot (source='predicted'), saves the
|
||||
plan, and re-establishes the Baseline from the predicted picture."""
|
||||
# Arrange
|
||||
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
||||
mock_plan = _plan_mock()
|
||||
|
|
@ -417,11 +417,16 @@ def test_prediction_path_saves_plan_without_epc_save(
|
|||
# Act
|
||||
_call_handler(_BODY)
|
||||
|
||||
# Assert — epc.save NOT called (no lodged cert), plan IS saved, no baseline
|
||||
mock_uow.epc.save.assert_not_called()
|
||||
# Assert — predicted EPC persisted in the predicted slot, plan saved, baseline run
|
||||
mock_uow.epc.save.assert_called_once_with(
|
||||
mock_predicted_epc,
|
||||
property_id=PROPERTY_ID,
|
||||
portfolio_id=PORTFOLIO_ID,
|
||||
source="predicted",
|
||||
)
|
||||
mock_uow.plan.save.assert_called_once()
|
||||
mock_uow.commit.assert_called_once()
|
||||
_baseline_orchestrator.return_value.run.assert_not_called()
|
||||
_baseline_orchestrator.return_value.run.assert_called_once_with([PROPERTY_ID])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -620,9 +625,15 @@ def test_empty_own_postcode_broadens_to_nearby_and_predicts() -> None:
|
|||
# Act
|
||||
_call_handler(_BODY)
|
||||
|
||||
# Assert — broadening fired, and the broadened cohort produced a saved plan.
|
||||
# Assert — broadening fired, and the broadened cohort produced a saved plan
|
||||
# with its predicted EPC persisted in the predicted slot.
|
||||
MockRepo.return_value.candidates_near.assert_called_once()
|
||||
mock_uow.epc.save.assert_not_called() # predicted, never lodged
|
||||
mock_uow.epc.save.assert_called_once_with(
|
||||
mock_predicted_epc,
|
||||
property_id=PROPERTY_ID,
|
||||
portfolio_id=PORTFOLIO_ID,
|
||||
source="predicted",
|
||||
)
|
||||
mock_uow.plan.save.assert_called_once()
|
||||
mock_uow.commit.assert_called_once()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue