diff --git a/app/api/v1/admin/letzshop.py b/app/api/v1/admin/letzshop.py index b12c91d5..b6a1488c 100644 --- a/app/api/v1/admin/letzshop.py +++ b/app/api/v1/admin/letzshop.py @@ -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)) diff --git a/app/services/letzshop/order_service.py b/app/services/letzshop/order_service.py index 781176be..486a0272 100644 --- a/app/services/letzshop/order_service.py +++ b/app/services/letzshop/order_service.py @@ -403,6 +403,56 @@ class LetzshopOrderService: order.sync_status = "rejected" return order + def update_inventory_unit_state( + self, order: LetzshopOrder, item_id: str, state: str + ) -> LetzshopOrder: + """ + Update the state of a single inventory unit in an order. + + Args: + order: The order containing the inventory unit. + item_id: The inventory unit ID to update. + state: The new state (confirmed_available, confirmed_unavailable). + + Returns: + The updated order. + """ + if not order.inventory_units: + return order + + # Update the specific item's state + updated_units = [] + for unit in order.inventory_units: + if unit.get("id") == item_id: + unit["state"] = state + updated_units.append(unit) + + order.inventory_units = updated_units + + # Check if all items are now processed and update order status accordingly + all_confirmed = all( + u.get("state") in ("confirmed_available", "confirmed_unavailable", "returned") + for u in updated_units + ) + + if all_confirmed: + # Determine order status based on item states + has_available = any( + u.get("state") == "confirmed_available" for u in updated_units + ) + all_unavailable = all( + u.get("state") == "confirmed_unavailable" for u in updated_units + ) + + if all_unavailable: + order.sync_status = "rejected" + order.rejected_at = datetime.now(UTC) + elif has_available: + order.sync_status = "confirmed" + order.confirmed_at = datetime.now(UTC) + + return order + def set_order_tracking( self, order: LetzshopOrder, diff --git a/app/templates/admin/marketplace-letzshop.html b/app/templates/admin/marketplace-letzshop.html index ea5c1597..d695fb4f 100644 --- a/app/templates/admin/marketplace-letzshop.html +++ b/app/templates/admin/marketplace-letzshop.html @@ -274,7 +274,7 @@ 'bg-red-100 text-red-700': selectedOrder?.sync_status === 'rejected', 'bg-blue-100 text-blue-700': selectedOrder?.sync_status === 'shipped' }" - x-text="selectedOrder?.sync_status?.toUpperCase()" + x-text="selectedOrder?.sync_status === 'rejected' ? 'DECLINED' : selectedOrder?.sync_status?.toUpperCase()" >
@@ -296,19 +296,79 @@
-

Items

-
-