77dbbd63a4
Box Now rate driver (fixed pricing only — no live pricing API) and fulfillment service (delivery request creation, label printing, cancellation). Unlike ACS, Box Now books courier pickup automatically on delivery request creation, so no manifest/pickup-list step is implemented. Storefront locker selection is not yet built; createShipment() expects the chosen locker's locationId to be supplied by the caller. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.2 KiB
PHP
98 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Carriers\BoxNow;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Modules\Core\Shipping\Carriers\BoxNow\Exceptions\BoxNowApiException;
|
|
|
|
class BoxNowClient
|
|
{
|
|
private const TOKEN_CACHE_KEY = 'boxnow.access_token';
|
|
|
|
public function __construct(private readonly array $config) {}
|
|
|
|
/**
|
|
* GET/POST against the main delivery/parcel API, authenticated with a
|
|
* cached Bearer token.
|
|
*/
|
|
public function request(string $method, string $path, array $payload = []): array
|
|
{
|
|
$response = Http::withToken($this->token())
|
|
->timeout($this->config['timeout'])
|
|
->{$method}("{$this->config['base_url']}{$path}", $payload);
|
|
|
|
if ($response->status() === 401) {
|
|
// Token expired early / was revoked — refresh once and retry.
|
|
Cache::forget(self::TOKEN_CACHE_KEY);
|
|
|
|
$response = Http::withToken($this->token())
|
|
->timeout($this->config['timeout'])
|
|
->{$method}("{$this->config['base_url']}{$path}", $payload);
|
|
}
|
|
|
|
if ($response->failed()) {
|
|
throw new BoxNowApiException(
|
|
"Box Now API error ({$response->status()}) on {$method} {$path}",
|
|
$response->json() ?? [],
|
|
);
|
|
}
|
|
|
|
return $response->json() ?? [];
|
|
}
|
|
|
|
/**
|
|
* The origins/destinations lookups are served faster from a separate
|
|
* location API host, per Box Now's own documentation.
|
|
*/
|
|
public function locationRequest(string $path, array $query = []): array
|
|
{
|
|
$response = Http::withToken($this->token())
|
|
->timeout($this->config['timeout'])
|
|
->get("{$this->config['location_api_url']}{$path}", $query);
|
|
|
|
if ($response->failed()) {
|
|
throw new BoxNowApiException(
|
|
"Box Now location API error ({$response->status()}) on GET {$path}",
|
|
$response->json() ?? [],
|
|
);
|
|
}
|
|
|
|
return $response->json() ?? [];
|
|
}
|
|
|
|
/**
|
|
* Fetch raw bytes (e.g. a PDF label) rather than JSON.
|
|
*/
|
|
public function requestRaw(string $path): string
|
|
{
|
|
$response = Http::withToken($this->token())
|
|
->timeout($this->config['timeout'])
|
|
->get("{$this->config['base_url']}{$path}");
|
|
|
|
if ($response->failed()) {
|
|
throw new BoxNowApiException("Box Now API error ({$response->status()}) on GET {$path}");
|
|
}
|
|
|
|
return $response->body();
|
|
}
|
|
|
|
private function token(): string
|
|
{
|
|
return Cache::remember(self::TOKEN_CACHE_KEY, now()->addMinutes(55), function () {
|
|
$response = Http::timeout($this->config['timeout'])
|
|
->post("{$this->config['base_url']}/auth-sessions", [
|
|
'grant_type' => 'client_credentials',
|
|
'client_id' => $this->config['client_id'],
|
|
'client_secret' => $this->config['client_secret'],
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
throw new BoxNowApiException('Box Now authentication failed', $response->json() ?? []);
|
|
}
|
|
|
|
return $response->json('access_token');
|
|
});
|
|
}
|
|
}
|