mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
feat(baseline): sap_code_to_fuel normalizes via the calculator's own helper
The fuel codes the calculator now puts on SapResult are its own codes — raw gov-API enums or already-Table-32, depending on the source mapper (ADR-0015). sap_code_to_fuel now runs the code through table_32.to_table_32_code (promoted from private _to_table_32_code) — T32-first, then API-translate, the SAME normalization the calculator's pricing/CO2 helpers use — before the Table-32 -> Fuel dispatch, so the bill's carrier matches what the calculator billed (incl. the API/T32 collision codes, e.g. 20 = wood-logs not heat-net). Falls back to the raw code for billing fuels the price table omits (the 41-58 heat-network range), which resolve to HEAT_NETWORK -> UnpricedFuel — stricter than, and intentionally divergent from, the calculator's lossy default-to-mains-gas for an unpriced code (ADR-0014 §5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3c0ac98122
commit
d559298de2
3 changed files with 42 additions and 13 deletions
|
|
@ -4,13 +4,13 @@ from typing import Final
|
||||||
|
|
||||||
from domain.fuel_rates.fuel import Fuel
|
from domain.fuel_rates.fuel import Fuel
|
||||||
from domain.sap10_calculator.exceptions import UnmappedSapCode
|
from domain.sap10_calculator.exceptions import UnmappedSapCode
|
||||||
|
from domain.sap10_calculator.tables.table_32 import to_table_32_code
|
||||||
|
|
||||||
# SAP 10.2 / Table 32 fuel code -> canonical billing Fuel (ADR-0014). Bounded to
|
# Table 32 fuel code -> canonical billing Fuel (ADR-0014). Bounded to the ~47
|
||||||
# the ~47 Table 32 fuel codes (the keys of `table_12.UNIT_PRICE_P_PER_KWH`) — the
|
# Table 32 fuel codes (the keys of `UNIT_PRICE_P_PER_KWH`) — the carrier, NOT the
|
||||||
# carrier, NOT the PCDB product, so a thousand PCDB heat pumps all share one code.
|
# PCDB product, so a thousand PCDB heat pumps all share one code. An unmapped code
|
||||||
# Input is a normalised Table 32 fuel code (the calculator sets `main_fuel_type`
|
# raises `UnmappedSapCode` rather than guessing — a bounded, self-surfacing
|
||||||
# to Table 32 codes); an unmapped code raises `UnmappedSapCode` rather than
|
# backlog [[reference-unmapped-sap-code]].
|
||||||
# guessing — a bounded, self-surfacing backlog [[reference-unmapped-sap-code]].
|
|
||||||
_CODE_TO_FUEL: Final[dict[int, Fuel]] = {
|
_CODE_TO_FUEL: Final[dict[int, Fuel]] = {
|
||||||
**dict.fromkeys([1, 7], Fuel.MAINS_GAS), # mains gas, grid biogas
|
**dict.fromkeys([1, 7], Fuel.MAINS_GAS), # mains gas, grid biogas
|
||||||
**dict.fromkeys([2, 3, 5, 9], Fuel.LPG),
|
**dict.fromkeys([2, 3, 5, 9], Fuel.LPG),
|
||||||
|
|
@ -29,13 +29,26 @@ _CODE_TO_FUEL: Final[dict[int, Fuel]] = {
|
||||||
|
|
||||||
|
|
||||||
def sap_code_to_fuel(code: int) -> Fuel:
|
def sap_code_to_fuel(code: int) -> Fuel:
|
||||||
"""Map a SAP 10.2 / Table 32 fuel code to its canonical billing Fuel.
|
"""Map one of the calculator's per-end-use fuel codes to its billing Fuel.
|
||||||
|
|
||||||
|
The code may be a raw gov-API `main_fuel_type` enum or an already-Table-32
|
||||||
|
code depending on the source mapper (until [[adr-0015]] normalizes the cert),
|
||||||
|
so it is first run through the calculator's own ``to_table_32_code`` —
|
||||||
|
T32-first, then API-translate — the **same** normalization the calculator's
|
||||||
|
pricing/CO2 helpers use, so the bill's carrier matches what the calculator
|
||||||
|
billed. The normalized Table-32 code is then dispatched to a billing Fuel.
|
||||||
|
|
||||||
Raises ``UnmappedSapCode`` on a code with no single billing carrier — e.g.
|
Raises ``UnmappedSapCode`` on a code with no single billing carrier — e.g.
|
||||||
dual fuel (10) or the grid-export codes (36/60), which are not an end use's
|
dual fuel (10) or the grid-export codes (36/60), which are not an end use's
|
||||||
input fuel.
|
input fuel.
|
||||||
"""
|
"""
|
||||||
fuel = _CODE_TO_FUEL.get(code)
|
# Normalize to a Table-32 code; fall back to the raw code for billing fuels
|
||||||
|
# the price table does not carry (the 41-58 heat-network range — `to_table_32_
|
||||||
|
# code` returns None there, but they still resolve to HEAT_NETWORK and so to
|
||||||
|
# UnpricedFuel, which is stricter — and correct — than the calculator's
|
||||||
|
# lossy default-to-mains-gas for an unpriced code).
|
||||||
|
normalized = to_table_32_code(code)
|
||||||
|
fuel = _CODE_TO_FUEL.get(normalized if normalized is not None else code)
|
||||||
if fuel is None:
|
if fuel is None:
|
||||||
raise UnmappedSapCode("fuel_code", code)
|
raise UnmappedSapCode("fuel_code", code)
|
||||||
return fuel
|
return fuel
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ _OFF_PEAK_STANDING_CODE: Final[dict[Tariff, int]] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _to_table_32_code(fuel_code: Optional[int]) -> Optional[int]:
|
def to_table_32_code(fuel_code: Optional[int]) -> Optional[int]:
|
||||||
"""Normalise a fuel code (Table 32 or API enum) to its Table 32 form."""
|
"""Normalise a fuel code (Table 32 or API enum) to its Table 32 form."""
|
||||||
if fuel_code is None:
|
if fuel_code is None:
|
||||||
return None
|
return None
|
||||||
|
|
@ -204,7 +204,7 @@ def _to_table_32_code(fuel_code: Optional[int]) -> Optional[int]:
|
||||||
|
|
||||||
|
|
||||||
def _is_gas_code(fuel_code: Optional[int]) -> bool:
|
def _is_gas_code(fuel_code: Optional[int]) -> bool:
|
||||||
code = _to_table_32_code(fuel_code)
|
code = to_table_32_code(fuel_code)
|
||||||
return code is not None and code in _GAS_FUEL_CODES
|
return code is not None and code in _GAS_FUEL_CODES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -219,9 +219,9 @@ def is_electric_fuel_code(fuel_code: Optional[int]) -> bool:
|
||||||
silently mis-classifies as electric. The S0380.135 EES-code →
|
silently mis-classifies as electric. The S0380.135 EES-code →
|
||||||
Table 32 mapper lookups set `main_fuel_type` to Table 32 codes
|
Table 32 mapper lookups set `main_fuel_type` to Table 32 codes
|
||||||
(BDI → 10 = dual fuel), so the literal-set checks fail loudly here
|
(BDI → 10 = dual fuel), so the literal-set checks fail loudly here
|
||||||
unless normalised through `_to_table_32_code` first.
|
unless normalised through `to_table_32_code` first.
|
||||||
"""
|
"""
|
||||||
code = _to_table_32_code(fuel_code)
|
code = to_table_32_code(fuel_code)
|
||||||
return code is not None and code in _ELECTRIC_FUEL_CODES
|
return code is not None and code in _ELECTRIC_FUEL_CODES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -235,7 +235,7 @@ def is_liquid_fuel_code(fuel_code: Optional[int]) -> bool:
|
||||||
LPG is treated as GAS by Table 4f (separate "Gas boiler" row,
|
LPG is treated as GAS by Table 4f (separate "Gas boiler" row,
|
||||||
45 kWh/yr) — `is_liquid_fuel_code` returns False for LPG codes.
|
45 kWh/yr) — `is_liquid_fuel_code` returns False for LPG codes.
|
||||||
"""
|
"""
|
||||||
code = _to_table_32_code(fuel_code)
|
code = to_table_32_code(fuel_code)
|
||||||
return code is not None and code in _LIQUID_FUEL_CODES
|
return code is not None and code in _LIQUID_FUEL_CODES
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,22 @@ def test_table_32_codes_map_to_their_billing_fuel(code: int, fuel: Fuel) -> None
|
||||||
assert sap_code_to_fuel(code) == fuel
|
assert sap_code_to_fuel(code) == fuel
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("api_code", "fuel"),
|
||||||
|
[
|
||||||
|
(26, Fuel.MAINS_GAS), # gov-API mains-gas enum -> Table 32 code 1
|
||||||
|
(0, Fuel.ELECTRICITY), # API "electricity" -> Table 32 code 30
|
||||||
|
(25, Fuel.HEAT_NETWORK), # API community heat -> Table 32 code 41
|
||||||
|
(14, Fuel.COAL), # API house coal -> Table 32 code 11
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_raw_api_fuel_codes_normalize_before_mapping(api_code: int, fuel: Fuel) -> None:
|
||||||
|
# Arrange — the calculator may carry a raw gov-API fuel code (not yet a Table
|
||||||
|
# 32 code); sap_code_to_fuel normalizes via the calculator's own helper first.
|
||||||
|
# Act / Assert
|
||||||
|
assert sap_code_to_fuel(api_code) == fuel
|
||||||
|
|
||||||
|
|
||||||
def test_an_unmapped_code_raises_rather_than_guessing() -> None:
|
def test_an_unmapped_code_raises_rather_than_guessing() -> None:
|
||||||
# Arrange — code 10 (dual fuel) has no single billing fuel.
|
# Arrange — code 10 (dual fuel) has no single billing fuel.
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue