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>
This commit is contained in:
2026-02-08 17:54:30 -05:00
parent 43a14eba2c
commit 6d5f44db55
18 changed files with 995 additions and 57 deletions

View File

@@ -1,4 +1,5 @@
import logging
from decimal import Decimal
from flask import request
from flask_login import current_user
from datetime import date, datetime, timedelta
@@ -24,6 +25,109 @@ from app.classes.auto import Tickets_Auto_Delivery, Tickets_Auto_Delivery_schema
# This endpoint is fine, but I've added some comments for clarity.
@delivery.route("/map", methods=["GET"])
@common_login_required
def get_deliveries_for_map():
"""Get deliveries for map view by date."""
date_str = request.args.get('date')
if date_str:
try:
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
return error_response("Invalid date format. Use YYYY-MM-DD", 400)
else:
target_date = date.today()
deliveries = (db.session
.query(Delivery_Delivery, Customer_Customer)
.join(Customer_Customer, Delivery_Delivery.customer_id == Customer_Customer.id)
.filter(Delivery_Delivery.expected_delivery_date == target_date)
.filter(Delivery_Delivery.delivery_status.notin_([1, 10]))
.order_by(Delivery_Delivery.customer_town.asc())
.all())
result = []
for delivery_item, customer in deliveries:
result.append({
'id': delivery_item.id,
'street': delivery_item.customer_address,
'town': delivery_item.customer_town,
'state': delivery_item.customer_state,
'zipcode': delivery_item.customer_zip,
'customerName': delivery_item.customer_name,
'notes': delivery_item.dispatcher_notes or '',
'latitude': customer.customer_latitude,
'longitude': customer.customer_longitude,
'gallonsOrdered': delivery_item.gallons_ordered,
'isFill': delivery_item.customer_asked_for_fill == 1,
'deliveryStatus': delivery_item.delivery_status,
'customerId': delivery_item.customer_id,
})
return success_response({'deliveries': result})
@delivery.route("/history", methods=["GET"])
@common_login_required
def get_deliveries_history():
"""Get completed deliveries (delivered or finalized) for a date range."""
start_date_str = request.args.get('start_date')
end_date_str = request.args.get('end_date')
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() if start_date_str else date.today()
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date() if end_date_str else date.today()
except ValueError:
return error_response("Invalid date format. Use YYYY-MM-DD", 400)
# Status 10 = finalized, 11 = delivered
deliveries = (db.session
.query(Delivery_Delivery)
.filter(Delivery_Delivery.expected_delivery_date >= start_date)
.filter(Delivery_Delivery.expected_delivery_date <= end_date)
.filter(Delivery_Delivery.delivery_status.in_([10, 11]))
.order_by(Delivery_Delivery.expected_delivery_date.desc(), Delivery_Delivery.customer_town.asc(), Delivery_Delivery.customer_name.asc())
.all())
result = []
total_gallons = 0
prime_count = 0
emergency_count = 0
same_day_count = 0
for d in deliveries:
gallons = float(d.gallons_delivered) if d.gallons_delivered and d.gallons_delivered > 0 else float(d.gallons_ordered or 0)
total_gallons += gallons
if d.prime == 1:
prime_count += 1
if d.emergency == 1:
emergency_count += 1
if d.same_day == 1:
same_day_count += 1
result.append({
'id': d.id,
'customerId': d.customer_id,
'customerName': d.customer_name,
'town': d.customer_town,
'address': f"{d.customer_address}, {d.customer_town}, {d.customer_state} {d.customer_zip}",
'gallonsDelivered': gallons,
'prime': d.prime == 1,
'emergency': d.emergency == 1,
'sameDay': d.same_day == 1,
'deliveryStatus': d.delivery_status,
})
return success_response({
'deliveries': result,
'totalGallons': total_gallons,
'totalDeliveries': len(result),
'primeCount': prime_count,
'emergencyCount': emergency_count,
'sameDayCount': same_day_count,
})
@delivery.route("/updatestatus", methods=["GET"])
@common_login_required
def move_deliveries():
@@ -507,6 +611,11 @@ def edit_a_delivery(delivery_id):
get_delivery.prime = 1 if data.get("prime") else 0
get_delivery.same_day = 1 if data.get("same_day") else 0
get_delivery.emergency = 1 if data.get("emergency") else 0
# Get tier selections (default to existing or 1)
get_delivery.pricing_tier_same_day = data.get("pricing_tier_same_day", get_delivery.pricing_tier_same_day or 1)
get_delivery.pricing_tier_prime = data.get("pricing_tier_prime", get_delivery.pricing_tier_prime or 1)
get_delivery.pricing_tier_emergency = data.get("pricing_tier_emergency", get_delivery.pricing_tier_emergency or 1)
# --- Handle Driver Assignment ---
driver_id = data.get("driver_employee_id")
@@ -551,14 +660,20 @@ def edit_a_delivery(delivery_id):
gallons_for_calc = 250 if customer_wants_fill else int(get_delivery.gallons_ordered)
base_price = gallons_for_calc * get_today_price.price_for_customer
# Add fees
# Add fees using tier pricing
total_price = base_price
if get_delivery.prime:
total_price += get_today_price.price_prime
tier_field = f"price_prime_tier{get_delivery.pricing_tier_prime}"
prime_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_prime))
total_price += prime_fee
if get_delivery.same_day:
total_price += get_today_price.price_same_day
tier_field = f"price_same_day_tier{get_delivery.pricing_tier_same_day}"
same_day_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_same_day))
total_price += same_day_fee
if get_delivery.emergency:
total_price += get_today_price.price_emergency
tier_field = f"price_emergency_tier{get_delivery.pricing_tier_emergency}"
emergency_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_emergency))
total_price += emergency_fee
get_delivery.total_price = base_price # Price before fees
get_delivery.pre_charge_amount = total_price # Price including fees
@@ -603,6 +718,11 @@ def create_a_delivery(user_id):
same_day_info = request.json["same_day"]
emergency_info = request.json["emergency"]
delivery_driver_id = request.json.get("driver_employee_id", 0)
# Get tier selections (default to tier 1)
pricing_tier_same_day = request.json.get("pricing_tier_same_day", 1)
pricing_tier_prime = request.json.get("pricing_tier_prime", 1)
pricing_tier_emergency = request.json.get("pricing_tier_emergency", 1)
card_payment = request.json["credit"]
cash_payment = request.json["cash"]
@@ -718,18 +838,23 @@ def create_a_delivery(user_id):
precharge_amount = int(gallons_ordered) * get_today_price.price_for_customer
# if prime/emergency/sameday
if same_day_asked == 1 and prime_asked == 0:
total_precharge_amount = precharge_amount + get_today_price.price_same_day
elif prime_asked == 1 and same_day_asked == 0:
total_precharge_amount = precharge_amount + get_today_price.price_prime
elif emergency_asked == 1:
total_precharge_amount = precharge_amount + get_today_price.price_emergency
else:
total_precharge_amount = precharge_amount + get_today_price.price_prime + get_today_price.price_same_day
# Calculate fees using tier pricing
total_precharge_amount = precharge_amount
if same_day_asked == 1:
tier_field = f"price_same_day_tier{pricing_tier_same_day}"
same_day_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_same_day))
total_precharge_amount += same_day_fee
if prime_asked == 1:
tier_field = f"price_prime_tier{pricing_tier_prime}"
prime_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_prime))
total_precharge_amount += prime_fee
if emergency_asked == 1:
tier_field = f"price_emergency_tier{pricing_tier_emergency}"
emergency_fee = Decimal(getattr(get_today_price, tier_field, get_today_price.price_emergency))
total_precharge_amount += emergency_fee
new_delivery = Delivery_Delivery(
@@ -757,6 +882,9 @@ def create_a_delivery(user_id):
prime=prime_asked,
same_day=same_day_asked,
emergency=emergency_asked,
pricing_tier_same_day=pricing_tier_same_day,
pricing_tier_prime=pricing_tier_prime,
pricing_tier_emergency=pricing_tier_emergency,
payment_type=delivery_payment_method,
payment_card_id=card_id_from_customer,
pre_charge_amount=total_precharge_amount,
@@ -955,17 +1083,23 @@ def calculate_total(delivery_id):
.first())
if get_delivery.prime == 1:
priceprime = float(get_price_query.price_prime)
tier = get_delivery.pricing_tier_prime or 1
tier_field = f"price_prime_tier{tier}"
priceprime = float(getattr(get_price_query, tier_field, get_price_query.price_prime))
else:
priceprime = 0
if get_delivery.emergency == 1:
priceemergency = float(get_price_query.price_emergency)
tier = get_delivery.pricing_tier_emergency or 1
tier_field = f"price_emergency_tier{tier}"
priceemergency = float(getattr(get_price_query, tier_field, get_price_query.price_emergency))
else:
priceemergency = 0
if get_delivery.same_day == 1:
pricesameday = float(get_price_query.price_same_day)
tier = get_delivery.pricing_tier_same_day or 1
tier_field = f"price_same_day_tier{tier}"
pricesameday = float(getattr(get_price_query, tier_field, get_price_query.price_same_day))
else:
pricesameday = 0