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:
+104
-1
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user