diff --git a/tests/domain/billing/test_off_peak_bill_integration.py b/tests/domain/billing/test_off_peak_bill_integration.py new file mode 100644 index 00000000..69d06148 --- /dev/null +++ b/tests/domain/billing/test_off_peak_bill_integration.py @@ -0,0 +1,77 @@ +"""End-to-end regression for the Off-Peak Meter day/night bill split (ADR-0014 +2026-06-24 amendment). + +Drives a real off-peak (Economy-7) storage-heating cert through the whole chain +— `cert_to_inputs` → `calculate_sap_from_inputs` → `EnergyBreakdown.from_sap_result` +→ `BillDerivation.derive` against the committed Fuel Rates snapshot. Before the +amendment this raised `UnpricedFuel` ("no rate for fuel ELECTRICITY_OFF_PEAK"), +which aborted the `modelling_e2e` batch (e.g. property 717572). It must now price +the dwelling day/night instead of crashing. +""" + +from __future__ import annotations + +from dataclasses import replace + +from domain.sap10_ml.tests._fixtures import ( + make_building_part, + make_minimal_sap10_epc, + make_sap_heating, +) +from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingDetail +from domain.sap10_calculator.calculator import calculate_sap_from_inputs +from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs +from domain.fuel_rates.fuel import Fuel +from domain.billing.bill import BillSection, EnergyBreakdown +from domain.billing.bill_derivation import BillDerivation +from repositories.fuel_rates.fuel_rates_static_file_repository import ( + FuelRatesStaticFileRepository, +) + +_TYPICAL_TFA_M2 = 60.0 + + +def _off_peak_storage_epc() -> EpcPropertyData: + # Integrated storage heaters (SAP code 408, Table 12a Grid 1 high-rate + # fraction 0.20) on a Dual / Economy-7 (7-hour) meter. + main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=29, # electricity + heat_emitter_type=1, + emitter_temperature=1, + main_heating_control=2401, + main_heating_category=7, # electric storage heaters + sap_main_heating_code=408, + ) + base = make_minimal_sap10_epc( + total_floor_area_m2=_TYPICAL_TFA_M2, + habitable_rooms_count=4, + country_code="ENG", + sap_building_parts=[make_building_part(construction_age_band="E")], + sap_heating=make_sap_heating(main_heating_details=[main]), + ) + return replace( + base, + sap_energy_source=replace(base.sap_energy_source, meter_type="1"), + ) + + +def test_off_peak_storage_dwelling_bills_heating_day_night_not_crashes() -> None: + # Arrange — score the off-peak cert and price it at the committed snapshot. + result = calculate_sap_from_inputs(cert_to_inputs(_off_peak_storage_epc())) + rates = FuelRatesStaticFileRepository().get_current() + + # Act — the previously-crashing path: off-peak electricity reaches the bill. + breakdown = EnergyBreakdown.from_sap_result(result) + bill = BillDerivation(rates).derive(breakdown) + + # Assert — heating bills on the off-peak carrier at the 0.20-day/0.80-night + # blend (0.20×29.73 + 0.80×13.89 = 17.058 p/kWh), not a crash and not the + # flat standard rate (24.67 p) or pure night rate (13.89 p). + heating = bill.sections[BillSection.HEATING] + assert heating.kwh > 0.0 + implied_rate_p = heating.cost_gbp / heating.kwh * 100.0 + assert abs(implied_rate_p - 17.058) <= 1e-6 + # The off-peak meter contributes one standing charge (56.99 p/day). + assert any(line.fuel is Fuel.ELECTRICITY_OFF_PEAK for line in breakdown.lines) + assert bill.total_gbp > 0.0