From d189a7559a959f68ea85888858419446534ff5e6 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Mon, 28 Sep 2026 17:10:57 +0300 Subject: [PATCH] Fix: Marking orded placed and setting order as awaiting payment when payment is bank transfer --- .../MarkOrderPlacedOnDeferredPayment.php | 12 +++++- .../Drivers/BankTransferPaymentDriver.php | 39 ++++++++++++------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/Order/Listeners/MarkOrderPlacedOnDeferredPayment.php b/src/Order/Listeners/MarkOrderPlacedOnDeferredPayment.php index 90ef307..8f74c61 100644 --- a/src/Order/Listeners/MarkOrderPlacedOnDeferredPayment.php +++ b/src/Order/Listeners/MarkOrderPlacedOnDeferredPayment.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Event; use Lunar\Models\Order; use Modules\Core\Checkout\Events\OrderPlaced; use Modules\Core\Order\Services\OrderPaymentResolutionService; +use Modules\Core\Order\Services\OrderStatusFlow; use Modules\Core\Payment\Events\PaymentDeferred; /** @@ -36,11 +37,18 @@ use Modules\Core\Payment\Events\PaymentDeferred; * 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. + * + * The status advance is skipped for a bank transfer order + * (OrderStatusFlow::isBankTransfer()) — unlike COD, it genuinely has + * something to await at 'awaiting_payment': the wire itself. Only + * OrderFulfillmentService::markPaid() ever advances it past that point + * (see BankTransferPaymentDriver's own docblock). */ class MarkOrderPlacedOnDeferredPayment { public function __construct( private readonly OrderPaymentResolutionService $resolution, + private readonly OrderStatusFlow $flow, ) {} public function handle(PaymentDeferred $event): void @@ -53,7 +61,9 @@ class MarkOrderPlacedOnDeferredPayment $order = Order::findOrFail($orderId); - $this->resolution->resolveDeferredPayment($order, self::class); + if (! $this->flow->isBankTransfer($order)) { + $this->resolution->resolveDeferredPayment($order, self::class); + } if (! blank($order->placed_at)) { return; diff --git a/src/Payment/Drivers/BankTransferPaymentDriver.php b/src/Payment/Drivers/BankTransferPaymentDriver.php index 1280a9f..7ff3866 100644 --- a/src/Payment/Drivers/BankTransferPaymentDriver.php +++ b/src/Payment/Drivers/BankTransferPaymentDriver.php @@ -9,6 +9,7 @@ use Modules\Core\Payment\Contracts\SupportsPay; use Modules\Core\Payment\Contracts\SupportsRefunds; use Modules\Core\Payment\DTOs\PaymentResult; use Modules\Core\Payment\Enums\PaymentResultStatus; +use Modules\Core\Payment\Events\PaymentDeferred; use Modules\Core\Payment\Events\PaymentRefunded; /** @@ -27,20 +28,24 @@ use Modules\Core\Payment\Events\PaymentRefunded; * payment requires the money to arrive BEFORE the order can be * considered paid (unlike cash-on-delivery, where payment happens on * delivery — see CashOnDeliveryPaymentDriver's own docblock for that - * driver's mirror-image reasoning). So pay() returns Pending, dispatching - * no event at all — no PaymentCaptured (nothing has been paid yet), and - * deliberately NOT PaymentDeferred either (unlike COD, whose - * MarkOrderPlacedOnDeferredPayment listener immediately advances the - * order past 'awaiting_payment' since a COD order has nothing to await at - * checkout). A bank transfer order genuinely DOES have something to - * await: it stays at 'awaiting_payment' with Order::paid false until - * staff confirm the wire arrived via OrderFulfillmentService::markPaid(), - * which — unlike its COD path — also advances the order's status, since - * nothing else ever will (see that method's own docblock). - * CheckoutController::placeOrder() already treats a Pending result with - * no continuation as a fully placed order (see its own docblock), so the - * order is still created and visible to the shopper immediately; only its - * payment/status is what's left outstanding. + * driver's mirror-image reasoning). So pay() returns Pending and DOES + * dispatch PaymentDeferred, same as COD — without it, nothing ever sets + * Order::placed_at or fires OrderPlaced, leaving the order invisible in + * customer order history, un-decremented in stock, and the checkout + * confirmation page unable to find it (see PaymentDeferred's and + * CashOnDeliveryPaymentDriver's own docblocks for that failure mode). + * Unlike COD, though, a bank transfer order genuinely DOES have something + * to await: MarkOrderPlacedOnDeferredPayment skips + * OrderPaymentResolutionService::resolveDeferredPayment() for a bank + * transfer order (via OrderStatusFlow::isBankTransfer()), so it stays at + * 'awaiting_payment' with Order::paid false until staff confirm the wire + * arrived via OrderFulfillmentService::markPaid(), which — unlike its COD + * path — also advances the order's status, since nothing else ever will + * (see that method's own docblock). CheckoutController::placeOrder() + * already treats a Pending result with no continuation as a fully placed + * order (see its own docblock), so the order is still created and visible + * to the shopper immediately; only its payment/status is what's left + * outstanding. * * $reference is generated here for the same reason as OfflinePaymentDriver's * pay(): there is no gateway to hand one back. refund()'s 'notes' (in @@ -63,11 +68,15 @@ class BankTransferPaymentDriver implements Configurable, SupportsPay, SupportsRe public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult { - return new PaymentResult( + $result = new PaymentResult( status: PaymentResultStatus::Pending, reference: 'bank-transfer-'.Str::uuid(), amount: $amount, ); + + PaymentDeferred::dispatch($type, $result, $context); + + return $result; } public function refund(string $reference, Price $amount, array $context = []): PaymentResult