Fix: Bank transfer orders should stay in awaiting payment

This commit is contained in:
2026-09-28 10:15:23 +03:00
parent e5679afa25
commit ee4d9b7952
4 changed files with 100 additions and 47 deletions
@@ -9,30 +9,47 @@ 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\PaymentCaptured;
use Modules\Core\Payment\Events\PaymentRefunded;
/**
* Manual/attested, same trust model as OfflinePaymentDriver — there is no
* bank API to call, so both pay() and refund() decide success immediately
* on a staff member's say-so (they've already sent/received the wire
* outside the system). Distinct from OfflinePaymentDriver in intent: this
* exists so a payment taken through a DIFFERENT method (e.g.
* cash-on-delivery) can still be REFUNDED via bank transfer — an admin
* chooses this driver explicitly in the refund action, independent of
* which driver the original payment went through (see
* refund() is manual/attested, same trust model as OfflinePaymentDriver —
* there is no bank API to call, so it decides success immediately on a
* staff member's say-so (they've already sent the wire outside the
* system). Distinct from OfflinePaymentDriver in intent: this exists so a
* payment taken through a DIFFERENT method (e.g. cash-on-delivery) can
* still be REFUNDED via bank transfer — an admin chooses this driver
* explicitly in the refund action, independent of which driver the
* original payment went through (see
* Payment\Support\TransactionDriverAdapter::refundVia() and
* Order\Filament\Extensions\OrderActionsExtension). pay() exists so
* the same driver also covers receiving a payment by bank transfer, but
* the admin UI for that (bank reference, notes, proof-of-transfer upload)
* is deliberately not built yet — see the follow-up work tracked from this
* session; pay() itself is complete and usable via the registry today.
* Order\Filament\Extensions\OrderActionsExtension).
*
* pay() is the opposite trust direction from refund(): a bank transfer
* 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.
*
* $reference is generated here for the same reason as OfflinePaymentDriver's
* pay(): there is no gateway to hand one back. 'notes' in $context (not
* $data — refund() has no $data parameter) is folded into
* PaymentResult::$meta, which Order\Services\TransactionRecorder::record()
* already writes straight into Transaction.meta with no extra plumbing.
* pay(): there is no gateway to hand one back. refund()'s 'notes' (in
* $context — it has no $data parameter) is folded into PaymentResult::$meta,
* which Order\Services\TransactionRecorder::record() already writes straight
* into Transaction.meta with no extra plumbing; pay() has no equivalent
* write, since nothing ever records a Transaction from its own result (see
* above) — any notes a shopper enters at checkout would need surfacing some
* other way, e.g. when staff mark the order paid.
*/
class BankTransferPaymentDriver implements Configurable, SupportsPay, SupportsRefunds
{
@@ -46,16 +63,11 @@ class BankTransferPaymentDriver implements Configurable, SupportsPay, SupportsRe
public function pay(string $type, Price $amount, array $data = [], array $context = []): PaymentResult
{
$result = new PaymentResult(
status: PaymentResultStatus::Succeeded,
return new PaymentResult(
status: PaymentResultStatus::Pending,
reference: 'bank-transfer-'.Str::uuid(),
amount: $amount,
meta: array_filter(['notes' => $data['notes'] ?? null]),
);
PaymentCaptured::dispatch($type, $result, $context);
return $result;
}
public function refund(string $reference, Price $amount, array $context = []): PaymentResult