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>
93 lines
3.5 KiB
PHP
93 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Carriers\BoxNow;
|
|
|
|
use Lunar\Models\Order;
|
|
use Modules\Core\Shipping\Carriers\BoxNow\Exceptions\BoxNowApiException;
|
|
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
|
use Modules\Core\Shipping\Models\Shipment;
|
|
|
|
/**
|
|
* Unlike ACS, Box Now has no manifest/pickup-list step — creating a
|
|
* delivery request also books the courier pickup, so this only implements
|
|
* CarrierFulfillmentInterface (not SupportsManifestBatching).
|
|
*
|
|
* Box Now delivers to lockers, not addresses. The storefront locker-picker
|
|
* is out of scope for this pass — createShipment() expects the chosen
|
|
* locker's Box Now locationId via $overrides['locationId'] (e.g. set
|
|
* manually by admin staff until checkout UI exists).
|
|
*/
|
|
class BoxNowFulfillmentService implements CarrierFulfillmentInterface
|
|
{
|
|
public function __construct(private readonly BoxNowClient $client) {}
|
|
|
|
public function createShipment(Order $order, array $overrides = []): Shipment
|
|
{
|
|
$address = $order->shippingAddress;
|
|
$destinationLocationId = $overrides['locationId'] ?? null;
|
|
|
|
if (! $destinationLocationId) {
|
|
throw new BoxNowApiException('No Box Now locker (locationId) was provided for this shipment.');
|
|
}
|
|
|
|
$response = $this->client->request('post', '/delivery-requests', [
|
|
'orderNumber' => $order->reference.'-'.$order->id,
|
|
'invoiceValue' => number_format($order->total->decimal, 2, '.', ''),
|
|
'paymentMode' => 'prepaid',
|
|
'amountToBeCollected' => '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' => $overrides['compartmentSize'] ?? 1,
|
|
'weight' => $overrides['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()]);
|
|
}
|
|
}
|