Model/tests/domain/billing/test_bill_derivation.py
Khalim Conn-Kowlessar ced6287baa refactor(billing): relocate Bill Derivation to domain/billing/ (cross-stage)
Bill / EnergyBreakdown / BillDerivation / sap_fuel were under
domain/property_baseline/ only because Baseline was built first. The Modelling
stage now needs them too, so move them (and their tests) to a neutral
domain/billing/ — Fuel/FuelRates already live in the shared domain/fuel_rates/.
Avoids a modelling -> property_baseline cross-stage import and a package name
that wrongly implies ownership (ADR-0011, ADR-0014 amendment). Pure git mv +
import rewrite across 10 files; 40 billing/baseline/repo tests pass, pyright
strict clean. CONTEXT.md Bill Derivation location updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:19:23 +00:00

95 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.billing.bill import BillSection, EnergyBreakdown, EnergyLine
from domain.billing.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)