Files
eamco_office_api/app/info/views.py
Edwin Eames 6d5f44db55 feat: 5-tier pricing, market ticker integration, and delivery stats
Major update spanning pricing, market data, and analytics:

- Pricing: Replace single-price service fees with 5-tier pricing for
  same-day, prime, and emergency deliveries across create/edit/finalize
- Market: Add Ticker_Price and CompanyPrice models with endpoints for
  live commodity prices (HO, CL, RB) and competitor price tracking
- Stats: Add daily/weekly/monthly gallons endpoints with multi-year
  comparison and YoY totals for the stats dashboard
- Delivery: Add map and history endpoints, fix finalize null-driver crash
- Schema: Change fill_location from INTEGER to VARCHAR(250), add
  pre_load normalization for customer updates, fix admin auth check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 17:54:30 -05:00

205 lines
6.9 KiB
Python
Executable File

import logging
from decimal import Decimal
from app.info import info
from app import db
from app.common.responses import error_response, success_response
from app.classes.pricing import Pricing_Oil_Oil, Pricing_Oil_Oil_schema
from app.classes.admin import Admin_Company
from app.classes.delivery import Delivery_Delivery
from app.classes.service import Service_Service
from app.classes.ticker import Ticker_Price, Ticker_Price_Schema
from app.classes.competitor import CompanyPrice
from flask_login import login_required
logger = logging.getLogger(__name__)
from datetime import datetime, timedelta
from flask import request
@info.route("/price/oil/tiers", methods=["GET"])
@login_required
def get_pricing_tiers():
logger.info("GET /info/price/oil/tiers - Fetching oil pricing tiers")
get_price_query = (db.session
.query(Pricing_Oil_Oil)
.order_by(Pricing_Oil_Oil.date.desc())
.first())
if not get_price_query:
return error_response("No pricing data available", 404)
# Get the single price per gallon from the database, e.g., Decimal('2.92')
price_per_gallon = get_price_query.price_for_customer
# Define the specific gallon amounts you want to display totals for
gallon_tiers = [100, 125, 150, 175, 200, 220]
# Calculate the total price for each gallon amount by multiplication
pricing_totals = {
gallons: price_per_gallon * gallons
for gallons in gallon_tiers
}
# Return the dictionary of totals
return success_response({"pricing_tiers": pricing_totals})
@info.route("/price/oil", methods=["GET"])
@login_required
def get_oil_price_today():
logger.info("GET /info/price/oil - Fetching current oil prices")
get_price_query = (db.session
.query(Pricing_Oil_Oil)
.order_by(Pricing_Oil_Oil.date.desc())
.first())
return success_response({
'price_from_supplier': get_price_query.price_from_supplier,
'price_for_customer': get_price_query.price_for_customer,
'price_for_employee': get_price_query.price_for_employee,
'price_same_day': get_price_query.price_same_day,
'price_prime': get_price_query.price_prime,
'price_emergency': get_price_query.price_emergency,
})
@info.route("/price/oil/table", methods=["GET"])
@login_required
def get_pricing():
logger.info("GET /info/price/oil/table - Fetching oil pricing table")
get_price_query = (db.session
.query(Pricing_Oil_Oil)
.order_by(Pricing_Oil_Oil.date.desc())
.first())
delivery_schema = Pricing_Oil_Oil_schema(many=False)
return success_response({"pricing": delivery_schema.dump(get_price_query)})
@info.route("/company", methods=["GET"])
@login_required
def get_company():
logger.info("GET /info/company - Fetching company information")
get_data_company = (db.session
.query(Admin_Company)
.first())
return success_response({
'name': get_data_company.company_name,
'telephone': get_data_company.company_phone_number,
})
@info.route("/price/ticker", methods=["GET"])
@login_required
def get_ticker_prices():
"""Get latest stock/commodity ticker prices."""
logger.info("GET /info/price/ticker - Fetching ticker prices")
# We want the latest price for each symbol
# HO=F, CL=F, RB=F
target_symbols = ["HO=F", "CL=F", "RB=F"]
results = {}
# 1. Fetch Market Tickers
for symbol in target_symbols:
latest = (db.session.query(Ticker_Price)
.filter(Ticker_Price.symbol == symbol)
.order_by(Ticker_Price.timestamp.desc())
.first())
if latest:
results[symbol] = latest.to_dict()
# 2. Fetch Competitor Prices
# Focusing on LMT Oil and Charlton Oil as requested, but fetching others too for completeness
competitors = [
"LMT OIL",
"CHARLTON OIL",
"LEBLANC OIL",
"ALS OIL",
"VALUE OIL",
"DADDY'S OIL"
]
for comp_name in competitors:
latest_comp = (db.session.query(CompanyPrice)
.filter(CompanyPrice.company_name.ilike(f"%{comp_name}%"))
.order_by(CompanyPrice.scrape_date.desc())
.first())
if latest_comp:
# Map to Ticker format for uniform frontend handling if possible, or distinct
# For simplicity, we'll just add them to the dictionary.
# Convert name to a key like 'LMT' or keep full name
key = comp_name.replace(" OIL", "").replace("'S", "").replace(" ", "")
results[key] = {
"symbol": comp_name, # Use full name as symbol/label
"price": float(latest_comp.price_decimal),
"currency": "USD",
"change": 0.0, # We'd need history to calc this, skipping for now
"percent_change": 0.0,
"timestamp": latest_comp.scrape_date.isoformat()
}
return success_response({"tickers": results})
@info.route("/price/ticker/history", methods=["GET"])
@login_required
def get_ticker_history():
"""
Get historical ticker prices for charting.
Query Params:
days (int): Number of days to look back (default 30)
Returns:
List of daily price points for each ticker.
"""
try:
days = int(request.args.get('days', 30))
except ValueError:
days = 30
logger.info(f"GET /info/price/ticker/history - Fetching history for {days} days")
start_date = datetime.utcnow() - timedelta(days=days)
# Fetch all records since start_date
# We want to group by date and symbol, taking the last price of the day
records = (db.session.query(Ticker_Price)
.filter(Ticker_Price.timestamp >= start_date)
.order_by(Ticker_Price.timestamp.asc())
.all())
# Organize data structure for Chart.js
# { date: { symbol: price, symbol2: price } }
daily_data = {}
target_symbols = ["HO=F", "CL=F", "RB=F"]
for record in records:
if record.symbol not in target_symbols:
continue
date_str = record.timestamp.strftime('%Y-%m-%d')
if date_str not in daily_data:
daily_data[date_str] = {}
# This acts as "last value for the day" since we ordered by asc timestamp
daily_data[date_str][record.symbol] = float(record.price_decimal)
# Convert to list for frontend
# [ { date: '2023-01-01', prices: { 'HO=F': 2.5, ... } }, ... ]
result = []
for date_str in sorted(daily_data.keys()):
result.append({
"date": date_str,
"prices": daily_data[date_str]
})
return success_response({"history": result})