Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
289 lines
8.9 KiB
Markdown
289 lines
8.9 KiB
Markdown
# 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
|
|
|
|
1. **Graceful Import** - Orders are imported even when products aren't found
|
|
2. **Exception Tracking** - Unmatched items are tracked in `order_item_exceptions` table
|
|
3. **Resolution Workflow** - Admin/store can assign correct products
|
|
4. **Confirmation Blocking** - Orders with unresolved exceptions cannot be confirmed
|
|
5. **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) |
|
|
| store_id | Integer | FK to stores (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-store 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 |
|
|
|
|
### Store Endpoints
|
|
|
|
| Method | Endpoint | Description |
|
|
|--------|----------|-------------|
|
|
| GET | `/api/v1/store/order-exceptions` | List store's exceptions |
|
|
| GET | `/api/v1/store/order-exceptions/stats` | Get store's stats |
|
|
| GET | `/api/v1/store/order-exceptions/{id}` | Get exception details |
|
|
| POST | `/api/v1/store/order-exceptions/{id}/resolve` | Resolve with product |
|
|
| POST | `/api/v1/store/order-exceptions/{id}/ignore` | Mark as ignored |
|
|
| POST | `/api/v1/store/order-exceptions/bulk-resolve` | Bulk resolve by GTIN |
|
|
|
|
## Exception Types
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `product_not_found` | GTIN not in store'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 store catalog (via copy_to_store_catalog), the system automatically:
|
|
|
|
1. Collects GTINs of newly imported products
|
|
2. Finds pending exceptions with matching GTINs
|
|
3. 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:
|
|
1. Queries products by GTIN
|
|
2. For missing GTINs, creates placeholder product
|
|
3. Creates order items with `needs_product_match=True`
|
|
4. Creates exception records
|
|
|
|
### Order Confirmation
|
|
|
|
Confirmation endpoints check for unresolved exceptions:
|
|
- Admin: `app/api/v1/admin/letzshop.py`
|
|
- Store: `app/api/v1/store/letzshop.py`
|
|
|
|
Raises `OrderHasUnresolvedExceptionsException` if exceptions exist.
|
|
|
|
### Product Import (`app/services/marketplace_product_service.py`)
|
|
|
|
The `copy_to_store_catalog()` method:
|
|
1. Copies GTIN from MarketplaceProduct to Product
|
|
2. Calls auto-match service after products are created
|
|
3. Returns `auto_matched` count 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/store/order_item_exceptions.py` | Store 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/store/letzshop.py` | Confirmation blocking check |
|
|
| `app/api/v1/admin/__init__.py` | Register exception router |
|
|
| `app/api/v1/store/__init__.py` | Register exception router |
|
|
| `app/exceptions/__init__.py` | Export new exceptions |
|
|
|
|
## Response Examples
|
|
|
|
### List Exceptions
|
|
|
|
```json
|
|
{
|
|
"exceptions": [
|
|
{
|
|
"id": 1,
|
|
"order_item_id": 42,
|
|
"store_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
|
|
|
|
```json
|
|
{
|
|
"pending": 15,
|
|
"resolved": 42,
|
|
"ignored": 3,
|
|
"total": 60,
|
|
"orders_with_exceptions": 8
|
|
}
|
|
```
|
|
|
|
### Resolve Exception
|
|
|
|
```json
|
|
POST /api/v1/admin/order-exceptions/1/resolve
|
|
{
|
|
"product_id": 123,
|
|
"notes": "Matched to correct product manually"
|
|
}
|
|
```
|
|
|
|
### Bulk Resolve
|
|
|
|
```json
|
|
POST /api/v1/admin/order-exceptions/bulk-resolve?store_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 store, inactive) |
|