power-compare/app/aer/tests/test_aer_mapper.py
dlawler489 e4c41c3eb9
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 35s
Fix missing GST on all AER-sourced usage rates and supply charges
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>
2026-09-11 15:06:39 +10:00

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