diff --git a/tests/applications/modelling_e2e/test_handler.py b/tests/applications/modelling_e2e/test_handler.py index 1959cd1a..0f73bbc2 100644 --- a/tests/applications/modelling_e2e/test_handler.py +++ b/tests/applications/modelling_e2e/test_handler.py @@ -1298,6 +1298,181 @@ def test_refetch_epc_true_always_calls_api_even_if_stored_epc_exists() -> None: # --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# repredict_epc flag +# --------------------------------------------------------------------------- + + +def test_repredict_epc_false_with_stored_predicted_epc_skips_prediction() -> None: + """repredict_epc=False + stored predicted EPC present: EpcPrediction.predict + is not called; the stored predicted EPC reaches run_modelling.""" + # Arrange + mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE]) + stored_predicted = MagicMock() + from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier + + mock_part = MagicMock() + mock_part.identifier = BuildingPartIdentifier.MAIN + stored_predicted.sap_building_parts = [mock_part] + + mock_plan = _plan_mock() + mock_uow = MagicMock() + + with ExitStack() as stack: + stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV)) + stack.enter_context( + patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcClientService") + ).return_value.get_by_uprn.return_value = None # no lodged EPC → prediction path + stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository")) + stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient")) + stack.enter_context( + patch("applications.modelling_e2e.handler._spatial_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.overlays_from", return_value=[]) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader") + ).return_value.overrides_for_many.return_value = {} + stack.enter_context( + patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") + ).return_value.get_many.return_value = [MagicMock()] + stack.enter_context( + patch("applications.modelling_e2e.handler.catalogue_snapshot_with_off_catalogue_overrides") + ) + stack.enter_context(patch("applications.modelling_e2e.handler.Session")) + stack.enter_context( + patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan) + ) + mock_predictor = stack.enter_context( + patch("applications.modelling_e2e.handler.EpcPrediction") + ).return_value + # Stored predicted EPC is present + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcPostgresRepository") + ).return_value.get_predicted_for_properties.return_value = { + PROPERTY_ID: stored_predicted + } + MockUoW = stack.enter_context( + patch("applications.modelling_e2e.handler.PostgresUnitOfWork") + ) + MockUoW.return_value.__enter__.return_value = mock_uow + MockUoW.return_value.__exit__.return_value = False + + # Act + _call_handler({**_BODY, "repredict_epc": False}) + + # Assert — EpcPrediction.predict never called; stored EPC persisted in predicted slot + mock_predictor.predict.assert_not_called() + mock_uow.epc.save.assert_called_once_with( + stored_predicted, + property_id=PROPERTY_ID, + portfolio_id=PORTFOLIO_ID, + source="predicted", + ) + + +def test_repredict_epc_false_without_stored_predicted_epc_falls_back_to_live_prediction() -> None: + """repredict_epc=False + no stored predicted EPC: handler falls back to live + prediction so the property is not silently skipped.""" + # Arrange + mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE]) + mock_plan = _plan_mock() + mock_uow = MagicMock() + + mock_predicted_epc = MagicMock() + from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier + + mock_part = MagicMock() + mock_part.identifier = BuildingPartIdentifier.MAIN + mock_predicted_epc.sap_building_parts = [mock_part] + + mock_comparables = MagicMock() + mock_comparables.members = [MagicMock()] + + with ExitStack() as stack: + stack.enter_context(patch("applications.modelling_e2e.handler.os.environ", _ENV)) + stack.enter_context( + patch("applications.modelling_e2e.handler._get_engine", return_value=mock_engine) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcClientService") + ).return_value.get_by_uprn.return_value = None # no lodged EPC + stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository")) + stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient")) + stack.enter_context( + patch("applications.modelling_e2e.handler._spatial_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler._solar_insights_for", return_value=None) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.overlays_from", return_value=[]) + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.PropertyOverridesPostgresReader") + ).return_value.overrides_for_many.return_value = {} + from domain.epc_prediction.prediction_target import PredictionTargetAttributes + + stack.enter_context( + patch("applications.modelling_e2e.handler.OverrideBackedPredictionAttributesReader") + ).return_value.attributes_for.return_value = PredictionTargetAttributes( + property_type="2" + ) + stack.enter_context( + patch("applications.modelling_e2e.handler.select_comparables") + ).return_value = mock_comparables + mock_predictor = stack.enter_context( + patch("applications.modelling_e2e.handler.EpcPrediction") + ).return_value + mock_predictor.predict.return_value = mock_predicted_epc + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcComparablePropertiesRepository") + ).return_value.candidates_for.return_value = [MagicMock()] + stack.enter_context( + patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") + ).return_value.get_many.return_value = [MagicMock()] + stack.enter_context( + patch("applications.modelling_e2e.handler.catalogue_snapshot_with_off_catalogue_overrides") + ) + stack.enter_context(patch("applications.modelling_e2e.handler.Session")) + stack.enter_context( + patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan) + ) + # No stored predicted EPC + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcPostgresRepository") + ).return_value.get_predicted_for_properties.return_value = {} + MockUoW = stack.enter_context( + patch("applications.modelling_e2e.handler.PostgresUnitOfWork") + ) + MockUoW.return_value.__enter__.return_value = mock_uow + MockUoW.return_value.__exit__.return_value = False + + # Act + _call_handler({**_BODY, "repredict_epc": False}) + + # Assert — live prediction was used as fallback + mock_predictor.predict.assert_called_once() + mock_uow.epc.save.assert_called_once_with( + mock_predicted_epc, + property_id=PROPERTY_ID, + portfolio_id=PORTFOLIO_ID, + source="predicted", + ) + + +# --------------------------------------------------------------------------- +# Dry-run +# --------------------------------------------------------------------------- + + def test_dry_run_skips_all_db_writes() -> None: """dry_run=True: run_modelling executes but PostgresUnitOfWork is never entered — no DB writes occur for any property in the batch."""