"""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