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(); } /** * 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 only. * Integrated carriers' 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)) { 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 { $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; } }