Feat: Shipping functionalities redesign, elta courier integration, commenting out acs since no integration can happen, flow updates

This commit is contained in:
2026-09-30 17:28:47 +03:00
parent f411dec1da
commit 49ec4333af
63 changed files with 3143 additions and 347 deletions
+67 -4
View File
@@ -12,6 +12,8 @@ use Modules\Core\Payment\DTOs\PaymentResult;
use Modules\Core\Payment\Enums\PaymentResultStatus;
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
use Modules\Core\Shipping\DTOs\ShipmentRequest;
use Modules\Core\Shipping\Enums\ExtraService;
use Modules\Core\Shipping\Models\Shipment;
use Throwable;
/**
@@ -56,7 +58,15 @@ class OrderFulfillmentService
return OrderFulfillmentResult::success('Order marked ready.');
}
public function createShipmentAndDispatch(Order $order, ShipmentRequest $request): OrderFulfillmentResult
/**
* Creates the shipment only — the order stays at ready_for_dispatch
* until the carrier actually picks the parcel up. That move (and the
* "on its way" email) happens in Modules\Core\Order\Listeners\
* AdvanceFulfillmentOnCarrierCheckpoint, on the first InTransit /
* CollectedFromSender checkpoint — synced from the carrier, or entered
* by hand for a manual carrier.
*/
public function createShipment(Order $order, ShipmentRequest $request): OrderFulfillmentResult
{
if ($order->status !== 'ready_for_dispatch') {
return OrderFulfillmentResult::failure('This order is not ready to be dispatched.');
@@ -76,9 +86,48 @@ class OrderFulfillmentService
return OrderFulfillmentResult::failure('Failed to create shipment: '.$e->getMessage());
}
$this->writer->write($order, 'dispatched', self::class.'::createShipmentAndDispatch');
return OrderFulfillmentResult::success('Shipment created. The order moves to Dispatched when the carrier picks it up.');
}
return OrderFulfillmentResult::success('Shipment created and order dispatched.');
/**
* Records an integrated carrier's voucher that wasn't created through
* our API — the carrier's system was down and a pre-numbered paper
* voucher was used, or the courier wrote his own at pickup. It's still
* that carrier's voucher, so PollShipmentTrackingJob picks up its
* history once the carrier's API has it.
*
* @param array<int, ExtraService> $services
*/
public function addManualVoucher(Order $order, string $carrier, string $voucherNumber, array $services = []): OrderFulfillmentResult
{
if (! $this->canAddManualVoucher($order)) {
return OrderFulfillmentResult::failure('A voucher can only be added from Ready for Dispatch until the order is delivered.');
}
$voucherNumber = trim($voucherNumber);
if (Shipment::where('tracking_reference', $voucherNumber)->exists()) {
return OrderFulfillmentResult::failure("Voucher {$voucherNumber} is already recorded.");
}
Shipment::create([
'order_id' => $order->id,
'carrier' => $carrier,
'source' => Shipment::SOURCE_MANUAL_VOUCHER,
'tracking_reference' => $voucherNumber,
'meta' => [
'services' => array_map(fn (ExtraService $service) => $service->value, $services),
'cod_amount' => $this->flow->isCod($order) ? $order->total->decimal : null,
],
]);
return OrderFulfillmentResult::success("Voucher {$voucherNumber} added.");
}
public function canAddManualVoucher(Order $order): bool
{
return in_array($order->status, ['ready_for_dispatch', 'dispatched', 'delivery_failed'], true)
&& ! $order->isStorePickupOrder();
}
public function markPickedUp(Order $order): OrderFulfillmentResult
@@ -175,14 +224,28 @@ class OrderFulfillmentService
return OrderFulfillmentResult::success('Order marked as paid.');
}
/**
* A cancelled shipment doesn't block creating a new one.
*/
public function canCreateShipment(Order $order): bool
{
return $order->status === 'ready_for_dispatch'
&& ! $order->isStorePickupOrder()
&& $order->shipments()->exists() === false
&& $order->shipments()->whereNull('cancelled_at')->doesntExist()
&& $this->resolveFulfillmentService($order) !== null;
}
/**
* The order's shipping method — manual carriers keep their name,
* tracking URL template and cash-on-delivery setting in its `data`.
*/
public function shippingMethodFor(Order $order): ?ShippingMethod
{
$code = $order->shippingAddress?->shipping_option;
return $code ? ShippingMethod::where('code', $code)->first() : null;
}
/**
* Public wrapper around resolveCarrier() — Modules\Core\Shipping\
* Extensions\OrderViewExtension needs to know which carrier an order