docs: add unified order schema implementation guide
Documents: - Design decision for Option B (single unified table) - Order and OrderItem schema with all fields - Status mapping between Letzshop and Order states - Customer handling (inactive until registered) - Shipping workflows for both auto and manual scenarios - Implementation status checklist 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
265
docs/implementation/unified-order-view.md
Normal file
265
docs/implementation/unified-order-view.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user