Feature: Shipment Updates to handle COD

This commit is contained in:
2026-07-19 18:27:24 +03:00
parent 435a4dd290
commit 9f30a7324e
5 changed files with 184 additions and 21 deletions
@@ -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', [