Feature: Order Updates, Events, Order Flows, Shipment And COD support
This commit is contained in:
@@ -21,10 +21,21 @@ use Modules\Core\Shipping\Models\Shipment;
|
||||
* Box Now delivers to lockers, not addresses. The storefront locker-picker
|
||||
* is out of scope for this pass — createShipment() requires the chosen
|
||||
* locker's Box Now locationId via ShipmentRequest::$destinationLocationId
|
||||
* (e.g. set manually by admin staff until checkout UI exists).
|
||||
* (e.g. set manually by admin staff until checkout UI exists — see
|
||||
* Modules\Core\Shipping\Extensions\OrderViewExtension, which locks the
|
||||
* field instead once the shopper's own checkout selection is present in
|
||||
* $order->shippingAddress->meta['box_now_locker']).
|
||||
*
|
||||
* Box Now ships by compartment size, not weight — unlike ACS, which bills
|
||||
* by kg. One 'items' entry per box in ShipmentRequest::$boxes, so an order
|
||||
* needing more than one physical parcel (doesn't fit one compartment)
|
||||
* sends that many entries in a single delivery request rather than
|
||||
* several separate ones.
|
||||
*/
|
||||
class BoxNowFulfillmentService implements CarrierFulfillmentInterface, SupportsTracking
|
||||
{
|
||||
private const COMPARTMENT_SIZES = ['S' => 1, 'M' => 2, 'L' => 3];
|
||||
|
||||
public function __construct(private readonly BoxNowClient $client) {}
|
||||
|
||||
public function createShipment(Order $order, ShipmentRequest $request): Shipment
|
||||
@@ -36,6 +47,10 @@ class BoxNowFulfillmentService implements CarrierFulfillmentInterface, SupportsT
|
||||
throw new BoxNowApiException('No Box Now locker (locationId) was provided for this shipment.');
|
||||
}
|
||||
|
||||
if (empty($request->boxes)) {
|
||||
throw new BoxNowApiException('At least one box (compartment size) is required for a Box Now shipment.');
|
||||
}
|
||||
|
||||
$isCod = $request->paymentMode === 'cod';
|
||||
|
||||
$response = $this->client->request('post', '/delivery-requests', [
|
||||
@@ -57,31 +72,37 @@ class BoxNowFulfillmentService implements CarrierFulfillmentInterface, SupportsT
|
||||
'contactName' => trim("{$address->first_name} {$address->last_name}"),
|
||||
'locationId' => $destinationLocationId,
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'id' => (string) $order->id,
|
||||
'name' => 'Order '.$order->reference,
|
||||
'value' => '0.00',
|
||||
'compartmentSize' => 1,
|
||||
'weight' => $request->weight ?? 0,
|
||||
],
|
||||
],
|
||||
'items' => collect($request->boxes)->values()->map(fn (string $size, int $index) => [
|
||||
'id' => $order->id.'-'.($index + 1),
|
||||
'name' => 'Order '.$order->reference.' (box '.($index + 1).')',
|
||||
'value' => '0.00',
|
||||
'compartmentSize' => self::COMPARTMENT_SIZES[$size] ?? self::COMPARTMENT_SIZES['S'],
|
||||
])->all(),
|
||||
]);
|
||||
|
||||
$parcelId = (string) ($response['parcels'][0]['id'] ?? throw new BoxNowApiException(
|
||||
'Box Now delivery request succeeded but returned no parcel id.',
|
||||
$response,
|
||||
));
|
||||
$parcels = collect($response['parcels'] ?? []);
|
||||
|
||||
return Shipment::create([
|
||||
if ($parcels->isEmpty()) {
|
||||
throw new BoxNowApiException('Box Now delivery request succeeded but returned no parcel ids.', $response);
|
||||
}
|
||||
|
||||
// One Shipment row per box/parcel — each is independently
|
||||
// trackable/printable/cancellable via its own tracking_reference
|
||||
// (printLabel()/cancelShipment()/trackShipment() below already
|
||||
// operate per-Shipment), even though all boxes were submitted in
|
||||
// one delivery request. Siblings are linked via the shared
|
||||
// delivery_request_id in meta.
|
||||
$shipments = $parcels->map(fn (array $parcel) => Shipment::create([
|
||||
'order_id' => $order->id,
|
||||
'carrier' => 'box-now',
|
||||
'tracking_reference' => $parcelId,
|
||||
'tracking_reference' => (string) $parcel['id'],
|
||||
'meta' => [
|
||||
'delivery_request_id' => $response['id'] ?? null,
|
||||
'locker_id' => $destinationLocationId,
|
||||
],
|
||||
]);
|
||||
]));
|
||||
|
||||
return $shipments->first();
|
||||
}
|
||||
|
||||
public function printLabel(Shipment $shipment): string
|
||||
@@ -136,6 +157,13 @@ class BoxNowFulfillmentService implements CarrierFulfillmentInterface, SupportsT
|
||||
|
||||
private function mapState(string $state): TrackingStatus
|
||||
{
|
||||
// TODO: confirm against a live BoxNow webhook payload whether a
|
||||
// distinct collected-from-sender state exists (e.g. between 'new'
|
||||
// and 'in-transit') before mapping it to
|
||||
// TrackingStatus::CollectedFromSender — BoxNow's own model is
|
||||
// locker-drop-off-based, so it may not have one. No guessed match
|
||||
// arm added; 'new' still falls through to Pending, InTransit
|
||||
// remains the earliest recognized checkpoint.
|
||||
return match ($state) {
|
||||
'new' => TrackingStatus::Pending,
|
||||
'in-transit', 'in-depot' => TrackingStatus::InTransit,
|
||||
|
||||
Reference in New Issue
Block a user