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; } }