Files
core/src/Shipping/Carriers/BoxNow/BoxNowClient.php
T
arvanitakis 77dbbd63a4 Feature: Add Box Now locker delivery integration
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>
2026-07-19 00:51:23 +03:00

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');
});
}
}