From 80f859db4bfcacaa1b3098560241eefc446baffa Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 19 Dec 2025 21:18:06 +0100 Subject: [PATCH] feat: add migration for unified order schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration c1d2e3f4a5b6 implements: - Drop old letzshop_orders table - Recreate orders table with snapshot fields - Recreate order_items with gtin/item_state fields - Recreate letzshop_fulfillment_queue referencing orders.id - Add composite indexes for common queries Uses safe_drop_index/safe_drop_table helpers for fresh databases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../c1d2e3f4a5b6_unified_order_schema.py | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 alembic/versions/c1d2e3f4a5b6_unified_order_schema.py diff --git a/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py b/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py new file mode 100644 index 00000000..0f70e8fb --- /dev/null +++ b/alembic/versions/c1d2e3f4a5b6_unified_order_schema.py @@ -0,0 +1,452 @@ +"""unified_order_schema + +Revision ID: c1d2e3f4a5b6 +Revises: 2362c2723a93 +Create Date: 2025-12-19 + +This migration implements the unified order schema: +- Removes the separate letzshop_orders table +- Enhances the orders table with: + - Customer/address snapshots (preserved at order time) + - External marketplace references + - Tracking provider field +- Enhances order_items with: + - GTIN fields + - External item references + - Item state for marketplace confirmation flow +- Updates letzshop_fulfillment_queue to reference orders table directly + +Design principles: +- Single orders table for all channels (direct, letzshop, etc.) +- Customer/address data snapshotted at order time +- Products must exist in catalog (enforced by FK) +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + + +# revision identifiers, used by Alembic. +revision: str = 'c1d2e3f4a5b6' +down_revision: Union[str, None] = '2362c2723a93' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def table_exists(table_name: str) -> bool: + """Check if a table exists in the database.""" + bind = op.get_bind() + inspector = inspect(bind) + return table_name in inspector.get_table_names() + + +def index_exists(index_name: str, table_name: str) -> bool: + """Check if an index exists on a table.""" + bind = op.get_bind() + inspector = inspect(bind) + try: + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) + except Exception: + return False + + +def safe_drop_index(index_name: str, table_name: str) -> None: + """Drop an index if it exists.""" + if index_exists(index_name, table_name): + op.drop_index(index_name, table_name=table_name) + + +def safe_drop_table(table_name: str) -> None: + """Drop a table if it exists.""" + if table_exists(table_name): + op.drop_table(table_name) + + +def upgrade() -> None: + # ========================================================================= + # Step 1: Drop old tables that will be replaced (if they exist) + # ========================================================================= + + # Drop letzshop_fulfillment_queue (references letzshop_orders) + if table_exists('letzshop_fulfillment_queue'): + safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue') + safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue') + safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue') + safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue') + op.drop_table('letzshop_fulfillment_queue') + + # Drop letzshop_orders table (replaced by unified orders) + if table_exists('letzshop_orders'): + safe_drop_index('idx_letzshop_order_sync', 'letzshop_orders') + safe_drop_index('idx_letzshop_order_state', 'letzshop_orders') + safe_drop_index('idx_letzshop_order_vendor', 'letzshop_orders') + safe_drop_index('ix_letzshop_orders_vendor_id', 'letzshop_orders') + safe_drop_index('ix_letzshop_orders_letzshop_shipment_id', 'letzshop_orders') + safe_drop_index('ix_letzshop_orders_letzshop_order_id', 'letzshop_orders') + safe_drop_index('ix_letzshop_orders_id', 'letzshop_orders') + op.drop_table('letzshop_orders') + + # Drop order_items (references orders) + if table_exists('order_items'): + safe_drop_index('ix_order_items_id', 'order_items') + safe_drop_index('ix_order_items_order_id', 'order_items') + op.drop_table('order_items') + + # Drop old orders table + if table_exists('orders'): + safe_drop_index('ix_orders_external_order_id', 'orders') + safe_drop_index('ix_orders_channel', 'orders') + safe_drop_index('ix_orders_vendor_id', 'orders') + safe_drop_index('ix_orders_status', 'orders') + safe_drop_index('ix_orders_order_number', 'orders') + safe_drop_index('ix_orders_id', 'orders') + safe_drop_index('ix_orders_customer_id', 'orders') + op.drop_table('orders') + + # ========================================================================= + # Step 2: Create new unified orders table + # ========================================================================= + op.create_table('orders', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=False), + sa.Column('customer_id', sa.Integer(), nullable=False), + sa.Column('order_number', sa.String(length=100), nullable=False), + + # Channel/Source + sa.Column('channel', sa.String(length=50), nullable=False, server_default='direct'), + + # External references (for marketplace orders) + sa.Column('external_order_id', sa.String(length=100), nullable=True), + sa.Column('external_shipment_id', sa.String(length=100), nullable=True), + sa.Column('external_order_number', sa.String(length=100), nullable=True), + sa.Column('external_data', sa.JSON(), nullable=True), + + # Status + sa.Column('status', sa.String(length=50), nullable=False, server_default='pending'), + + # Financials + sa.Column('subtotal', sa.Float(), nullable=True), + sa.Column('tax_amount', sa.Float(), nullable=True), + sa.Column('shipping_amount', sa.Float(), nullable=True), + sa.Column('discount_amount', sa.Float(), nullable=True), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True), + + # Customer snapshot + sa.Column('customer_first_name', sa.String(length=100), nullable=False), + sa.Column('customer_last_name', sa.String(length=100), nullable=False), + sa.Column('customer_email', sa.String(length=255), nullable=False), + sa.Column('customer_phone', sa.String(length=50), nullable=True), + sa.Column('customer_locale', sa.String(length=10), nullable=True), + + # Shipping address snapshot + sa.Column('ship_first_name', sa.String(length=100), nullable=False), + sa.Column('ship_last_name', sa.String(length=100), nullable=False), + sa.Column('ship_company', sa.String(length=200), nullable=True), + sa.Column('ship_address_line_1', sa.String(length=255), nullable=False), + sa.Column('ship_address_line_2', sa.String(length=255), nullable=True), + sa.Column('ship_city', sa.String(length=100), nullable=False), + sa.Column('ship_postal_code', sa.String(length=20), nullable=False), + sa.Column('ship_country_iso', sa.String(length=5), nullable=False), + + # Billing address snapshot + sa.Column('bill_first_name', sa.String(length=100), nullable=False), + sa.Column('bill_last_name', sa.String(length=100), nullable=False), + sa.Column('bill_company', sa.String(length=200), nullable=True), + sa.Column('bill_address_line_1', sa.String(length=255), nullable=False), + sa.Column('bill_address_line_2', sa.String(length=255), nullable=True), + sa.Column('bill_city', sa.String(length=100), nullable=False), + sa.Column('bill_postal_code', sa.String(length=20), nullable=False), + sa.Column('bill_country_iso', sa.String(length=5), nullable=False), + + # Tracking + sa.Column('shipping_method', sa.String(length=100), nullable=True), + sa.Column('tracking_number', sa.String(length=100), nullable=True), + sa.Column('tracking_provider', sa.String(length=100), nullable=True), + + # Notes + sa.Column('customer_notes', sa.Text(), nullable=True), + sa.Column('internal_notes', sa.Text(), nullable=True), + + # Timestamps + sa.Column('order_date', sa.DateTime(timezone=True), nullable=False), + sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('shipped_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('delivered_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + + # Foreign keys + sa.ForeignKeyConstraint(['customer_id'], ['customers.id']), + sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), + sa.PrimaryKeyConstraint('id') + ) + + # Indexes for orders + op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False) + op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False) + op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False) + op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True) + op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False) + op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False) + op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False) + op.create_index(op.f('ix_orders_external_shipment_id'), 'orders', ['external_shipment_id'], unique=False) + op.create_index('idx_order_vendor_status', 'orders', ['vendor_id', 'status'], unique=False) + op.create_index('idx_order_vendor_channel', 'orders', ['vendor_id', 'channel'], unique=False) + op.create_index('idx_order_vendor_date', 'orders', ['vendor_id', 'order_date'], unique=False) + + # ========================================================================= + # Step 3: Create new order_items table + # ========================================================================= + op.create_table('order_items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('order_id', sa.Integer(), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=False), + + # Product snapshot + sa.Column('product_name', sa.String(length=255), nullable=False), + sa.Column('product_sku', sa.String(length=100), nullable=True), + sa.Column('gtin', sa.String(length=50), nullable=True), + sa.Column('gtin_type', sa.String(length=20), nullable=True), + + # Pricing + sa.Column('quantity', sa.Integer(), nullable=False), + sa.Column('unit_price', sa.Float(), nullable=False), + sa.Column('total_price', sa.Float(), nullable=False), + + # External references (for marketplace items) + sa.Column('external_item_id', sa.String(length=100), nullable=True), + sa.Column('external_variant_id', sa.String(length=100), nullable=True), + + # Item state (for marketplace confirmation flow) + sa.Column('item_state', sa.String(length=50), nullable=True), + + # Inventory tracking + sa.Column('inventory_reserved', sa.Boolean(), server_default='0', nullable=True), + sa.Column('inventory_fulfilled', sa.Boolean(), server_default='0', nullable=True), + + # Timestamps + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + + # Foreign keys + sa.ForeignKeyConstraint(['order_id'], ['orders.id']), + sa.ForeignKeyConstraint(['product_id'], ['products.id']), + sa.PrimaryKeyConstraint('id') + ) + + # Indexes for order_items + op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False) + op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False) + op.create_index(op.f('ix_order_items_product_id'), 'order_items', ['product_id'], unique=False) + op.create_index(op.f('ix_order_items_gtin'), 'order_items', ['gtin'], unique=False) + + # ========================================================================= + # Step 4: Create updated letzshop_fulfillment_queue (references orders) + # ========================================================================= + op.create_table('letzshop_fulfillment_queue', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=False), + sa.Column('order_id', sa.Integer(), nullable=False), + + # Operation type + sa.Column('operation', sa.String(length=50), nullable=False), + + # Operation payload + sa.Column('payload', sa.JSON(), nullable=False), + + # Status and retry + sa.Column('status', sa.String(length=50), server_default='pending', nullable=True), + sa.Column('attempts', sa.Integer(), server_default='0', nullable=True), + sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True), + sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + + # Response from Letzshop + sa.Column('response_data', sa.JSON(), nullable=True), + + # Timestamps + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + + # Foreign keys + sa.ForeignKeyConstraint(['order_id'], ['orders.id']), + sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), + sa.PrimaryKeyConstraint('id') + ) + + # Indexes for letzshop_fulfillment_queue + op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False) + op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False) + op.create_index(op.f('ix_letzshop_fulfillment_queue_order_id'), 'letzshop_fulfillment_queue', ['order_id'], unique=False) + op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False) + op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False) + op.create_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue', ['order_id'], unique=False) + + +def downgrade() -> None: + # Drop new letzshop_fulfillment_queue + safe_drop_index('idx_fulfillment_queue_order', 'letzshop_fulfillment_queue') + safe_drop_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue') + safe_drop_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue') + safe_drop_index('ix_letzshop_fulfillment_queue_order_id', 'letzshop_fulfillment_queue') + safe_drop_index('ix_letzshop_fulfillment_queue_vendor_id', 'letzshop_fulfillment_queue') + safe_drop_index('ix_letzshop_fulfillment_queue_id', 'letzshop_fulfillment_queue') + safe_drop_table('letzshop_fulfillment_queue') + + # Drop new order_items + safe_drop_index('ix_order_items_gtin', 'order_items') + safe_drop_index('ix_order_items_product_id', 'order_items') + safe_drop_index('ix_order_items_order_id', 'order_items') + safe_drop_index('ix_order_items_id', 'order_items') + safe_drop_table('order_items') + + # Drop new orders + safe_drop_index('idx_order_vendor_date', 'orders') + safe_drop_index('idx_order_vendor_channel', 'orders') + safe_drop_index('idx_order_vendor_status', 'orders') + safe_drop_index('ix_orders_external_shipment_id', 'orders') + safe_drop_index('ix_orders_external_order_id', 'orders') + safe_drop_index('ix_orders_status', 'orders') + safe_drop_index('ix_orders_channel', 'orders') + safe_drop_index('ix_orders_order_number', 'orders') + safe_drop_index('ix_orders_customer_id', 'orders') + safe_drop_index('ix_orders_vendor_id', 'orders') + safe_drop_index('ix_orders_id', 'orders') + safe_drop_table('orders') + + # Recreate old orders table + op.create_table('orders', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=False), + sa.Column('customer_id', sa.Integer(), nullable=False), + sa.Column('order_number', sa.String(), nullable=False), + sa.Column('channel', sa.String(length=50), nullable=True, server_default='direct'), + sa.Column('external_order_id', sa.String(length=100), nullable=True), + sa.Column('external_channel_data', sa.JSON(), nullable=True), + sa.Column('status', sa.String(), nullable=False), + sa.Column('subtotal', sa.Float(), nullable=False), + sa.Column('tax_amount', sa.Float(), nullable=True), + sa.Column('shipping_amount', sa.Float(), nullable=True), + sa.Column('discount_amount', sa.Float(), nullable=True), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('currency', sa.String(), nullable=True), + sa.Column('shipping_address_id', sa.Integer(), nullable=False), + sa.Column('billing_address_id', sa.Integer(), nullable=False), + sa.Column('shipping_method', sa.String(), nullable=True), + sa.Column('tracking_number', sa.String(), nullable=True), + sa.Column('customer_notes', sa.Text(), nullable=True), + sa.Column('internal_notes', sa.Text(), nullable=True), + sa.Column('paid_at', sa.DateTime(), nullable=True), + sa.Column('shipped_at', sa.DateTime(), nullable=True), + sa.Column('delivered_at', sa.DateTime(), nullable=True), + sa.Column('cancelled_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['billing_address_id'], ['customer_addresses.id']), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id']), + sa.ForeignKeyConstraint(['shipping_address_id'], ['customer_addresses.id']), + sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_orders_customer_id'), 'orders', ['customer_id'], unique=False) + op.create_index(op.f('ix_orders_id'), 'orders', ['id'], unique=False) + op.create_index(op.f('ix_orders_order_number'), 'orders', ['order_number'], unique=True) + op.create_index(op.f('ix_orders_status'), 'orders', ['status'], unique=False) + op.create_index(op.f('ix_orders_vendor_id'), 'orders', ['vendor_id'], unique=False) + op.create_index(op.f('ix_orders_channel'), 'orders', ['channel'], unique=False) + op.create_index(op.f('ix_orders_external_order_id'), 'orders', ['external_order_id'], unique=False) + + # Recreate old order_items table + op.create_table('order_items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('order_id', sa.Integer(), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=False), + sa.Column('product_name', sa.String(), nullable=False), + sa.Column('product_sku', sa.String(), nullable=True), + sa.Column('quantity', sa.Integer(), nullable=False), + sa.Column('unit_price', sa.Float(), nullable=False), + sa.Column('total_price', sa.Float(), nullable=False), + sa.Column('inventory_reserved', sa.Boolean(), nullable=True), + sa.Column('inventory_fulfilled', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['order_id'], ['orders.id']), + sa.ForeignKeyConstraint(['product_id'], ['products.id']), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_order_items_id'), 'order_items', ['id'], unique=False) + op.create_index(op.f('ix_order_items_order_id'), 'order_items', ['order_id'], unique=False) + + # Recreate old letzshop_orders table + op.create_table('letzshop_orders', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=False), + sa.Column('letzshop_order_id', sa.String(length=100), nullable=False), + sa.Column('letzshop_shipment_id', sa.String(length=100), nullable=True), + sa.Column('letzshop_order_number', sa.String(length=100), nullable=True), + sa.Column('local_order_id', sa.Integer(), nullable=True), + sa.Column('letzshop_state', sa.String(length=50), nullable=True), + sa.Column('customer_email', sa.String(length=255), nullable=True), + sa.Column('customer_name', sa.String(length=255), nullable=True), + sa.Column('total_amount', sa.String(length=50), nullable=True), + sa.Column('currency', sa.String(length=10), server_default='EUR', nullable=True), + sa.Column('customer_locale', sa.String(length=10), nullable=True), + sa.Column('shipping_country_iso', sa.String(length=5), nullable=True), + sa.Column('billing_country_iso', sa.String(length=5), nullable=True), + sa.Column('order_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('raw_order_data', sa.JSON(), nullable=True), + sa.Column('inventory_units', sa.JSON(), nullable=True), + sa.Column('sync_status', sa.String(length=50), server_default='pending', nullable=True), + sa.Column('last_synced_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('sync_error', sa.Text(), nullable=True), + sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('tracking_set_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('tracking_number', sa.String(length=100), nullable=True), + sa.Column('tracking_carrier', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['local_order_id'], ['orders.id']), + sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_letzshop_orders_id'), 'letzshop_orders', ['id'], unique=False) + op.create_index(op.f('ix_letzshop_orders_letzshop_order_id'), 'letzshop_orders', ['letzshop_order_id'], unique=False) + op.create_index(op.f('ix_letzshop_orders_letzshop_shipment_id'), 'letzshop_orders', ['letzshop_shipment_id'], unique=False) + op.create_index(op.f('ix_letzshop_orders_vendor_id'), 'letzshop_orders', ['vendor_id'], unique=False) + op.create_index('idx_letzshop_order_vendor', 'letzshop_orders', ['vendor_id', 'letzshop_order_id'], unique=False) + op.create_index('idx_letzshop_order_state', 'letzshop_orders', ['vendor_id', 'letzshop_state'], unique=False) + op.create_index('idx_letzshop_order_sync', 'letzshop_orders', ['vendor_id', 'sync_status'], unique=False) + + # Recreate old letzshop_fulfillment_queue table + op.create_table('letzshop_fulfillment_queue', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=False), + sa.Column('letzshop_order_id', sa.Integer(), nullable=False), + sa.Column('operation', sa.String(length=50), nullable=False), + sa.Column('payload', sa.JSON(), nullable=False), + sa.Column('status', sa.String(length=50), server_default='pending', nullable=True), + sa.Column('attempts', sa.Integer(), server_default='0', nullable=True), + sa.Column('max_attempts', sa.Integer(), server_default='3', nullable=True), + sa.Column('last_attempt_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_retry_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('response_data', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['letzshop_order_id'], ['letzshop_orders.id']), + sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id']), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_letzshop_fulfillment_queue_id'), 'letzshop_fulfillment_queue', ['id'], unique=False) + op.create_index(op.f('ix_letzshop_fulfillment_queue_vendor_id'), 'letzshop_fulfillment_queue', ['vendor_id'], unique=False) + op.create_index('idx_fulfillment_queue_status', 'letzshop_fulfillment_queue', ['status', 'vendor_id'], unique=False) + op.create_index('idx_fulfillment_queue_retry', 'letzshop_fulfillment_queue', ['status', 'next_retry_at'], unique=False)