mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
`table_32.unit_price_p_per_kwh` silently returned the mains-gas default (3.48 p/kWh) for any fuel code it could not resolve to a Table 32 price or a translatable gov-API enum. An unhandled fuel billed at the gas rate mis-costs the dwelling (same failure mode as the dual-main wood-vs-electric over-cost). Raise `UnpricedFuelCode` (new, mirrors MissingMainFuelType / UnmappedSapCode) so the gap surfaces at the price boundary. `None` (no fuel lodged) still defaults — callers resolve "no system" upstream. 0 corpus impact: all 1000 certs compute (every lodged fuel resolves), so this is a forward guard against future/unmapped fuels. Unit pin added; existing None-default test docstring tightened. pyright not installed locally — strict type gate not run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.7 KiB
Python
86 lines
3.7 KiB
Python
"""Calculator-side strict-raise exception types.
|
|
|
|
Shared across `domain/sap10_calculator/` modules so any cascade-dispatch
|
|
helper can raise a consistent exception when it encounters a SAP/Table
|
|
code outside its dispatch dict. Mirrors the mapper-side
|
|
`UnmappedApiCode` / `UnmappedElmhurstLabel` pattern at
|
|
`datatypes/epc/domain/mapper.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class UnmappedSapCode(ValueError):
|
|
"""A SAP/Table integer code lodged on the cert that the calculator
|
|
does not yet know how to translate to a dispatch result.
|
|
|
|
Raised by strict cascade-dispatch helpers (Table 4e control codes,
|
|
Table 4d emitter codes, Appendix M PV pitch / overshading, meter →
|
|
tariff, Table 12c heat-network DLF age band, Table 11 secondary
|
|
heating fraction by category, etc.) to surface spec-coverage gaps
|
|
at the cascade boundary instead of silently defaulting to a
|
|
fallback value.
|
|
|
|
Distinguish "lodging absent" (code is None / 0 / "" — cascade
|
|
default OK, spec "assume as-built" applies) from "lodging present
|
|
but unmapped" (raise — fixture exposes a dispatch-dict gap that
|
|
needs an entry).
|
|
"""
|
|
|
|
def __init__(self, field: str, value: object) -> None:
|
|
super().__init__(
|
|
f"unmapped SAP code in {field}: {value!r}; "
|
|
f"add an entry to the corresponding cascade dispatch dict"
|
|
)
|
|
self.field = field
|
|
self.value = value
|
|
|
|
|
|
class MissingMainFuelType(ValueError):
|
|
"""The cascade was asked to resolve `MainHeatingDetail.main_fuel_type`
|
|
but the mapper produced no usable SAP fuel code (None / empty string
|
|
/ unmapped string label).
|
|
|
|
Unlike the Table 4d/4e dispatch sites where "absent" maps to a spec-
|
|
blessed "assume as-built" default, heating fuel has no defensible
|
|
default: silently routing to mains gas produces a misleading cascade
|
|
output where cost may happen to be close but CO2 / PE / efficiency
|
|
are completely wrong for the actual heating system. The fix is
|
|
upstream in the mapper — extract the fuel from the appropriate
|
|
Summary / EPC field, or derive it from `sap_main_heating_code`
|
|
via SAP 10.2 Table 4a/4b/4f.
|
|
"""
|
|
|
|
def __init__(self, value: object, sap_main_heating_code: object) -> None:
|
|
super().__init__(
|
|
f"MainHeatingDetail.main_fuel_type is not resolvable to a SAP "
|
|
f"fuel code (got {value!r}); sap_main_heating_code="
|
|
f"{sap_main_heating_code!r}. Fix the mapper to populate "
|
|
f"main_fuel_type as an int via Summary / EPC fields or via "
|
|
f"SAP 10.2 Table 4a/4b/4f derivation from the SAP code."
|
|
)
|
|
self.value = value
|
|
self.sap_main_heating_code = sap_main_heating_code
|
|
|
|
|
|
class UnpricedFuelCode(ValueError):
|
|
"""A concrete (non-None) fuel code reached the Table 32 unit-price
|
|
lookup but resolves to neither a Table 32 price code nor a gov-API
|
|
enum that translates to one.
|
|
|
|
Raised instead of silently falling back to the mains-gas price
|
|
(3.48 p/kWh): an unrecognised fuel billed at the gas rate produces a
|
|
misleading cost (e.g. the dual-main wood-vs-electric over-cost on
|
|
cert 10032957680). Surface the gap at the price boundary so the fuel
|
|
is either added to `UNIT_PRICE_P_PER_KWH` / `API_FUEL_TO_TABLE_32`
|
|
or canonicalised upstream. A `None` code (no fuel lodged) is NOT an
|
|
error — callers resolve "no system" before pricing.
|
|
"""
|
|
|
|
def __init__(self, fuel_code: object) -> None:
|
|
super().__init__(
|
|
f"no Table 32 unit price for fuel code {fuel_code!r}; add it to "
|
|
f"UNIT_PRICE_P_PER_KWH or API_FUEL_TO_TABLE_32, or canonicalise "
|
|
f"the code upstream before pricing"
|
|
)
|
|
self.fuel_code = fuel_code
|