$order->uuid]); // Guard 1: Verify packing completed with valid dimensions if (! $this->validatePackingGate($order)) { return response()->json([ 'error' => 'Order not yet packed with valid dimensions', 'details' => [ 'packing_completed_at' => $order->packing_completed_at, 'dimensions' => [ 'width' => $order->packing_width, 'length' => $order->packing_length, 'weight' => $order->packing_weight, ], ], ], 400); } // Guard 2: Verify payment and approval rules if (! $this->validatePaymentAndApprovalGate($order)) { $message = $order->is_custom_order ? 'Custom order requires proof approved and balance paid' : 'Standard order must be fully paid'; return response()->json([ 'error' => $message, 'payment_status' => $order->payment_status, ], 403); } // Guard 3: Check order status is Ready to Ship if ($order->status !== 'ready_to_ship') { return response()->json([ 'error' => 'Order must be in Ready to Ship state', 'current_status' => $order->status, ], 409); } // Guard 4: Idempotency - if already shipped, return existing shipment if ($order->courier_waybill_id) { Log::info('Shipment already exists, returning existing', ['order_uuid' => $order->uuid]); return response()->json([ 'success' => true, 'message' => 'Shipment already created', 'shipment' => [ 'waybill_id' => $order->courier_waybill_id, 'tracking_number' => $order->courier_tracking_number, ], ]); } try { // Call courier API $shipmentData = $this->courierService->createShipment( $order->id, $order->packing_width, $order->packing_length, $order->packing_weight, ); // Save shipment details $order->update([ 'courier_waybill_id' => $shipmentData['waybill_id'], 'courier_tracking_number' => $shipmentData['tracking_number'], 'courier_status' => 'awaiting_collection', 'status' => 'awaiting_collection', ]); Log::info('Shipment created with courier', [ 'order_uuid' => $order->uuid, 'waybill_id' => $shipmentData['waybill_id'], 'tracking_number' => $shipmentData['tracking_number'], ]); // Fetch and store shipping documents $stickerPath = $this->courierService->fetchSticker($shipmentData['shipment_id'], $order->id); $waybillPath = $this->courierService->fetchWaybill($shipmentData['shipment_id'], $order->id); // Emit event to trigger Trello update, Slack notification, document attachment ShipmentCreated::dispatch( $order, $shipmentData['waybill_id'], $shipmentData['tracking_number'], $stickerPath, $waybillPath, ); return response()->json([ 'success' => true, 'message' => 'Shipment created successfully', 'shipment' => [ 'waybill_id' => $shipmentData['waybill_id'], 'tracking_number' => $shipmentData['tracking_number'], 'sticker' => $stickerPath ? route('storage.file', $stickerPath) : null, 'waybill' => $waybillPath ? route('storage.file', $waybillPath) : null, ], ]); } catch (\Exception $e) { Log::error('Failed to create shipment', [ 'order_uuid' => $order->uuid, 'error' => $e->getMessage(), ]); // Emit failure event ShipmentCreationFailed::dispatch($order, $e->getMessage()); return response()->json([ 'error' => 'Failed to create shipment', 'message' => $e->getMessage(), ], 500); } } /** * Retry shipment creation after previous failure * * POST /admin/orders/{id}/retry-shipment */ public function retryShipment(Request $request, Order $order) { // Verify order exists and has not already been successfully shipped if ($order->courier_waybill_id) { return response()->json([ 'error' => 'Order already has a valid shipment', 'waybill_id' => $order->courier_waybill_id, ], 409); } // Re-run the shipment creation return $this->createShipment($request, $order); } /** * Validate packing gate: order must be packed with dimensions */ private function validatePackingGate(Order $order): bool { return $order->packing_completed_at !== null && $order->packing_width > 0 && $order->packing_length > 0 && $order->packing_weight > 0; } /** * Validate payment and approval gate */ private function validatePaymentAndApprovalGate(Order $order): bool { // Standard orders: must be fully paid if ($order->is_custom_order === false) { return $order->payment_status === 'paid'; } // Custom orders: proof must be approved and balance must be paid // TODO: Add proof_approved and balance_status fields to CustomOrder model return true; // Placeholder - update when custom order model is ready } }