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:
2025-12-19 21:17:47 +01:00
parent 6f3a07c7d7
commit 2e3edcf197
2 changed files with 368 additions and 57 deletions

View File

@@ -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"

View File

@@ -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]