mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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.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
|
||||
# the ~47 Table 32 fuel codes (the keys of `table_12.UNIT_PRICE_P_PER_KWH`) — the
|
||||
# carrier, NOT the PCDB product, so a thousand PCDB heat pumps all share one code.
|
||||
# Input is a normalised Table 32 fuel code (the calculator sets `main_fuel_type`
|
||||
# to Table 32 codes); an unmapped code raises `UnmappedSapCode` rather than
|
||||
# guessing — a bounded, self-surfacing backlog [[reference-unmapped-sap-code]].
|
||||
# Table 32 fuel code -> canonical billing Fuel (ADR-0014). Bounded to the ~47
|
||||
# Table 32 fuel codes (the keys of `UNIT_PRICE_P_PER_KWH`) — the carrier, NOT the
|
||||
# PCDB product, so a thousand PCDB heat pumps all share one code. An unmapped code
|
||||
# raises `UnmappedSapCode` rather than guessing — a bounded, self-surfacing
|
||||
# backlog [[reference-unmapped-sap-code]].
|
||||
_CODE_TO_FUEL: Final[dict[int, Fuel]] = {
|
||||
**dict.fromkeys([1, 7], Fuel.MAINS_GAS), # mains gas, grid biogas
|
||||
**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:
|
||||
"""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.
|
||||
dual fuel (10) or the grid-export codes (36/60), which are not an end use's
|
||||
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:
|
||||
raise UnmappedSapCode("fuel_code", code)
|
||||
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."""
|
||||
if fuel_code is 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:
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -219,9 +219,9 @@ def is_electric_fuel_code(fuel_code: Optional[int]) -> bool:
|
|||
silently mis-classifies as electric. The S0380.135 EES-code →
|
||||
Table 32 mapper lookups set `main_fuel_type` to Table 32 codes
|
||||
(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
|
||||
|
||||
|
||||
|
|
@ -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,
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
@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:
|
||||
# Arrange — code 10 (dual fuel) has no single billing fuel.
|
||||
# Act / Assert
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue