From 17a9f0aafc45402a3b6fe43dfce56038f0c608ae Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 26 Jun 2026 10:24:27 +0000 Subject: [PATCH] refetch_epc=False skips API entirely; EPC-less properties go straight to prediction path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When refetch_epc=False and no stored lodged EPC exists, the handler no longer falls back to a live EPC API call — it treats the property as EPC-less and hands it to the prediction path. This keeps REFETCH_EPC (lodged path) and REPREDICT_EPC (prediction path) cleanly independent. Co-Authored-By: Claude Sonnet 4.6 --- applications/modelling_e2e/handler.py | 8 ++-- scripts/trigger_modelling_e2e_sqs.py | 10 ++-- .../modelling_e2e/test_handler.py | 46 +++++++++++++++---- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/applications/modelling_e2e/handler.py b/applications/modelling_e2e/handler.py index d9b86c39..532d5fb4 100644 --- a/applications/modelling_e2e/handler.py +++ b/applications/modelling_e2e/handler.py @@ -544,11 +544,13 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, ) stored_lodged = stored_lodged_epcs.get(pid) - if not refetch_epc and stored_lodged is not None: + if refetch_epc: + epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn) + elif stored_lodged is not None: logger.info(f"property={pid} using stored lodged EPC (refetch_epc=False)") - epc: Optional[EpcPropertyData] = stored_lodged + epc = stored_lodged else: - epc = epc_client.get_by_uprn(uprn) + epc = None # no stored lodged EPC; prediction path handles this property overrides = overlays_from(overrides_reader.overrides_for(pid)) predicted_epc: Optional[EpcPropertyData] = None diff --git a/scripts/trigger_modelling_e2e_sqs.py b/scripts/trigger_modelling_e2e_sqs.py index 35095430..7521b1ec 100644 --- a/scripts/trigger_modelling_e2e_sqs.py +++ b/scripts/trigger_modelling_e2e_sqs.py @@ -45,14 +45,12 @@ DRY_RUN: bool = False # False → Lambda skips the Google Solar fetch (re-uses stored Solar data). REFETCH_SOLAR: bool = True -# False → skip the EPC API call for properties that already have a stored lodged -# EPC; the API is still called for any property that has no stored lodged EPC. +# False → use stored lodged EPC for properties that have one; properties with no +# stored lodged EPC are treated as EPC-less and routed to prediction (no API call). REFETCH_EPC: bool = True -# False → skip live EPC prediction for properties that already have a stored -# predicted EPC; live prediction still runs for any property that reaches the -# prediction branch with no stored predicted EPC. Only relevant for properties -# without a lodged EPC (either stored or freshly fetched). +# False → use stored predicted EPC for EPC-less properties that have one; live +# prediction still runs when no stored predicted EPC exists for the property. REPREDICT_EPC: bool = True # --------------------------------------------------------------------------- diff --git a/tests/applications/modelling_e2e/test_handler.py b/tests/applications/modelling_e2e/test_handler.py index 0f73bbc2..90b3e97c 100644 --- a/tests/applications/modelling_e2e/test_handler.py +++ b/tests/applications/modelling_e2e/test_handler.py @@ -1176,15 +1176,24 @@ def test_refetch_epc_false_with_stored_epc_skips_api_call() -> None: ) -def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None: - """refetch_epc=False + no stored lodged EPC: handler falls back to the live - EPC API call rather than silently skipping the property.""" +def test_refetch_epc_false_without_stored_epc_skips_api_and_goes_to_prediction() -> None: + """refetch_epc=False + no stored lodged EPC: the EPC API is not called; + the property is treated as EPC-less and prediction runs instead.""" # Arrange mock_engine = _engine_mock([PROPERTY_ID], [UPRN], [POSTCODE]) - live_epc = MagicMock() 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( @@ -1193,7 +1202,7 @@ def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None: mock_epc_client = stack.enter_context( patch("applications.modelling_e2e.handler.EpcClientService") ).return_value - mock_epc_client.get_by_uprn.return_value = live_epc + mock_epc_client.get_by_uprn.return_value = MagicMock() # would be called if flag ignored stack.enter_context(patch("applications.modelling_e2e.handler.GeospatialS3Repository")) stack.enter_context(patch("applications.modelling_e2e.handler.GoogleSolarApiClient")) stack.enter_context( @@ -1208,6 +1217,22 @@ def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None: 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 + stack.enter_context( + patch("applications.modelling_e2e.handler.EpcPrediction") + ).return_value.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()] @@ -1218,7 +1243,7 @@ def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None: stack.enter_context( patch("applications.modelling_e2e.handler.run_modelling", return_value=mock_plan) ) - # No stored lodged EPC for this property + # No stored lodged EPC stack.enter_context( patch("applications.modelling_e2e.handler.EpcPostgresRepository") ).return_value.get_for_properties.return_value = {} @@ -1231,10 +1256,13 @@ def test_refetch_epc_false_without_stored_epc_falls_back_to_api() -> None: # Act _call_handler({**_BODY, "refetch_epc": False}) - # Assert — API was called as fallback; live EPC is persisted - mock_epc_client.get_by_uprn.assert_called_once_with(UPRN) + # Assert — API was NOT called; prediction ran and its output was persisted + mock_epc_client.get_by_uprn.assert_not_called() mock_uow.epc.save.assert_called_once_with( - live_epc, property_id=PROPERTY_ID, portfolio_id=PORTFOLIO_ID + mock_predicted_epc, + property_id=PROPERTY_ID, + portfolio_id=PORTFOLIO_ID, + source="predicted", )