Feat: Updating PaymentDrivers and CheckoutService to handle payment methods

This commit is contained in:
2026-08-31 14:12:21 +03:00
parent 2db1e1331f
commit 9529036585
7 changed files with 179 additions and 71 deletions
+80 -20
View File
@@ -20,6 +20,7 @@ use Modules\Core\Checkout\Events\ShippingAddressSet;
use Modules\Core\Checkout\Events\ShippingOptionSelected;
use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
use Modules\Core\Payment\Models\PaymentMethod;
/**
* Storefront-facing checkout operations, mirroring
@@ -104,6 +105,10 @@ class CheckoutService
* stock adjusted the total, another tab modified the cart) rather than
* silently placing an order at a different total than what was shown.
*
* Not called directly by a storefront — see confirmPayment(), which is
* the only caller and supplies the fingerprint captured in
* selectPaymentMethod(), not one the storefront has to obtain itself.
*
* No exception wrapping: Lunar\Validation\Cart\ValidateCartForOrderCreation
* (run inside Cart::createOrder()) already throws
* Lunar\Exceptions\Carts\CartException with a field-keyed MessageBag
@@ -127,26 +132,66 @@ class CheckoutService
return $order;
}
/**
* Every payment type currently offered to the storefront — every key
* in config('lunar.payments.types') that is BOTH administratively
* enabled (Modules\Core\Payment\Models\PaymentMethod::enabled) AND
* whose registered PaymentDriver reports itself usable right now
* (PaymentDriver::isConfigured() — e.g. Stripe with no API key set is
* never offered, regardless of the enabled toggle). A type with no
* PaymentMethod row at all (never seeded) is treated as not offered,
* same as disabled — nothing here creates one; see
* InstallLunarCommand::seedPaymentMethods().
*
* @return array<string>
*/
public function getPaymentMethods(): array
{
return PaymentMethod::where('enabled', true)
->pluck('type')
->filter(fn (string $type) => $this->resolvePaymentDriver($type)?->isConfigured() ?? false)
->values()
->all();
}
/**
* Records which payment type the shopper picked (Cart::meta
* ['payment_method']) — read by e.g. Modules\Core\Payment\Pipelines\
* Cart\ApplyCashOnDeliveryFee to add that type's own cart-total
* adjustments before the shopper reaches placeOrder()/confirmPayment().
* adjustments before recalculation.
*
* Also snapshots Cart::fingerprint() into meta, *after* saving the
* chosen type — the fingerprint has to reflect the final total
* including any payment-type-specific adjustment (e.g. a COD
* surcharge), which only exists once payment_method is set and the
* cart recalculates. Captured here, server-side, rather than asked of
* the storefront: this is the last moment before confirmPayment() that
* the shopper's reviewed total is known, and confirmPayment() reads it
* back internally instead of taking a fingerprint parameter — a
* storefront should never need to know Cart::fingerprint() exists.
*
* Does not itself call a PaymentDriver — selecting a method and
* confirming payment against it are deliberately separate steps, same
* as selecting a shipping option happens before placing the order.
*
* @throws UnknownPaymentTypeException if $type has no registered
* PaymentDriver (config('lunar.payments.types.<type>.payment_driver'))
* @throws UnknownPaymentTypeException if $type isn't currently offered
* — see getPaymentMethods() for what that means (registered,
* administratively enabled, and its driver reports itself usable)
*/
public function selectPaymentMethod(string $type): Cart
{
$this->paymentDriverFor($type);
if (! in_array($type, $this->getPaymentMethods(), true)) {
throw new UnknownPaymentTypeException($type);
}
$cart = $this->cart->currentOrCreate();
$cart->meta = [...$cart->meta->toArray(), 'payment_method' => $type];
$cart->save();
$cart = $cart->calculate();
$cart->meta = [...$cart->meta->toArray(), 'checkout_fingerprint' => $cart->fingerprint()];
$cart->save();
Event::dispatch(new PaymentMethodSelected($cart, $type));
return $cart;
@@ -159,30 +204,45 @@ class CheckoutService
* carries whatever that driver needs (Stripe's payment_intent id, a
* future redirect-based provider's callback payload).
*
* The fingerprint passed to the driver is the one captured by
* selectPaymentMethod(), not supplied by the caller — see that
* method's docblock. Throws the same FingerprintMismatchException a
* caller-supplied one would if the cart's total has since changed;
* missing entirely (selectPaymentMethod() was never called for this
* cart) is treated the same as a mismatch, not a different error.
*
* @param array<string, mixed> $data
*
* @throws UnknownPaymentTypeException if $type has no registered driver
* @throws UnknownPaymentTypeException if $type isn't currently offered
* (see getPaymentMethods()) — re-checked here, not just in
* selectPaymentMethod(), since a type could be disabled between
* selection and confirmation
* @throws \Lunar\Exceptions\FingerprintMismatchException
* @throws \Lunar\Exceptions\Carts\CartException
*/
public function confirmPayment(string $type, string $fingerprint, array $data = []): Order
public function confirmPayment(string $type, array $data = []): Order
{
$driver = $this->paymentDriverFor($type);
return $driver->confirm($this->cart->currentOrCreate(), $fingerprint, $data);
}
/**
* @throws UnknownPaymentTypeException
*/
private function paymentDriverFor(string $type): PaymentDriver
{
$driverClass = config("lunar.payments.types.{$type}.payment_driver");
if (! $driverClass) {
if (! in_array($type, $this->getPaymentMethods(), true)) {
throw new UnknownPaymentTypeException($type);
}
return app($driverClass);
$cart = $this->cart->currentOrCreate();
$fingerprint = $cart->meta['checkout_fingerprint'] ?? '';
return $this->resolvePaymentDriver($type)->confirm($cart, $type, $fingerprint, $data);
}
/**
* Resolves $type's registered PaymentDriver, or null if $type has no
* 'payment_driver' registered in config('lunar.payments.types.<type>')
* at all — deliberately non-throwing so getPaymentMethods() can filter
* unresolvable types silently rather than treating "not registered"
* as an error condition when just checking availability.
*/
private function resolvePaymentDriver(string $type): ?PaymentDriver
{
$driverClass = config("lunar.payments.types.{$type}.payment_driver");
return $driverClass ? app($driverClass) : null;
}
}