mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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>
95 lines
3.4 KiB
Python
95 lines
3.4 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.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)
|