feat: complete unified order model integration for Letzshop API
Update API endpoints and schemas to use the unified Order model: - Update Letzshop order schemas with OrderItem support - Update API responses to use new field names (external_*, status, etc.) - Update confirm/reject endpoints to use OrderItem.external_item_id - Update Letzshop order service for unified Order model queries - Update documentation to reflect completed implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user