feat: rewrite K-factor engine with history tracking and outlier detection
Replace simple exponential smoothing with a rolling-average K-factor system backed by a new auto_kfactor_history table. Budget fills are detected and excluded from calculations, outliers beyond 2-sigma are flagged, and confidence scores track data quality per customer. Adds backfill endpoint, auto-create for missing estimation records, and manual house_factor PUT endpoints for both auto and regular customers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import logging
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from database import session
|
||||
from sqlalchemy import func
|
||||
from datetime import date
|
||||
@@ -27,6 +28,9 @@ TANK_MAX_FILLS = {
|
||||
}
|
||||
|
||||
|
||||
class HouseFactorUpdate(BaseModel):
|
||||
house_factor: float
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/fixstuff_customer",
|
||||
@@ -256,10 +260,32 @@ def estimate_customer_gallons_specific(customer_id: int):
|
||||
).first()
|
||||
|
||||
if not customer_estimate:
|
||||
return JSONResponse(content={
|
||||
"error": f"No fuel estimation data found for customer {customer_id}",
|
||||
"solution": "Run the populate_estimates endpoint first to initialize customer data."
|
||||
})
|
||||
# Auto-create record from customer data
|
||||
customer = session.query(Customer_Customer).filter(
|
||||
Customer_Customer.id == customer_id
|
||||
).first()
|
||||
if not customer:
|
||||
return JSONResponse(content={"error": f"Customer {customer_id} not found"}, status_code=404)
|
||||
|
||||
customer_estimate = Customer_estimate_gallons(
|
||||
customer_id=customer.id,
|
||||
account_number=customer.account_number,
|
||||
customer_town=customer.customer_town,
|
||||
customer_state=customer.customer_state,
|
||||
customer_address=customer.customer_address,
|
||||
customer_zip=customer.customer_zip,
|
||||
customer_full_name=f"{customer.customer_first_name} {customer.customer_last_name}".strip(),
|
||||
estimated_gallons_left=Decimal('100'),
|
||||
estimated_gallons_left_prev_day=Decimal('100'),
|
||||
tank_size='275',
|
||||
house_factor=Decimal('0.12'),
|
||||
auto_status=1,
|
||||
hot_water_summer=0
|
||||
)
|
||||
session.add(customer_estimate)
|
||||
session.commit()
|
||||
session.refresh(customer_estimate)
|
||||
logger.info(f"Auto-created Customer_estimate_gallons record for customer {customer_id}")
|
||||
|
||||
deliveries = session.query(Delivery).filter(
|
||||
Delivery.customer_id == customer_estimate.customer_id,
|
||||
@@ -448,3 +474,25 @@ def populate_customer_estimates():
|
||||
}
|
||||
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
|
||||
@router.put("/house_factor/{customer_id}", status_code=200)
|
||||
def update_customer_house_factor(customer_id: int, body: HouseFactorUpdate):
|
||||
logger.info(f"PUT /fixstuff_customer/house_factor/{customer_id}")
|
||||
customer_estimate = session.query(Customer_estimate_gallons).filter(
|
||||
Customer_estimate_gallons.customer_id == customer_id
|
||||
).first()
|
||||
|
||||
if not customer_estimate:
|
||||
return JSONResponse(content={"error": "Customer estimate record not found"}, status_code=404)
|
||||
|
||||
customer_estimate.house_factor = Decimal(str(round(body.house_factor, 4)))
|
||||
session.commit()
|
||||
session.refresh(customer_estimate)
|
||||
|
||||
return JSONResponse(content=jsonable_encoder({
|
||||
"id": customer_estimate.id,
|
||||
"customer_id": customer_estimate.customer_id,
|
||||
"house_factor": float(customer_estimate.house_factor),
|
||||
"message": "House factor updated"
|
||||
}), status_code=200)
|
||||
|
||||
Reference in New Issue
Block a user