apiKey = config('courier.api_key'); $this->baseUrl = config('courier.api_base_url', 'https://api.shiplogic.com/api'); } /** * Create shipment with full validation and database updates * * @param Order $order * @return array{waybill_id: string, tracking_number: string, sticker_path: ?string, waybill_path: ?string} * @throws \Exception */ public function createShipmentForOrder(Order $order): array { Log::info('Creating shipment for order', ['order_uuid' => $order->uuid]); // Guard 1: Validate packing if (! $this->validatePacking($order)) { throw new \Exception('Order not yet packed with valid dimensions'); } // Guard 2: Validate payment/approval if (! $this->validatePayment($order)) { throw new \Exception($order->is_custom_order ? 'Custom order requires proof approved and balance paid' : 'Standard order must be fully paid' ); } // Guard 3: Validate order status if ($order->status !== 'ready_to_ship') { throw new \Exception('Order must be in Ready to Ship state'); } // Guard 4: Prevent duplicates if ($order->courier_waybill_id) { throw new \Exception('Shipment already exists for this order'); } try { // Call API to create shipment $shipmentData = $this->callCreateShipmentApi( $order->id, $order->packing_width, $order->packing_length, $order->packing_weight, ); // Save shipment details to database $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 successfully', [ 'order_uuid' => $order->uuid, 'waybill_id' => $shipmentData['waybill_id'], ]); // Fetch shipping documents $stickerPath = $this->fetchSticker($shipmentData['shipment_id'], $order->id); $waybillPath = $this->fetchWaybill($shipmentData['shipment_id'], $order->id); return [ 'waybill_id' => $shipmentData['waybill_id'], 'tracking_number' => $shipmentData['tracking_number'], 'sticker_path' => $stickerPath, 'waybill_path' => $waybillPath, ]; } catch (\Exception $e) { Log::error('Failed to create shipment', [ 'order_uuid' => $order->uuid, 'error' => $e->getMessage(), ]); throw $e; } } /** * Create a shipment with Shiplogic API * * @param string $orderId * @param float $width Width in cm * @param float $length Length in cm * @param float $weight Weight in kg * @return array{shipment_id: string, waybill_id: string, tracking_number: string} * @throws \Exception */ private function callCreateShipmentApi(string $orderId, float $width, float $length, float $weight): array { if (! $this->isConfigured()) { throw new \Exception('Courier API not configured'); } if ($width <= 0 || $length <= 0 || $weight <= 0) { throw new \Exception('Invalid dimensions or weight: all must be greater than 0'); } try { // Build shipment payload for Shiplogic $payload = [ 'parcel' => [ 'weight' => $weight, 'height' => 10, // TODO: Update when height is captured separately 'width' => $width, 'length' => $length, ], 'destination' => [ // TODO: Get from order's shipping address ], 'reference' => $orderId, ]; $response = Http::withHeaders([ 'Authorization' => "Bearer {$this->apiKey}", ])->post("{$this->baseUrl}/shipments", $payload); if (! $response->successful()) { $errorMessage = $response->json('error.message', 'Unknown error'); throw new \Exception("Courier API error: {$errorMessage}"); } $data = $response->json(); return [ 'shipment_id' => $data['id'] ?? null, 'waybill_id' => $data['waybill_number'] ?? null, 'tracking_number' => $data['tracking_number'] ?? null, ]; } catch (\Exception $e) { Log::error('Failed to call courier API', [ 'order_id' => $orderId, 'error' => $e->getMessage(), ]); throw $e; } } /** * Validate packing gate: order must be packed with dimensions */ private function validatePacking(Order $order): bool { $valid = $order->packing_completed_at !== null && $order->packing_width > 0 && $order->packing_length > 0 && $order->packing_weight > 0; if (! $valid) { Log::warning('Packing validation failed', [ 'order_uuid' => $order->uuid, 'packing_completed_at' => $order->packing_completed_at, 'width' => $order->packing_width, 'length' => $order->packing_length, 'weight' => $order->packing_weight, ]); } return $valid; } /** * Validate payment and approval gate */ private function validatePayment(Order $order): bool { // Standard orders: must be fully paid if ($order->is_custom_order === false) { $valid = $order->payment_status === 'paid'; } else { // Custom orders: proof must be approved and balance must be paid // TODO: Add proof_approved and balance_status fields to CustomOrder model $valid = true; // Placeholder } if (! $valid) { Log::warning('Payment validation failed', [ 'order_uuid' => $order->uuid, 'is_custom_order' => $order->is_custom_order, 'payment_status' => $order->payment_status, ]); } return $valid; } /** * Fetch shipping label/sticker PDF from Shiplogic */ public function fetchSticker(string $shipmentId, string $orderId): ?string { if (! $this->isConfigured()) { return null; } try { $response = Http::withHeaders([ 'Authorization' => "Bearer {$this->apiKey}", ])->get("{$this->baseUrl}/shipments/{$shipmentId}/sticker"); if ($response->successful()) { $path = "shipments/{$orderId}/sticker.pdf"; Storage::disk('public')->put($path, $response->body()); return $path; } Log::warning('Failed to fetch sticker from courier', ['shipment_id' => $shipmentId]); return null; } catch (\Exception $e) { Log::error('Exception fetching sticker', ['error' => $e->getMessage()]); return null; } } /** * Fetch waybill PDF from Shiplogic */ public function fetchWaybill(string $shipmentId, string $orderId): ?string { if (! $this->isConfigured()) { return null; } try { $response = Http::withHeaders([ 'Authorization' => "Bearer {$this->apiKey}", ])->get("{$this->baseUrl}/shipments/{$shipmentId}/label"); if ($response->successful()) { $path = "shipments/{$orderId}/waybill.pdf"; Storage::disk('public')->put($path, $response->body()); return $path; } Log::warning('Failed to fetch waybill from courier', ['shipment_id' => $shipmentId]); return null; } catch (\Exception $e) { Log::error('Exception fetching waybill', ['error' => $e->getMessage()]); return null; } } /** * Check if courier is configured */ protected function isConfigured(): bool { return ! empty($this->apiKey); } }