feat: add /main/verify endpoint and fix estimation column precision

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:05:31 -04:00
parent ad905a2d89
commit f1e311a0b1
+104 -1
View File
@@ -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))