from __future__ import annotations from datetime import time as time_ from typing import Literal from fastapi import APIRouter from pydantic import BaseModel from app.costing.tariff_models import ( ALL_DAYS, ConditionalDiscount, FeedInComponent, TariffPlan, TieredFeedIn, TimeWindow, UsageComponent, ) from app.db.repositories import get_current_plan, get_household_config, set_current_plan from app.db.session import get_session router = APIRouter(prefix="/api/settings", tags=["settings"]) class TouPeriodIn(BaseModel): label: str rate_c_per_kwh: float start_time: time_ end_time: time_ days: list[str] | None = None # None = every day class CurrentPlanIn(BaseModel): retailer_name: str plan_name: str supply_charge_dollars_per_day: float tariff_type: Literal["flat", "time_of_use"] flat_rate_c_per_kwh: float | None = None tou_periods: list[TouPeriodIn] | None = None feed_in_type: Literal["flat", "tiered"] = "flat" feed_in_tariff_c_per_kwh: float = 0.0 tiered_daily_threshold_kwh: float | None = None tiered_rate_below_c_per_kwh: float | None = None tiered_rate_above_c_per_kwh: float | None = None discount_percent: float | None = None # a discount already baked into the plan you signed up for class HouseholdIn(BaseModel): distributor: str postcode: str isolarcloud_ps_id: str | None = None def _plan_from_form(form: CurrentPlanIn) -> TariffPlan: if form.tariff_type == "flat": if form.flat_rate_c_per_kwh is None: raise ValueError("flat_rate_c_per_kwh is required for a flat tariff") usage_components = [UsageComponent(label="Usage", rate_c_per_kwh=form.flat_rate_c_per_kwh)] else: if not form.tou_periods: raise ValueError("tou_periods is required for a time_of_use tariff") usage_components = [ UsageComponent( label=p.label, rate_c_per_kwh=p.rate_c_per_kwh, time_windows=[TimeWindow(days=p.days or list(ALL_DAYS), start_time=p.start_time, end_time=p.end_time)], ) for p in form.tou_periods ] discounts = [] if form.discount_percent: discounts.append( ConditionalDiscount(description="Plan discount", percent=form.discount_percent, applies_to="usage", auto_apply=True) ) feed_in_tariffs: list[FeedInComponent] = [] tiered_feed_in: TieredFeedIn | None = None if form.feed_in_type == "tiered": if form.tiered_daily_threshold_kwh is None or form.tiered_rate_below_c_per_kwh is None or form.tiered_rate_above_c_per_kwh is None: raise ValueError("tiered_daily_threshold_kwh, tiered_rate_below_c_per_kwh and tiered_rate_above_c_per_kwh are required for a tiered feed-in tariff") tiered_feed_in = TieredFeedIn( daily_threshold_kwh=form.tiered_daily_threshold_kwh, rate_below_c_per_kwh=form.tiered_rate_below_c_per_kwh, rate_above_c_per_kwh=form.tiered_rate_above_c_per_kwh, ) else: feed_in_tariffs = [FeedInComponent(label="Feed-in tariff", rate_c_per_kwh=form.feed_in_tariff_c_per_kwh)] return TariffPlan( plan_id="current-plan", retailer=form.retailer_name, plan_name=form.plan_name, supply_charge_c_per_day=form.supply_charge_dollars_per_day * 100, usage_components=usage_components, feed_in_tariffs=feed_in_tariffs, tiered_feed_in=tiered_feed_in, discounts=discounts, source="manual", ) @router.get("/current-plan") def get_current_plan_route() -> dict | None: with get_session() as session: plan = get_current_plan(session) if plan is None: return None return plan.model_dump(mode="json") @router.post("/current-plan") def save_current_plan(form: CurrentPlanIn) -> dict: plan = _plan_from_form(form) with get_session() as session: set_current_plan(session, form.retailer_name, form.plan_name, plan) return plan.model_dump(mode="json") @router.get("/household") def get_household() -> dict: with get_session() as session: cfg = get_household_config(session) return {"distributor": cfg.distributor, "postcode": cfg.postcode, "isolarcloud_ps_id": cfg.isolarcloud_ps_id} @router.post("/household") def save_household(form: HouseholdIn) -> dict: with get_session() as session: cfg = get_household_config(session) cfg.distributor = form.distributor cfg.postcode = form.postcode if form.isolarcloud_ps_id is not None: cfg.isolarcloud_ps_id = form.isolarcloud_ps_id session.flush() return {"distributor": cfg.distributor, "postcode": cfg.postcode, "isolarcloud_ps_id": cfg.isolarcloud_ps_id}