Files
HickLab3D_Dashboard/app/Services/Parcel2Go.php
T
Hickmeister 55c987ef47
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
Fall back when Parcel2Go profile scope is denied
2026-10-11 17:02:13 +01:00

160 lines
6.5 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class Parcel2Go
{
private ?string $token = null;
private ?string $activeScope = null;
private bool $reducedScope = false;
public function __construct(private IntegrationSettings $settings) {}
public function hasScope(string $scope): bool
{
return in_array($scope, explode(' ', $this->activeScope ?? (string) ($this->settings->for('parcel2go')['scope'] ?? 'public-api')), true);
}
public function scopeWasReduced(): bool
{
return $this->reducedScope;
}
public function verifyCredentials(): void
{
$this->accessToken();
}
public function fetch(string $path): array
{
return Http::withToken($this->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
{
if ($this->token !== null) {
return $this->token;
}
$settings = $this->settings->for('parcel2go');
if (! $settings['client_id'] || ! $settings['client_secret'] || ! $settings['auth_url']) {
throw new RuntimeException('Parcel2Go credentials are not configured.');
}
$scope = (string) ($settings['scope'] ?? 'public-api');
$response = $this->tokenResponse($settings, $scope);
if ($scope !== 'public-api' && $response->status() === 400 && $response->json('error') === 'invalid_scope') {
$scope = 'public-api';
$response = $this->tokenResponse($settings, $scope);
$this->reducedScope = true;
}
$token = $response->throw()->json('access_token');
if (! $token) {
throw new RuntimeException('Parcel2Go did not return an access token.');
}
$this->activeScope = $scope;
return $this->token = $token;
}
/** @param array{auth_url: string, client_id: string, client_secret: string} $settings */
private function tokenResponse(array $settings, string $scope): Response
{
return Http::asForm()->timeout(10)->post($settings['auth_url'], [
'grant_type' => 'client_credentials',
'scope' => $scope,
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
]);
}
}