import logging from decimal import Decimal from datetime import date, timedelta from typing import Optional from fastapi import APIRouter, HTTPException, Query from database import session logger = logging.getLogger(__name__) from app.script.fuel_estimator import FuelEstimator from app.script.temp_getter import fetch_and_store_daily_temp from app.script.fuel_estimator_customer import FuelEstimatorCustomer from app.models.auto import Auto_Delivery, Auto_Temp from app.constants import TANK_MAX_FILLS, DEFAULT_MAX_FILL_GALLONS router = APIRouter( prefix="/main", tags=["main"], responses={404: {"description": "Not found"}}, ) @router.get("/temp", status_code=200) def update_temp_manually(): """ Manually triggers the fetch and storage of today's temperature. This is useful for testing or for manual intervention if the cron job fails. """ try: success = fetch_and_store_daily_temp() if success: session.commit() return {"ok": True, "message": "Temperature updated or already exists."} else: # The function already rolled back, so just return an error return HTTPException(status_code=500, detail="Failed to fetch temperature from the weather API.") except Exception as e: session.rollback() raise HTTPException(status_code=500, detail=f"An unexpected server error occurred: {str(e)}") @router.get("/update/auto", status_code=200) def update_all_customer_fuel_levels_auto(): """ This endpoint triggers the daily update for all customers. It should be called once per day by a cron job or scheduler. """ try: estimator = FuelEstimator(session) result = estimator.run_daily_update() session.commit() return result except Exception as e: session.rollback() # Log the exception e logger.error(str(e)) return {"ok": False, "message": "An internal error occurred."} @router.get("/update/normal", status_code=200) def update_all_customer_fuel_levels_normal(): """ This endpoint triggers the daily update for all customers. It should be called once per day by a cron job or scheduler. """ try: estimator = FuelEstimatorCustomer(session) result = estimator.run_daily_update() session.commit() return result except Exception as e: session.rollback() # Log the exception e logger.error(str(e)) return {"ok": False, "message": "An internal error occurred."} @router.get("/verify", status_code=200) def verify_auto_estimations(count: int = Query(default=10, ge=1, le=50)): """ Spot-check auto delivery estimations against temperature history. For each sampled customer, recomputes expected gallons from the last fill date using daily HDD records, then compares to the stored estimate. Drift > 15 gal is flagged as a warning. """ try: today = date.today() import random customers = ( session.query(Auto_Delivery) .filter(Auto_Delivery.auto_status == 1, Auto_Delivery.last_fill.isnot(None)) .all() ) sample = random.sample(customers, min(count, len(customers))) # Build a date→degree_day lookup using raw temp_avg (matches estimator logic) temps = session.query(Auto_Temp).all() hdd_by_date: dict[date, Decimal] = { t.todays_date: Decimal(str(max(0, 65 - float(t.temp_avg)))) for t in temps if t.temp_avg is not None } HOT_WATER_DAILY = Decimal("1.0") results = [] warnings = 0 for c in sample: tank_size = float(Decimal(c.tank_size)) if c.tank_size else 275.0 max_fill = Decimal(str(TANK_MAX_FILLS.get(tank_size, DEFAULT_MAX_FILL_GALLONS))) k = Decimal(str(c.house_factor)) if c.house_factor else Decimal("0.12") hot_water = c.hot_water_summer == 1 # Accumulate usage from day after last fill through today total_hdd = Decimal("0") total_days = 0 cursor = c.last_fill + timedelta(days=1) missing_temps = [] while cursor <= today: dd = hdd_by_date.get(cursor) if dd is None: missing_temps.append(str(cursor)) else: total_hdd += dd total_days += 1 cursor += timedelta(days=1) heating_used = k * total_hdd hot_water_used = HOT_WATER_DAILY * total_days if hot_water else Decimal("0") expected = max(Decimal("0"), max_fill - heating_used - hot_water_used) actual = Decimal(str(c.estimated_gallons_left)) if c.estimated_gallons_left is not None else Decimal("0") drift = float(actual - expected) is_warning = abs(drift) > 15 if is_warning: warnings += 1 results.append({ "customer_id": c.customer_id, "name": c.customer_full_name, "town": c.customer_town, "tank_size": c.tank_size, "k_factor": float(k), "k_factor_source": c.k_factor_source, "hot_water": hot_water, "last_fill": str(c.last_fill), "days_since_fill": total_days, "total_hdd": float(total_hdd), "max_fill": float(max_fill), "expected_gallons": round(float(expected), 1), "actual_gallons": float(actual), "drift_gallons": round(drift, 1), "status": "WARNING" if is_warning else "OK", "missing_temp_dates": missing_temps, }) results.sort(key=lambda r: abs(r["drift_gallons"]), reverse=True) return { "ok": True, "checked": len(results), "warnings": warnings, "summary": "All OK" if warnings == 0 else f"{warnings} customer(s) have drift > 15 gal", "customers": results, } except Exception as e: logger.error(f"verify endpoint error: {e}") raise HTTPException(status_code=500, detail=str(e))