feat: add order item exception system for graceful product matching

Replaces the "fail on missing product" behavior with graceful handling:
- Orders import even when products aren't found by GTIN
- Unmatched items link to a per-vendor placeholder product
- Exceptions tracked in order_item_exceptions table for QC resolution
- Order confirmation blocked until exceptions are resolved
- Auto-matching when products are imported via catalog sync

New files:
- OrderItemException model and migration
- OrderItemExceptionService with CRUD and resolution logic
- Admin and vendor API endpoints for exception management
- Domain exceptions for error handling

Modified:
- OrderItem: added needs_product_match flag and exception relationship
- OrderService: graceful handling with placeholder products
- MarketplaceProductService: auto-match on product import
- Letzshop confirm endpoints: blocking check for unresolved exceptions

🤖 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-20 13:11:47 +01:00
parent e0a0da85f8
commit d6d658dd85
19 changed files with 2215 additions and 16 deletions

View File

@@ -24,6 +24,10 @@ from sqlalchemy import (
String,
Text,
)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models.database.order_item_exception import OrderItemException
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
@@ -201,9 +205,19 @@ class OrderItem(Base, TimestampMixin):
inventory_reserved = Column(Boolean, default=False)
inventory_fulfilled = Column(Boolean, default=False)
# === Exception Tracking ===
# True if product was not found by GTIN during import (linked to placeholder)
needs_product_match = Column(Boolean, default=False, index=True)
# === Relationships ===
order = relationship("Order", back_populates="items")
product = relationship("Product")
exception = relationship(
"OrderItemException",
back_populates="order_item",
uselist=False,
cascade="all, delete-orphan",
)
def __repr__(self):
return f"<OrderItem(id={self.id}, order_id={self.order_id}, product_id={self.product_id}, gtin='{self.gtin}')>"
@@ -222,3 +236,10 @@ class OrderItem(Base, TimestampMixin):
def is_declined(self) -> bool:
"""Check if item was declined (unavailable)."""
return self.item_state == "confirmed_unavailable"
@property
def has_unresolved_exception(self) -> bool:
"""Check if item has an unresolved exception blocking confirmation."""
if not self.exception:
return False
return self.exception.blocks_confirmation