Feature: Order Updates, Events, Order Flows, Shipment And COD support
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Order\Services;
|
||||
|
||||
use Lunar\Models\Order;
|
||||
use Lunar\Shipping\Models\ShippingMethod;
|
||||
use Modules\Core\Order\DTOs\OrderFulfillmentResult;
|
||||
use Modules\Core\Order\Events\OrderPickedUp;
|
||||
use Modules\Core\Order\Events\OrderReadyForDispatch;
|
||||
use Modules\Core\Order\Events\OrderReadyForPickup;
|
||||
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
||||
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The staff-facing fulfillment/return/payment workflow behind the three
|
||||
* header actions in Modules\Core\Shipping\Extensions\OrderViewExtension
|
||||
* ("Create Shipment", "Update Status", "Mark Paid") — every guard check,
|
||||
* status write (via Modules\Core\Order\Services\OrderStatusWriter), and
|
||||
* event dispatch lives here, keeping this workflow usable and testable
|
||||
* independent of Filament.
|
||||
*
|
||||
* Every method re-validates its own precondition internally (not just
|
||||
* trusted from the caller's own visible()-equivalent check) — protects
|
||||
* against a stale page load racing a concurrent automatic transition
|
||||
* (e.g. a carrier tracking checkpoint advancing the same order between
|
||||
* page load and button click).
|
||||
*/
|
||||
class OrderFulfillmentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderStatusWriter $writer,
|
||||
private readonly OrderStatusFlow $flow,
|
||||
) {}
|
||||
|
||||
public function markReady(Order $order): OrderFulfillmentResult
|
||||
{
|
||||
if ($order->status !== 'processing') {
|
||||
return OrderFulfillmentResult::failure('This order must be in Processing before it can be marked ready.');
|
||||
}
|
||||
|
||||
$target = $order->isStorePickupOrder() ? 'ready_for_pickup' : 'ready_for_dispatch';
|
||||
|
||||
$this->writer->write($order, $target, self::class.'::markReady');
|
||||
|
||||
if ($order->isStorePickupOrder()) {
|
||||
OrderReadyForPickup::dispatch($order);
|
||||
} else {
|
||||
OrderReadyForDispatch::dispatch($order);
|
||||
}
|
||||
|
||||
return OrderFulfillmentResult::success('Order marked ready.');
|
||||
}
|
||||
|
||||
public function createShipmentAndDispatch(Order $order, ShipmentRequest $request): OrderFulfillmentResult
|
||||
{
|
||||
if ($order->status !== 'ready_for_dispatch') {
|
||||
return OrderFulfillmentResult::failure('This order is not ready to be dispatched.');
|
||||
}
|
||||
|
||||
$service = $this->resolveFulfillmentService($order);
|
||||
|
||||
if (! $service) {
|
||||
return OrderFulfillmentResult::failure('No carrier fulfillment integration is configured for this order.');
|
||||
}
|
||||
|
||||
try {
|
||||
$service->createShipment($order, $request);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return OrderFulfillmentResult::failure('Failed to create shipment: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$this->writer->write($order, 'dispatched', self::class.'::createShipmentAndDispatch');
|
||||
|
||||
return OrderFulfillmentResult::success('Shipment created and order dispatched.');
|
||||
}
|
||||
|
||||
public function markPickedUp(Order $order): OrderFulfillmentResult
|
||||
{
|
||||
if ($order->status !== 'ready_for_pickup') {
|
||||
return OrderFulfillmentResult::failure('This order is not ready for pickup.');
|
||||
}
|
||||
|
||||
$this->writer->write($order, 'picked_up', self::class.'::markPickedUp');
|
||||
|
||||
OrderPickedUp::dispatch($order);
|
||||
|
||||
return OrderFulfillmentResult::success('Order marked as picked up.');
|
||||
}
|
||||
|
||||
/**
|
||||
* The general-purpose entry point for any transition with no special
|
||||
* side effect — a manual override, not restricted to the guided next
|
||||
* step(s), so staff can revert to an earlier status in the order's
|
||||
* own branch. Validates $to is actually a member of
|
||||
* OrderStatusFlow::allOptions() before writing (server-side
|
||||
* re-validation of whatever the Select offered) — still refuses a
|
||||
* status from the WRONG branch or an unknown value.
|
||||
*/
|
||||
public function transitionTo(Order $order, string $to): OrderFulfillmentResult
|
||||
{
|
||||
if (! array_key_exists($to, $this->flow->allOptions($order))) {
|
||||
return OrderFulfillmentResult::failure('That status is not valid for this order.');
|
||||
}
|
||||
|
||||
$this->writer->write($order, $to, self::class.'::transitionTo');
|
||||
|
||||
return OrderFulfillmentResult::success('Order status updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Independent of `status` entirely — offered by the single "Update
|
||||
* Status" action regardless of current status (see
|
||||
* OrderStatusFlow::canMarkPaid()).
|
||||
*/
|
||||
public function markPaid(Order $order): OrderFulfillmentResult
|
||||
{
|
||||
if (! $this->flow->canMarkPaid($order)) {
|
||||
return OrderFulfillmentResult::failure('This order cannot be marked paid right now.');
|
||||
}
|
||||
|
||||
$this->writer->markPaid($order, self::class.'::markPaid');
|
||||
|
||||
return OrderFulfillmentResult::success('Order marked as paid.');
|
||||
}
|
||||
|
||||
public function canCreateShipment(Order $order): bool
|
||||
{
|
||||
return $order->status === 'ready_for_dispatch'
|
||||
&& ! $order->isStorePickupOrder()
|
||||
&& $order->shipments()->exists() === false
|
||||
&& $this->resolveFulfillmentService($order) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public wrapper around resolveCarrier() — Modules\Core\Shipping\
|
||||
* Extensions\OrderViewExtension needs to know which carrier an order
|
||||
* uses to branch the "Create Shipment" form (Box Now's box-size
|
||||
* repeater vs. every other carrier's plain weight field).
|
||||
*/
|
||||
public function carrierFor(Order $order): ?string
|
||||
{
|
||||
return $this->resolveCarrier($order);
|
||||
}
|
||||
|
||||
private function resolveCarrier(Order $order): ?string
|
||||
{
|
||||
$code = $order->shippingAddress?->shipping_option;
|
||||
|
||||
if (! $code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ShippingMethod::where('code', $code)->value('driver');
|
||||
}
|
||||
|
||||
private function resolveFulfillmentService(Order $order): ?CarrierFulfillmentInterface
|
||||
{
|
||||
$carrier = $this->resolveCarrier($order);
|
||||
|
||||
if (! $carrier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app(CarrierFulfillmentInterface::class, ['carrier' => $carrier]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user