Files
orion/docs/implementation/order-item-exceptions.md
Samir Boulahtit a19c84ea4e feat: integer cents money handling, order page fixes, and vendor filter persistence
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>
2025-12-20 20:33:48 +01:00

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

  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/vendor 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)
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:

  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
  • 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:

  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/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)