feat: add item-level order confirmation/decline support
Per Letzshop API, each inventory unit must be confirmed/declined individually.
This enables partial confirmation (some items confirmed, others declined).
Admin API endpoints:
- POST /vendors/{id}/orders/{id}/confirm - confirm all items
- POST /vendors/{id}/orders/{id}/reject - decline all items
- POST /vendors/{id}/orders/{id}/items/{id}/confirm - confirm single item
- POST /vendors/{id}/orders/{id}/items/{id}/decline - decline single item
Order detail modal now shows:
- Product name, EAN, SKU, MPN, price per item
- Per-item state badge (unconfirmed/confirmed/declined)
- Per-item confirm/decline buttons for pending items
- Bulk confirm/decline all buttons
Order status logic:
- If all items declined -> order is "declined"
- If any item confirmed -> order is "confirmed"
- Partial confirmation supported
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,10 +22,12 @@ from app.services.letzshop import (
|
||||
LetzshopClientError,
|
||||
LetzshopCredentialsService,
|
||||
LetzshopOrderService,
|
||||
OrderNotFoundError,
|
||||
VendorNotFoundError,
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.schema.letzshop import (
|
||||
FulfillmentOperationResponse,
|
||||
LetzshopConnectionTestRequest,
|
||||
LetzshopConnectionTestResponse,
|
||||
LetzshopCredentialsCreate,
|
||||
@@ -647,3 +649,246 @@ def get_import_summary(
|
||||
"success": True,
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fulfillment Operations (Admin)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vendors/{vendor_id}/orders/{order_id}/confirm",
|
||||
response_model=FulfillmentOperationResponse,
|
||||
)
|
||||
def confirm_order(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
order_id: int = Path(..., description="Order ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Confirm all inventory units for a Letzshop order.
|
||||
|
||||
Sends confirmInventoryUnits mutation with isAvailable=true for all items.
|
||||
"""
|
||||
order_service = get_order_service(db)
|
||||
creds_service = get_credentials_service(db)
|
||||
|
||||
try:
|
||||
order = order_service.get_order_or_raise(vendor_id, order_id)
|
||||
except OrderNotFoundError:
|
||||
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
||||
|
||||
# Get inventory unit IDs from order
|
||||
if not order.inventory_units:
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="No inventory units found in order",
|
||||
)
|
||||
|
||||
inventory_unit_ids = [u.get("id") for u in order.inventory_units if u.get("id")]
|
||||
|
||||
if not inventory_unit_ids:
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="No inventory unit IDs found in order",
|
||||
)
|
||||
|
||||
try:
|
||||
with creds_service.create_client(vendor_id) as client:
|
||||
result = client.confirm_inventory_units(inventory_unit_ids)
|
||||
|
||||
if not result.get("inventoryUnits"):
|
||||
error_messages = [
|
||||
e.get("message", "Unknown error")
|
||||
for e in result.get("errors", [])
|
||||
]
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="Some inventory units could not be confirmed",
|
||||
errors=error_messages,
|
||||
)
|
||||
|
||||
# Update order status
|
||||
order_service.mark_order_confirmed(order)
|
||||
db.commit()
|
||||
|
||||
return FulfillmentOperationResponse(
|
||||
success=True,
|
||||
message=f"Confirmed {len(inventory_unit_ids)} inventory units",
|
||||
confirmed_units=[u.get("id") for u in result.get("inventoryUnits", [])],
|
||||
)
|
||||
|
||||
except LetzshopClientError as e:
|
||||
return FulfillmentOperationResponse(success=False, message=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vendors/{vendor_id}/orders/{order_id}/reject",
|
||||
response_model=FulfillmentOperationResponse,
|
||||
)
|
||||
def reject_order(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
order_id: int = Path(..., description="Order ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Decline all inventory units for a Letzshop order.
|
||||
|
||||
Sends confirmInventoryUnits mutation with isAvailable=false for all items.
|
||||
"""
|
||||
order_service = get_order_service(db)
|
||||
creds_service = get_credentials_service(db)
|
||||
|
||||
try:
|
||||
order = order_service.get_order_or_raise(vendor_id, order_id)
|
||||
except OrderNotFoundError:
|
||||
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
||||
|
||||
# Get inventory unit IDs from order
|
||||
if not order.inventory_units:
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="No inventory units found in order",
|
||||
)
|
||||
|
||||
inventory_unit_ids = [u.get("id") for u in order.inventory_units if u.get("id")]
|
||||
|
||||
if not inventory_unit_ids:
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="No inventory unit IDs found in order",
|
||||
)
|
||||
|
||||
try:
|
||||
with creds_service.create_client(vendor_id) as client:
|
||||
result = client.reject_inventory_units(inventory_unit_ids)
|
||||
|
||||
if not result.get("inventoryUnits"):
|
||||
error_messages = [
|
||||
e.get("message", "Unknown error")
|
||||
for e in result.get("errors", [])
|
||||
]
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="Some inventory units could not be declined",
|
||||
errors=error_messages,
|
||||
)
|
||||
|
||||
# Update order status
|
||||
order_service.mark_order_rejected(order)
|
||||
db.commit()
|
||||
|
||||
return FulfillmentOperationResponse(
|
||||
success=True,
|
||||
message=f"Declined {len(inventory_unit_ids)} inventory units",
|
||||
)
|
||||
|
||||
except LetzshopClientError as e:
|
||||
return FulfillmentOperationResponse(success=False, message=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vendors/{vendor_id}/orders/{order_id}/items/{item_id}/confirm",
|
||||
response_model=FulfillmentOperationResponse,
|
||||
)
|
||||
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"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Confirm a single inventory unit in an order.
|
||||
|
||||
Sends confirmInventoryUnits mutation with isAvailable=true for one item.
|
||||
"""
|
||||
order_service = get_order_service(db)
|
||||
creds_service = get_credentials_service(db)
|
||||
|
||||
try:
|
||||
order = order_service.get_order_or_raise(vendor_id, order_id)
|
||||
except OrderNotFoundError:
|
||||
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
||||
|
||||
try:
|
||||
with creds_service.create_client(vendor_id) as client:
|
||||
result = client.confirm_inventory_units([item_id])
|
||||
|
||||
if not result.get("inventoryUnits"):
|
||||
error_messages = [
|
||||
e.get("message", "Unknown error")
|
||||
for e in result.get("errors", [])
|
||||
]
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="Failed to confirm item",
|
||||
errors=error_messages,
|
||||
)
|
||||
|
||||
# Update local inventory unit state
|
||||
order_service.update_inventory_unit_state(order, item_id, "confirmed_available")
|
||||
db.commit()
|
||||
|
||||
return FulfillmentOperationResponse(
|
||||
success=True,
|
||||
message="Item confirmed",
|
||||
confirmed_units=[item_id],
|
||||
)
|
||||
|
||||
except LetzshopClientError as e:
|
||||
return FulfillmentOperationResponse(success=False, message=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vendors/{vendor_id}/orders/{order_id}/items/{item_id}/decline",
|
||||
response_model=FulfillmentOperationResponse,
|
||||
)
|
||||
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"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Decline a single inventory unit in an order.
|
||||
|
||||
Sends confirmInventoryUnits mutation with isAvailable=false for one item.
|
||||
"""
|
||||
order_service = get_order_service(db)
|
||||
creds_service = get_credentials_service(db)
|
||||
|
||||
try:
|
||||
order = order_service.get_order_or_raise(vendor_id, order_id)
|
||||
except OrderNotFoundError:
|
||||
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
||||
|
||||
try:
|
||||
with creds_service.create_client(vendor_id) as client:
|
||||
result = client.reject_inventory_units([item_id])
|
||||
|
||||
if not result.get("inventoryUnits"):
|
||||
error_messages = [
|
||||
e.get("message", "Unknown error")
|
||||
for e in result.get("errors", [])
|
||||
]
|
||||
return FulfillmentOperationResponse(
|
||||
success=False,
|
||||
message="Failed to decline item",
|
||||
errors=error_messages,
|
||||
)
|
||||
|
||||
# Update local inventory unit state
|
||||
order_service.update_inventory_unit_state(order, item_id, "confirmed_unavailable")
|
||||
db.commit()
|
||||
|
||||
return FulfillmentOperationResponse(
|
||||
success=True,
|
||||
message="Item declined",
|
||||
)
|
||||
|
||||
except LetzshopClientError as e:
|
||||
return FulfillmentOperationResponse(success=False, message=str(e))
|
||||
|
||||
Reference in New Issue
Block a user