shippingAddress; $destinationLocationId = $request->destinationLocationId; if (! $destinationLocationId) { throw new BoxNowApiException('No Box Now locker (locationId) was provided for this shipment.'); } $isCod = $request->paymentMode === 'cod'; $response = $this->client->request('post', '/delivery-requests', [ 'orderNumber' => $order->reference.'-'.$order->id, 'invoiceValue' => number_format($order->total->decimal, 2, '.', ''), 'paymentMode' => $isCod ? 'cod' : 'prepaid', 'amountToBeCollected' => $isCod ? number_format($request->amountToCollect ?? $order->total->decimal, 2, '.', '') : '0.00', 'origin' => [ 'contactNumber' => config('boxnow.sender.phone'), 'contactEmail' => config('boxnow.sender.email'), 'contactName' => config('boxnow.sender.name'), 'locationId' => config('boxnow.origin_location_id'), ], 'destination' => [ 'contactNumber' => $address->contact_phone, 'contactEmail' => $address->contact_email, 'contactName' => trim("{$address->first_name} {$address->last_name}"), 'locationId' => $destinationLocationId, ], 'items' => [ [ 'id' => (string) $order->id, 'name' => 'Order '.$order->reference, 'value' => '0.00', 'compartmentSize' => 1, 'weight' => $request->weight ?? 0, ], ], ]); $parcelId = (string) ($response['parcels'][0]['id'] ?? throw new BoxNowApiException( 'Box Now delivery request succeeded but returned no parcel id.', $response, )); return Shipment::create([ 'order_id' => $order->id, 'carrier' => 'box-now', 'tracking_reference' => $parcelId, 'meta' => [ 'delivery_request_id' => $response['id'] ?? null, 'locker_id' => $destinationLocationId, ], ]); } public function printLabel(Shipment $shipment): string { $bytes = $this->client->requestRaw("/parcels/{$shipment->tracking_reference}/label.pdf"); $shipment->update(['label_printed_at' => now()]); return $bytes; } public function cancelShipment(Shipment $shipment): void { $this->client->request('post', "/parcels/{$shipment->tracking_reference}:cancel"); $shipment->update(['cancelled_at' => now()]); } public function trackShipment(Shipment $shipment): Collection { $response = $this->client->request('get', '/parcels', [ 'parcelId' => $shipment->tracking_reference, ]); $parcel = $response['data'][0] ?? null; if (! $parcel) { return collect(); } $events = $parcel['events'] ?? []; // Fall back to a single checkpoint from the parcel's current state // if Box Now didn't return a detailed events history. if (empty($events)) { $events = [[ 'type' => $parcel['state'] ?? 'new', 'locationDisplayName' => null, 'createTime' => $parcel['updateTime'] ?? $parcel['createTime'] ?? now()->toIso8601String(), ]]; } return collect($events)->map(fn (array $event) => new TrackingCheckpoint( status: $this->mapState($event['type'] ?? $parcel['state'] ?? 'new'), carrierStatus: $event['type'] ?? $parcel['state'] ?? null, message: null, location: $event['locationDisplayName'] ?? null, occurredAt: Carbon::parse($event['createTime']), meta: $event, )); } private function mapState(string $state): TrackingStatus { return match ($state) { 'new' => TrackingStatus::Pending, 'in-transit', 'in-depot' => TrackingStatus::InTransit, 'in-final-destination', 'wait-for-load' => TrackingStatus::OutForDelivery, 'delivered' => TrackingStatus::Delivered, 'returned', 'accepted-for-return' => TrackingStatus::Returned, 'cancelled' => TrackingStatus::Cancelled, 'expired-return', 'missing' => TrackingStatus::Failed, default => TrackingStatus::Unknown, }; } }