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:
2025-12-19 21:18:15 +01:00
parent 80f859db4b
commit e36d8db7ca

View 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