mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(modelling_e2e): persist Baseline Performance for lodged properties
The handler wrote epc/spatial/solar/plan and marked the property modelled, but never established its Baseline Performance — so no row was created in property_baseline_performance for any property modelled through the Lambda (noticed on portfolio 796 / scenario 1268 / property 727218, a lodged property). Mirror the e2e runner: after the plan UoW commits (so the EPC is persisted for the orchestrator to re-hydrate), run PropertyBaselineOrchestrator for lodged properties. Predicted properties have no lodged figures and no persisted EPC, so they are skipped — consistent with the e2e runner and the ara_first_run Baseline stage. Verified 727218's baseline pipeline builds end-to-end in-memory (lodged_performance → CalculatorRebaseliner → bill → PropertyBaselinePerformance, reason pre_sap10). Tests: lodged path asserts the orchestrator runs once; prediction path asserts it does not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f4ec32e51
commit
2ee1b35dca
2 changed files with 50 additions and 6 deletions
|
|
@ -51,8 +51,13 @@ from domain.geospatial.coordinates import Coordinates
|
|||
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
||||
from domain.geospatial.spatial_reference import SpatialReference
|
||||
from domain.property.property import Property, PropertyIdentity
|
||||
from domain.property_baseline.calculator_rebaseliner import CalculatorRebaseliner
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||
from domain.tasks.tasks import Source
|
||||
from harness.console import run_modelling
|
||||
from orchestration.property_baseline_orchestrator import (
|
||||
PropertyBaselineOrchestrator,
|
||||
)
|
||||
from infrastructure.epc_client.epc_client_service import EpcClientService
|
||||
from infrastructure.postcodes_io.postcodes_io_client import PostcodesIoClient
|
||||
from infrastructure.postgres.config import PostgresConfig
|
||||
|
|
@ -72,6 +77,9 @@ from repositories.geospatial.geospatial_s3_repository import (
|
|||
GeospatialS3Repository,
|
||||
ParquetReader,
|
||||
)
|
||||
from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
||||
FuelRatesStaticFileRepository,
|
||||
)
|
||||
from repositories.postgres_unit_of_work import PostgresUnitOfWork
|
||||
from repositories.product.composite_product_repository import (
|
||||
catalogue_with_off_catalogue_overrides,
|
||||
|
|
@ -266,6 +274,16 @@ def handler(body: dict[str, Any], context: Any) -> Optional[dict[str, Any]]:
|
|||
)
|
||||
return _nearby_cohort_cache[key]
|
||||
|
||||
# Re-establishes each lodged Property's Baseline Performance from the just-
|
||||
# persisted EPC (one UoW per property, committed after the Plan's). Predicted
|
||||
# Properties have no lodged figures, so they get no baseline (mirrors the e2e
|
||||
# runner and the ara_first_run Baseline stage).
|
||||
baseline_orchestrator = PropertyBaselineOrchestrator(
|
||||
unit_of_work=lambda: PostgresUnitOfWork(lambda: Session(engine)),
|
||||
rebaseliner=CalculatorRebaseliner(Sap10Calculator()),
|
||||
fuel_rates=FuelRatesStaticFileRepository(),
|
||||
)
|
||||
|
||||
read_session = Session(engine)
|
||||
try:
|
||||
scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0]
|
||||
|
|
@ -399,6 +417,13 @@ 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")
|
||||
|
||||
except Exception as error: # noqa: BLE001
|
||||
logger.error(
|
||||
f"property={property_id}: {type(error).__name__}: {error}",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ is needed. One test per distinct behaviour path.
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack
|
||||
from typing import Any
|
||||
from typing import Any, Iterator
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -84,6 +84,16 @@ def _clear_cohort_cache() -> None:
|
|||
h._nearby_cohort_cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _baseline_orchestrator() -> Iterator[MagicMock]:
|
||||
"""Patch the Baseline orchestrator for every test — construction stays cheap
|
||||
and ``.run`` is a no-op mock the baseline tests assert against."""
|
||||
with patch(
|
||||
"applications.modelling_e2e.handler.PropertyBaselineOrchestrator"
|
||||
) as orchestrator:
|
||||
yield orchestrator
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trigger body validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -117,9 +127,12 @@ def test_trigger_body_rejects_missing_property_ids() -> None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_lodged_epc_path_saves_epc_plan_and_marks_modelled() -> None:
|
||||
def test_lodged_epc_path_saves_epc_plan_and_marks_modelled(
|
||||
_baseline_orchestrator: MagicMock,
|
||||
) -> None:
|
||||
"""When get_by_uprn returns an EPC the handler saves it, saves the plan,
|
||||
and marks the property as modelled — all inside one UoW per property."""
|
||||
marks the property as modelled — all inside one UoW per property — and
|
||||
re-establishes its Baseline Performance after the plan commits."""
|
||||
# Arrange
|
||||
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
||||
mock_epc = MagicMock()
|
||||
|
|
@ -197,6 +210,8 @@ def test_lodged_epc_path_saves_epc_plan_and_marks_modelled() -> None:
|
|||
PROPERTY_ID, has_recommendations=False
|
||||
)
|
||||
mock_uow.commit.assert_called_once()
|
||||
# Baseline Performance is re-established for the lodged property.
|
||||
_baseline_orchestrator.return_value.run.assert_called_once_with([PROPERTY_ID])
|
||||
|
||||
|
||||
def test_skipped_cohort_certs_are_surfaced_in_the_outputs() -> None:
|
||||
|
|
@ -297,9 +312,12 @@ def test_skipped_cohort_certs_are_surfaced_in_the_outputs() -> None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_prediction_path_saves_plan_without_epc_save() -> None:
|
||||
def test_prediction_path_saves_plan_without_epc_save(
|
||||
_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."""
|
||||
prediction and saves the plan — but never calls epc.save, and (having no
|
||||
lodged figures) never establishes a Baseline."""
|
||||
# Arrange
|
||||
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
||||
mock_plan = _plan_mock()
|
||||
|
|
@ -399,10 +417,11 @@ def test_prediction_path_saves_plan_without_epc_save() -> None:
|
|||
# Act
|
||||
_call_handler(_BODY)
|
||||
|
||||
# Assert — epc.save NOT called (no lodged cert), plan IS saved
|
||||
# Assert — epc.save NOT called (no lodged cert), plan IS saved, no baseline
|
||||
mock_uow.epc.save.assert_not_called()
|
||||
mock_uow.plan.save.assert_called_once()
|
||||
mock_uow.commit.assert_called_once()
|
||||
_baseline_orchestrator.return_value.run.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue