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.planning_restrictions import PlanningRestrictions
|
||||||
from domain.geospatial.spatial_reference import SpatialReference
|
from domain.geospatial.spatial_reference import SpatialReference
|
||||||
from domain.property.property import Property, PropertyIdentity
|
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 domain.tasks.tasks import Source
|
||||||
from harness.console import run_modelling
|
from harness.console import run_modelling
|
||||||
|
from orchestration.property_baseline_orchestrator import (
|
||||||
|
PropertyBaselineOrchestrator,
|
||||||
|
)
|
||||||
from infrastructure.epc_client.epc_client_service import EpcClientService
|
from infrastructure.epc_client.epc_client_service import EpcClientService
|
||||||
from infrastructure.postcodes_io.postcodes_io_client import PostcodesIoClient
|
from infrastructure.postcodes_io.postcodes_io_client import PostcodesIoClient
|
||||||
from infrastructure.postgres.config import PostgresConfig
|
from infrastructure.postgres.config import PostgresConfig
|
||||||
|
|
@ -72,6 +77,9 @@ from repositories.geospatial.geospatial_s3_repository import (
|
||||||
GeospatialS3Repository,
|
GeospatialS3Repository,
|
||||||
ParquetReader,
|
ParquetReader,
|
||||||
)
|
)
|
||||||
|
from repositories.fuel_rates.fuel_rates_static_file_repository import (
|
||||||
|
FuelRatesStaticFileRepository,
|
||||||
|
)
|
||||||
from repositories.postgres_unit_of_work import PostgresUnitOfWork
|
from repositories.postgres_unit_of_work import PostgresUnitOfWork
|
||||||
from repositories.product.composite_product_repository import (
|
from repositories.product.composite_product_repository import (
|
||||||
catalogue_with_off_catalogue_overrides,
|
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]
|
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)
|
read_session = Session(engine)
|
||||||
try:
|
try:
|
||||||
scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0]
|
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()
|
uow.commit()
|
||||||
logger.info(f"property={property_id} plan saved")
|
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
|
except Exception as error: # noqa: BLE001
|
||||||
logger.error(
|
logger.error(
|
||||||
f"property={property_id}: {type(error).__name__}: {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 __future__ import annotations
|
||||||
|
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from typing import Any
|
from typing import Any, Iterator
|
||||||
from unittest.mock import MagicMock, call, patch
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -84,6 +84,16 @@ def _clear_cohort_cache() -> None:
|
||||||
h._nearby_cohort_cache.clear()
|
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
|
# 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,
|
"""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
|
# Arrange
|
||||||
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
||||||
mock_epc = MagicMock()
|
mock_epc = MagicMock()
|
||||||
|
|
@ -197,6 +210,8 @@ def test_lodged_epc_path_saves_epc_plan_and_marks_modelled() -> None:
|
||||||
PROPERTY_ID, has_recommendations=False
|
PROPERTY_ID, has_recommendations=False
|
||||||
)
|
)
|
||||||
mock_uow.commit.assert_called_once()
|
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:
|
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
|
"""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
|
# Arrange
|
||||||
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE])
|
||||||
mock_plan = _plan_mock()
|
mock_plan = _plan_mock()
|
||||||
|
|
@ -399,10 +417,11 @@ def test_prediction_path_saves_plan_without_epc_save() -> None:
|
||||||
# Act
|
# Act
|
||||||
_call_handler(_BODY)
|
_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.epc.save.assert_not_called()
|
||||||
mock_uow.plan.save.assert_called_once()
|
mock_uow.plan.save.assert_called_once()
|
||||||
mock_uow.commit.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