# Unified Order Schema Implementation ## Overview This document describes the unified order schema that consolidates all order types (direct and marketplace) into a single `orders` table with snapshotted customer and address data. ## Design Decision: Option B - Single Unified Table After analyzing the gap between internal orders and Letzshop orders, we chose **Option B: Full Import to Order Table** with the following key principles: 1. **Single `orders` table** for all channels (direct, letzshop, future marketplaces) 2. **Customer/address snapshots** preserved at order time (not just FK references) 3. **Products must exist** in catalog - GTIN lookup errors trigger investigation 4. **Inactive customers** created for marketplace imports until they register on storefront 5. **No separate `letzshop_orders` table** - eliminates sync issues ## Schema Design ### Order Table The `orders` table now includes: ``` orders ├── Identity │ ├── id (PK) │ ├── vendor_id (FK → vendors) │ ├── customer_id (FK → customers) │ └── order_number (unique) │ ├── Channel/Source │ ├── channel (direct | letzshop) │ ├── external_order_id │ ├── external_shipment_id │ ├── external_order_number │ └── external_data (JSON - raw marketplace data) │ ├── Status │ └── status (pending | processing | shipped | delivered | cancelled | refunded) │ ├── Financials │ ├── subtotal (nullable for marketplace) │ ├── tax_amount │ ├── shipping_amount │ ├── discount_amount │ ├── total_amount │ └── currency │ ├── Customer Snapshot │ ├── customer_first_name │ ├── customer_last_name │ ├── customer_email │ ├── customer_phone │ └── customer_locale │ ├── Shipping Address Snapshot │ ├── ship_first_name │ ├── ship_last_name │ ├── ship_company │ ├── ship_address_line_1 │ ├── ship_address_line_2 │ ├── ship_city │ ├── ship_postal_code │ └── ship_country_iso │ ├── Billing Address Snapshot │ ├── bill_first_name │ ├── bill_last_name │ ├── bill_company │ ├── bill_address_line_1 │ ├── bill_address_line_2 │ ├── bill_city │ ├── bill_postal_code │ └── bill_country_iso │ ├── Tracking │ ├── shipping_method │ ├── tracking_number │ └── tracking_provider │ ├── Notes │ ├── customer_notes │ └── internal_notes │ └── Timestamps ├── order_date (when customer placed order) ├── confirmed_at ├── shipped_at ├── delivered_at ├── cancelled_at ├── created_at └── updated_at ``` ### OrderItem Table The `order_items` table includes: ``` order_items ├── Identity │ ├── id (PK) │ ├── order_id (FK → orders) │ └── product_id (FK → products, NOT NULL) │ ├── Product Snapshot │ ├── product_name │ ├── product_sku │ ├── gtin │ └── gtin_type (ean13, upc, isbn, etc.) │ ├── Pricing │ ├── quantity │ ├── unit_price │ └── total_price │ ├── External References │ ├── external_item_id (Letzshop inventory unit ID) │ └── external_variant_id │ ├── Item State (marketplace confirmation) │ └── item_state (confirmed_available | confirmed_unavailable) │ └── Inventory ├── inventory_reserved └── inventory_fulfilled ``` ## Status Mapping | Letzshop State | Order Status | Description | |----------------|--------------|-------------| | `unconfirmed` | `pending` | Order received, awaiting confirmation | | `confirmed` | `processing` | Items confirmed, being prepared | | `confirmed` + tracking | `shipped` | Shipped with tracking info | | `declined` | `cancelled` | All items declined | ## Customer Handling When importing marketplace orders: 1. Look up customer by `(vendor_id, email)` 2. If not found, create with `is_active=False` 3. Customer becomes active when they register on storefront 4. Customer info is always snapshotted in order (regardless of customer record) This ensures: - Customer history is preserved even if customer info changes - Marketplace customers can later claim their order history - No data loss if customer record is modified ## Shipping Workflows ### Scenario 1: Letzshop Auto-Shipping When using Letzshop's shipping service: 1. Order confirmed → `status = processing` 2. Letzshop auto-creates shipment with their carrier 3. Operator picks & packs 4. Operator clicks "Retrieve Shipping Info" 5. App fetches tracking from Letzshop API 6. Order updated → `status = shipped` ### Scenario 2: Vendor Own Shipping When vendor uses their own carrier: 1. Order confirmed → `status = processing` 2. Operator picks & packs with own carrier 3. Operator enters tracking info in app 4. App sends tracking to Letzshop API 5. Order updated → `status = shipped` ## Removed: LetzshopOrder Table The `letzshop_orders` table has been removed. All data now goes directly into the unified `orders` table with `channel = 'letzshop'`. ### Migration of Existing References - `LetzshopFulfillmentQueue.letzshop_order_id` → `order_id` (FK to `orders`) - `LetzshopSyncLog` - unchanged (no order reference) - `LetzshopHistoricalImportJob` - unchanged (no order reference) ## Files Modified | File | Changes | |------|---------| | `models/database/order.py` | Complete rewrite with snapshots | | `models/database/letzshop.py` | Removed `LetzshopOrder`, updated `LetzshopFulfillmentQueue` | | `models/schema/order.py` | Updated schemas for new structure | | `models/schema/letzshop.py` | Updated schemas for unified Order model | | `app/services/order_service.py` | Unified service with `create_letzshop_order()` | | `app/services/letzshop/order_service.py` | Updated to use unified Order model | | `app/api/v1/admin/letzshop.py` | Updated endpoints for unified model | | `alembic/versions/c1d2e3f4a5b6_unified_order_schema.py` | Migration | ## API Endpoints All Letzshop order endpoints now use the unified Order model: | Endpoint | Description | |----------|-------------| | `GET /admin/letzshop/vendors/{id}/orders` | List orders with `channel='letzshop'` filter | | `GET /admin/letzshop/orders/{id}` | Get order detail with items | | `POST /admin/letzshop/vendors/{id}/orders/{id}/confirm` | Confirm items via `external_item_id` | | `POST /admin/letzshop/vendors/{id}/orders/{id}/reject` | Decline items via `external_item_id` | | `POST /admin/letzshop/vendors/{id}/orders/{id}/items/{item_id}/confirm` | Confirm single item | | `POST /admin/letzshop/vendors/{id}/orders/{id}/items/{item_id}/decline` | Decline single item | ## Order Number Format | Channel | Format | Example | |---------|--------|---------| | Direct | `ORD-{vendor_id}-{date}-{random}` | `ORD-1-20251219-A1B2C3` | | Letzshop | `LS-{vendor_id}-{letzshop_order_number}` | `LS-1-ORD-123456` | ## Error Handling ### Product Not Found by GTIN When importing a Letzshop order, if a product cannot be found by its GTIN: ```python raise ValidationException( f"Product not found for GTIN {gtin}. " f"Please ensure the product catalog is in sync." ) ``` This is intentional - the Letzshop catalog is sourced from the vendor catalog, so missing products indicate a sync issue that must be investigated. ## Future Considerations ### Performance at Scale As the orders table grows, consider: 1. **Partitioning** by `order_date` or `vendor_id` 2. **Archiving** old orders to separate tables 3. **Read replicas** for reporting queries 4. **Materialized views** for dashboard statistics ### Additional Marketplaces The schema supports additional channels: ```python channel = Column(String(50)) # direct, letzshop, amazon, ebay, etc. ``` Each marketplace would use: - `external_order_id` - Marketplace order ID - `external_shipment_id` - Marketplace shipment ID - `external_order_number` - Display order number - `external_data` - Raw marketplace data (JSON) ## Implementation Status - [x] Order model with snapshots - [x] OrderItem model with GTIN fields - [x] LetzshopFulfillmentQueue updated - [x] LetzshopOrder removed - [x] Database migration created - [x] Order schemas updated - [x] Unified order service created - [x] Letzshop order service updated - [x] Letzshop schemas updated - [x] API endpoints updated - [x] Frontend updated - [x] Orders tab template (status badges, filters, table) - [x] Order detail page (snapshots, items, tracking) - [x] JavaScript (API params, response handling) - [x] Tracking modal (tracking_provider field) - [x] Order items modal (items array, item_state)