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