feat: update Pydantic schemas for unified order model
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user