96 lines
4.2 KiB
PHP
96 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Payment\Drivers;
|
|
|
|
use Illuminate\Support\Str;
|
|
use Lunar\DataTypes\Price;
|
|
use Modules\Core\Payment\Contracts\Configurable;
|
|
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;
|
|
|
|
/**
|
|
* 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() 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 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
|
|
* $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
|
|
{
|
|
/**
|
|
* Always true — no external dependency to be missing.
|
|
*/
|
|
public function isConfigured(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function pay(string $type, Price $amount, array $data = [], array $context = []): 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
|
|
{
|
|
$result = new PaymentResult(
|
|
status: PaymentResultStatus::Succeeded,
|
|
reference: 'bank-transfer-'.Str::uuid(),
|
|
amount: $amount,
|
|
meta: array_filter(['notes' => $context['notes'] ?? null]),
|
|
);
|
|
|
|
PaymentRefunded::dispatch('bank-transfer', $result, $context);
|
|
|
|
return $result;
|
|
}
|
|
}
|