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
|
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
|
from database import session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -7,6 +10,9 @@ logger = logging.getLogger(__name__)
|
|||||||
from app.script.fuel_estimator import FuelEstimator
|
from app.script.fuel_estimator import FuelEstimator
|
||||||
from app.script.temp_getter import fetch_and_store_daily_temp
|
from app.script.temp_getter import fetch_and_store_daily_temp
|
||||||
from app.script.fuel_estimator_customer import FuelEstimatorCustomer
|
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(
|
router = APIRouter(
|
||||||
prefix="/main",
|
prefix="/main",
|
||||||
tags=["main"],
|
tags=["main"],
|
||||||
@@ -68,3 +74,100 @@ def update_all_customer_fuel_levels_normal():
|
|||||||
# Log the exception e
|
# Log the exception e
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return {"ok": False, "message": "An internal error occurred."}
|
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