Money Handling Architecture: - Store all monetary values as integer cents (€105.91 = 10591) - Add app/utils/money.py with Money class and conversion helpers - Add static/shared/js/money.js for frontend formatting - Update all database models to use _cents columns (Product, Order, etc.) - Update CSV processor to convert prices to cents on import - Add Alembic migration for Float to Integer conversion - Create .architecture-rules/money.yaml with 7 validation rules - Add docs/architecture/money-handling.md documentation Order Details Page Fixes: - Fix customer name showing 'undefined undefined' - use flat field names - Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse - Fix shipping address using wrong nested object structure - Enrich order detail API response with vendor info Vendor Filter Persistence Fixes: - Fix orders.js: restoreSavedVendor now sets selectedVendor and filters - Fix orders.js: init() only loads orders if no saved vendor to restore - Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor() - Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown - Align vendor selector placeholder text between pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.9 KiB
Order Item Exception System
Overview
The Order Item Exception system handles unmatched products during marketplace order imports. Instead of blocking imports when products cannot be found by GTIN, the system gracefully imports orders with placeholder products and creates exception records for QC resolution.
Design Principles
- Graceful Import - Orders are imported even when products aren't found
- Exception Tracking - Unmatched items are tracked in
order_item_exceptionstable - Resolution Workflow - Admin/vendor can assign correct products
- Confirmation Blocking - Orders with unresolved exceptions cannot be confirmed
- Auto-Match - Exceptions auto-resolve when matching products are imported
Database Schema
order_item_exceptions Table
| Column | Type | Description |
|---|---|---|
| id | Integer | Primary key |
| order_item_id | Integer | FK to order_items (unique) |
| vendor_id | Integer | FK to vendors (indexed) |
| original_gtin | String(50) | GTIN from marketplace |
| original_product_name | String(500) | Product name from marketplace |
| original_sku | String(100) | SKU from marketplace |
| exception_type | String(50) | product_not_found, gtin_mismatch, duplicate_gtin |
| status | String(50) | pending, resolved, ignored |
| resolved_product_id | Integer | FK to products (nullable) |
| resolved_at | DateTime | When resolved |
| resolved_by | Integer | FK to users |
| resolution_notes | Text | Optional notes |
| created_at | DateTime | Created timestamp |
| updated_at | DateTime | Updated timestamp |
order_items Table (Modified)
Added column:
needs_product_match: Boolean (default False, indexed)
Placeholder Product
Per-vendor placeholder with:
gtin = "0000000000000"gtin_type = "placeholder"is_active = False
Workflow
Import Order from Marketplace
│
▼
Query Products by GTIN
│
┌────┴────┐
│ │
Found Not Found
│ │
▼ ▼
Normal Create with placeholder
Item + Set needs_product_match=True
+ Create OrderItemException
│
▼
QC Dashboard shows pending
│
┌─────┴─────┐
│ │
Resolve Ignore
(assign (with
product) reason)
│ │
▼ ▼
Update item Mark ignored
product_id (still blocks)
│
▼
Order can now be confirmed
API Endpoints
Admin Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/admin/order-exceptions |
List all exceptions |
| GET | /api/v1/admin/order-exceptions/stats |
Get exception statistics |
| GET | /api/v1/admin/order-exceptions/{id} |
Get exception details |
| POST | /api/v1/admin/order-exceptions/{id}/resolve |
Resolve with product |
| POST | /api/v1/admin/order-exceptions/{id}/ignore |
Mark as ignored |
| POST | /api/v1/admin/order-exceptions/bulk-resolve |
Bulk resolve by GTIN |
Vendor Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/vendor/order-exceptions |
List vendor's exceptions |
| GET | /api/v1/vendor/order-exceptions/stats |
Get vendor's stats |
| GET | /api/v1/vendor/order-exceptions/{id} |
Get exception details |
| POST | /api/v1/vendor/order-exceptions/{id}/resolve |
Resolve with product |
| POST | /api/v1/vendor/order-exceptions/{id}/ignore |
Mark as ignored |
| POST | /api/v1/vendor/order-exceptions/bulk-resolve |
Bulk resolve by GTIN |
Exception Types
| Type | Description |
|---|---|
product_not_found |
GTIN not in vendor's product catalog |
gtin_mismatch |
GTIN format issue |
duplicate_gtin |
Multiple products with same GTIN |
Exception Statuses
| Status | Description | Blocks Confirmation |
|---|---|---|
pending |
Awaiting resolution | Yes |
resolved |
Product assigned | No |
ignored |
Marked as ignored | Yes |
Note: Both pending and ignored statuses block order confirmation.
Auto-Matching
When products are imported to the vendor catalog (via copy_to_vendor_catalog), the system automatically:
- Collects GTINs of newly imported products
- Finds pending exceptions with matching GTINs
- Resolves them by assigning the new product
This happens automatically during:
- Single product import
- Bulk product import (marketplace sync)
Integration Points
Order Creation (app/services/order_service.py)
The create_letzshop_order() method:
- Queries products by GTIN
- For missing GTINs, creates placeholder product
- Creates order items with
needs_product_match=True - Creates exception records
Order Confirmation
Confirmation endpoints check for unresolved exceptions:
- Admin:
app/api/v1/admin/letzshop.py - Vendor:
app/api/v1/vendor/letzshop.py
Raises OrderHasUnresolvedExceptionsException if exceptions exist.
Product Import (app/services/marketplace_product_service.py)
The copy_to_vendor_catalog() method:
- Copies GTIN from MarketplaceProduct to Product
- Calls auto-match service after products are created
- Returns
auto_matchedcount in response
Files Created/Modified
New Files
| File | Description |
|---|---|
models/database/order_item_exception.py |
Database model |
models/schema/order_item_exception.py |
Pydantic schemas |
app/services/order_item_exception_service.py |
Business logic |
app/exceptions/order_item_exception.py |
Domain exceptions |
app/api/v1/admin/order_item_exceptions.py |
Admin endpoints |
app/api/v1/vendor/order_item_exceptions.py |
Vendor endpoints |
alembic/versions/d2e3f4a5b6c7_add_order_item_exceptions.py |
Migration |
Modified Files
| File | Changes |
|---|---|
models/database/order.py |
Added needs_product_match, exception relationship |
models/database/__init__.py |
Export OrderItemException |
models/schema/order.py |
Added exception info to OrderItemResponse |
app/services/order_service.py |
Graceful handling of missing products |
app/services/marketplace_product_service.py |
Auto-match on product import |
app/api/v1/admin/letzshop.py |
Confirmation blocking check |
app/api/v1/vendor/letzshop.py |
Confirmation blocking check |
app/api/v1/admin/__init__.py |
Register exception router |
app/api/v1/vendor/__init__.py |
Register exception router |
app/exceptions/__init__.py |
Export new exceptions |
Response Examples
List Exceptions
{
"exceptions": [
{
"id": 1,
"order_item_id": 42,
"vendor_id": 1,
"original_gtin": "4006381333931",
"original_product_name": "Funko Pop! Marvel...",
"original_sku": "MH-FU-56757",
"exception_type": "product_not_found",
"status": "pending",
"order_number": "LS-1-R702236251",
"order_date": "2025-12-19T10:30:00Z",
"created_at": "2025-12-19T11:00:00Z"
}
],
"total": 15,
"skip": 0,
"limit": 50
}
Exception Stats
{
"pending": 15,
"resolved": 42,
"ignored": 3,
"total": 60,
"orders_with_exceptions": 8
}
Resolve Exception
POST /api/v1/admin/order-exceptions/1/resolve
{
"product_id": 123,
"notes": "Matched to correct product manually"
}
Bulk Resolve
POST /api/v1/admin/order-exceptions/bulk-resolve?vendor_id=1
{
"gtin": "4006381333931",
"product_id": 123,
"notes": "New product imported"
}
Response:
{
"resolved_count": 5,
"gtin": "4006381333931",
"product_id": 123
}
Admin UI
The exceptions tab is available in the Letzshop management page:
Location: /admin/marketplace/letzshop → Exceptions tab
Features
- Stats Cards: Shows pending, resolved, ignored, and affected orders counts
- Filters: Search by GTIN/product name/order number, filter by status
- Exception Table: Paginated list with product info, GTIN, order link, status
- Actions:
- Resolve: Opens modal with product search (autocomplete)
- Ignore: Marks exception as ignored (still blocks confirmation)
- Bulk Resolve: Checkbox to apply resolution to all exceptions with same GTIN
Files
| File | Description |
|---|---|
app/templates/admin/partials/letzshop-exceptions-tab.html |
Tab HTML template |
app/templates/admin/marketplace-letzshop.html |
Main page (includes tab) |
static/admin/js/marketplace-letzshop.js |
JavaScript handlers |
Error Handling
| Exception | HTTP Status | When |
|---|---|---|
OrderItemExceptionNotFoundException |
404 | Exception not found |
OrderHasUnresolvedExceptionsException |
400 | Trying to confirm order with exceptions |
ExceptionAlreadyResolvedException |
400 | Trying to resolve already resolved exception |
InvalidProductForExceptionException |
400 | Invalid product (wrong vendor, inactive) |