feat(overrides): GREEN — BoilerEfficiencyBand enum + SEDBUK band parser

BoilerEfficiencyBand (A-G + UNKNOWN sentinel) and boiler_efficiency_band_guard,
the shared deterministic parser for the fifth Heating Companion (ADR-0068).
UNKNOWN is never persisted; absence of a band -> no value -> Table 4b default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-29 16:04:06 +00:00
parent 4844d030d7
commit b41c25e756
2 changed files with 74 additions and 0 deletions

View file

@ -0,0 +1,35 @@
from enum import Enum
class BoilerEfficiencyBand(Enum):
"""The SEDBUK A-G efficiency band a landlord carries in the Boiler Landlord
Description (``Boiler: C rated Combi``) the fifth Heating Companion
(ADR-0068).
An *attribute* of a ``main_heating_system`` override, orthogonal to the
``MainHeatingSystemType`` archetype: the archetype names the *system*, the
band names *how well it burns*. It is deliberately NOT a per-band archetype
that enum is FE-owned ([[main-heating-system-pgenum-is-fe-owned]]), and 7
bands x N boiler types would explode a cross-team contract.
Each band resolves (via the main-heating overlay) to a ``(winter, summer)``
seasonal-efficiency pair the calculator consumes ahead of the Table 4b code
default applied to the SEDBUK-rated wet boilers only (gas/LPG/biogas and
oil); solid-fuel / electric / CPSU boilers carry an intrinsic fixed
efficiency and take no band.
``UNKNOWN`` is the classifier's "no band recognised" sentinel (a plain
boiler, or a non-boiler heating system). It is never persisted as an
override absence of a band is modelled as *no value*, so the boiler keeps
its Table 4b code default mirroring how the finaliser drops the other
``UNKNOWN`` classifier outputs.
"""
A = "A"
B = "B"
C = "C"
D = "D"
E = "E"
F = "F"
G = "G"
UNKNOWN = "Unknown"

View file

@ -0,0 +1,39 @@
"""Parse the SEDBUK A-G efficiency band out of a Landlord "Heating" description.
The single source of truth for the band, shared by three call sites so they
cannot drift (mirroring how ``main_heating_guard`` backs both the live
classifier path and ``reclassify_main_heating``):
* the modelling read path (``overlays_from`` the main-heating overlay slot),
* the ``boiler_efficiency_band`` backfill,
* the live classifier column (as its deterministic guard).
The Landlord "Heating" column carries the band as ``Boiler: <A-G> rated
<Combi|Regular Boiler|NA>``. A cell can name a second system after ``System 2:``;
the primary (system 1) band leads the cell, so the first ``Boiler: <letter>
rated`` occurrence is the one that scores the dwelling's main system. A
description with no band phrasing (a plain boiler, or a non-boiler heating
system) returns ``None`` absence of a band, not a guess.
"""
from __future__ import annotations
import re
from typing import Optional
from domain.epc.property_overrides.boiler_efficiency_band import BoilerEfficiencyBand
# `Boiler: <A-G> rated` — the SEDBUK band phrasing. Case-insensitive (the
# backfill lowercases the stored description). `\b` after the letter rejects a
# stray "H rated" or a multi-letter token; the leading "Boiler:" anchors it so a
# bare "A rated" elsewhere in free text cannot match.
_BAND_RE = re.compile(r"boiler:\s*([a-g])\s+rated\b", re.IGNORECASE)
def boiler_efficiency_band_guard(description: str) -> Optional[BoilerEfficiencyBand]:
"""The SEDBUK band of the primary (system 1) boiler in a Landlord "Heating"
description, or ``None`` when the description carries no band phrasing."""
match = _BAND_RE.search(description)
if match is None:
return None
return BoilerEfficiencyBand(match.group(1).upper())