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\OrderActionsExtension 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\ * OrderActionsExtension 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); } }