From 79fe54a82289d5b4c2a374a41decf59d79a1b83b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 30 Jun 2026 17:16:14 +0000 Subject: [PATCH] =?UTF-8?q?Log=20a=20landlord=20fuel=20that=20contradicts?= =?UTF-8?q?=20the=20heating=20archetype's=20natural=20fuel=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main_heating_system_overlay.py | 10 +++++ .../property/landlord_override_overlays.py | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index b08b230cf..636fa6191 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -198,6 +198,16 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay: ) +def natural_fuel_for(main_heating_value: str) -> Optional[int]: + """The RdSAP `main_fuel` code a heating archetype unambiguously implies — its + natural fuel (ADR-0041) — or None for an unmapped archetype. Exposed so a + plausibility check can compare it against a landlord `main_fuel` override.""" + code = _MAIN_HEATING_CODES.get(main_heating_value) + if code is None: + return None + return _natural_fuel_for(code) + + def main_heating_overlay_for( main_heating_value: str, building_part: int ) -> Optional[EpcSimulation]: diff --git a/repositories/property/landlord_override_overlays.py b/repositories/property/landlord_override_overlays.py index 7477cc785..5f47a3b39 100644 --- a/repositories/property/landlord_override_overlays.py +++ b/repositories/property/landlord_override_overlays.py @@ -28,6 +28,7 @@ values are live vs no-op before any write. from __future__ import annotations +import logging from typing import Callable, Optional from domain.epc.property_overlays.attribute_overlay import ( @@ -41,6 +42,7 @@ from domain.epc.property_overlays.glazing_overlay import glazing_overlay_for from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for from domain.epc.property_overlays.main_heating_system_overlay import ( main_heating_overlay_for, + natural_fuel_for, ) from domain.epc.property_overlays.water_heating_overlay import ( water_heating_overlay_for, @@ -50,6 +52,8 @@ from domain.epc.property_overlays.wall_type_overlay import wall_overlay_for from domain.modelling.simulation import EpcSimulation from repositories.property.property_overrides_reader import ResolvedPropertyOverrides +logger = logging.getLogger(__name__) + # Each override component maps its value (+ building part) to an overlay, or None # when the value isn't resolvable. Fabric (wall/roof) folds onto building parts; # property_type / built_form_type are whole-dwelling categorical corrections @@ -90,3 +94,37 @@ def overlays_from(overrides: ResolvedPropertyOverrides) -> list[EpcSimulation]: if overlay is not None: overlays.append(overlay) return overlays + + +def _override_value(overrides: ResolvedPropertyOverrides, component: str) -> Optional[str]: + for row in overrides.rows: + if row.override_component == component: + return row.override_value + return None + + +def flag_fuel_mismatch(overrides: ResolvedPropertyOverrides) -> None: + """Log (not raise) when a landlord `main_fuel` override contradicts the + `main_heating_system` archetype's natural fuel — a plausibility signal (e.g. + a gas fuel on an electric room heater). The override is still honoured (it + wins, ADR-0041); we only record the implausibility. Deferred handling.""" + heating_value = _override_value(overrides, "main_heating_system") + fuel_value = _override_value(overrides, "main_fuel") + if heating_value is None or fuel_value is None: + return + natural = natural_fuel_for(heating_value) + fuel_overlay = fuel_overlay_for(fuel_value, 0) + override_fuel = ( + fuel_overlay.heating.main_fuel_type + if fuel_overlay is not None and fuel_overlay.heating is not None + else None + ) + if natural is None or override_fuel is None: + return + if natural != override_fuel: + logger.warning( + "Landlord main_fuel %r contradicts the natural fuel of heating " + "system %r (override is honoured; flagged for review)", + fuel_value, + heating_value, + )