diff --git a/applications/modelling_e2e/handler.py b/applications/modelling_e2e/handler.py index 2a02d8b2..de7b5542 100644 --- a/applications/modelling_e2e/handler.py +++ b/applications/modelling_e2e/handler.py @@ -12,9 +12,10 @@ When no lodged EPC is found, EPC Prediction (Path 3, ADR-0031) synthesises one from the postcode cohort. ``_cohort_cache`` is module-level so warm Lambda containers re-processing the same postcode avoid redundant fetches. -``secondary_heating_removal`` is excluded unconditionally: the live ``material`` -catalogue does not yet carry this measure type, causing a crash during catalogue -reads for properties with a lodged secondary heater. +All Measure Types are considered: pricing goes through +``catalogue_with_off_catalogue_overrides`` so the measures the live ``material`` +catalogue cannot supply (``secondary_heating_removal``, the glazing and heating +gaps) are priced from the committed off-catalogue overlay instead of crashing. DB engine is module-scoped so the connection pool is reused across warm invocations (ADR-0012). @@ -45,7 +46,6 @@ from domain.epc_prediction.prediction_target import build_prediction_target from domain.geospatial.coordinates import Coordinates from domain.geospatial.planning_restrictions import PlanningRestrictions from domain.geospatial.spatial_reference import SpatialReference -from domain.modelling.measure_type import MeasureType from domain.property.property import Property, PropertyIdentity from domain.tasks.tasks import Source from harness.console import run_modelling @@ -67,7 +67,9 @@ from repositories.geospatial.geospatial_s3_repository import ( ParquetReader, ) from repositories.postgres_unit_of_work import PostgresUnitOfWork -from repositories.product.product_postgres_repository import ProductPostgresRepository +from repositories.product.composite_product_repository import ( + catalogue_with_off_catalogue_overrides, +) from repositories.property.landlord_override_overlays import overlays_from from repositories.property.override_backed_prediction_attributes_reader import ( OverrideBackedPredictionAttributesReader, @@ -212,7 +214,7 @@ def handler(body: dict[str, Any], context: Any) -> None: read_session = Session(engine) try: scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0] - products = ProductPostgresRepository(read_session) + products = catalogue_with_off_catalogue_overrides(read_session) errors: list[int] = [] @@ -282,21 +284,14 @@ def handler(body: dict[str, Any], context: Any) -> None: None if no_solar else _solar_insights_for(solar_client, spatial) ) - # secondary_heating_removal is absent from the live material.type - # enum; exclude unconditionally until the catalogue gap is resolved. - # system_tune_up and system_tune_up_zoned have no active product. - considered: Optional[frozenset[MeasureType]] = ( - frozenset(MeasureType) - - {MeasureType.SECONDARY_HEATING_REMOVAL} - - {MeasureType.SYSTEM_TUNE_UP_ZONED} - - {MeasureType.SYSTEM_TUNE_UP} - ) - + # All Measure Types are considered: the off-catalogue overlay + # (catalogue_with_off_catalogue_overrides) prices the measures the + # live material catalogue cannot supply, so none need excluding. plan = run_modelling( effective_epc, planning_restrictions=restrictions, solar_insights=solar_insights, - considered_measures=considered, + considered_measures=None, products=products, scenario=scenario, print_table=False, diff --git a/infrastructure/epc_client/epc_client_service.py b/infrastructure/epc_client/epc_client_service.py index fe9d130f..111de1f5 100644 --- a/infrastructure/epc_client/epc_client_service.py +++ b/infrastructure/epc_client/epc_client_service.py @@ -19,10 +19,8 @@ from datatypes.epc.search import EpcSearchResult class EpcClientService: BASE_URL = "https://api.get-energy-performance-data.communities.gov.uk" # The gov API's per-UPRN search latency is variable: usually ~0.2s but with - # intermittent slow spells. 10s was low enough that a slow spell timed out and - # call_with_retry then re-issued it (compounding the cost); 30s rides out the - # spell instead. Rate-limit (429) handling stays with call_with_retry. - REQUEST_TIMEOUT = 30.0 + # intermittent slow spells. + REQUEST_TIMEOUT = 10.0 def __init__(self, auth_token: str) -> None: self._headers = { diff --git a/tests/applications/modelling_e2e/test_handler.py b/tests/applications/modelling_e2e/test_handler.py index 5979fe1b..762cea60 100644 --- a/tests/applications/modelling_e2e/test_handler.py +++ b/tests/applications/modelling_e2e/test_handler.py @@ -167,7 +167,7 @@ def test_lodged_epc_path_saves_epc_plan_and_marks_modelled() -> None: patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context( patch("applications.modelling_e2e.handler.Session") @@ -285,7 +285,7 @@ def test_prediction_path_saves_plan_without_epc_save() -> None: patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context( patch("applications.modelling_e2e.handler.Session") @@ -381,7 +381,7 @@ def test_empty_cohort_gates_property_out_and_raises() -> None: patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context(patch("applications.modelling_e2e.handler.Session")) MockUoW = stack.enter_context( @@ -462,7 +462,7 @@ def test_partial_batch_failure_raises_runtime_error_listing_failed_ids() -> None patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context( @@ -576,7 +576,7 @@ def test_cohort_cache_prevents_duplicate_candidates_for_calls() -> None: patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context( @@ -659,7 +659,7 @@ def test_dry_run_skips_all_db_writes() -> None: patch("applications.modelling_e2e.handler.ScenarioPostgresRepository") ).return_value.get_many.return_value = [MagicMock()] stack.enter_context( - patch("applications.modelling_e2e.handler.ProductPostgresRepository") + patch("applications.modelling_e2e.handler.catalogue_with_off_catalogue_overrides") ) stack.enter_context(patch("applications.modelling_e2e.handler.Session")) stack.enter_context(