from __future__ import annotations import logging from typing import Optional import pytest from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.property_baseline.calculator_rebaseliner import CalculatorRebaseliner from domain.property_baseline.performance import Performance from domain.sap10_calculator.calculator import SapCalculator, SapResult from domain.sap10_calculator.exceptions import UnmappedSapCode def _epc(*, sap_version: Optional[float]) -> EpcPropertyData: epc = object.__new__(EpcPropertyData) epc.sap_version = sap_version return epc def _lodged() -> Performance: return Performance( sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180 ) def _sap_result( *, sap_score: int = 72, co2_kg_per_yr: float = 1800.0, primary_energy_kwh_per_m2: float = 180.0, ) -> SapResult: return SapResult( sap_score=sap_score, sap_score_continuous=float(sap_score), ecf=0.0, total_fuel_cost_gbp=0.0, co2_kg_per_yr=co2_kg_per_yr, space_heating_kwh_per_yr=0.0, space_cooling_kwh_per_yr=0.0, fabric_energy_efficiency_kwh_per_m2_yr=0.0, main_heating_fuel_kwh_per_yr=0.0, main_2_heating_fuel_kwh_per_yr=0.0, secondary_heating_fuel_kwh_per_yr=0.0, space_cooling_fuel_kwh_per_yr=0.0, hot_water_kwh_per_yr=0.0, pumps_fans_kwh_per_yr=0.0, lighting_kwh_per_yr=0.0, appliances_kwh_per_yr=0.0, cooking_kwh_per_yr=0.0, main_heating_fuel_code=None, main_2_heating_fuel_code=None, secondary_heating_fuel_code=None, hot_water_fuel_code=None, pv_exported_kwh_per_yr=0.0, primary_energy_kwh_per_yr=0.0, primary_energy_kwh_per_m2=primary_energy_kwh_per_m2, monthly=(), intermediate={}, ) class _StubCalculator(SapCalculator): def __init__(self, result: SapResult) -> None: self._result = result def calculate(self, epc: EpcPropertyData) -> SapResult: return self._result def test_pre_10_2_cert_is_rebaselined_to_the_calculator_output() -> None: # Arrange — a SAP 10.0 cert: lodged figures are a superseded methodology, so # the calculator's output becomes Effective Performance (ADR-0013 amendment). sap_result = _sap_result( sap_score=70, co2_kg_per_yr=1900.0, primary_energy_kwh_per_m2=185.4 ) calculator = _StubCalculator(sap_result) rebaseliner = CalculatorRebaseliner(calculator) epc = _epc(sap_version=10.0) # Act result = rebaseliner.rebaseline( property_id=10, effective_epc=epc, lodged=_lodged() ) # Assert — calculated Performance: band from the score, CO2 kg->t, PEUI rounded; # the SapResult rides on the result for Bill Derivation. assert result.effective == Performance( sap_score=70, epc_band=Epc.C, co2_emissions=1.9, primary_energy_intensity=185 ) assert result.reason == "pre_sap10" assert result.sap_result is sap_result def test_a_10_2_cert_keeps_the_lodged_figures() -> None: # Arrange — a SAP 10.2 cert: the API's lodged figures are on-target, so they # stand; the calculator runs only to validate. sap_result = _sap_result(sap_score=72) calculator = _StubCalculator(sap_result) rebaseliner = CalculatorRebaseliner(calculator) epc = _epc(sap_version=10.2) # Act result = rebaseliner.rebaseline( property_id=10, effective_epc=epc, lodged=_lodged() ) # Assert — lodged kept as effective, but the SapResult still rides along for # Bill Derivation (the bill prices it regardless of which figures win). assert result.effective == _lodged() assert result.reason == "none" assert result.sap_result is sap_result def test_a_10_2_cert_logs_divergence_when_the_calculator_disagrees( caplog: pytest.LogCaptureFixture, ) -> None: # Arrange — calculated SAP 76 vs lodged 72 (> 0.5 out) on a 10.2 cert. calculator = _StubCalculator(_sap_result(sap_score=76)) rebaseliner = CalculatorRebaseliner(calculator) epc = _epc(sap_version=10.2) # Act with caplog.at_level(logging.WARNING): rebaseliner.rebaseline(property_id=42, effective_epc=epc, lodged=_lodged()) # Assert — a divergence warning, tagged with property_id + sap_version. assert len(caplog.records) == 1 message = caplog.records[0].getMessage() assert "sap_score" in message assert "property_id=42" in message assert "sap_version=10.2" in message def test_a_calculator_raise_propagates_and_aborts() -> None: # Arrange — the calculator is load-bearing, so a raise is not swallowed. class _Raising(SapCalculator): def calculate(self, epc: EpcPropertyData) -> SapResult: raise UnmappedSapCode("heat_emitter_type", 99) rebaseliner = CalculatorRebaseliner(_Raising()) epc = _epc(sap_version=10.0) # Act / Assert with pytest.raises(UnmappedSapCode): rebaseliner.rebaseline(property_id=10, effective_epc=epc, lodged=_lodged())