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>
205 lines
6.9 KiB
Python
Executable File
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})
|