import json from pathlib import Path from zoneinfo import ZoneInfo from datetime import datetime import pytest from app.costing.aer_mapper import 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) 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