Files
core/src/Shipping/Jobs/PollShipmentTrackingJob.php
T

108 lines
3.6 KiB
PHP
Raw Normal View History

2026-07-19 17:41:09 +03:00
<?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]);
}
}