refetch_epc=False skips API entirely; EPC-less properties go straight to prediction path

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-06-26 10:24:27 +00:00
parent b1fd9d9368
commit 17a9f0aafc
3 changed files with 46 additions and 18 deletions

View file

@ -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

View file

@ -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
# ---------------------------------------------------------------------------

View file

@ -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",
)