diff --git a/docs/implementation/unified-order-view.md b/docs/implementation/unified-order-view.md new file mode 100644 index 00000000..82eebc4a --- /dev/null +++ b/docs/implementation/unified-order-view.md @@ -0,0 +1,265 @@ +# 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 | +| `app/services/order_service.py` | Unified service with `create_letzshop_order()` | +| `alembic/versions/c1d2e3f4a5b6_unified_order_schema.py` | Migration | + +## API Endpoints (To Be Updated) + +The following endpoints need updates to use the unified model: + +| Endpoint | Changes Needed | +|----------|----------------| +| `GET /admin/orders` | Use unified Order model | +| `GET /admin/orders/{id}` | Return based on channel | +| `POST /admin/letzshop/orders/sync` | Use `order_service.create_letzshop_order()` | +| `POST /admin/letzshop/orders/{id}/confirm` | Use `order_service.update_item_state()` | +| `POST /admin/letzshop/orders/{id}/tracking` | Use `order_service.set_order_tracking()` | + +## 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 +- [ ] Letzshop order service updated +- [ ] API endpoints updated +- [ ] Frontend updated