Feature: Shipment Updates to handle COD
This commit is contained in:
@@ -2,26 +2,33 @@
|
||||
|
||||
namespace Modules\Core\Shipping\Carriers\Acs;
|
||||
|
||||
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\DataTransferObjects\ManifestResult;
|
||||
use Modules\Core\Shipping\DataTransferObjects\ShipmentRequest;
|
||||
use Modules\Core\Shipping\DataTransferObjects\TrackingCheckpoint;
|
||||
use Modules\Core\Shipping\Enums\TrackingStatus;
|
||||
use Modules\Core\Shipping\Models\Shipment;
|
||||
|
||||
class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsManifestBatching
|
||||
class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsManifestBatching, SupportsTracking
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AcsClient $client,
|
||||
private readonly AreaResolver $areaResolver,
|
||||
) {}
|
||||
|
||||
public function createShipment(Order $order, array $overrides = []): Shipment
|
||||
public function createShipment(Order $order, ShipmentRequest $request): Shipment
|
||||
{
|
||||
$address = $order->shippingAddress;
|
||||
$destination = $this->areaResolver->resolve($address->postcode);
|
||||
$weight = $request->weight ?? 0.5;
|
||||
|
||||
$response = $this->client->call('ACS_Create_Voucher', array_merge([
|
||||
$params = [
|
||||
'Pickup_Date' => now()->toDateString(),
|
||||
'Sender' => config('acs.sender.name'),
|
||||
'Recipient_Name' => trim("{$address->first_name} {$address->last_name}"),
|
||||
@@ -33,9 +40,17 @@ class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsMani
|
||||
'Acs_Station_Branch_Destination' => $destination->branchId,
|
||||
'Billing_Code' => config('acs.billing_code'),
|
||||
'Charge_Type' => 2,
|
||||
'Item_Quantity' => 1,
|
||||
'Weight' => 0.5,
|
||||
], $overrides))->throwIfError();
|
||||
'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'];
|
||||
|
||||
@@ -45,12 +60,12 @@ class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsMani
|
||||
'tracking_reference' => $voucherNo,
|
||||
'meta' => [
|
||||
'station_destination' => $destination->stationId,
|
||||
'weight' => $overrides['Weight'] ?? 0.5,
|
||||
'weight' => $weight,
|
||||
'pickup_date' => now()->toDateString(),
|
||||
],
|
||||
]);
|
||||
|
||||
if (($overrides['Item_Quantity'] ?? 1) > 1) {
|
||||
if ($request->packageCount > 1) {
|
||||
$this->persistMultipartVouchers($shipment);
|
||||
}
|
||||
|
||||
@@ -114,6 +129,60 @@ class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsMani
|
||||
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', [
|
||||
|
||||
Reference in New Issue
Block a user