*/ public function syncAll(CarbonInterface $from, CarbonInterface $to): array { return collect(Shipping::getSupportedDrivers())->keys() ->filter(fn (string $carrier) => $this->service($carrier) instanceof SupportsVoucherListing) ->mapWithKeys(function (string $carrier) use ($from, $to) { try { return [$carrier => $this->sync($carrier, $from, $to)]; } catch (Throwable $e) { report($e); return [$carrier => ['seen' => 0, 'created' => 0, 'suggested' => 0, 'updated' => 0, 'error' => $e->getMessage()]]; } }) ->all(); } /** * @return array{seen: int, created: int, suggested: int, updated: int} */ public function sync(string $carrier, CarbonInterface $from, CarbonInterface $to): array { $service = $this->service($carrier); $stats = ['seen' => 0, 'created' => 0, 'suggested' => 0, 'updated' => 0]; if (! $service instanceof SupportsVoucherListing) { return $stats; } foreach ($service->listVouchers($from, $to) as $voucher) { $stats['seen']++; $this->checkpointsAdded = 0; $shipment = $this->record($voucher); if ($shipment?->wasRecentlyCreated) { $stats['created']++; $stats['suggested'] += filled($shipment->meta['suggested_order_ids'] ?? []) ? 1 : 0; } elseif ($this->checkpointsAdded > 0) { $stats['updated']++; } } return $stats; } /** * A voucher that hasn't synced yet, looked up by number. Returns the * shipment it's recorded as (existing or new), or null when the carrier * doesn't know it. */ public function lookup(string $carrier, string $voucherNumber): ?Shipment { $service = $this->service($carrier); if (! $service instanceof SupportsVoucherLookup) { return null; } $voucher = $service->lookupVoucher($voucherNumber); return $voucher ? $this->record($voucher) : null; } private function record(CarrierVoucher $voucher): ?Shipment { if (blank($voucher->voucherNumber)) { return null; } $existing = Shipment::where('tracking_reference', $voucher->voucherNumber)->first(); if ($existing) { $this->refreshExisting($existing, $voucher); return $existing; } $shipment = Shipment::create([ 'carrier' => $voucher->carrier, 'source' => Shipment::SOURCE_SYNCED, 'tracking_reference' => $voucher->voucherNumber, 'meta' => [ 'reference' => $voucher->reference, 'recipient_name' => $voucher->recipientName, 'postcode' => $voucher->postcode, 'phone' => $voucher->phone, 'cod_amount' => $voucher->codAmount, 'is_return' => $voucher->isReturn, 'original_voucher' => $voucher->originalVoucher, 'voucher_date' => $voucher->date?->toDateString(), 'suggested_order_ids' => $this->matcher->suggestions($voucher), ], ]); $this->recordHistory($shipment, $voucher); return $shipment; } /** * A voucher we already have, seen again: its history is brought up to * date (any source, unless cancelled here), and a voucher that came * from the carrier also takes the carrier's latest details. The order * link and our own shipments' details are never changed. */ private function refreshExisting(Shipment $shipment, CarrierVoucher $voucher): void { $meta = $shipment->meta?->toArray() ?? []; if ($voucher->isReturn) { $meta['reported_return'] = true; } if ($shipment->source === Shipment::SOURCE_SYNCED) { // Only what the carrier reported this time — ACS's list, for // one, has no recipient, which mustn't wipe what we have. $meta = [...$meta, ...array_filter([ 'recipient_name' => $voucher->recipientName, 'postcode' => $voucher->postcode, 'phone' => $voucher->phone, 'cod_amount' => $voucher->codAmount, 'original_voucher' => $voucher->originalVoucher, ], fn ($value) => $value !== null)]; $meta['is_return'] = ($meta['is_return'] ?? false) || $voucher->isReturn; if ($shipment->order_id === null) { $meta['suggested_order_ids'] = $this->matcher->suggestions($voucher); } } if ($meta !== ($shipment->meta?->toArray() ?? [])) { $shipment->update(['meta' => $meta]); } if (! $shipment->isCancelled()) { $this->recordHistory($shipment, $voucher); } } /** * What the carrier reports becomes the voucher's history, like any * shipment's: new checkpoints from its tracking when the carrier has * it; otherwise the status from the voucher list, when it differs from * the latest one we have. */ private function recordHistory(Shipment $shipment, CarrierVoucher $voucher): void { try { $this->checkpointsAdded += $this->tracking->refresh($shipment); } catch (Throwable $e) { report($e); } if ($this->checkpointsAdded > 0 || ! $voucher->status) { return; } $latest = $shipment->shipmentInfo()->latest('occurred_at')->first(); if ($latest?->status === $voucher->status) { return; } $this->checkpointsAdded += $this->tracking->record($shipment, [new TrackingCheckpoint( status: $voucher->status, carrierStatus: $voucher->statusText, message: $voucher->statusText, location: null, occurredAt: $latest ? now() : ($voucher->date ?? now()), )]); } private function service(string $carrier): ?CarrierFulfillmentInterface { return app(CarrierFulfillmentInterface::class, ['carrier' => $carrier]); } }