145 lines
6.2 KiB
PHP
145 lines
6.2 KiB
PHP
<?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\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);
|
|
}
|
|
}
|