shippingAddress; $destination = $this->areaResolver->resolve($address->postcode); $weight = $request->weight ?? 0.5; $params = [ 'Pickup_Date' => now()->toDateString(), 'Sender' => config('acs.sender.name'), 'Recipient_Name' => trim("{$address->first_name} {$address->last_name}"), 'Recipient_Address' => $address->line_one, 'Recipient_Zipcode' => $address->postcode, 'Recipient_Region' => $address->city, 'Recipient_Phone' => $address->contact_phone, 'Recipient_Country' => 'GR', 'Acs_Station_Branch_Destination' => $destination->branchId, 'Billing_Code' => config('acs.billing_code'), 'Charge_Type' => 2, 'Item_Quantity' => $request->packageCount, 'Weight' => $weight, ]; if ($request->paymentMode === 'cod') { $params['Cod_Ammount'] = $request->amountToCollect ?? $order->total->decimal; $params['Cod_Payment_Way'] = 0; // cash $params['Acs_Delivery_Products'] = 'COD'; } $response = $this->client->call('ACS_Create_Voucher', $params)->throwIfError(); $voucherNo = (string) $response->valueOutput['Voucher_No']; $shipment = Shipment::create([ 'order_id' => $order->id, 'carrier' => 'acs', 'tracking_reference' => $voucherNo, 'meta' => [ 'station_destination' => $destination->stationId, 'weight' => $weight, 'pickup_date' => now()->toDateString(), ], ]); if ($request->packageCount > 1) { $this->persistMultipartVouchers($shipment); } return $shipment; } public function printLabel(Shipment $shipment): string { $response = $this->client->call('ACS_Print_Voucher', [ 'Voucher_No' => $shipment->tracking_reference, 'Print_Type' => 2, 'Start_Position' => 1, ])->throwIfError(); $shipment->update(['label_printed_at' => now()]); return $response->valueOutput[$shipment->tracking_reference] ?? ''; } public function cancelShipment(Shipment $shipment): void { if ($shipment->manifest_id) { throw new RuntimeException('Cannot cancel a shipment already included in an issued manifest.'); } $this->client->call('ACS_Delete_Voucher', [ 'Voucher_No' => $shipment->tracking_reference, ])->throwIfError(); $shipment->update(['cancelled_at' => now()]); } public function pendingForManifest(): Collection { return Shipment::query() ->where('carrier', 'acs') ->whereNull('manifest_id') ->whereNull('cancelled_at') ->get(); } public function issueManifest(Collection $shipments): ManifestResult { $unprinted = $shipments->whereNull('label_printed_at'); if ($unprinted->isNotEmpty()) { return ManifestResult::blocked($unprinted, 'unprinted'); } $response = $this->client->call('ACS_Issue_Pickup_List', [ 'Pickup_Date' => now()->toDateString(), 'MyData' => null, ])->throwIfError(); $pickupListNo = (string) $response->valueOutput['PickupList_No']; $manifest = Manifest::create([ 'carrier' => 'acs', 'reference' => $pickupListNo, 'shipment_count' => $shipments->count(), 'issued_at' => now(), ]); $shipments->each(fn (Shipment $shipment) => $shipment->update([ 'manifest_id' => $manifest->id, ])); return ManifestResult::success($manifest, $shipments); } public function trackShipment(Shipment $shipment): Collection { $response = $this->client->call('ACS_TrackingDetails', [ 'Voucher_No' => $shipment->tracking_reference, ])->throwIfError(); $rows = $response->tableOutput['Table_Data'] ?? []; // ACS's per-checkpoint data (checkpoint_action) is free text with no // status code, so the final checkpoint's status is corroborated // against the structured summary call rather than guessed from text. $isDelivered = $this->isDelivered($shipment); return collect($rows)->values()->map(function (array $row, int $index) use ($rows, $isDelivered) { $isLast = $index === count($rows) - 1; return new TrackingCheckpoint( status: $isLast && $isDelivered ? TrackingStatus::Delivered : $this->guessStatusFromAction($row['checkpoint_action'] ?? ''), carrierStatus: $row['checkpoint_action'] ?? null, message: $row['checkpoint_action'] ?? null, location: $row['checkpoint_location'] ?? null, occurredAt: Carbon::parse($row['checkpoint_date_time']), meta: $row, ); }); } private function isDelivered(Shipment $shipment): bool { try { $response = $this->client->call('ACS_Trackingsummary', [ 'Voucher_No' => $shipment->tracking_reference, ])->throwIfError(); } catch (AcsApiException) { return false; } return (int) ($response->valueOutput['shipment_status'] ?? 0) === 4; } private function guessStatusFromAction(string $action): TrackingStatus { $action = strtolower($action); // TODO: no live ACS payload sample yet showing a distinct // collection checkpoint separate from transit ("arrival"/ // "departure" already map to InTransit) — add a str_contains() // arm mapping to TrackingStatus::CollectedFromSender here once // one is confirmed. return match (true) { str_contains($action, 'delivery to consignee') => TrackingStatus::Delivered, str_contains($action, 'on delivery') => TrackingStatus::OutForDelivery, str_contains($action, 'arrival') || str_contains($action, 'departure') => TrackingStatus::InTransit, default => TrackingStatus::Pending, }; } private function persistMultipartVouchers(Shipment $mainShipment): void { $response = $this->client->call('ACS_Get_Multipart_Vouchers', [ 'Main_Voucher_No' => $mainShipment->tracking_reference, ])->throwIfError(); foreach ($response->tableOutput['Table_Data'] ?? [] as $row) { Shipment::create([ 'order_id' => $mainShipment->order_id, 'carrier' => 'acs', 'tracking_reference' => $row['MultiPart_Voucher_No'], 'parent_reference' => $mainShipment->tracking_reference, 'meta' => $mainShipment->meta?->toArray() ?? [], ]); } } }