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

@@ -0,0 +1,67 @@
"""Add 5-tier pricing support for service fees
Revision ID: 3d217261c994
Revises: c7d2e8f1a3b9
Create Date: 2026-02-07 22:10:07.946719
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3d217261c994'
down_revision = 'c7d2e8f1a3b9'
branch_labels = None
depends_on = None
def upgrade():
# Add pricing tier columns to delivery_delivery table
with op.batch_alter_table('delivery_delivery', schema=None) as batch_op:
batch_op.add_column(sa.Column('pricing_tier_same_day', sa.INTEGER(), nullable=True))
batch_op.add_column(sa.Column('pricing_tier_prime', sa.INTEGER(), nullable=True))
batch_op.add_column(sa.Column('pricing_tier_emergency', sa.INTEGER(), nullable=True))
# Add tier pricing columns to pricing_oil_oil table
with op.batch_alter_table('pricing_oil_oil', schema=None) as batch_op:
batch_op.add_column(sa.Column('price_same_day_tier1', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_same_day_tier2', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_same_day_tier3', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_same_day_tier4', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_same_day_tier5', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_prime_tier1', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_prime_tier2', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_prime_tier3', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_prime_tier4', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_prime_tier5', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_emergency_tier1', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_emergency_tier2', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_emergency_tier3', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_emergency_tier4', sa.DECIMAL(precision=6, scale=2), nullable=True))
batch_op.add_column(sa.Column('price_emergency_tier5', sa.DECIMAL(precision=6, scale=2), nullable=True))
def downgrade():
# Remove tier pricing columns from pricing_oil_oil table
with op.batch_alter_table('pricing_oil_oil', schema=None) as batch_op:
batch_op.drop_column('price_emergency_tier5')
batch_op.drop_column('price_emergency_tier4')
batch_op.drop_column('price_emergency_tier3')
batch_op.drop_column('price_emergency_tier2')
batch_op.drop_column('price_emergency_tier1')
batch_op.drop_column('price_prime_tier5')
batch_op.drop_column('price_prime_tier4')
batch_op.drop_column('price_prime_tier3')
batch_op.drop_column('price_prime_tier2')
batch_op.drop_column('price_prime_tier1')
batch_op.drop_column('price_same_day_tier5')
batch_op.drop_column('price_same_day_tier4')
batch_op.drop_column('price_same_day_tier3')
batch_op.drop_column('price_same_day_tier2')
batch_op.drop_column('price_same_day_tier1')
# Remove pricing tier columns from delivery_delivery table
with op.batch_alter_table('delivery_delivery', schema=None) as batch_op:
batch_op.drop_column('pricing_tier_emergency')
batch_op.drop_column('pricing_tier_prime')
batch_op.drop_column('pricing_tier_same_day')

View File

@@ -0,0 +1,69 @@
"""Add K-factor history table and estimation columns
Revision ID: c7d2e8f1a3b9
Revises: b43a39b1cf25
Create Date: 2026-02-07 00:00:00.000000
NOTE: Move this file to migrations/versions/ before running flask db upgrade.
The migrations/versions/ directory is root-owned and cannot be written to directly.
Run: sudo mv eamco_office_api/c7d2e8f1a3b9_add_kfactor_history.py eamco_office_api/migrations/versions/
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c7d2e8f1a3b9'
down_revision = 'b43a39b1cf25'
branch_labels = None
depends_on = None
def upgrade():
# Create auto_kfactor_history table
op.create_table(
'auto_kfactor_history',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('customer_id', sa.Integer(), nullable=False),
sa.Column('ticket_id', sa.Integer(), nullable=True),
sa.Column('fill_date', sa.Date(), nullable=True),
sa.Column('gallons_delivered', sa.DECIMAL(precision=6, scale=2), nullable=True),
sa.Column('total_hdd', sa.DECIMAL(precision=8, scale=2), nullable=True),
sa.Column('days_in_period', sa.Integer(), nullable=True),
sa.Column('k_factor', sa.DECIMAL(precision=7, scale=4), nullable=True),
sa.Column('is_budget_fill', sa.Boolean(), server_default='false', nullable=False),
sa.Column('is_outlier', sa.Boolean(), server_default='false', nullable=False),
sa.Column('created_at', sa.Date(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_auto_kfactor_history_customer_id', 'auto_kfactor_history', ['customer_id'])
op.create_index('ix_auto_kfactor_history_customer_fill', 'auto_kfactor_history', ['customer_id', sa.text('fill_date DESC')])
# Add columns to auto_delivery
with op.batch_alter_table('auto_delivery', schema=None) as batch_op:
batch_op.add_column(sa.Column('confidence_score', sa.Integer(), server_default='20', nullable=True))
batch_op.add_column(sa.Column('k_factor_source', sa.VARCHAR(length=20), server_default='default', nullable=True))
batch_op.alter_column('house_factor',
existing_type=sa.DECIMAL(precision=5, scale=2),
type_=sa.DECIMAL(precision=7, scale=4),
existing_nullable=True)
# Add is_budget_fill to auto_tickets
with op.batch_alter_table('auto_tickets', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_budget_fill', sa.Boolean(), server_default='false', nullable=True))
def downgrade():
with op.batch_alter_table('auto_tickets', schema=None) as batch_op:
batch_op.drop_column('is_budget_fill')
with op.batch_alter_table('auto_delivery', schema=None) as batch_op:
batch_op.alter_column('house_factor',
existing_type=sa.DECIMAL(precision=7, scale=4),
type_=sa.DECIMAL(precision=5, scale=2),
existing_nullable=True)
batch_op.drop_column('k_factor_source')
batch_op.drop_column('confidence_score')
op.drop_index('ix_auto_kfactor_history_customer_fill', table_name='auto_kfactor_history')
op.drop_index('ix_auto_kfactor_history_customer_id', table_name='auto_kfactor_history')
op.drop_table('auto_kfactor_history')