All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 35s
AER's Consumer Data Right unitPrice fields are GST-exclusive, confirmed by comparing a real, currently-effective EnergyAustralia contract's own GST-inclusive rates against this plan's cached AER data: usage rates and the supply charge all matched the cached value multiplied by exactly 1.1 (four independent figures). A real AGL bill confirmed the same convention separately (excl-GST line items, GST added back as one combined amount at the end). This understated every one of the ~700+ cached comparison plans' costs by ~10% relative to manually-entered current plans (which were correctly GST-inclusive, taken from real bills), biasing every "savings vs current" figure across the board. Feed-in/solar buy-back rates are the exception -- confirmed GST-free in that same contract's own terms -- so the multiplier applies only to usage rates and the supply charge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
5.5 KiB
Python
118 lines
5.5 KiB
Python
import json
|
|
from pathlib import Path
|
|
from zoneinfo import ZoneInfo
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
from app.costing.aer_mapper import GST_MULTIPLIER, map_aer_plan
|
|
from app.costing.engine import IntervalReading, evaluate
|
|
from app.costing.tariff_models import find_usage_coverage_gaps
|
|
|
|
FIXTURES = Path(__file__).parent
|
|
|
|
BNE = ZoneInfo("Australia/Brisbane")
|
|
|
|
|
|
def _load(name: str) -> dict:
|
|
return json.loads((FIXTURES / name).read_text())
|
|
|
|
|
|
def test_maps_real_agl_time_of_use_plan():
|
|
fixture = _load("sample_agl_qld_plan.json")
|
|
plan = map_aer_plan(fixture["list_entry"], fixture["detail"])
|
|
|
|
assert plan.modelable
|
|
assert plan.retailer == "AGL"
|
|
assert "Energex" in plan.distributors
|
|
assert len(plan.usage_components) == 3 # peak, off-peak, shoulder
|
|
assert plan.supply_charge_c_per_day == pytest.approx(1.536 * 100 * GST_MULTIPLIER)
|
|
|
|
labels = {c.label for c in plan.usage_components}
|
|
assert {"Time of Use Peak", "Time of Use Off Peak", "Time of Use Shoulder"} <= labels
|
|
|
|
# The three TOU windows (4pm-9pm, 9pm-11am, 11am-4pm) should fully
|
|
# partition every hour with no gap or overlap.
|
|
assert find_usage_coverage_gaps(plan.usage_components) == []
|
|
|
|
# Retailer FiT (2c) should be picked up; the legacy government premium
|
|
# scheme (44c) must be excluded since it's not available to a plan comparison.
|
|
assert len(plan.feed_in_tariffs) == 1
|
|
assert plan.feed_in_tariffs[0].rate_c_per_kwh == pytest.approx(2.0)
|
|
|
|
# Sanity-check the engine can evaluate this real plan end-to-end.
|
|
readings = [
|
|
IntervalReading(ts=datetime(2026, 1, 1, 18, tzinfo=BNE), import_kwh=2, export_kwh=0),
|
|
IntervalReading(ts=datetime(2026, 1, 1, 8, tzinfo=BNE), import_kwh=1, export_kwh=3),
|
|
]
|
|
result = evaluate(plan, readings)
|
|
assert result.total_dollars > 0
|
|
# Controlled load (separately metered) and the sign-up incentive are
|
|
# real features of this plan, informational rather than costing errors.
|
|
assert any("controlled load" in note for note in result.modeling_notes)
|
|
assert any("sign-up incentive" in note for note in result.modeling_notes)
|
|
|
|
|
|
def test_time_varying_feed_in_tariff_only_credits_its_own_window():
|
|
# Regression test: solarFeedInTariff's timeVaryingTariffs blocks key their
|
|
# windows as "timeVariations", not "timeOfUse" (the key used by the
|
|
# unrelated usage-side timeOfUseRates shape) -- using the wrong key made
|
|
# every time-varying FiT component's time_windows empty, which matches
|
|
# ALL day per TimeWindow semantics, so the first-listed component (here,
|
|
# the 28c PEAK rate) silently credited 100% of exported energy at the
|
|
# peak rate regardless of when it was actually exported.
|
|
fixture = _load("sample_origin_qld_battery_plan.json")
|
|
plan = map_aer_plan(fixture["list_entry"], fixture["detail"])
|
|
|
|
assert len(plan.feed_in_tariffs) == 2
|
|
for component in plan.feed_in_tariffs:
|
|
assert component.time_windows, f"{component.label} has no time windows and will match all day"
|
|
|
|
# 10 kWh exported at 9am (off-peak) must be credited at 3c, not 28c.
|
|
readings = [IntervalReading(ts=datetime(2026, 1, 1, 9, tzinfo=BNE), import_kwh=0, export_kwh=10)]
|
|
result = evaluate(plan, readings)
|
|
off_peak = next(c for c in result.feed_in_breakdown if c.label == "OFF PEAK")
|
|
peak = next(c for c in result.feed_in_breakdown if c.label == "PEAK")
|
|
assert off_peak.kwh == pytest.approx(10.0)
|
|
assert peak.kwh == pytest.approx(0.0)
|
|
|
|
|
|
def test_two_tier_volume_capped_feed_in_maps_to_tiered_feed_in():
|
|
# Regression test: some plans (e.g. real-world EnergyAustralia "Solar
|
|
# Max") represent a daily-volume-based feed-in tariff as two separate
|
|
# solarFeedInTariff singleTariff entries -- a capped tier carrying a
|
|
# "volume" threshold, and an uncapped "thereafter" tier -- rather than
|
|
# multiple rates within one entry. Previously both were mapped as
|
|
# independent flat-rate FeedInComponents with no way to distinguish
|
|
# which kWh belongs to which tier, so the first-listed (higher, capped)
|
|
# rate silently won every match and credited 100% of exported energy
|
|
# at the premium rate.
|
|
fixture = _load("sample_energyaustralia_qld_tiered_feedin_plan.json")
|
|
plan = map_aer_plan(fixture["list_entry"], fixture["detail"])
|
|
|
|
assert plan.feed_in_tariffs == []
|
|
assert plan.tiered_feed_in is not None
|
|
assert plan.tiered_feed_in.daily_threshold_kwh == pytest.approx(10.0)
|
|
assert plan.tiered_feed_in.rate_below_c_per_kwh == pytest.approx(8.0)
|
|
assert plan.tiered_feed_in.rate_above_c_per_kwh == pytest.approx(4.0)
|
|
|
|
# 15 kWh exported in one day: 10 kWh at 8c + 5 kWh at 4c.
|
|
readings = [IntervalReading(ts=datetime(2026, 1, 1, 12, tzinfo=BNE), import_kwh=0, export_kwh=15)]
|
|
result = evaluate(plan, readings)
|
|
feed_in_credit = sum(c.cost_dollars for c in result.feed_in_breakdown)
|
|
assert feed_in_credit == pytest.approx(-(10 * 0.08 + 5 * 0.04))
|
|
|
|
|
|
def test_maps_real_energyaustralia_plan_with_guaranteed_discount():
|
|
fixture = _load("sample_ea_qld_plan.json")
|
|
plan = map_aer_plan(fixture["list_entry"], fixture["detail"])
|
|
|
|
assert len(plan.discounts) == 1
|
|
discount = plan.discounts[0]
|
|
assert discount.auto_apply is True # "GUARANTEED" discount type
|
|
assert discount.percent == pytest.approx(10.0)
|
|
|
|
readings = [IntervalReading(ts=datetime(2026, 1, 1, 12, tzinfo=BNE), import_kwh=5, export_kwh=0)]
|
|
result = evaluate(plan, readings)
|
|
assert result.unconditional_discount_dollars > 0
|
|
assert result.potential_conditional_savings_dollars == 0
|