diff --git a/app/api/v1/admin/letzshop.py b/app/api/v1/admin/letzshop.py index 5afad2a5..6247d043 100644 --- a/app/api/v1/admin/letzshop.py +++ b/app/api/v1/admin/letzshop.py @@ -39,8 +39,10 @@ from models.schema.letzshop import ( LetzshopJobItem, LetzshopJobsListResponse, LetzshopOrderDetailResponse, + LetzshopOrderItemResponse, LetzshopOrderListResponse, LetzshopOrderResponse, + LetzshopOrderStats, LetzshopSuccessResponse, LetzshopSyncTriggerRequest, LetzshopSyncTriggerResponse, @@ -348,7 +350,7 @@ def list_vendor_letzshop_orders( vendor_id: int = Path(..., description="Vendor ID"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), - sync_status: str | None = Query(None, description="Filter by sync status"), + status: str | None = Query(None, description="Filter by order status"), has_declined_items: bool | None = Query( None, description="Filter orders with declined/unavailable items" ), @@ -370,7 +372,7 @@ def list_vendor_letzshop_orders( vendor_id=vendor_id, skip=skip, limit=limit, - sync_status=sync_status, + status=status, has_declined_items=has_declined_items, search=search, ) @@ -383,37 +385,50 @@ def list_vendor_letzshop_orders( LetzshopOrderResponse( id=order.id, vendor_id=order.vendor_id, - letzshop_order_id=order.letzshop_order_id, - letzshop_shipment_id=order.letzshop_shipment_id, - letzshop_order_number=order.letzshop_order_number, - letzshop_state=order.letzshop_state, + order_number=order.order_number, + external_order_id=order.external_order_id, + external_shipment_id=order.external_shipment_id, + external_order_number=order.external_order_number, + status=order.status, customer_email=order.customer_email, - customer_name=order.customer_name, + customer_name=order.customer_full_name, customer_locale=order.customer_locale, - shipping_country_iso=order.shipping_country_iso, - billing_country_iso=order.billing_country_iso, + ship_country_iso=order.ship_country_iso, + bill_country_iso=order.bill_country_iso, total_amount=order.total_amount, currency=order.currency, - local_order_id=order.local_order_id, - sync_status=order.sync_status, - last_synced_at=order.last_synced_at, - sync_error=order.sync_error, - confirmed_at=order.confirmed_at, - rejected_at=order.rejected_at, - tracking_set_at=order.tracking_set_at, tracking_number=order.tracking_number, - tracking_carrier=order.tracking_carrier, - inventory_units=order.inventory_units, + tracking_provider=order.tracking_provider, order_date=order.order_date, + confirmed_at=order.confirmed_at, + shipped_at=order.shipped_at, + cancelled_at=order.cancelled_at, created_at=order.created_at, updated_at=order.updated_at, + items=[ + LetzshopOrderItemResponse( + id=item.id, + product_id=item.product_id, + product_name=item.product_name, + product_sku=item.product_sku, + gtin=item.gtin, + gtin_type=item.gtin_type, + quantity=item.quantity, + unit_price=item.unit_price, + total_price=item.total_price, + external_item_id=item.external_item_id, + external_variant_id=item.external_variant_id, + item_state=item.item_state, + ) + for item in order.items + ], ) for order in orders ], total=total, skip=skip, limit=limit, - stats=stats, + stats=LetzshopOrderStats(**stats), ) @@ -436,31 +451,63 @@ def get_letzshop_order_detail( return LetzshopOrderDetailResponse( id=order.id, vendor_id=order.vendor_id, - letzshop_order_id=order.letzshop_order_id, - letzshop_shipment_id=order.letzshop_shipment_id, - letzshop_order_number=order.letzshop_order_number, - letzshop_state=order.letzshop_state, + order_number=order.order_number, + external_order_id=order.external_order_id, + external_shipment_id=order.external_shipment_id, + external_order_number=order.external_order_number, + status=order.status, customer_email=order.customer_email, - customer_name=order.customer_name, + customer_name=order.customer_full_name, customer_locale=order.customer_locale, - shipping_country_iso=order.shipping_country_iso, - billing_country_iso=order.billing_country_iso, + customer_first_name=order.customer_first_name, + customer_last_name=order.customer_last_name, + customer_phone=order.customer_phone, + ship_country_iso=order.ship_country_iso, + ship_first_name=order.ship_first_name, + ship_last_name=order.ship_last_name, + ship_company=order.ship_company, + ship_address_line_1=order.ship_address_line_1, + ship_address_line_2=order.ship_address_line_2, + ship_city=order.ship_city, + ship_postal_code=order.ship_postal_code, + bill_country_iso=order.bill_country_iso, + bill_first_name=order.bill_first_name, + bill_last_name=order.bill_last_name, + bill_company=order.bill_company, + bill_address_line_1=order.bill_address_line_1, + bill_address_line_2=order.bill_address_line_2, + bill_city=order.bill_city, + bill_postal_code=order.bill_postal_code, total_amount=order.total_amount, currency=order.currency, - local_order_id=order.local_order_id, - sync_status=order.sync_status, - last_synced_at=order.last_synced_at, - sync_error=order.sync_error, - confirmed_at=order.confirmed_at, - rejected_at=order.rejected_at, - tracking_set_at=order.tracking_set_at, tracking_number=order.tracking_number, - tracking_carrier=order.tracking_carrier, - inventory_units=order.inventory_units, + tracking_provider=order.tracking_provider, order_date=order.order_date, + confirmed_at=order.confirmed_at, + shipped_at=order.shipped_at, + cancelled_at=order.cancelled_at, created_at=order.created_at, updated_at=order.updated_at, - raw_order_data=order.raw_order_data, + external_data=order.external_data, + customer_notes=order.customer_notes, + internal_notes=order.internal_notes, + items=[ + LetzshopOrderItemResponse( + id=item.id, + product_id=item.product_id, + product_name=item.product_name, + product_sku=item.product_sku, + gtin=item.gtin, + gtin_type=item.gtin_type, + quantity=item.quantity, + unit_price=item.unit_price, + total_price=item.total_price, + external_item_id=item.external_item_id, + external_variant_id=item.external_variant_id, + item_state=item.item_state, + ) + for item in order.items + ], ) @@ -743,21 +790,24 @@ def confirm_order( try: order = order_service.get_order_or_raise(vendor_id, order_id) except OrderNotFoundError: - raise ResourceNotFoundException("LetzshopOrder", str(order_id)) + raise ResourceNotFoundException("Order", str(order_id)) - # Get inventory unit IDs from order - if not order.inventory_units: + # Get inventory unit IDs from order items + items = order_service.get_order_items(order) + if not items: return FulfillmentOperationResponse( success=False, - message="No inventory units found in order", + message="No items found in order", ) - inventory_unit_ids = [u.get("id") for u in order.inventory_units if u.get("id")] + inventory_unit_ids = [ + item.external_item_id for item in items if item.external_item_id + ] if not inventory_unit_ids: return FulfillmentOperationResponse( success=False, - message="No inventory unit IDs found in order", + message="No inventory unit IDs found in order items", ) try: @@ -775,7 +825,12 @@ def confirm_order( errors=error_messages, ) - # Update order status + # Update order status and item states + for item in items: + if item.external_item_id: + order_service.update_inventory_unit_state( + order, item.external_item_id, "confirmed_available" + ) order_service.mark_order_confirmed(order) db.commit() @@ -810,21 +865,24 @@ def reject_order( try: order = order_service.get_order_or_raise(vendor_id, order_id) except OrderNotFoundError: - raise ResourceNotFoundException("LetzshopOrder", str(order_id)) + raise ResourceNotFoundException("Order", str(order_id)) - # Get inventory unit IDs from order - if not order.inventory_units: + # Get inventory unit IDs from order items + items = order_service.get_order_items(order) + if not items: return FulfillmentOperationResponse( success=False, - message="No inventory units found in order", + message="No items found in order", ) - inventory_unit_ids = [u.get("id") for u in order.inventory_units if u.get("id")] + inventory_unit_ids = [ + item.external_item_id for item in items if item.external_item_id + ] if not inventory_unit_ids: return FulfillmentOperationResponse( success=False, - message="No inventory unit IDs found in order", + message="No inventory unit IDs found in order items", ) try: @@ -842,7 +900,12 @@ def reject_order( errors=error_messages, ) - # Update order status + # Update item states and order status + for item in items: + if item.external_item_id: + order_service.update_inventory_unit_state( + order, item.external_item_id, "confirmed_unavailable" + ) order_service.mark_order_rejected(order) db.commit() @@ -862,7 +925,7 @@ def reject_order( def confirm_single_item( vendor_id: int = Path(..., description="Vendor ID"), order_id: int = Path(..., description="Order ID"), - item_id: str = Path(..., description="Inventory Unit ID"), + item_id: str = Path(..., description="External Item ID (Letzshop inventory unit ID)"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): @@ -877,7 +940,7 @@ def confirm_single_item( try: order = order_service.get_order_or_raise(vendor_id, order_id) except OrderNotFoundError: - raise ResourceNotFoundException("LetzshopOrder", str(order_id)) + raise ResourceNotFoundException("Order", str(order_id)) try: with creds_service.create_client(vendor_id) as client: @@ -894,7 +957,7 @@ def confirm_single_item( errors=error_messages, ) - # Update local inventory unit state + # Update local order item state order_service.update_inventory_unit_state(order, item_id, "confirmed_available") db.commit() @@ -915,7 +978,7 @@ def confirm_single_item( def decline_single_item( vendor_id: int = Path(..., description="Vendor ID"), order_id: int = Path(..., description="Order ID"), - item_id: str = Path(..., description="Inventory Unit ID"), + item_id: str = Path(..., description="External Item ID (Letzshop inventory unit ID)"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): @@ -930,7 +993,7 @@ def decline_single_item( try: order = order_service.get_order_or_raise(vendor_id, order_id) except OrderNotFoundError: - raise ResourceNotFoundException("LetzshopOrder", str(order_id)) + raise ResourceNotFoundException("Order", str(order_id)) try: with creds_service.create_client(vendor_id) as client: @@ -947,7 +1010,7 @@ def decline_single_item( errors=error_messages, ) - # Update local inventory unit state + # Update local order item state order_service.update_inventory_unit_state(order, item_id, "confirmed_unavailable") db.commit() diff --git a/app/services/letzshop/order_service.py b/app/services/letzshop/order_service.py index cf74fe4a..2b98a294 100644 --- a/app/services/letzshop/order_service.py +++ b/app/services/letzshop/order_service.py @@ -2,25 +2,27 @@ """ Letzshop order service for handling order-related database operations. -This service moves database queries out of the API layer to comply with -architecture rules (API-002: endpoints should not contain business logic). +This service handles Letzshop-specific order operations while using the +unified Order model. All Letzshop orders are stored in the `orders` table +with `channel='letzshop'`. """ import logging from datetime import UTC, datetime from typing import Any, Callable -from sqlalchemy import String, func +from sqlalchemy import String, and_, func, or_ from sqlalchemy.orm import Session +from app.services.order_service import order_service as unified_order_service from models.database.letzshop import ( LetzshopFulfillmentQueue, LetzshopHistoricalImportJob, - LetzshopOrder, LetzshopSyncLog, VendorLetzshopCredentials, ) from models.database.marketplace_import_job import MarketplaceImportJob +from models.database.order import Order, OrderItem from models.database.product import Product from models.database.vendor import Vendor @@ -32,11 +34,11 @@ class VendorNotFoundError(Exception): class OrderNotFoundError(Exception): - """Raised when a Letzshop order is not found.""" + """Raised when an order is not found.""" class LetzshopOrderService: - """Service for Letzshop order database operations.""" + """Service for Letzshop order database operations using unified Order model.""" def __init__(self, db: Session): self.db = db @@ -67,7 +69,6 @@ class LetzshopOrderService: Returns a tuple of (vendor_overviews, total_count). """ - # Build query query = self.db.query(Vendor).filter(Vendor.is_active == True) # noqa: E712 if configured_only: @@ -76,38 +77,37 @@ class LetzshopOrderService: Vendor.id == VendorLetzshopCredentials.vendor_id, ) - # Get total count total = query.count() - - # Get vendors vendors = query.order_by(Vendor.name).offset(skip).limit(limit).all() - # Build response with Letzshop status vendor_overviews = [] for vendor in vendors: - # Get credentials credentials = ( self.db.query(VendorLetzshopCredentials) .filter(VendorLetzshopCredentials.vendor_id == vendor.id) .first() ) - # Get order counts + # Count Letzshop orders from unified orders table pending_orders = 0 total_orders = 0 if credentials: pending_orders = ( - self.db.query(func.count(LetzshopOrder.id)) + self.db.query(func.count(Order.id)) .filter( - LetzshopOrder.vendor_id == vendor.id, - LetzshopOrder.sync_status == "pending", + Order.vendor_id == vendor.id, + Order.channel == "letzshop", + Order.status == "pending", ) .scalar() or 0 ) total_orders = ( - self.db.query(func.count(LetzshopOrder.id)) - .filter(LetzshopOrder.vendor_id == vendor.id) + self.db.query(func.count(Order.id)) + .filter( + Order.vendor_id == vendor.id, + Order.channel == "letzshop", + ) .scalar() or 0 ) @@ -133,45 +133,50 @@ class LetzshopOrderService: return vendor_overviews, total # ========================================================================= - # Order Operations + # Order Operations (using unified Order model) # ========================================================================= - def get_order(self, vendor_id: int, order_id: int) -> LetzshopOrder | None: + def get_order(self, vendor_id: int, order_id: int) -> Order | None: """Get a Letzshop order by ID for a specific vendor.""" return ( - self.db.query(LetzshopOrder) + self.db.query(Order) .filter( - LetzshopOrder.id == order_id, - LetzshopOrder.vendor_id == vendor_id, + Order.id == order_id, + Order.vendor_id == vendor_id, + Order.channel == "letzshop", ) .first() ) - def get_order_or_raise(self, vendor_id: int, order_id: int) -> LetzshopOrder: + def get_order_or_raise(self, vendor_id: int, order_id: int) -> Order: """Get a Letzshop order or raise OrderNotFoundError.""" order = self.get_order(vendor_id, order_id) if order is None: - raise OrderNotFoundError(f"Letzshop order {order_id} not found") + raise OrderNotFoundError(f"Order {order_id} not found") return order def get_order_by_shipment_id( self, vendor_id: int, shipment_id: str - ) -> LetzshopOrder | None: - """Get a Letzshop order by shipment ID.""" + ) -> Order | None: + """Get a Letzshop order by external shipment ID.""" return ( - self.db.query(LetzshopOrder) + self.db.query(Order) .filter( - LetzshopOrder.vendor_id == vendor_id, - LetzshopOrder.letzshop_shipment_id == shipment_id, + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + Order.external_shipment_id == shipment_id, ) .first() ) - def get_order_by_id(self, order_id: int) -> LetzshopOrder | None: + def get_order_by_id(self, order_id: int) -> Order | None: """Get a Letzshop order by its database ID.""" return ( - self.db.query(LetzshopOrder) - .filter(LetzshopOrder.id == order_id) + self.db.query(Order) + .filter( + Order.id == order_id, + Order.channel == "letzshop", + ) .first() ) @@ -180,11 +185,10 @@ class LetzshopOrderService: vendor_id: int, skip: int = 0, limit: int = 50, - sync_status: str | None = None, - letzshop_state: str | None = None, + status: str | None = None, has_declined_items: bool | None = None, search: str | None = None, - ) -> tuple[list[LetzshopOrder], int]: + ) -> tuple[list[Order], int]: """ List Letzshop orders for a vendor. @@ -192,47 +196,45 @@ class LetzshopOrderService: vendor_id: Vendor ID to filter by. skip: Number of records to skip. limit: Maximum number of records to return. - sync_status: Filter by order sync status (pending, confirmed, etc.) - letzshop_state: Filter by Letzshop shipment state. - has_declined_items: If True, only return orders with at least one - declined/unavailable item (confirmed_unavailable state). - search: Search by order number, customer name, or customer email. + status: Filter by order status (pending, processing, shipped, etc.) + has_declined_items: If True, only return orders with declined items. + search: Search by order number, customer name, or email. Returns a tuple of (orders, total_count). """ - from sqlalchemy import or_ - - query = self.db.query(LetzshopOrder).filter( - LetzshopOrder.vendor_id == vendor_id + query = self.db.query(Order).filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", ) - if sync_status: - query = query.filter(LetzshopOrder.sync_status == sync_status) - if letzshop_state: - query = query.filter(LetzshopOrder.letzshop_state == letzshop_state) + if status: + query = query.filter(Order.status == status) - # Search by order number, customer name, or email if search: search_term = f"%{search}%" query = query.filter( or_( - LetzshopOrder.letzshop_order_number.ilike(search_term), - LetzshopOrder.customer_name.ilike(search_term), - LetzshopOrder.customer_email.ilike(search_term), + Order.order_number.ilike(search_term), + Order.external_order_number.ilike(search_term), + Order.customer_email.ilike(search_term), + Order.customer_first_name.ilike(search_term), + Order.customer_last_name.ilike(search_term), ) ) - # Filter for orders with declined items (confirmed_unavailable state) + # Filter for orders with declined items if has_declined_items is True: - # Use JSON contains check for SQLite/PostgreSQL - query = query.filter( - LetzshopOrder.inventory_units.isnot(None), - LetzshopOrder.inventory_units.cast(String).contains("confirmed_unavailable"), + # Subquery to find orders with declined items + declined_order_ids = ( + self.db.query(OrderItem.order_id) + .filter(OrderItem.item_state == "confirmed_unavailable") + .subquery() ) + query = query.filter(Order.id.in_(declined_order_ids)) total = query.count() orders = ( - query.order_by(LetzshopOrder.created_at.desc()) + query.order_by(Order.order_date.desc()) .offset(skip) .limit(limit) .all() @@ -242,35 +244,46 @@ class LetzshopOrderService: def get_order_stats(self, vendor_id: int) -> dict[str, int]: """ - Get order counts by sync_status for a vendor. + Get order counts by status for a vendor's Letzshop orders. Returns: - Dict with counts for each status: pending, confirmed, rejected, shipped, - and has_declined_items (orders with at least one declined item). + Dict with counts for each status. """ status_counts = ( self.db.query( - LetzshopOrder.sync_status, - func.count(LetzshopOrder.id).label("count"), + Order.status, + func.count(Order.id).label("count"), ) - .filter(LetzshopOrder.vendor_id == vendor_id) - .group_by(LetzshopOrder.sync_status) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) + .group_by(Order.status) .all() ) - # Convert to dict with default 0 for missing statuses - stats = {"pending": 0, "confirmed": 0, "rejected": 0, "shipped": 0} + stats = { + "pending": 0, + "processing": 0, + "shipped": 0, + "delivered": 0, + "cancelled": 0, + "refunded": 0, + "total": 0, + } for status, count in status_counts: if status in stats: stats[status] = count + stats["total"] += count - # Count orders with declined items (confirmed_unavailable state) + # Count orders with declined items declined_items_count = ( - self.db.query(func.count(LetzshopOrder.id)) + self.db.query(func.count(func.distinct(OrderItem.order_id))) + .join(Order, OrderItem.order_id == Order.id) .filter( - LetzshopOrder.vendor_id == vendor_id, - LetzshopOrder.inventory_units.isnot(None), - LetzshopOrder.inventory_units.cast(String).contains("confirmed_unavailable"), + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + OrderItem.item_state == "confirmed_unavailable", ) .scalar() or 0 @@ -283,273 +296,182 @@ class LetzshopOrderService: self, vendor_id: int, shipment_data: dict[str, Any], - ) -> LetzshopOrder: - """Create a new Letzshop order from shipment data.""" - order_data = shipment_data.get("order", {}) + ) -> Order: + """ + Create a new Letzshop order from shipment data. - # Handle total - can be a string like "99.99 EUR" or just a number - total = order_data.get("total", "") - total_amount = str(total) if total else "" - # Default currency to EUR (Letzshop is Luxembourg-based) - currency = "EUR" - - # Extract customer name from shipping address - ship_address = order_data.get("shipAddress", {}) or {} - first_name = ship_address.get("firstName", "") or "" - last_name = ship_address.get("lastName", "") or "" - customer_name = f"{first_name} {last_name}".strip() or None - - # Extract customer locale (language preference for invoicing) - customer_locale = order_data.get("locale") - - # Extract order date (completedAt from Letzshop) - order_date = None - completed_at_str = order_data.get("completedAt") - if completed_at_str: - try: - from datetime import datetime - - # Handle ISO format with timezone - if completed_at_str.endswith("Z"): - completed_at_str = completed_at_str[:-1] + "+00:00" - order_date = datetime.fromisoformat(completed_at_str) - except (ValueError, TypeError): - pass # Keep None if parsing fails - - # Extract country codes - ship_country = ship_address.get("country", {}) or {} - shipping_country_iso = ship_country.get("iso") - - bill_address = order_data.get("billAddress", {}) or {} - bill_country = bill_address.get("country", {}) or {} - billing_country_iso = bill_country.get("iso") - - # inventoryUnits is a direct array, not wrapped in nodes - inventory_units_data = shipment_data.get("inventoryUnits", []) - if isinstance(inventory_units_data, dict): - # Handle legacy format with nodes wrapper - inventory_units_data = inventory_units_data.get("nodes", []) - - # Extract enriched inventory unit data with product details - enriched_units = [] - for unit in inventory_units_data: - variant = unit.get("variant", {}) or {} - product = variant.get("product", {}) or {} - trade_id = variant.get("tradeId") or {} - product_name = product.get("name", {}) or {} - - enriched_unit = { - "id": unit.get("id"), - "state": unit.get("state"), - # Product identifiers - "ean": trade_id.get("number"), - "ean_type": trade_id.get("parser"), - "sku": variant.get("sku"), - "mpn": variant.get("mpn"), - # Product info - "product_name": ( - product_name.get("en") - or product_name.get("fr") - or product_name.get("de") - ), - "product_name_translations": product_name, - # Pricing - "price": variant.get("price"), - "variant_id": variant.get("id"), - } - enriched_units.append(enriched_unit) - - # Map Letzshop state to sync_status - # Letzshop shipment states (from docs): - # - unconfirmed: needs to be confirmed/rejected - # - confirmed: at least one product confirmed - # - declined: all products rejected - # Note: "shipped" is not a state - tracking is set separately via tracking field - letzshop_state = shipment_data.get("state", "unconfirmed") - state_mapping = { - "unconfirmed": "pending", - "confirmed": "confirmed", - "declined": "rejected", - } - sync_status = state_mapping.get(letzshop_state, "confirmed") - - order = LetzshopOrder( + Uses the unified order service to create the order. + """ + return unified_order_service.create_letzshop_order( + db=self.db, vendor_id=vendor_id, - letzshop_order_id=order_data.get("id", ""), - letzshop_shipment_id=shipment_data["id"], - letzshop_order_number=order_data.get("number"), - letzshop_state=letzshop_state, - customer_email=order_data.get("email"), - customer_name=customer_name, - customer_locale=customer_locale, - shipping_country_iso=shipping_country_iso, - billing_country_iso=billing_country_iso, - total_amount=total_amount, - currency=currency, - order_date=order_date, - raw_order_data=shipment_data, - inventory_units=enriched_units, - sync_status=sync_status, + shipment_data=shipment_data, ) - self.db.add(order) - return order def update_order_from_shipment( self, - order: LetzshopOrder, + order: Order, shipment_data: dict[str, Any], - ) -> LetzshopOrder: + ) -> Order: """Update an existing order from shipment data.""" order_data = shipment_data.get("order", {}) - # Update letzshop_state and sync_status - # Letzshop states: unconfirmed, confirmed, declined + # Map Letzshop state to status letzshop_state = shipment_data.get("state", "unconfirmed") state_mapping = { "unconfirmed": "pending", - "confirmed": "confirmed", - "declined": "rejected", + "confirmed": "processing", + "declined": "cancelled", } - order.letzshop_state = letzshop_state - order.sync_status = state_mapping.get(letzshop_state, "confirmed") - order.raw_order_data = shipment_data + new_status = state_mapping.get(letzshop_state, "processing") - # Update locale if not already set + # Update status if changed + if order.status != new_status: + order.status = new_status + now = datetime.now(UTC) + if new_status == "processing": + order.confirmed_at = now + elif new_status == "cancelled": + order.cancelled_at = now + + # Update external data + order.external_data = shipment_data + + # Update locale if not set if not order.customer_locale and order_data.get("locale"): order.customer_locale = order_data.get("locale") - # Update order_date if not already set + # Update order_date if not set if not order.order_date: completed_at_str = order_data.get("completedAt") if completed_at_str: try: - from datetime import datetime - if completed_at_str.endswith("Z"): completed_at_str = completed_at_str[:-1] + "+00:00" order.order_date = datetime.fromisoformat(completed_at_str) except (ValueError, TypeError): pass - # Update country codes if not already set - if not order.shipping_country_iso: - ship_address = order_data.get("shipAddress", {}) or {} - ship_country = ship_address.get("country", {}) or {} - order.shipping_country_iso = ship_country.get("iso") - - if not order.billing_country_iso: - bill_address = order_data.get("billAddress", {}) or {} - bill_country = bill_address.get("country", {}) or {} - order.billing_country_iso = bill_country.get("iso") - - # Update enriched inventory units + # Update inventory unit states in order items inventory_units_data = shipment_data.get("inventoryUnits", []) if isinstance(inventory_units_data, dict): inventory_units_data = inventory_units_data.get("nodes", []) - enriched_units = [] for unit in inventory_units_data: - variant = unit.get("variant", {}) or {} - product = variant.get("product", {}) or {} - trade_id = variant.get("tradeId") or {} - product_name = product.get("name", {}) or {} + unit_id = unit.get("id") + unit_state = unit.get("state") + if unit_id and unit_state: + # Find and update the corresponding order item + item = ( + self.db.query(OrderItem) + .filter( + OrderItem.order_id == order.id, + OrderItem.external_item_id == unit_id, + ) + .first() + ) + if item: + item.item_state = unit_state - enriched_unit = { - "id": unit.get("id"), - "state": unit.get("state"), - "ean": trade_id.get("number"), - "ean_type": trade_id.get("parser"), - "sku": variant.get("sku"), - "mpn": variant.get("mpn"), - "product_name": ( - product_name.get("en") - or product_name.get("fr") - or product_name.get("de") - ), - "product_name_translations": product_name, - "price": variant.get("price"), - "variant_id": variant.get("id"), - } - enriched_units.append(enriched_unit) - - order.inventory_units = enriched_units + order.updated_at = datetime.now(UTC) return order - def mark_order_confirmed(self, order: LetzshopOrder) -> LetzshopOrder: - """Mark an order as confirmed.""" + def mark_order_confirmed(self, order: Order) -> Order: + """Mark an order as confirmed (processing).""" order.confirmed_at = datetime.now(UTC) - order.sync_status = "confirmed" + order.status = "processing" + order.updated_at = datetime.now(UTC) return order - def mark_order_rejected(self, order: LetzshopOrder) -> LetzshopOrder: - """Mark an order as rejected.""" - order.rejected_at = datetime.now(UTC) - order.sync_status = "rejected" + def mark_order_rejected(self, order: Order) -> Order: + """Mark an order as rejected (cancelled).""" + order.cancelled_at = datetime.now(UTC) + order.status = "cancelled" + order.updated_at = datetime.now(UTC) return order def update_inventory_unit_state( - self, order: LetzshopOrder, item_id: str, state: str - ) -> LetzshopOrder: + self, order: Order, item_id: str, state: str + ) -> Order: """ - Update the state of a single inventory unit in an order. + Update the state of a single order item. Args: - order: The order containing the inventory unit. - item_id: The inventory unit ID to update. + order: The order containing the item. + item_id: The external item ID (Letzshop inventory unit ID). state: The new state (confirmed_available, confirmed_unavailable). Returns: The updated order. """ - if not order.inventory_units: - return order + # Find and update the item + item = ( + self.db.query(OrderItem) + .filter( + OrderItem.order_id == order.id, + OrderItem.external_item_id == item_id, + ) + .first() + ) - # Update the specific item's state - updated_units = [] - for unit in order.inventory_units: - if unit.get("id") == item_id: - unit["state"] = state - updated_units.append(unit) + if item: + item.item_state = state + item.updated_at = datetime.now(UTC) - order.inventory_units = updated_units + # Check if all items are now processed + all_items = ( + self.db.query(OrderItem) + .filter(OrderItem.order_id == order.id) + .all() + ) - # Check if all items are now processed and update order status accordingly all_confirmed = all( - u.get("state") in ("confirmed_available", "confirmed_unavailable", "returned") - for u in updated_units + i.item_state in ("confirmed_available", "confirmed_unavailable", "returned") + for i in all_items ) if all_confirmed: - # Determine order status based on item states has_available = any( - u.get("state") == "confirmed_available" for u in updated_units + i.item_state == "confirmed_available" for i in all_items ) all_unavailable = all( - u.get("state") == "confirmed_unavailable" for u in updated_units + i.item_state == "confirmed_unavailable" for i in all_items ) + now = datetime.now(UTC) if all_unavailable: - order.sync_status = "rejected" - order.rejected_at = datetime.now(UTC) + order.status = "cancelled" + order.cancelled_at = now elif has_available: - order.sync_status = "confirmed" - order.confirmed_at = datetime.now(UTC) + order.status = "processing" + order.confirmed_at = now + + order.updated_at = now return order def set_order_tracking( self, - order: LetzshopOrder, + order: Order, tracking_number: str, - tracking_carrier: str, - ) -> LetzshopOrder: + tracking_provider: str, + ) -> Order: """Set tracking information for an order.""" order.tracking_number = tracking_number - order.tracking_carrier = tracking_carrier - order.tracking_set_at = datetime.now(UTC) - order.sync_status = "shipped" + order.tracking_provider = tracking_provider + order.shipped_at = datetime.now(UTC) + order.status = "shipped" + order.updated_at = datetime.now(UTC) return order + def get_order_items(self, order: Order) -> list[OrderItem]: + """Get all items for an order.""" + return ( + self.db.query(OrderItem) + .filter(OrderItem.order_id == order.id) + .all() + ) + # ========================================================================= # Sync Log Operations # ========================================================================= @@ -560,11 +482,7 @@ class LetzshopOrderService: skip: int = 0, limit: int = 50, ) -> tuple[list[LetzshopSyncLog], int]: - """ - List sync logs for a vendor. - - Returns a tuple of (logs, total_count). - """ + """List sync logs for a vendor.""" query = self.db.query(LetzshopSyncLog).filter( LetzshopSyncLog.vendor_id == vendor_id ) @@ -588,11 +506,7 @@ class LetzshopOrderService: limit: int = 50, status: str | None = None, ) -> tuple[list[LetzshopFulfillmentQueue], int]: - """ - List fulfillment queue items for a vendor. - - Returns a tuple of (items, total_count). - """ + """List fulfillment queue items for a vendor.""" query = self.db.query(LetzshopFulfillmentQueue).filter( LetzshopFulfillmentQueue.vendor_id == vendor_id ) @@ -609,6 +523,24 @@ class LetzshopOrderService: ) return items, total + def add_to_fulfillment_queue( + self, + vendor_id: int, + order_id: int, + operation: str, + payload: dict[str, Any], + ) -> LetzshopFulfillmentQueue: + """Add an operation to the fulfillment queue.""" + queue_item = LetzshopFulfillmentQueue( + vendor_id=vendor_id, + order_id=order_id, + operation=operation, + payload=payload, + status="pending", + ) + self.db.add(queue_item) + return queue_item + # ========================================================================= # Unified Jobs Operations # ========================================================================= @@ -626,18 +558,6 @@ class LetzshopOrderService: Combines product imports from marketplace_import_jobs and order syncs from letzshop_sync_logs. - - Args: - vendor_id: Vendor ID - job_type: Filter by type ('import', 'order_sync', or None for all) - status: Filter by status - skip: Pagination offset - limit: Pagination limit - - Returns: - Tuple of (jobs_list, total_count) where jobs_list contains dicts - with id, type, status, created_at, started_at, completed_at, - records_processed, records_succeeded, records_failed. """ jobs = [] @@ -701,7 +621,6 @@ class LetzshopOrderService: # Sort all jobs by created_at descending jobs.sort(key=lambda x: x["created_at"], reverse=True) - # Get total count and apply pagination total = len(jobs) jobs = jobs[skip : skip + limit] @@ -719,34 +638,29 @@ class LetzshopOrderService: progress_callback: Callable[[int, int, int, int], None] | None = None, ) -> dict[str, Any]: """ - Import historical shipments into the database. + Import historical shipments into the unified orders table. Args: vendor_id: Vendor ID to import for. shipments: List of shipment data from Letzshop API. - match_products: Whether to match EAN to local products. + match_products: Whether to match GTIN to local products. progress_callback: Optional callback(processed, imported, updated, skipped) - for progress updates during import. Returns: - Dict with import statistics: - - total: Total shipments processed - - imported: New orders created - - updated: Existing orders updated - - skipped: Already up-to-date orders - - products_matched: Products matched by EAN - - products_not_found: Products not found in local catalog + Dict with import statistics. """ stats = { "total": len(shipments), "imported": 0, "updated": 0, "skipped": 0, + "errors": 0, "products_matched": 0, "products_not_found": 0, "eans_processed": set(), "eans_matched": set(), "eans_not_found": set(), + "error_messages": [], } for i, shipment in enumerate(shipments): @@ -758,36 +672,31 @@ class LetzshopOrderService: existing_order = self.get_order_by_shipment_id(vendor_id, shipment_id) if existing_order: - # Check if we need to update (e.g., state changed or missing data) - shipment_state = shipment.get("state") - needs_update = False + # Check if we need to update + letzshop_state = shipment.get("state") + state_mapping = { + "unconfirmed": "pending", + "confirmed": "processing", + "declined": "cancelled", + } + expected_status = state_mapping.get(letzshop_state, "processing") - if existing_order.letzshop_state != shipment_state: + needs_update = False + if existing_order.status != expected_status: self.update_order_from_shipment(existing_order, shipment) needs_update = True - else: - # Also fix sync_status if it's out of sync with letzshop_state - state_mapping = { - "unconfirmed": "pending", - "confirmed": "confirmed", - "declined": "rejected", - } - expected_sync_status = state_mapping.get(shipment_state, "confirmed") - if existing_order.sync_status != expected_sync_status: - existing_order.sync_status = expected_sync_status - needs_update = True - # Populate order_date if missing (for orders imported before this field existed) + # Update order_date if missing if not existing_order.order_date: order_data = shipment.get("order", {}) completed_at_str = order_data.get("completedAt") if completed_at_str: try: - from datetime import datetime - if completed_at_str.endswith("Z"): completed_at_str = completed_at_str[:-1] + "+00:00" - existing_order.order_date = datetime.fromisoformat(completed_at_str) + existing_order.order_date = datetime.fromisoformat( + completed_at_str + ) needs_update = True except (ValueError, TypeError): pass @@ -797,22 +706,29 @@ class LetzshopOrderService: else: stats["skipped"] += 1 else: - # Create new order - self.create_order(vendor_id, shipment) - stats["imported"] += 1 + # Create new order using unified service + try: + self.create_order(vendor_id, shipment) + stats["imported"] += 1 + except Exception as e: + stats["errors"] += 1 + stats["error_messages"].append( + f"Shipment {shipment_id}: {str(e)}" + ) + logger.error(f"Error importing shipment {shipment_id}: {e}") - # Process EANs for matching + # Process GTINs for matching if match_products: inventory_units = shipment.get("inventoryUnits", []) for unit in inventory_units: variant = unit.get("variant", {}) or {} trade_id = variant.get("tradeId") or {} - ean = trade_id.get("number") + gtin = trade_id.get("number") - if ean: - stats["eans_processed"].add(ean) + if gtin: + stats["eans_processed"].add(gtin) - # Report progress every 10 shipments or at the end + # Report progress if progress_callback and ((i + 1) % 10 == 0 or i == len(shipments) - 1): progress_callback( i + 1, @@ -821,9 +737,9 @@ class LetzshopOrderService: stats["skipped"], ) - # Match EANs to local products + # Match GTINs to local products if match_products and stats["eans_processed"]: - matched, not_found = self._match_eans_to_products( + matched, not_found = self._match_gtins_to_products( vendor_id, list(stats["eans_processed"]) ) stats["eans_matched"] = matched @@ -838,65 +754,47 @@ class LetzshopOrderService: return stats - def _match_eans_to_products( + def _match_gtins_to_products( self, vendor_id: int, - eans: list[str], + gtins: list[str], ) -> tuple[set[str], set[str]]: - """ - Match EAN codes to local products. - - Args: - vendor_id: Vendor ID to search products for. - eans: List of EAN codes to match. - - Returns: - Tuple of (matched_eans, not_found_eans). - """ - if not eans: + """Match GTIN codes to local products.""" + if not gtins: return set(), set() - # Query products by GTIN for this vendor products = ( self.db.query(Product) .filter( Product.vendor_id == vendor_id, - Product.gtin.in_(eans), + Product.gtin.in_(gtins), ) .all() ) - matched_eans = {p.gtin for p in products if p.gtin} - not_found_eans = set(eans) - matched_eans + matched_gtins = {p.gtin for p in products if p.gtin} + not_found_gtins = set(gtins) - matched_gtins logger.info( - f"EAN matching: {len(matched_eans)} matched, {len(not_found_eans)} not found" + f"GTIN matching: {len(matched_gtins)} matched, " + f"{len(not_found_gtins)} not found" ) - return matched_eans, not_found_eans + return matched_gtins, not_found_gtins - def get_products_by_eans( + def get_products_by_gtins( self, vendor_id: int, - eans: list[str], + gtins: list[str], ) -> dict[str, Product]: - """ - Get products by their EAN codes. - - Args: - vendor_id: Vendor ID to search products for. - eans: List of EAN codes to search. - - Returns: - Dict mapping EAN to Product. - """ - if not eans: + """Get products by their GTIN codes.""" + if not gtins: return {} products = ( self.db.query(Product) .filter( Product.vendor_id == vendor_id, - Product.gtin.in_(eans), + Product.gtin.in_(gtins), ) .all() ) @@ -907,57 +805,67 @@ class LetzshopOrderService: self, vendor_id: int, ) -> dict[str, Any]: - """ - Get summary of historical order data for a vendor. - - Returns: - Dict with summary statistics. - """ - # Count orders by state - order_counts = ( + """Get summary of Letzshop orders for a vendor.""" + # Count orders by status + status_counts = ( self.db.query( - LetzshopOrder.letzshop_state, - func.count(LetzshopOrder.id).label("count"), + Order.status, + func.count(Order.id).label("count"), ) - .filter(LetzshopOrder.vendor_id == vendor_id) - .group_by(LetzshopOrder.letzshop_state) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) + .group_by(Order.status) .all() ) # Count orders by locale locale_counts = ( self.db.query( - LetzshopOrder.customer_locale, - func.count(LetzshopOrder.id).label("count"), + Order.customer_locale, + func.count(Order.id).label("count"), ) - .filter(LetzshopOrder.vendor_id == vendor_id) - .group_by(LetzshopOrder.customer_locale) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) + .group_by(Order.customer_locale) .all() ) # Count orders by country country_counts = ( self.db.query( - LetzshopOrder.shipping_country_iso, - func.count(LetzshopOrder.id).label("count"), + Order.ship_country_iso, + func.count(Order.id).label("count"), ) - .filter(LetzshopOrder.vendor_id == vendor_id) - .group_by(LetzshopOrder.shipping_country_iso) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) + .group_by(Order.ship_country_iso) .all() ) - # Total revenue + # Total orders total_orders = ( - self.db.query(func.count(LetzshopOrder.id)) - .filter(LetzshopOrder.vendor_id == vendor_id) + self.db.query(func.count(Order.id)) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) .scalar() or 0 ) # Unique customers unique_customers = ( - self.db.query(func.count(func.distinct(LetzshopOrder.customer_email))) - .filter(LetzshopOrder.vendor_id == vendor_id) + self.db.query(func.count(func.distinct(Order.customer_email))) + .filter( + Order.vendor_id == vendor_id, + Order.channel == "letzshop", + ) .scalar() or 0 ) @@ -965,9 +873,13 @@ class LetzshopOrderService: return { "total_orders": total_orders, "unique_customers": unique_customers, - "orders_by_state": {state: count for state, count in order_counts}, - "orders_by_locale": {locale or "unknown": count for locale, count in locale_counts}, - "orders_by_country": {country or "unknown": count for country, count in country_counts}, + "orders_by_status": {status: count for status, count in status_counts}, + "orders_by_locale": { + locale or "unknown": count for locale, count in locale_counts + }, + "orders_by_country": { + country or "unknown": count for country, count in country_counts + }, } # ========================================================================= @@ -978,15 +890,7 @@ class LetzshopOrderService: self, vendor_id: int, ) -> LetzshopHistoricalImportJob | None: - """ - Get any running historical import job for a vendor. - - Args: - vendor_id: Vendor ID to check. - - Returns: - Running job or None if no active job. - """ + """Get any running historical import job for a vendor.""" return ( self.db.query(LetzshopHistoricalImportJob) .filter( @@ -1003,16 +907,7 @@ class LetzshopOrderService: vendor_id: int, user_id: int, ) -> LetzshopHistoricalImportJob: - """ - Create a new historical import job. - - Args: - vendor_id: Vendor ID to import for. - user_id: User ID who initiated the import. - - Returns: - Created job record. - """ + """Create a new historical import job.""" job = LetzshopHistoricalImportJob( vendor_id=vendor_id, user_id=user_id, @@ -1028,16 +923,7 @@ class LetzshopOrderService: vendor_id: int, job_id: int, ) -> LetzshopHistoricalImportJob | None: - """ - Get a historical import job by ID. - - Args: - vendor_id: Vendor ID to verify ownership. - job_id: Job ID to fetch. - - Returns: - Job record or None if not found. - """ + """Get a historical import job by ID.""" return ( self.db.query(LetzshopHistoricalImportJob) .filter( diff --git a/docs/implementation/unified-order-view.md b/docs/implementation/unified-order-view.md index 82eebc4a..33332923 100644 --- a/docs/implementation/unified-order-view.md +++ b/docs/implementation/unified-order-view.md @@ -189,20 +189,24 @@ The `letzshop_orders` table has been removed. All data now goes directly into th | `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 | +| `models/schema/letzshop.py` | Updated schemas for unified Order model | | `app/services/order_service.py` | Unified service with `create_letzshop_order()` | +| `app/services/letzshop/order_service.py` | Updated to use unified Order model | +| `app/api/v1/admin/letzshop.py` | Updated endpoints for unified model | | `alembic/versions/c1d2e3f4a5b6_unified_order_schema.py` | Migration | -## API Endpoints (To Be Updated) +## API Endpoints -The following endpoints need updates to use the unified model: +All Letzshop order endpoints now use the unified Order 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()` | +| Endpoint | Description | +|----------|-------------| +| `GET /admin/letzshop/vendors/{id}/orders` | List orders with `channel='letzshop'` filter | +| `GET /admin/letzshop/orders/{id}` | Get order detail with items | +| `POST /admin/letzshop/vendors/{id}/orders/{id}/confirm` | Confirm items via `external_item_id` | +| `POST /admin/letzshop/vendors/{id}/orders/{id}/reject` | Decline items via `external_item_id` | +| `POST /admin/letzshop/vendors/{id}/orders/{id}/items/{item_id}/confirm` | Confirm single item | +| `POST /admin/letzshop/vendors/{id}/orders/{id}/items/{item_id}/decline` | Decline single item | ## Order Number Format @@ -260,6 +264,7 @@ Each marketplace would use: - [x] Database migration created - [x] Order schemas updated - [x] Unified order service created -- [ ] Letzshop order service updated -- [ ] API endpoints updated +- [x] Letzshop order service updated +- [x] Letzshop schemas updated +- [x] API endpoints updated - [ ] Frontend updated diff --git a/models/schema/letzshop.py b/models/schema/letzshop.py index 27607979..73e1807c 100644 --- a/models/schema/letzshop.py +++ b/models/schema/letzshop.py @@ -71,76 +71,119 @@ class LetzshopCredentialsStatus(BaseModel): # ============================================================================ -# Letzshop Order Schemas +# Letzshop Order Schemas (using unified Order model) # ============================================================================ -class LetzshopInventoryUnit(BaseModel): - """Schema for Letzshop inventory unit.""" +class LetzshopOrderItemResponse(BaseModel): + """Schema for order item in Letzshop order response.""" - id: str - state: str + model_config = ConfigDict(from_attributes=True) + + id: int + product_id: int + product_name: str + product_sku: str | None = None + gtin: str | None = None + gtin_type: str | None = None + quantity: int + unit_price: float + total_price: float + external_item_id: str | None = None # Letzshop inventory unit ID + external_variant_id: str | None = None + item_state: str | None = None # confirmed_available, confirmed_unavailable -class LetzshopOrderBase(BaseModel): - """Base schema for Letzshop order.""" - - letzshop_order_id: str - letzshop_shipment_id: str | None = None - letzshop_order_number: str | None = None - letzshop_state: str | None = None - customer_email: str | None = None - customer_name: str | None = None - customer_locale: str | None = None - shipping_country_iso: str | None = None - billing_country_iso: str | None = None - total_amount: str | None = None - currency: str = "EUR" - - -class LetzshopOrderCreate(LetzshopOrderBase): - """Schema for creating a Letzshop order record.""" - - vendor_id: int - raw_order_data: dict[str, Any] | None = None - inventory_units: list[dict[str, Any]] | None = None - - -class LetzshopOrderResponse(LetzshopOrderBase): - """Schema for Letzshop order response.""" +class LetzshopOrderResponse(BaseModel): + """Schema for Letzshop order response (from unified Order model).""" model_config = ConfigDict(from_attributes=True) id: int vendor_id: int - local_order_id: int | None - sync_status: str - last_synced_at: datetime | None - sync_error: str | None - confirmed_at: datetime | None - rejected_at: datetime | None - tracking_set_at: datetime | None - tracking_number: str | None - tracking_carrier: str | None - inventory_units: list[dict[str, Any]] | None - order_date: datetime | None + order_number: str + + # External references + external_order_id: str | None = None + external_shipment_id: str | None = None + external_order_number: str | None = None + + # Status + status: str # pending, processing, shipped, delivered, cancelled + + # Customer info + customer_email: str + customer_name: str # computed: customer_first_name + customer_last_name + customer_locale: str | None = None + + # Address info + ship_country_iso: str + bill_country_iso: str + + # Financial + total_amount: float + currency: str = "EUR" + + # Tracking + tracking_number: str | None = None + tracking_provider: str | None = None + + # Timestamps + order_date: datetime + confirmed_at: datetime | None = None + shipped_at: datetime | None = None + cancelled_at: datetime | None = None created_at: datetime updated_at: datetime + # Items (for list view, may be empty) + items: list[LetzshopOrderItemResponse] = Field(default_factory=list) + class LetzshopOrderDetailResponse(LetzshopOrderResponse): - """Schema for detailed Letzshop order response with raw data.""" + """Schema for detailed Letzshop order response with all data.""" - raw_order_data: dict[str, Any] | None = None + # Full customer snapshot + customer_first_name: str + customer_last_name: str + customer_phone: str | None = None + + # Full shipping address + ship_first_name: str + ship_last_name: str + ship_company: str | None = None + ship_address_line_1: str + ship_address_line_2: str | None = None + ship_city: str + ship_postal_code: str + + # Full billing address + bill_first_name: str + bill_last_name: str + bill_company: str | None = None + bill_address_line_1: str + bill_address_line_2: str | None = None + bill_city: str + bill_postal_code: str + + # Raw marketplace data + external_data: dict[str, Any] | None = None + + # Notes + customer_notes: str | None = None + internal_notes: str | None = None class LetzshopOrderStats(BaseModel): """Schema for order statistics by status.""" pending: int = 0 - confirmed: int = 0 - rejected: int = 0 + processing: int = 0 shipped: int = 0 + delivered: int = 0 + cancelled: int = 0 + total: int = 0 + has_declined_items: int = 0 # Orders with at least one declined item class LetzshopOrderListResponse(BaseModel): @@ -150,7 +193,7 @@ class LetzshopOrderListResponse(BaseModel): total: int skip: int limit: int - stats: LetzshopOrderStats | None = None # Order counts by sync_status + stats: LetzshopOrderStats | None = None # ============================================================================ @@ -191,7 +234,7 @@ class FulfillmentQueueItemResponse(BaseModel): id: int vendor_id: int - letzshop_order_id: int + order_id: int # FK to unified orders table operation: str payload: dict[str, Any] status: str