Feature: Updating Listreners, Separating Logic from listeners, Queuing Policies
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Order\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||
use Modules\Core\Order\Enums\PaymentStatus;
|
||||
use Modules\Core\Order\Support\OrderStatus;
|
||||
|
||||
/**
|
||||
* The actual business decisions behind reacting to a payment outcome —
|
||||
* previously these lived entirely inside Modules\Core\Order\Listeners\
|
||||
* ApplyResolvedPaymentStatus, a listener with no Service behind it, even
|
||||
* though "should this order be marked paid," "should its status advance,
|
||||
* and to what," and "what does a refund do to status" are all genuine
|
||||
* decisions about Order state, not side effects of Payment's own events.
|
||||
* That listener is now a thin reactor: extract the order id from
|
||||
* $event->context, load the Order, call this service, done.
|
||||
*
|
||||
* See ApplyResolvedPaymentStatus's own docblock for the full business
|
||||
* reasoning (re-confirmed with the user) behind each rule enforced here —
|
||||
* this class only re-documents what's specific to the decision logic
|
||||
* itself, not the "why" already recorded there.
|
||||
*/
|
||||
class OrderPaymentResolutionService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderStatusWriter $writer,
|
||||
private readonly OrderStatusFlow $flow,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* A captured or authorized payment: marks the order paid (capture
|
||||
* only — an authorization is not yet captured funds), advances status
|
||||
* out of 'awaiting_payment' (capture only), and marks the order
|
||||
* placed if this is the first payment outcome it's seen.
|
||||
*/
|
||||
public function resolveCaptureOrAuthorization(Order $order, string $causeClass, bool $isCapture): void
|
||||
{
|
||||
$wasPlaced = ! blank($order->placed_at);
|
||||
|
||||
$this->writer->markPaid($order, $causeClass);
|
||||
|
||||
if ($isCapture) {
|
||||
$this->advancePastAwaitingPayment($order, $causeClass);
|
||||
}
|
||||
|
||||
if (! $wasPlaced) {
|
||||
$order->update(['placed_at' => $order->placed_at ?? now()]);
|
||||
Event::dispatch(new OrderPlaced($order));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the refund Transaction row to already exist (Modules\Core\
|
||||
* Order\Listeners\RecordPaymentTransaction must run first — see
|
||||
* OrderServiceProvider's listener registration order for
|
||||
* PaymentRefunded), so the relation is refreshed here rather than
|
||||
* trusted from a possibly-stale $order instance.
|
||||
*/
|
||||
public function resolveRefund(Order $order, string $causeClass): void
|
||||
{
|
||||
$order->load('transactions');
|
||||
|
||||
$target = match (OrderStatus::payment($order)) {
|
||||
PaymentStatus::Refunded => 'refunded',
|
||||
PaymentStatus::PartialRefund => 'partially_refunded',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($target !== null && $order->status !== $target) {
|
||||
$this->writer->write($order, $target, $causeClass);
|
||||
}
|
||||
}
|
||||
|
||||
private function advancePastAwaitingPayment(Order $order, string $causeClass): void
|
||||
{
|
||||
if ($order->status !== 'awaiting_payment') {
|
||||
return;
|
||||
}
|
||||
|
||||
$next = $this->flow->nextOptions($order);
|
||||
$target = array_key_first($next);
|
||||
|
||||
if ($target !== null) {
|
||||
$this->writer->write($order, $target, $causeClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,22 @@ class OrderStatusFlow
|
||||
return ! $order->paid && $this->isCod($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether moving $order to $to is a valid transition from its CURRENT
|
||||
* status — the single source of truth for "is this a legal next step,"
|
||||
* so a caller reacting to an external event (a carrier tracking
|
||||
* checkpoint, a staff action) doesn't need to hardcode its own "only
|
||||
* fire from status X" guard duplicating what nextOptions() already
|
||||
* knows. See e.g. Modules\Core\Order\Listeners\
|
||||
* AdvanceFulfillmentOnCarrierCheckpoint, which used to compare
|
||||
* $order->status to a literal 'ready_for_dispatch' inline instead of
|
||||
* asking this class.
|
||||
*/
|
||||
public function isValidTransition(Order $order, string $to): bool
|
||||
{
|
||||
return array_key_exists($to, $this->nextOptions($order));
|
||||
}
|
||||
|
||||
private function label(string $status): string
|
||||
{
|
||||
return (string) str($status)->replace('_', ' ')->title();
|
||||
|
||||
Reference in New Issue
Block a user