Fix: Updates to OrderFullfilmentServices and box now clients, order views and checkout services
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Order\Listeners;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||
use Modules\Core\Order\Services\OrderPaymentResolutionService;
|
||||
use Modules\Core\Payment\Events\PaymentDeferred;
|
||||
|
||||
/**
|
||||
* Deliberately NOT queued — the storefront's own post-checkout
|
||||
* confirmation page (Modules\Core\Checkout\Http\Controllers\
|
||||
* CheckoutController::orderStatus()/confirmation(), per docs/checkout.md)
|
||||
* looks up the order by placed_at being set immediately after
|
||||
* initiatePayment() returns; a stalled queue would show the shopper a
|
||||
* blank/failed confirmation for an order that, in the database, already
|
||||
* exists and was genuinely placed. Same reasoning as
|
||||
* DecrementStockOnOrderPlaced staying synchronous — this is the listener
|
||||
* that makes DecrementStockOnOrderPlaced fire at all for a COD order (see
|
||||
* OrderServiceProvider: OrderPlaced => DecrementStockOnOrderPlaced),
|
||||
* so queueing this one would just move the same stock-oversell risk one
|
||||
* hop earlier.
|
||||
*
|
||||
* A thin reactor, same shape as ApplyResolvedPaymentStatus — the actual
|
||||
* decisions ("this order counts as placed the moment a deferred-payment
|
||||
* driver resolves, independent of Order::paid" and "such an order also
|
||||
* has nothing to sit at awaiting_payment for") live in PaymentDeferred's
|
||||
* and OrderPaymentResolutionService::resolveDeferredPayment()'s own
|
||||
* docblocks, re-confirmed with the user; this only extracts the order id
|
||||
* and applies both, guarded against a duplicate/replayed event the same
|
||||
* way OrderPaymentResolutionService::resolveCaptureOrAuthorization() is.
|
||||
*
|
||||
* Without the status advance below, a COD order was left sitting at
|
||||
* 'awaiting_payment' forever — placed_at/OrderPlaced alone fixed order
|
||||
* visibility and stock decrement, but nothing ever moved `status` off its
|
||||
* initial value, since resolveCaptureOrAuthorization() only does that for
|
||||
* an actual capture. Caught and fixed after the fact.
|
||||
*/
|
||||
class MarkOrderPlacedOnDeferredPayment
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderPaymentResolutionService $resolution,
|
||||
) {}
|
||||
|
||||
public function handle(PaymentDeferred $event): void
|
||||
{
|
||||
$orderId = $event->context['order_id'] ?? null;
|
||||
|
||||
if ($orderId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = Order::findOrFail($orderId);
|
||||
|
||||
$this->resolution->resolveDeferredPayment($order, self::class);
|
||||
|
||||
if (! blank($order->placed_at)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->update(['placed_at' => now()]);
|
||||
|
||||
Event::dispatch(new OrderPlaced($order));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ 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\Payment\DTOs\PaymentResult;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
||||
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
||||
use Throwable;
|
||||
@@ -31,6 +33,7 @@ class OrderFulfillmentService
|
||||
public function __construct(
|
||||
private readonly OrderStatusWriter $writer,
|
||||
private readonly OrderStatusFlow $flow,
|
||||
private readonly TransactionRecorder $transactions,
|
||||
) {}
|
||||
|
||||
public function markReady(Order $order): OrderFulfillmentResult
|
||||
@@ -121,6 +124,39 @@ class OrderFulfillmentService
|
||||
return OrderFulfillmentResult::failure('This order cannot be marked paid right now.');
|
||||
}
|
||||
|
||||
// canMarkPaid() only ever returns true for an order whose payment
|
||||
// method resolves to the cash-on-delivery DRIVER (see
|
||||
// OrderStatusFlow::isCod(), which checks PaymentMethod::driver,
|
||||
// never the merchant-chosen `type` slug directly — a store could
|
||||
// name that method "cod", "pay-on-delivery", anything). Such an
|
||||
// order never runs through Payment's pay()/authorize() flow at
|
||||
// checkout, so nothing else records a Transaction for it. Money
|
||||
// changes hands right here, at this click, so this is the one
|
||||
// place that write can happen; there is no earlier Payment event
|
||||
// to hang it off of the way Modules\Core\Order\Listeners\
|
||||
// RecordPaymentTransaction does for a gateway driver. See
|
||||
// TransactionRecorder's own docblock — it already anticipated
|
||||
// exactly this "manually-triggered ... from Filament" call site.
|
||||
//
|
||||
// $driver below is the payment method's own `type` slug (whatever
|
||||
// the merchant named it, e.g. 'cash-on-delivery' or 'cod') —
|
||||
// Transaction.driver's established meaning everywhere else in this
|
||||
// codebase (see RecordPaymentTransaction/TransactionRecorder's own
|
||||
// docblocks) is that type key, never the underlying driver CLASS.
|
||||
// No fallback guess here: CheckoutService::initiatePayment() always
|
||||
// writes Order.meta['payment_method'] before charging, and
|
||||
// canMarkPaid() already guarantees this order got that far.
|
||||
$this->transactions->record(
|
||||
$order,
|
||||
type: 'capture',
|
||||
driver: (string) $order->meta['payment_method'],
|
||||
result: new PaymentResult(
|
||||
status: PaymentResultStatus::Succeeded,
|
||||
reference: 'cod-manual-'.$order->id,
|
||||
amount: $order->total,
|
||||
),
|
||||
);
|
||||
|
||||
$this->writer->markPaid($order, self::class.'::markPaid');
|
||||
|
||||
return OrderFulfillmentResult::success('Order marked as paid.');
|
||||
|
||||
@@ -52,6 +52,24 @@ class OrderPaymentResolutionService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A deferred-capture payment (currently only cash-on-delivery — see
|
||||
* Payment\Events\PaymentDeferred's own docblock): no money has moved,
|
||||
* so unlike resolveCaptureOrAuthorization() this never calls
|
||||
* $writer->markPaid() — Order::paid stays false until staff explicitly
|
||||
* mark it received. But per OrderStatusFlow's own docblock, payment
|
||||
* method never affects the status SEQUENCE at all — a COD order has
|
||||
* nothing to "await" at checkout (no payment attempt happens), so
|
||||
* 'awaiting_payment' is simply the wrong first status for it. Reuses
|
||||
* the exact same advancePastAwaitingPayment() a capture uses, since
|
||||
* the status-sequence logic itself doesn't differ by payment method,
|
||||
* only whether `paid` also flips alongside it.
|
||||
*/
|
||||
public function resolveDeferredPayment(Order $order, string $causeClass): void
|
||||
{
|
||||
$this->advancePastAwaitingPayment($order, $causeClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the refund Transaction row to already exist (Modules\Core\
|
||||
* Order\Listeners\RecordPaymentTransaction must run first — see
|
||||
|
||||
Reference in New Issue
Block a user