204 lines
7.3 KiB
PHP
204 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Carriers\Acs;
|
|
|
|
use RuntimeException;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Lunar\Models\Order;
|
|
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
|
use Modules\Core\Shipping\Contracts\SupportsManifestBatching;
|
|
use Modules\Core\Shipping\Contracts\SupportsTracking;
|
|
use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException;
|
|
use Modules\Core\Shipping\DTOs\ManifestResult;
|
|
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
|
use Modules\Core\Shipping\DTOs\TrackingCheckpoint;
|
|
use Modules\Core\Shipping\Enums\TrackingStatus;
|
|
use Modules\Core\Shipping\Models\Shipment;
|
|
|
|
class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsManifestBatching, SupportsTracking
|
|
{
|
|
public function __construct(
|
|
private readonly AcsClient $client,
|
|
private readonly AreaResolver $areaResolver,
|
|
) {}
|
|
|
|
public function createShipment(Order $order, ShipmentRequest $request): Shipment
|
|
{
|
|
$address = $order->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_reference) {
|
|
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_reference')
|
|
->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'];
|
|
|
|
$shipments->each(fn (Shipment $shipment) => $shipment->update([
|
|
'manifest_reference' => $pickupListNo,
|
|
]));
|
|
|
|
return ManifestResult::success($pickupListNo, $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);
|
|
|
|
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() ?? [],
|
|
]);
|
|
}
|
|
}
|
|
}
|