Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c455c8adf2 | ||
|
|
dcff693b99 | ||
|
|
3020bd070c | ||
|
|
162827dff7 | ||
|
|
cbb76070d9 | ||
|
|
966f099ffb |
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.30.0] - 2026-09-30
|
||||
### Added
|
||||
- Manual vouchers (an integrated carrier's voucher typed in by hand) can be
|
||||
edited and tracked by hand from the order page: "Edit number" (the number
|
||||
stays required) and "Add tracking update" now show for them, as they do
|
||||
for manual carriers. An In Transit / Collected update dispatches the
|
||||
order and sends the "on its way" email; Delivered marks it delivered and
|
||||
stops tracking polls for it. The carrier's own checkpoints keep coming in
|
||||
alongside.
|
||||
- A shipping method's "Tracking URL" can now be set for every driver, not
|
||||
only `manual`. For an integrated carrier it's copied onto vouchers added
|
||||
with "Add manual voucher", so they get the "Carrier tracking" link in the
|
||||
admin and a "Track your parcel" link in the dispatched email.
|
||||
API-created shipments still send customers to the order page. Vouchers
|
||||
added before this have no link.
|
||||
- The customer's delivery instructions are sent to the carrier: ELTA's
|
||||
remarks (ΠΑΡΑΤΗΡΗΣΕΙΣ, one line, 90 characters) and ACS's
|
||||
`Delivery_Notes`. ELTA labels print them in the ΠΑΡΑΤΗΡΗΣΕΙΣ box (up to 3
|
||||
lines on A4, 2 on A6 and the payment stub).
|
||||
|
||||
### Changed
|
||||
- Tracking polls run one queued job per shipment: `PollShipmentTrackingJob`
|
||||
now only finds the shipments to poll and dispatches a
|
||||
`RefreshShipmentTrackingJob` for each as a batch (`allowFailures`). A
|
||||
slow or failing carrier call only affects its own job, with its own 45s
|
||||
timeout; a failed one is retried by the next poll, five minutes later.
|
||||
Needs the `job_batches` table (Laravel's default jobs migration).
|
||||
- `PollShipmentTrackingJob` is unique, so a scheduled run is dropped while
|
||||
the previous one is still pending. `RefreshShipmentTrackingJob` is unique
|
||||
per shipment, so the same shipment is never refreshed twice at once,
|
||||
which could record a checkpoint twice and fire its emails twice.
|
||||
|
||||
### Fixed
|
||||
- "Add manual voucher" and a manual carrier's "Create Shipment" failed
|
||||
with an SQL error ("missing FROM-clause entry for table lunar_orders")
|
||||
whenever a voucher number was entered: their unique check on the
|
||||
shipments table also excluded the current order's id.
|
||||
|
||||
## [0.29.3] - 2026-09-30
|
||||
### Fixed
|
||||
- Queued shipping jobs did nothing: `PollShipmentTrackingJob` and
|
||||
`SyncCarrierVouchersJob` ran every time but finished in milliseconds
|
||||
without contacting any carrier. The queue worker clears facade instances
|
||||
before each job, and the Shipping manager was bound with `bind()`, so the
|
||||
facade then built a fresh manager with no drivers registered. It's now a
|
||||
singleton. Tracking from the scheduler never worked on a deployed app
|
||||
because of this (a "Sync now" click worked, since it doesn't go through
|
||||
the queue).
|
||||
- ELTA extra parcels are no longer tracked by their own number: ELTA only
|
||||
tracks a multi-parcel send by its main voucher and rejects the extra
|
||||
parcels' numbers ("Λάθος ΣΥΔΕΤΑ"), which logged an error for each of them
|
||||
on every tracking run.
|
||||
|
||||
## [0.29.2] - 2026-09-30
|
||||
### Added
|
||||
- `core.display_timezone` (env `DISPLAY_TIMEZONE`, default `Europe/Athens`):
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "boboko/core",
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"type": "library",
|
||||
"version": "0.29.2",
|
||||
"version": "0.30.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Core\\": "src/"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@boboko/core",
|
||||
"version": "0.29.2",
|
||||
"version": "0.30.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
||||
|
||||
@@ -206,6 +206,10 @@
|
||||
{{-- ΠΑΡΑΤΗΡΗΣΕΙΣ --}}
|
||||
<div class="a6-box" style="top:10.60cm;left:0.10cm;width:5.82cm;height:0.80cm;">
|
||||
<div class="a6-small-title">* ΠΑΡΑΤΗΡΗΣΕΙΣ *</div>
|
||||
{{-- sxolia_1/2 (textbox49/50) --}}
|
||||
@foreach (array_slice($sxolia, 0, 2) as $i => $line)
|
||||
<div style="position:absolute;top:{{ 0.24 + $i * 0.26 }}cm;left:0.10cm;width:5.62cm;font-size:6pt;line-height:1;white-space:nowrap;overflow:hidden;">{{ $line }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($multiPiece)
|
||||
|
||||
@@ -139,6 +139,10 @@
|
||||
|
||||
<div class="mc-box" style="top:4.97cm;left:6.60cm;width:4.37cm;height:1.31cm;border-top:none;">
|
||||
<div style="position:absolute;top:0.03cm;left:0.08cm;font-size:6pt;">* ΠΑΡΑΤΗΡΗΣΕΙΣ *</div>
|
||||
{{-- sxolia_1..3 (textbox92/95/94) --}}
|
||||
@foreach ($sxolia as $i => $line)
|
||||
<div style="position:absolute;top:{{ 0.30 + $i * 0.31 }}cm;left:0.10cm;width:4.18cm;font-size:7pt;line-height:1;white-space:nowrap;overflow:hidden;">{{ $line }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Right-hand signature / print-timestamp column, differs by copy type --}}
|
||||
|
||||
@@ -73,6 +73,11 @@
|
||||
<div class="mc-box" style="top:2.45cm;left:6.06cm;width:4.71cm;height:1.17cm;border-bottom:none;">
|
||||
<div style="position:absolute;top:0.02cm;left:0.16cm;font-size:6pt;">Συν.Χρέωσης (€):</div>
|
||||
<div style="position:absolute;top:0.30cm;left:0.16cm;font-size:6pt;">Πρόσθετες Υπηρεσίες</div>
|
||||
{{-- sxolia_1..3 (textbox91/132/133) sit under this heading in the RDLC;
|
||||
this stub's boxes are shorter than the RDLC's, so only 2 lines fit. --}}
|
||||
@foreach (array_slice($sxolia, 0, 2) as $i => $line)
|
||||
<div style="position:absolute;top:{{ 0.50 + $i * 0.16 }}cm;left:0.16cm;width:4.47cm;font-size:5pt;line-height:1;white-space:nowrap;overflow:hidden;">{{ $line }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mc-box mc-partybox" style="top:3.35cm;left:0.00cm;width:5.97cm;height:1.63cm;overflow:hidden;border-top:none;">
|
||||
|
||||
@@ -57,7 +57,7 @@ class OrderDispatchedNotification extends BaseNotification
|
||||
'reference' => $reference,
|
||||
// Active, numbered, non-return shipments: the voucher numbers
|
||||
// to show, plus the courier's link for manual carriers
|
||||
// (Shipment::trackingUrl()). Integrated carriers' history is
|
||||
// and typed-in vouchers (Shipment::trackingUrl()). API-created shipments' history is
|
||||
// on the customer's order page.
|
||||
'shipments' => $order->shipments()->get()
|
||||
->reject(fn (Shipment $shipment) => $shipment->isCancelled() || $shipment->isReturn() || blank($shipment->tracking_reference))
|
||||
|
||||
@@ -118,6 +118,12 @@ class OrderFulfillmentService
|
||||
'meta' => [
|
||||
'services' => array_map(fn (ExtraService $service) => $service->value, $services),
|
||||
'cod_amount' => $this->flow->isCod($order) ? $order->total->decimal : null,
|
||||
// Copied like ManualFulfillmentService does, so the link
|
||||
// keeps working if the method is edited later.
|
||||
'tracking_url' => ShippingMethod::where('driver', $carrier)->get()
|
||||
->map(fn (ShippingMethod $method) => $method->data['tracking_url'] ?? null)
|
||||
->filter()
|
||||
->first(),
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -154,7 +154,13 @@ class ShippingServiceProvider extends ServiceProvider
|
||||
// run before the Shipping::extend() calls below, which resolve —
|
||||
// and the facade then CACHES — whatever's bound at that moment).
|
||||
$this->app->booted(function () {
|
||||
$this->app->bind(ShippingMethodManagerInterface::class, fn ($app) => $app->make(ShippingManager::class));
|
||||
// A singleton, not bind(): the queue worker clears every
|
||||
// facade's cached instance before each job
|
||||
// (Facade::clearResolvedInstances()), so with bind() the Shipping
|
||||
// facade then built a fresh, empty manager — without the drivers
|
||||
// extended below — and every queued job (tracking, voucher sync)
|
||||
// saw no carriers at all.
|
||||
$this->app->singleton(ShippingMethodManagerInterface::class, fn ($app) => $app->make(ShippingManager::class));
|
||||
|
||||
// Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class));
|
||||
Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class));
|
||||
|
||||
@@ -72,6 +72,11 @@ class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsExtr
|
||||
'Reference_Key1' => (string) $order->reference,
|
||||
];
|
||||
|
||||
// The customer's delivery instructions, for the courier.
|
||||
if ($instructions = trim(preg_replace('/\s+/u', ' ', (string) $address->delivery_instructions))) {
|
||||
$params['Delivery_Notes'] = $instructions;
|
||||
}
|
||||
|
||||
$codAmount = $request->paymentMode === 'cod'
|
||||
? (float) ($request->amountToCollect ?? $order->total->decimal)
|
||||
: null;
|
||||
|
||||
@@ -63,6 +63,11 @@ class EltaFulfillmentService implements CarrierFulfillmentInterface, IssuesVouch
|
||||
$weight = $request->weight ?? 0.5;
|
||||
$surcharges = $this->surchargeCodes($request);
|
||||
|
||||
// The customer's delivery instructions, as ELTA's remarks
|
||||
// (ΠΑΡΑΤΗΡΗΣΕΙΣ): one line, at most 90 characters — the ELTA
|
||||
// client's own field limit.
|
||||
$remarks = mb_substr(trim(preg_replace('/\s+/u', ' ', (string) $address->delivery_instructions)), 0, 90);
|
||||
|
||||
$params = [
|
||||
'pel_apost_code' => config('elta.apost_code'),
|
||||
'pel_paral_name' => trim("{$address->first_name} {$address->last_name}"),
|
||||
@@ -78,7 +83,7 @@ class EltaFulfillmentService implements CarrierFulfillmentInterface, IssuesVouch
|
||||
'pel_y' => '',
|
||||
'pel_z' => '',
|
||||
'pel_temaxia' => (string) $request->packageCount,
|
||||
'pel_paral_sxolia' => '',
|
||||
'pel_paral_sxolia' => $remarks,
|
||||
'pel_sur_1' => $surcharges[0] ?? '',
|
||||
'pel_sur_2' => $surcharges[1] ?? '',
|
||||
'pel_sur_3' => $surcharges[2] ?? '',
|
||||
@@ -129,6 +134,7 @@ class EltaFulfillmentService implements CarrierFulfillmentInterface, IssuesVouch
|
||||
'meta' => [
|
||||
'carrier_id' => $this->findPendingVoucherId((string) $order->reference),
|
||||
'reference' => (string) $order->reference,
|
||||
'delivery_instructions' => $remarks ?: null,
|
||||
'weight' => $weight,
|
||||
'package_count' => $pieces,
|
||||
'cod_amount' => $codAmount,
|
||||
@@ -278,6 +284,13 @@ class EltaFulfillmentService implements CarrierFulfillmentInterface, IssuesVouch
|
||||
|
||||
public function trackShipment(Shipment $shipment): Collection
|
||||
{
|
||||
// ELTA tracks a multi-parcel send by its main voucher only — an
|
||||
// extra parcel's own number is rejected ("Λάθος ΣΥΔΕΤΑ"). Its
|
||||
// history is the main voucher's, which is tracked on its own row.
|
||||
if (! $shipment->is($this->masterOf($shipment))) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$response = $this->client->getTracking([
|
||||
'web_vg' => $shipment->tracking_reference,
|
||||
'pel_code' => config('elta.apost_code'),
|
||||
|
||||
@@ -240,6 +240,11 @@ class EltaLabelRenderer
|
||||
// older vouchers were sent the order id).
|
||||
'order_reference' => $isChild ? null : (string) $order->reference,
|
||||
|
||||
// ΠΑΡΑΤΗΡΗΣΕΙΣ: the remarks we sent ELTA (the customer's delivery
|
||||
// instructions), in the client's 27-character lines — up to 3
|
||||
// on A4, the first 2 on A6. Extra parcels print them too.
|
||||
'sxolia' => array_slice(mb_str_split((string) ($meta['delivery_instructions'] ?? ''), 27), 0, 3),
|
||||
|
||||
// sydetaE.rdlc's "Πρόσθετες Υπηρεσίες" (sur_1..4): the ticked
|
||||
// extra services, in ELTA's own wording and slots.
|
||||
'sur_1' => $surcharges[1],
|
||||
|
||||
@@ -124,12 +124,13 @@ class OrderShipmentsExtension extends ViewPageExtension
|
||||
->schema(fn (Shipment $record) => [
|
||||
TextInput::make('tracking_reference')
|
||||
->label('Voucher / tracking number')
|
||||
->required($record->source === Shipment::SOURCE_MANUAL_VOUCHER)
|
||||
->unique(Shipment::class, 'tracking_reference', ignorable: $record),
|
||||
])
|
||||
->action(fn (Shipment $record, array $data) => $record->update([
|
||||
'tracking_reference' => filled($data['tracking_reference']) ? trim($data['tracking_reference']) : null,
|
||||
]))
|
||||
->visible(fn (Shipment $record) => $record->source === Shipment::SOURCE_MANUAL && ! $record->isCancelled()),
|
||||
->visible(fn (Shipment $record) => in_array($record->source, [Shipment::SOURCE_MANUAL, Shipment::SOURCE_MANUAL_VOUCHER], true) && ! $record->isCancelled()),
|
||||
Action::make('add_tracking_update')
|
||||
->label('Add tracking update')
|
||||
->icon('heroicon-o-plus-circle')
|
||||
@@ -152,7 +153,7 @@ class OrderShipmentsExtension extends ViewPageExtension
|
||||
->helperText('Shown to the customer on their order page.'),
|
||||
])
|
||||
->action(fn (Shipment $record, array $data) => $this->addTrackingUpdate($record, $data))
|
||||
->visible(fn (Shipment $record) => $record->source === Shipment::SOURCE_MANUAL && ! $record->isCancelled()),
|
||||
->visible(fn (Shipment $record) => in_array($record->source, [Shipment::SOURCE_MANUAL, Shipment::SOURCE_MANUAL_VOUCHER], true) && ! $record->isCancelled()),
|
||||
Action::make('cancel_shipment')
|
||||
->label('Cancel')
|
||||
->icon('heroicon-o-x-circle')
|
||||
|
||||
@@ -130,7 +130,7 @@ class OrderViewExtension extends ViewPageExtension
|
||||
return [
|
||||
TextInput::make('tracking_reference')
|
||||
->label('Voucher / tracking number')
|
||||
->unique(Shipment::class, 'tracking_reference')
|
||||
->unique(Shipment::class, 'tracking_reference', ignoreRecord: false)
|
||||
->helperText('Optional — add it later from the shipment if the courier hasn\'t given one yet.'),
|
||||
TextInput::make('package_count')
|
||||
->label('Number of packages')
|
||||
@@ -305,7 +305,7 @@ class OrderViewExtension extends ViewPageExtension
|
||||
TextInput::make('voucher_number')
|
||||
->label('Voucher number')
|
||||
->required()
|
||||
->unique(Shipment::class, 'tracking_reference')
|
||||
->unique(Shipment::class, 'tracking_reference', ignoreRecord: false)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn (Set $set) => $set('lookup', null))
|
||||
->suffixAction(
|
||||
|
||||
@@ -145,9 +145,8 @@ class ShippingMethodResourceExtension extends ResourceExtension
|
||||
->required($isManual),
|
||||
TextInput::make('tracking_url')
|
||||
->label('Tracking URL')
|
||||
->helperText('Optional. The carrier\'s tracking page, with {number} where the voucher number goes, e.g. https://example.com/track?number={number}.')
|
||||
->rule('starts_with:http://,https://')
|
||||
->visible($isManual),
|
||||
->helperText('Optional. The carrier\'s tracking page, with {number} where the voucher number goes, e.g. https://example.com/track?number={number}. For an integrated carrier it\'s only used on vouchers typed in by hand.')
|
||||
->rule('starts_with:http://,https://'),
|
||||
Toggle::make('collects_cash')
|
||||
->label('Courier collects cash on delivery')
|
||||
->helperText('Offers cash on delivery at checkout for this shipping method.')
|
||||
|
||||
@@ -3,40 +3,43 @@
|
||||
namespace Modules\Core\Shipping\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
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\Models\Shipment;
|
||||
use Modules\Core\Shipping\Services\ShipmentTrackingRecorder;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Carrier-agnostic: finds every Shipment not yet in a terminal state on a
|
||||
* carrier whose fulfillment service implements SupportsTracking, and
|
||||
* dispatches one RefreshShipmentTrackingJob per shipment as a batch. The
|
||||
* carrier calls happen in those jobs — each with its own timeout, and a
|
||||
* failure in one (a carrier 500, a malformed parcel response) doesn't
|
||||
* touch the others (allowFailures).
|
||||
*
|
||||
* Each shipment's trackShipment() call is individually try/caught in
|
||||
* pollCarrierShipments() — one shipment's tracking lookup failing (a
|
||||
* carrier 500, a malformed parcel response) must not stop the rest of that
|
||||
* carrier's shipments in the same batch from being polled. The failure is
|
||||
* reported and the loop continues.
|
||||
* Unique: a scheduled run is dropped while the previous one is still
|
||||
* queued or building its batch. The schedule's withoutOverlapping() alone
|
||||
* can't do this — for a queued job it only covers the dispatch itself.
|
||||
*/
|
||||
class PollShipmentTrackingJob implements ShouldQueue
|
||||
class PollShipmentTrackingJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
/** Only queries and dispatches — no carrier calls. */
|
||||
public int $timeout = 60;
|
||||
|
||||
public int $backoff = 60;
|
||||
public bool $failOnTimeout = true;
|
||||
|
||||
public int $tries = 1;
|
||||
|
||||
public int $uniqueFor = 300;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
@@ -48,7 +51,7 @@ class PollShipmentTrackingJob implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
Shipment::query()
|
||||
$jobs = Shipment::query()
|
||||
->whereIn('carrier', $trackableCarriers)
|
||||
->whereNull('cancelled_at')
|
||||
// Pending vouchers (issued on print) have no number to track yet.
|
||||
@@ -60,26 +63,17 @@ class PollShipmentTrackingJob implements ShouldQueue
|
||||
TrackingStatus::Cancelled->value,
|
||||
]);
|
||||
})
|
||||
->chunkById(50, function ($shipments) {
|
||||
$shipments->groupBy('carrier')->each(
|
||||
fn ($group, $carrier) => $this->pollCarrierShipments($carrier, $group)
|
||||
);
|
||||
});
|
||||
}
|
||||
->pluck('id')
|
||||
->map(fn (int $id) => new RefreshShipmentTrackingJob($id));
|
||||
|
||||
private function pollCarrierShipments(string $carrier, $shipments): void
|
||||
{
|
||||
if (! $this->fulfillmentService($carrier) instanceof SupportsTracking) {
|
||||
if ($jobs->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($shipments as $shipment) {
|
||||
try {
|
||||
app(ShipmentTrackingRecorder::class)->refresh($shipment);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
Bus::batch($jobs)
|
||||
->name('Poll shipment tracking')
|
||||
->allowFailures()
|
||||
->dispatch();
|
||||
}
|
||||
|
||||
private function fulfillmentService(string $carrier): ?CarrierFulfillmentInterface
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Shipping\Jobs;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Modules\Core\Shipping\Models\Shipment;
|
||||
use Modules\Core\Shipping\Services\ShipmentTrackingRecorder;
|
||||
|
||||
/**
|
||||
* Refreshes one shipment's tracking from its carrier — one job per
|
||||
* shipment in PollShipmentTrackingJob's batch, so a slow or failing
|
||||
* carrier call only costs its own job, not the rest of the run.
|
||||
*
|
||||
* Unique per shipment: while one is queued or running, another for the
|
||||
* same shipment isn't queued, so two refreshes can't both record the same
|
||||
* checkpoint (the recorder's dedup reads, then writes).
|
||||
*/
|
||||
class RefreshShipmentTrackingJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Batchable;
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
|
||||
/** A few carrier calls at a 10–15s HTTP timeout each; under the queue's 90s retry_after. */
|
||||
public int $timeout = 45;
|
||||
|
||||
public bool $failOnTimeout = true;
|
||||
|
||||
/** The next poll, five minutes later, is the retry. */
|
||||
public int $tries = 1;
|
||||
|
||||
/** Releases the lock if a worker dies mid-job and never releases it. */
|
||||
public int $uniqueFor = 300;
|
||||
|
||||
public function __construct(public readonly int $shipmentId) {}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return (string) $this->shipmentId;
|
||||
}
|
||||
|
||||
public function handle(ShipmentTrackingRecorder $recorder): void
|
||||
{
|
||||
if ($this->batch()?->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shipment = Shipment::find($this->shipmentId);
|
||||
|
||||
// Cancelled (or deleted) since the batch was built.
|
||||
if (! $shipment || $shipment->isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$recorder->refresh($shipment);
|
||||
}
|
||||
}
|
||||
@@ -67,15 +67,15 @@ class Shipment extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to the carrier's own tracking page — manual carriers only.
|
||||
* Integrated carriers' history is synced, so customers follow it on
|
||||
* our own order page instead.
|
||||
* A link to the carrier's own tracking page — manual carriers and
|
||||
* typed-in vouchers only. API-created shipments' history is synced, so
|
||||
* customers follow it on our own order page instead.
|
||||
*/
|
||||
public function trackingUrl(): ?string
|
||||
{
|
||||
$template = $this->meta['tracking_url'] ?? null;
|
||||
|
||||
if ($this->source !== self::SOURCE_MANUAL || blank($template) || blank($this->tracking_reference)) {
|
||||
if (! in_array($this->source, [self::SOURCE_MANUAL, self::SOURCE_MANUAL_VOUCHER], true) || blank($template) || blank($this->tracking_reference)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user