201 lines
6.4 KiB
PHP
201 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Lunar\Models\Order;
|
|
use Lunar\Shipping\Facades\Shipping;
|
|
use Modules\Core\Shipping\DTOs\VoucherRecipient;
|
|
|
|
class Shipment extends Model
|
|
{
|
|
/** Created from our admin through the carrier's API. */
|
|
public const SOURCE_CREATED = 'created';
|
|
|
|
/** An integrated carrier's voucher whose number staff typed in (API down, or the courier's own voucher). */
|
|
public const SOURCE_MANUAL_VOUCHER = 'manual_voucher';
|
|
|
|
/** A carrier with no integration (the `manual` shipping driver); tracked by hand. */
|
|
public const SOURCE_MANUAL = 'manual';
|
|
|
|
/** Pulled from a carrier's own voucher list; may not be linked to an order yet. */
|
|
public const SOURCE_SYNCED = 'synced';
|
|
|
|
protected $guarded = [];
|
|
|
|
protected $casts = [
|
|
'meta' => AsArrayObject::class,
|
|
'label_printed_at' => 'datetime',
|
|
'cancelled_at' => 'datetime',
|
|
];
|
|
|
|
public function order(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Order::class);
|
|
}
|
|
|
|
public function manifest(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Manifest::class);
|
|
}
|
|
|
|
public function shipmentInfo(): HasMany
|
|
{
|
|
return $this->hasMany(ShipmentInfo::class);
|
|
}
|
|
|
|
public function latestShipmentInfo(): ?ShipmentInfo
|
|
{
|
|
return $this->shipmentInfo()->latest('occurred_at')->first();
|
|
}
|
|
|
|
/**
|
|
* Who the voucher goes to, as saved when it was created
|
|
* (meta.recipient). Vouchers created before that was saved fall back to
|
|
* their order's shipping address; an extra parcel of a multi-parcel
|
|
* send falls back to its main voucher's.
|
|
*/
|
|
public function recipient(): ?VoucherRecipient
|
|
{
|
|
if (filled($this->meta['recipient'] ?? null)) {
|
|
return VoucherRecipient::fromArray((array) $this->meta['recipient']);
|
|
}
|
|
|
|
if ($address = $this->order?->shippingAddress) {
|
|
return VoucherRecipient::fromAddress($address);
|
|
}
|
|
|
|
$masterId = $this->meta['master_shipment_id'] ?? null;
|
|
|
|
return $masterId ? self::find($masterId)?->recipient() : null;
|
|
}
|
|
|
|
/**
|
|
* The reference we sent the carrier with this voucher (saved since
|
|
* 0.29.2); older vouchers were sent their order's reference.
|
|
*/
|
|
public function reference(): ?string
|
|
{
|
|
return filled($this->meta['reference'] ?? null)
|
|
? (string) $this->meta['reference']
|
|
: $this->order?->reference;
|
|
}
|
|
|
|
/**
|
|
* The carrier's display name: a manual carrier's own name (snapshotted
|
|
* from its shipping method), otherwise the shipping driver's name().
|
|
*/
|
|
public function carrierLabel(): string
|
|
{
|
|
if (filled($this->meta['carrier_name'] ?? null)) {
|
|
return $this->meta['carrier_name'];
|
|
}
|
|
|
|
$driver = collect(Shipping::getSupportedDrivers())->get($this->carrier);
|
|
|
|
return $driver?->name() ?? ucwords(str_replace('-', ' ', $this->carrier));
|
|
}
|
|
|
|
/**
|
|
* 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 (! in_array($this->source, [self::SOURCE_MANUAL, self::SOURCE_MANUAL_VOUCHER], true) || blank($template) || blank($this->tracking_reference)) {
|
|
return null;
|
|
}
|
|
|
|
return str_replace('{number}', rawurlencode($this->tracking_reference), $template);
|
|
}
|
|
|
|
public function isCancelled(): bool
|
|
{
|
|
return $this->cancelled_at !== null;
|
|
}
|
|
|
|
/**
|
|
* A return voucher (coming back to us), from a carrier's list. Its
|
|
* checkpoints must never drive the order's own status.
|
|
*/
|
|
public function isReturn(): bool
|
|
{
|
|
return (bool) ($this->meta['is_return'] ?? false);
|
|
}
|
|
|
|
/**
|
|
* Whether this shipment's carrier checkpoints should move its order's
|
|
* status (dispatched / delivered / delivery failed).
|
|
*/
|
|
public function drivesOrderStatus(): bool
|
|
{
|
|
return $this->order_id !== null && ! $this->isReturn() && ! $this->isCancelled();
|
|
}
|
|
|
|
/**
|
|
* Only shipments created through the carrier's API have a label to
|
|
* print — manual carriers, typed-in vouchers and synced ones don't.
|
|
*/
|
|
public function hasCarrierLabel(): bool
|
|
{
|
|
return $this->source === self::SOURCE_CREATED && ! $this->isCancelled();
|
|
}
|
|
|
|
/**
|
|
* Cancelling a manual carrier's shipment or a typed-in voucher is a
|
|
* local record change only — there's nothing to cancel at the carrier
|
|
* through the API.
|
|
*/
|
|
public function cancelsLocallyOnly(): bool
|
|
{
|
|
return in_array($this->source, [self::SOURCE_MANUAL, self::SOURCE_MANUAL_VOUCHER, self::SOURCE_SYNCED], true);
|
|
}
|
|
|
|
/**
|
|
* This parcel's place in a multi-parcel send, for display — e.g.
|
|
* "Main voucher · parcel 1 of 3" or "Parcel 2 of 3 · of NZ000987399GR".
|
|
* Null for a single-parcel shipment.
|
|
*
|
|
* Read from what's already stored: the piece numbers in meta (sends
|
|
* created as multi-parcel), else parent_reference (the main voucher's
|
|
* number, set on issued extra parcels — also ACS multi-part vouchers).
|
|
*/
|
|
public function parcelDescription(): ?string
|
|
{
|
|
// Box Now: every box is its own voucher (no main voucher).
|
|
if (($this->meta['boxes'] ?? 0) > 1) {
|
|
return "Box {$this->meta['box']} of {$this->meta['boxes']}";
|
|
}
|
|
|
|
$piece = $this->meta['piece'] ?? null;
|
|
$pieces = $this->meta['pieces'] ?? null;
|
|
|
|
if ($piece && $pieces > 1) {
|
|
if ((int) $piece === 1) {
|
|
return "Main voucher · parcel 1 of {$pieces}";
|
|
}
|
|
|
|
$main = $this->parent_reference ?? 'the main voucher (not issued yet)';
|
|
|
|
return "Parcel {$piece} of {$pieces} · of {$main}";
|
|
}
|
|
|
|
if (filled($this->parent_reference)) {
|
|
return "Extra parcel · of {$this->parent_reference}";
|
|
}
|
|
|
|
if (filled($this->tracking_reference)
|
|
&& static::where('parent_reference', $this->tracking_reference)->exists()) {
|
|
return 'Main voucher';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|