accessToken())->acceptJson()->connectTimeout(3)->timeout(15) ->get($this->url($path))->throw()->json() ?? []; } public function quote(array $details): array { return Http::withToken($this->accessToken())->acceptJson()->connectTimeout(3)->timeout(15) ->post($this->url('quotes'), [ 'CollectionAddress' => ['Country' => 'GBR', 'Postcode' => $details['collection_postcode']], 'DeliveryAddress' => ['Country' => 'GBR', 'Postcode' => $details['delivery_postcode']], 'Parcels' => [[ 'Value' => $details['value'], 'Weight' => $details['weight'], 'Length' => $details['length'], 'Width' => $details['width'], 'Height' => $details['height'], ]], ])->throw()->json() ?? []; } public function collectionDates(string $service, string $street, string $town, string $postcode): array { return Http::withToken($this->accessToken())->acceptJson()->connectTimeout(3)->timeout(15) ->post($this->url('collectiondates'), [ 'ServiceSlug' => $service, 'Address' => $street, 'City' => $town, 'Postcode' => $postcode, 'CountryISO3Code' => 'GBR', ])->throw()->json() ?? []; } public function bookCollection(array $details, string $service, array $booking): array { $collectionAddress = [ 'ContactName' => $booking['sender_name'], 'Email' => $booking['sender_email'], 'Phone' => $booking['sender_phone'], 'Property' => $booking['sender_property'], 'Street' => $booking['sender_street'], 'Town' => $booking['sender_town'], 'Postcode' => $details['collection_postcode'], 'CountryIsoCode' => 'GBR', ]; $deliveryAddress = [ 'ContactName' => $booking['recipient_name'], 'Email' => $booking['recipient_email'], 'Phone' => $booking['recipient_phone'], 'Property' => $booking['recipient_property'], 'Street' => $booking['recipient_street'], 'Town' => $booking['recipient_town'], 'Postcode' => $details['delivery_postcode'], 'CountryIsoCode' => 'GBR', ]; [$forename, $surname] = array_pad(explode(' ', trim($booking['sender_name']), 2), 2, ''); return Http::withToken($this->accessToken())->acceptJson()->connectTimeout(3)->timeout(20) ->post($this->url('orders'), [ 'Items' => [[ 'Id' => (string) Str::uuid(), 'CollectionDate' => $booking['collection_date'], 'OriginCountry' => 'GBR', 'Service' => $service, 'CollectionAddress' => $collectionAddress, 'Parcels' => [[ 'Id' => (string) Str::uuid(), 'Height' => $details['height'], 'Length' => $details['length'], 'Width' => $details['width'], 'Weight' => $details['weight'], 'EstimatedValue' => $details['value'], 'ContentsSummary' => $booking['contents'], 'DeliveryAddress' => $deliveryAddress, ]], ]], 'CustomerDetails' => ['Email' => $booking['sender_email'], 'Forename' => $forename, 'Surname' => $surname ?: $forename], ])->throw()->json() ?? []; } public function labelPdf(string $reference, string $hash): string { $data = Http::withToken($this->accessToken())->acceptJson()->connectTimeout(3)->timeout(20) ->get($this->url('labels/'.$reference), [ 'referenceType' => 'OrderLineId', 'detailLevel' => 'Labels', 'labelMedia' => 'A4', 'labelFormat' => 'PDF', 'hash' => $hash, ])->throw()->json(); $encoded = $data['Base64EncodedLabels'][0] ?? ''; $pdf = is_string($encoded) ? base64_decode($encoded, true) : false; if ($pdf === false || ! str_starts_with($pdf, '%PDF-')) { throw new RuntimeException('Parcel2Go did not return a valid PDF label.'); } return $pdf; } private function url(string $path): string { return rtrim($this->settings->for('parcel2go')['base_url'], '/').'/'.ltrim($path, '/'); } private function accessToken(): string { $settings = $this->settings->for('parcel2go'); if (! $settings['client_id'] || ! $settings['client_secret'] || ! $settings['auth_url']) { throw new RuntimeException('Parcel2Go credentials are not configured.'); } $token = Http::asForm()->timeout(10)->post($settings['auth_url'], [ 'grant_type' => 'client_credentials', 'scope' => 'public-api my-profile', 'client_id' => $settings['client_id'], 'client_secret' => $settings['client_secret'], ])->throw()->json('access_token'); if (! $token) { throw new RuntimeException('Parcel2Go did not return an access token.'); } return $token; } }