Update all frontend templates and JavaScript to use new unified Order model: - Orders tab: use status field, processing/cancelled values, items array - Order detail: use snapshot fields, items array, tracking_provider - JavaScript: update API params (status vs sync_status), orderStats fields - Tracking modal: use tracking_provider instead of tracking_carrier - Order items modal: use items array with item_state field All status mappings: - pending → pending (unconfirmed) - processing → confirmed (at least one item available) - cancelled → declined (all items unavailable) - shipped → shipped (with tracking) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
276 lines
8.7 KiB
Markdown
276 lines
8.7 KiB
Markdown
# 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)
|