mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 2 of Bill Derivation. BillDerivation(fuel_rates).derive(breakdown) takes a delivered-energy breakdown (per-section EnergyLine(section, fuel, kwh) + exported_kwh) and produces a Bill: per-section kWh + cost, standing charges, SEG credit, and total. - Each end-use line billed at its fuel's unit rate. - Standing charge added ONCE per distinct fuel used (a meter, not an end use); off-gas fuels carry 0 so contribute nothing — no metered/unmetered special case. - SEG export credit subtracted. - Deterministic (ADR-0006); raises UnpricedFuel (via FuelRates) on an unpriced fuel (e.g. heat network) rather than billing at a wrong default. Pure domain — no calculator dependency; the SapResult->EnergyBreakdown adapter is slice 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from domain.fuel_rates.fuel import Fuel, UnpricedFuel
|
||
from domain.fuel_rates.fuel_rates import FuelRate, FuelRates
|
||
from domain.property_baseline.bill import BillSection, EnergyBreakdown, EnergyLine
|
||
from domain.property_baseline.bill_derivation import BillDerivation
|
||
|
||
|
||
def _rates() -> FuelRates:
|
||
return FuelRates(
|
||
period="test",
|
||
seg_export_p_per_kwh=15.0,
|
||
rates={
|
||
Fuel.MAINS_GAS: FuelRate(unit_rate_p_per_kwh=5.74, standing_charge_p_per_day=29.09),
|
||
Fuel.ELECTRICITY: FuelRate(unit_rate_p_per_kwh=24.67, standing_charge_p_per_day=57.21),
|
||
Fuel.OIL: FuelRate(unit_rate_p_per_kwh=9.16, standing_charge_p_per_day=0.0),
|
||
},
|
||
)
|
||
|
||
|
||
def test_derive_prices_a_single_gas_heating_line_with_its_standing_charge() -> None:
|
||
# Arrange — 10,000 kWh of mains-gas heating.
|
||
breakdown = EnergyBreakdown(
|
||
lines=[EnergyLine(section=BillSection.HEATING, fuel=Fuel.MAINS_GAS, kwh=10000.0)]
|
||
)
|
||
derivation = BillDerivation(_rates())
|
||
|
||
# Act
|
||
bill = derivation.derive(breakdown)
|
||
|
||
# Assert — heating = 10000 × 5.74p = £574; standing = 29.09p × 365 = £106.1785.
|
||
assert abs(bill.sections[BillSection.HEATING].cost_gbp - 574.0) <= 1e-9
|
||
assert abs(bill.standing_charges_gbp - 106.1785) <= 1e-9
|
||
assert abs(bill.total_gbp - 680.1785) <= 1e-9
|
||
|
||
|
||
def test_two_sections_on_the_same_fuel_share_one_standing_charge() -> None:
|
||
# Arrange — gas heating + gas hot water are one meter, not two.
|
||
breakdown = EnergyBreakdown(
|
||
lines=[
|
||
EnergyLine(section=BillSection.HEATING, fuel=Fuel.MAINS_GAS, kwh=8000.0),
|
||
EnergyLine(section=BillSection.HOT_WATER, fuel=Fuel.MAINS_GAS, kwh=2000.0),
|
||
]
|
||
)
|
||
|
||
# Act
|
||
bill = BillDerivation(_rates()).derive(breakdown)
|
||
|
||
# Assert — one gas standing charge (29.09p × 365 = £106.1785), not two.
|
||
assert abs(bill.standing_charges_gbp - 106.1785) <= 1e-9
|
||
assert abs(bill.sections[BillSection.HOT_WATER].cost_gbp - 114.8) <= 1e-9
|
||
|
||
|
||
def test_distinct_fuels_each_add_their_own_standing_charge() -> None:
|
||
# Arrange — gas heating + electric lighting: two meters.
|
||
breakdown = EnergyBreakdown(
|
||
lines=[
|
||
EnergyLine(section=BillSection.HEATING, fuel=Fuel.MAINS_GAS, kwh=8000.0),
|
||
EnergyLine(section=BillSection.LIGHTING, fuel=Fuel.ELECTRICITY, kwh=500.0),
|
||
]
|
||
)
|
||
|
||
# Act
|
||
bill = BillDerivation(_rates()).derive(breakdown)
|
||
|
||
# Assert — gas 29.09 + elec 57.21 = 86.30 p/day × 365 = £314.995.
|
||
assert abs(bill.standing_charges_gbp - 314.995) <= 1e-9
|
||
|
||
|
||
def test_exported_pv_is_credited_at_the_seg_rate() -> None:
|
||
# Arrange — 1000 kWh exported at 15p, against a single gas heating line.
|
||
breakdown = EnergyBreakdown(
|
||
lines=[EnergyLine(section=BillSection.HEATING, fuel=Fuel.MAINS_GAS, kwh=10000.0)],
|
||
exported_kwh=1000.0,
|
||
)
|
||
|
||
# Act
|
||
bill = BillDerivation(_rates()).derive(breakdown)
|
||
|
||
# Assert — SEG credit £150 subtracted from the £680.1785 gross.
|
||
assert abs(bill.seg_credit_gbp - 150.0) <= 1e-9
|
||
assert abs(bill.total_gbp - 530.1785) <= 1e-9
|
||
|
||
|
||
def test_an_unpriced_fuel_in_a_line_raises() -> None:
|
||
# Arrange — a heat-network line; the snapshot prices no heat network.
|
||
breakdown = EnergyBreakdown(
|
||
lines=[EnergyLine(section=BillSection.HEATING, fuel=Fuel.HEAT_NETWORK, kwh=5000.0)]
|
||
)
|
||
|
||
# Act / Assert
|
||
with pytest.raises(UnpricedFuel):
|
||
BillDerivation(_rates()).derive(breakdown)
|