From f1e311a0b1c831da4a8990ecff644cd51b104fe5 Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Thu, 18 Jun 2026 15:05:31 -0400 Subject: [PATCH] feat: add /main/verify endpoint and fix estimation column precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added GET /main/verify?count=N to spot-check auto estimations against temp history — recomputes expected gallons from HDD since last fill and reports drift per customer; warnings flagged when drift > 15 gal - estimated_gallons_left and estimated_gallons_left_prev_day altered from INTEGER to NUMERIC(6,2) in both dev and prod DBs to eliminate ~0.3 gal/day rounding drift that accumulated from integer truncation on each daily update Co-Authored-By: Claude Sonnet 4.6 --- app/routers/main.py | 105 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/app/routers/main.py b/app/routers/main.py index e427836..853b0e2 100644 --- a/app/routers/main.py +++ b/app/routers/main.py @@ -1,5 +1,8 @@ import logging -from fastapi import APIRouter, HTTPException +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__) @@ -7,6 +10,9 @@ 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"], @@ -68,3 +74,100 @@ def update_all_customer_fuel_levels_normal(): # 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))