Feature: Moving Payment Methods to DB, adding fees, Transaction Updates, Refund Updates, General Updates to Payments
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Payment\Support;
|
||||
|
||||
use Lunar\Base\DataTransferObjects\PaymentCapture;
|
||||
use Lunar\Base\DataTransferObjects\PaymentChecks;
|
||||
use Lunar\Base\DataTransferObjects\PaymentRefund;
|
||||
use Lunar\DataTypes\Price;
|
||||
use Lunar\Models\Contracts\Transaction;
|
||||
use Modules\Core\Payment\Contracts\SupportsCaptures;
|
||||
use Modules\Core\Payment\Contracts\SupportsRefunds;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Payment\Services\PaymentDriverRegistry;
|
||||
use Modules\Core\Payment\Services\PaymentMethodCache;
|
||||
|
||||
/**
|
||||
* What Modules\Core\Payment\Models\CoreTransaction::driver() returns
|
||||
* instead of Lunar\Facades\Payments::driver($this->driver) — the point
|
||||
* where every Lunar-native caller of a transaction's driver (today: the
|
||||
* admin panel's "Refund"/"Capture" header actions on the order page,
|
||||
* ManageOrder::getRefundAction()/getCaptureAction() — see
|
||||
* $transaction->refund()/->capture() in vendor/lunarphp/core/src/Models/
|
||||
* Transaction.php) transparently lands on OUR real payment system instead
|
||||
* of Lunar's own, entirely separate, unused PaymentManager.
|
||||
*
|
||||
* Implements Lunar\Base\PaymentTypeInterface's refund()/capture()/
|
||||
* getPaymentChecks() signatures exactly — each takes the Transaction as
|
||||
* its own first argument (confirmed from vendor/lunarphp/core/src/Models/
|
||||
* Transaction.php: `$this->driver()->refund($this, $amount, $notes)`),
|
||||
* so this class holds no transaction state of its own; CoreTransaction's
|
||||
* driver() can return one shared instance for any transaction.
|
||||
*
|
||||
* $transaction->driver is a Modules\Core\Payment\Models\PaymentMethod.type
|
||||
* value (what Modules\Core\Order\Services\TransactionRecorder writes into
|
||||
* Transaction.driver) — this resolves the REAL registry key from that
|
||||
* type via PaymentMethodCache, then the real driver instance from
|
||||
* PaymentDriverRegistry, so refund()/capture() called here call the
|
||||
* ACTUAL Stripe/etc. driver, never a fake/no-op stand-in. If either
|
||||
* lookup fails (the PaymentMethod row or its driver no longer exists),
|
||||
* refund()/capture() report failure rather than silently doing nothing.
|
||||
*/
|
||||
class TransactionDriverAdapter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PaymentMethodCache $paymentMethods,
|
||||
private readonly PaymentDriverRegistry $registry,
|
||||
) {}
|
||||
|
||||
public function refund(Transaction $transaction, int $amount, ?string $notes = null): PaymentRefund
|
||||
{
|
||||
return $this->refundVia($transaction, $this->driverKeyFor($transaction), $amount, $notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* The PaymentDriverRegistry key $transaction was originally taken
|
||||
* through — what refund()/capture() resolve against by default, and
|
||||
* what Order\Filament\Extensions\OrderRefundActionsExtension defaults
|
||||
* its "Refund via" driver Select to, before an admin overrides it.
|
||||
*/
|
||||
public function driverKeyFor(Transaction $transaction): ?string
|
||||
{
|
||||
return $this->paymentMethods->all()->firstWhere('type', $transaction->driver)?->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as refund(), but against an explicitly chosen driver rather than
|
||||
* the one $transaction was originally taken through — e.g. refunding a
|
||||
* cash-on-delivery order via a Bank Transfer driver instead of trying
|
||||
* (and failing) to refund through the offline driver that took the
|
||||
* original payment. $driverKey is a PaymentDriverRegistry key (e.g.
|
||||
* 'bank-transfer'), not a PaymentMethod.type — the two only coincide
|
||||
* when refunding through the transaction's own original driver.
|
||||
*
|
||||
* Called directly by Order\Filament\Extensions\
|
||||
* OrderRefundActionsExtension when the admin picks a different driver
|
||||
* in the refund modal, bypassing Lunar\Models\Transaction::refund()
|
||||
* (whose fixed refund(int $amount, $notes = null) signature has no
|
||||
* room for a driver override) — see that extension's own docblock.
|
||||
*/
|
||||
public function refundVia(Transaction $transaction, ?string $driverKey, int $amount, ?string $notes = null): PaymentRefund
|
||||
{
|
||||
$driver = $driverKey !== null ? $this->registry->resolve($driverKey) : null;
|
||||
|
||||
if (! $driver instanceof SupportsRefunds) {
|
||||
return new PaymentRefund(success: false, message: 'This payment method does not support refunds.');
|
||||
}
|
||||
|
||||
$result = $driver->refund(
|
||||
$transaction->reference,
|
||||
$this->priceFor($transaction, $amount),
|
||||
['notes' => $notes, 'order_id' => $transaction->order_id],
|
||||
);
|
||||
|
||||
return new PaymentRefund(
|
||||
success: $result->status === PaymentResultStatus::Succeeded,
|
||||
message: $result->failureReason,
|
||||
);
|
||||
}
|
||||
|
||||
public function capture(Transaction $transaction, int $amount = 0): PaymentCapture
|
||||
{
|
||||
$driver = $this->resolveDriver($transaction);
|
||||
|
||||
if (! $driver instanceof SupportsCaptures) {
|
||||
return new PaymentCapture(success: false, message: 'This payment method does not support a separate capture step.');
|
||||
}
|
||||
|
||||
$result = $driver->capture(
|
||||
$transaction->reference,
|
||||
$this->priceFor($transaction, $amount ?: $transaction->amount->value),
|
||||
['order_id' => $transaction->order_id],
|
||||
);
|
||||
|
||||
return new PaymentCapture(
|
||||
success: $result->status === PaymentResultStatus::Succeeded,
|
||||
message: $result->failureReason ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lunar's own PaymentChecks DTO (address/postcode/CVC verification
|
||||
* results) has no equivalent in our own contracts — none of our
|
||||
* drivers currently surface this level of gateway-specific detail.
|
||||
* Empty, not null: Lunar's admin panel iterates this collection to
|
||||
* render a checks list, so it needs to always be a valid (possibly
|
||||
* empty) PaymentChecks, never missing entirely.
|
||||
*/
|
||||
public function getPaymentChecks(Transaction $transaction): PaymentChecks
|
||||
{
|
||||
return new PaymentChecks;
|
||||
}
|
||||
|
||||
private function resolveDriver(Transaction $transaction): ?object
|
||||
{
|
||||
$driverKey = $this->driverKeyFor($transaction);
|
||||
|
||||
return $driverKey !== null ? $this->registry->resolve($driverKey) : null;
|
||||
}
|
||||
|
||||
private function priceFor(Transaction $transaction, int $amount): Price
|
||||
{
|
||||
return new Price($amount, $transaction->order->currency);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user