From 2e3edcf197ddde16b49031383ddd8475307fd778 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 19 Dec 2025 21:17:47 +0100 Subject: [PATCH] feat: update Pydantic schemas for unified order model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AddressSnapshot and CustomerSnapshot schemas - Update OrderItemResponse with gtin fields and item_state - Update OrderResponse with all snapshot fields - Add OrderListItem for simplified list views - Add Letzshop-specific schemas (LetzshopOrderImport, LetzshopShippingInfo) - Update AdminOrderItem with new fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- models/schema/letzshop.py | 56 ++++++ models/schema/order.py | 369 ++++++++++++++++++++++++++++++++------ 2 files changed, 368 insertions(+), 57 deletions(-) diff --git a/models/schema/letzshop.py b/models/schema/letzshop.py index 5d50e96c..27607979 100644 --- a/models/schema/letzshop.py +++ b/models/schema/letzshop.py @@ -91,6 +91,9 @@ class LetzshopOrderBase(BaseModel): 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" @@ -120,6 +123,7 @@ class LetzshopOrderResponse(LetzshopOrderBase): tracking_number: str | None tracking_carrier: str | None inventory_units: list[dict[str, Any]] | None + order_date: datetime | None created_at: datetime updated_at: datetime @@ -368,3 +372,55 @@ class LetzshopJobsListResponse(BaseModel): jobs: list[LetzshopJobItem] total: int + + +# ============================================================================ +# Historical Import Job Schemas +# ============================================================================ + + +class LetzshopHistoricalImportJobResponse(BaseModel): + """Schema for historical import job status (polling endpoint).""" + + model_config = ConfigDict(from_attributes=True) + + id: int + vendor_id: int + status: str # pending, fetching, processing, completed, failed + current_phase: str | None = None # "confirmed" or "declined" + + # Fetch progress + current_page: int = 0 + total_pages: int | None = None + shipments_fetched: int = 0 + + # Processing progress + orders_processed: int = 0 + orders_imported: int = 0 + orders_updated: int = 0 + orders_skipped: int = 0 + + # EAN matching stats + products_matched: int = 0 + products_not_found: int = 0 + + # Phase-specific stats (when complete) + confirmed_stats: dict[str, Any] | None = None + declined_stats: dict[str, Any] | None = None + + # Error handling + error_message: str | None = None + + # Timing + started_at: datetime | None = None + completed_at: datetime | None = None + created_at: datetime + updated_at: datetime + + +class LetzshopHistoricalImportStartResponse(BaseModel): + """Schema for starting a historical import job.""" + + job_id: int + status: str = "pending" + message: str = "Historical import job started" diff --git a/models/schema/order.py b/models/schema/order.py index b28bc87c..c21e3a9c 100644 --- a/models/schema/order.py +++ b/models/schema/order.py @@ -1,12 +1,50 @@ # models/schema/order.py """ -Pydantic schema for order operations. +Pydantic schemas for unified order operations. + +Supports both direct orders and marketplace orders (Letzshop, etc.) +with snapshotted customer and address data. """ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field +# ============================================================================ +# Address Snapshot Schemas +# ============================================================================ + + +class AddressSnapshot(BaseModel): + """Address snapshot for order creation.""" + + first_name: str = Field(..., min_length=1, max_length=100) + last_name: str = Field(..., min_length=1, max_length=100) + company: str | None = Field(None, max_length=200) + address_line_1: str = Field(..., min_length=1, max_length=255) + address_line_2: str | None = Field(None, max_length=255) + city: str = Field(..., min_length=1, max_length=100) + postal_code: str = Field(..., min_length=1, max_length=20) + country_iso: str = Field(..., min_length=2, max_length=5) + + +class AddressSnapshotResponse(BaseModel): + """Address snapshot in order response.""" + + first_name: str + last_name: str + company: str | None + address_line_1: str + address_line_2: str | None + city: str + postal_code: str + country_iso: str + + @property + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}".strip() + + # ============================================================================ # Order Item Schemas # ============================================================================ @@ -29,48 +67,69 @@ class OrderItemResponse(BaseModel): product_id: int product_name: str product_sku: str | None + gtin: str | None + gtin_type: str | None quantity: int unit_price: float total_price: float + + # External references (for marketplace items) + external_item_id: str | None = None + external_variant_id: str | None = None + + # Item state (for marketplace confirmation flow) + item_state: str | None = None + + # Inventory tracking inventory_reserved: bool inventory_fulfilled: bool + created_at: datetime updated_at: datetime + @property + def is_confirmed(self) -> bool: + """Check if item has been confirmed (available or unavailable).""" + return self.item_state in ("confirmed_available", "confirmed_unavailable") + + @property + def is_available(self) -> bool: + """Check if item is confirmed as available.""" + return self.item_state == "confirmed_available" + + @property + def is_declined(self) -> bool: + """Check if item was declined (unavailable).""" + return self.item_state == "confirmed_unavailable" + # ============================================================================ -# Order Address Schemas +# Customer Snapshot Schemas # ============================================================================ -class OrderAddressCreate(BaseModel): - """Schema for order address (shipping/billing).""" +class CustomerSnapshot(BaseModel): + """Customer snapshot for order creation.""" first_name: str = Field(..., min_length=1, max_length=100) last_name: str = Field(..., min_length=1, max_length=100) - company: str | None = Field(None, max_length=200) - address_line_1: str = Field(..., min_length=1, max_length=255) - address_line_2: str | None = Field(None, max_length=255) - city: str = Field(..., min_length=1, max_length=100) - postal_code: str = Field(..., min_length=1, max_length=20) - country: str = Field(..., min_length=2, max_length=100) + email: str = Field(..., max_length=255) + phone: str | None = Field(None, max_length=50) + locale: str | None = Field(None, max_length=10) -class OrderAddressResponse(BaseModel): - """Schema for order address response.""" +class CustomerSnapshotResponse(BaseModel): + """Customer snapshot in order response.""" - model_config = ConfigDict(from_attributes=True) - - id: int - address_type: str first_name: str last_name: str - company: str | None - address_line_1: str - address_line_2: str | None - city: str - postal_code: str - country: str + email: str + phone: str | None + locale: str | None + + @property + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}".strip() # ============================================================================ @@ -79,14 +138,17 @@ class OrderAddressResponse(BaseModel): class OrderCreate(BaseModel): - """Schema for creating an order.""" + """Schema for creating an order (direct channel).""" customer_id: int | None = None # Optional for guest checkout items: list[OrderItemCreate] = Field(..., min_length=1) - # Addresses - shipping_address: OrderAddressCreate - billing_address: OrderAddressCreate | None = None # Use shipping if not provided + # Customer info snapshot + customer: CustomerSnapshot + + # Addresses (snapshots) + shipping_address: AddressSnapshot + billing_address: AddressSnapshot | None = None # Use shipping if not provided # Optional fields shipping_method: str | None = None @@ -103,9 +165,24 @@ class OrderUpdate(BaseModel): None, pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$" ) tracking_number: str | None = None + tracking_provider: str | None = None internal_notes: str | None = None +class OrderTrackingUpdate(BaseModel): + """Schema for setting tracking information.""" + + tracking_number: str = Field(..., min_length=1, max_length=100) + tracking_provider: str = Field(..., min_length=1, max_length=100) + + +class OrderItemStateUpdate(BaseModel): + """Schema for updating item state (marketplace confirmation).""" + + item_id: int + state: str = Field(..., pattern="^(confirmed_available|confirmed_unavailable)$") + + # ============================================================================ # Order Response Schemas # ============================================================================ @@ -120,39 +197,86 @@ class OrderResponse(BaseModel): vendor_id: int customer_id: int order_number: str + + # Channel/Source + channel: str + external_order_id: str | None = None + external_shipment_id: str | None = None + external_order_number: str | None = None + + # Status status: str # Financial - subtotal: float - tax_amount: float - shipping_amount: float - discount_amount: float + subtotal: float | None + tax_amount: float | None + shipping_amount: float | None + discount_amount: float | None total_amount: float currency: str - # Shipping + # Customer snapshot + customer_first_name: str + customer_last_name: str + customer_email: str + customer_phone: str | None + customer_locale: str | None + + # Shipping address snapshot + ship_first_name: str + ship_last_name: str + ship_company: str | None + ship_address_line_1: str + ship_address_line_2: str | None + ship_city: str + ship_postal_code: str + ship_country_iso: str + + # Billing address snapshot + bill_first_name: str + bill_last_name: str + bill_company: str | None + bill_address_line_1: str + bill_address_line_2: str | None + bill_city: str + bill_postal_code: str + bill_country_iso: str + + # Tracking shipping_method: str | None tracking_number: str | None + tracking_provider: str | None # Notes customer_notes: str | None internal_notes: str | None # Timestamps - created_at: datetime - updated_at: datetime - paid_at: datetime | None + order_date: datetime + confirmed_at: datetime | None shipped_at: datetime | None delivered_at: datetime | None cancelled_at: datetime | None + created_at: datetime + updated_at: datetime + + @property + def customer_full_name(self) -> str: + return f"{self.customer_first_name} {self.customer_last_name}".strip() + + @property + def ship_full_name(self) -> str: + return f"{self.ship_first_name} {self.ship_last_name}".strip() + + @property + def is_marketplace_order(self) -> bool: + return self.channel != "direct" class OrderDetailResponse(OrderResponse): - """Schema for detailed order response with items and addresses.""" + """Schema for detailed order response with items.""" - items: list[OrderItemResponse] - shipping_address: OrderAddressResponse - billing_address: OrderAddressResponse + items: list[OrderItemResponse] = [] class OrderListResponse(BaseModel): @@ -164,6 +288,49 @@ class OrderListResponse(BaseModel): limit: int +# ============================================================================ +# Order List Item (Simplified for list views) +# ============================================================================ + + +class OrderListItem(BaseModel): + """Simplified order item for list views.""" + + model_config = ConfigDict(from_attributes=True) + + id: int + vendor_id: int + order_number: str + channel: str + status: str + + # External references + external_order_number: str | None = None + + # Customer + customer_full_name: str + customer_email: str + + # Financial + total_amount: float + currency: str + + # Shipping + ship_country_iso: str + + # Tracking + tracking_number: str | None + tracking_provider: str | None + + # Item count + item_count: int = 0 + + # Timestamps + order_date: datetime + confirmed_at: datetime | None + shipped_at: datetime | None + + # ============================================================================ # Admin Order Schemas # ============================================================================ @@ -179,34 +346,42 @@ class AdminOrderItem(BaseModel): vendor_name: str | None = None vendor_code: str | None = None customer_id: int - customer_name: str | None = None - customer_email: str | None = None order_number: str channel: str status: str + # External references + external_order_number: str | None = None + external_shipment_id: str | None = None + + # Customer snapshot + customer_full_name: str + customer_email: str + # Financial - subtotal: float - tax_amount: float - shipping_amount: float - discount_amount: float + subtotal: float | None + tax_amount: float | None + shipping_amount: float | None + discount_amount: float | None total_amount: float currency: str # Shipping - shipping_method: str | None + ship_country_iso: str tracking_number: str | None + tracking_provider: str | None # Item count item_count: int = 0 # Timestamps - created_at: datetime - updated_at: datetime - paid_at: datetime | None + order_date: datetime + confirmed_at: datetime | None shipped_at: datetime | None delivered_at: datetime | None cancelled_at: datetime | None + created_at: datetime + updated_at: datetime class AdminOrderListResponse(BaseModel): @@ -221,15 +396,21 @@ class AdminOrderListResponse(BaseModel): class AdminOrderStats(BaseModel): """Order statistics for admin dashboard.""" - total_orders: int - pending_orders: int - processing_orders: int - shipped_orders: int - delivered_orders: int - cancelled_orders: int - refunded_orders: int - total_revenue: float - vendors_with_orders: int + total_orders: int = 0 + pending_orders: int = 0 + processing_orders: int = 0 + shipped_orders: int = 0 + delivered_orders: int = 0 + cancelled_orders: int = 0 + refunded_orders: int = 0 + total_revenue: float = 0.0 + + # By channel + direct_orders: int = 0 + letzshop_orders: int = 0 + + # Vendors + vendors_with_orders: int = 0 class AdminOrderStatusUpdate(BaseModel): @@ -239,6 +420,7 @@ class AdminOrderStatusUpdate(BaseModel): ..., pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$" ) tracking_number: str | None = None + tracking_provider: str | None = None reason: str | None = Field(None, description="Reason for status change") @@ -255,3 +437,76 @@ class AdminVendorsWithOrdersResponse(BaseModel): """Response for vendors with orders list.""" vendors: list[AdminVendorWithOrders] + + +# ============================================================================ +# Letzshop-specific Schemas +# ============================================================================ + + +class LetzshopOrderImport(BaseModel): + """Schema for importing a Letzshop order from shipment data.""" + + shipment_id: str + order_id: str + order_number: str + order_date: datetime + + # Customer + customer_email: str + customer_locale: str | None = None + + # 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 + ship_country_iso: str + + # 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 + bill_country_iso: str + + # Totals + total_amount: float + currency: str = "EUR" + + # State + letzshop_state: str # unconfirmed, confirmed, declined + + # Items + inventory_units: list[dict] + + # Raw data + raw_data: dict | None = None + + +class LetzshopShippingInfo(BaseModel): + """Shipping info retrieved from Letzshop.""" + + tracking_number: str + tracking_provider: str + shipment_id: str + + +class LetzshopOrderConfirmItem(BaseModel): + """Schema for confirming/declining a single item.""" + + item_id: int + external_item_id: str + action: str = Field(..., pattern="^(confirm|decline)$") + + +class LetzshopOrderConfirmRequest(BaseModel): + """Schema for confirming/declining order items.""" + + items: list[LetzshopOrderConfirmItem]