108 lines
3.6 KiB
PHP
108 lines
3.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Modules\Core\Shipping\Jobs;
|
||
|
|
|
||
|
|
use Illuminate\Bus\Queueable;
|
||
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||
|
|
use Illuminate\Queue\InteractsWithQueue;
|
||
|
|
use Illuminate\Queue\SerializesModels;
|
||
|
|
use Lunar\Shipping\Facades\Shipping;
|
||
|
|
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
||
|
|
use Modules\Core\Shipping\Contracts\SupportsTracking;
|
||
|
|
use Modules\Core\Shipping\Enums\TrackingStatus;
|
||
|
|
use Modules\Core\Shipping\Events\ShipmentStatusUpdatedByCarrier;
|
||
|
|
use Modules\Core\Shipping\Models\Shipment;
|
||
|
|
use Modules\Core\Shipping\Models\ShipmentInfo;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Carrier-agnostic: polls every Shipment not yet in a terminal state,
|
||
|
|
* skipping carriers whose fulfillment service doesn't implement
|
||
|
|
* SupportsTracking. New checkpoints are recorded in shipment_info and
|
||
|
|
* dispatch ShipmentStatusUpdatedByCarrier — one event per new checkpoint.
|
||
|
|
*/
|
||
|
|
class PollShipmentTrackingJob implements ShouldQueue
|
||
|
|
{
|
||
|
|
use Dispatchable;
|
||
|
|
use InteractsWithQueue;
|
||
|
|
use Queueable;
|
||
|
|
use SerializesModels;
|
||
|
|
|
||
|
|
public int $tries = 3;
|
||
|
|
|
||
|
|
public int $backoff = 60;
|
||
|
|
|
||
|
|
public function handle(): void
|
||
|
|
{
|
||
|
|
$trackableCarriers = collect(Shipping::getSupportedDrivers())->keys()->filter(
|
||
|
|
fn (string $carrier) => $this->fulfillmentService($carrier) instanceof SupportsTracking
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($trackableCarriers->isEmpty()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Shipment::query()
|
||
|
|
->whereIn('carrier', $trackableCarriers)
|
||
|
|
->whereNull('cancelled_at')
|
||
|
|
->whereDoesntHave('shipmentInfo', function ($query) {
|
||
|
|
$query->whereIn('status', [
|
||
|
|
TrackingStatus::Delivered->value,
|
||
|
|
TrackingStatus::Returned->value,
|
||
|
|
TrackingStatus::Cancelled->value,
|
||
|
|
]);
|
||
|
|
})
|
||
|
|
->chunkById(50, function ($shipments) {
|
||
|
|
$shipments->groupBy('carrier')->each(
|
||
|
|
fn ($group, $carrier) => $this->pollCarrierShipments($carrier, $group)
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private function pollCarrierShipments(string $carrier, $shipments): void
|
||
|
|
{
|
||
|
|
$service = $this->fulfillmentService($carrier);
|
||
|
|
|
||
|
|
if (! $service instanceof SupportsTracking) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($shipments as $shipment) {
|
||
|
|
$this->recordNewCheckpoints($shipment, $service->trackShipment($shipment));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function recordNewCheckpoints(Shipment $shipment, $checkpoints): void
|
||
|
|
{
|
||
|
|
$existing = $shipment->shipmentInfo()
|
||
|
|
->get(['status', 'occurred_at'])
|
||
|
|
->map(fn ($info) => $info->status->value.'|'.$info->occurred_at->toIso8601String())
|
||
|
|
->flip();
|
||
|
|
|
||
|
|
foreach ($checkpoints as $checkpoint) {
|
||
|
|
$fingerprint = $checkpoint->status->value.'|'.$checkpoint->occurredAt->toIso8601String();
|
||
|
|
|
||
|
|
if ($existing->has($fingerprint)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$info = ShipmentInfo::create([
|
||
|
|
'shipment_id' => $shipment->id,
|
||
|
|
'status' => $checkpoint->status,
|
||
|
|
'carrier_status' => $checkpoint->carrierStatus,
|
||
|
|
'message' => $checkpoint->message,
|
||
|
|
'location' => $checkpoint->location,
|
||
|
|
'occurred_at' => $checkpoint->occurredAt,
|
||
|
|
'meta' => $checkpoint->meta,
|
||
|
|
]);
|
||
|
|
|
||
|
|
ShipmentStatusUpdatedByCarrier::dispatch($info);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function fulfillmentService(string $carrier): ?CarrierFulfillmentInterface
|
||
|
|
{
|
||
|
|
return app(CarrierFulfillmentInterface::class, ['carrier' => $carrier]);
|
||
|
|
}
|
||
|
|
}
|