Feat: Adding Payment Drivers, configuring stripe
This commit is contained in:
+2
-1
@@ -17,7 +17,8 @@
|
|||||||
"lunarphp/table-rate-shipping": "^1.3",
|
"lunarphp/table-rate-shipping": "^1.3",
|
||||||
"lunarphp/search": "*",
|
"lunarphp/search": "*",
|
||||||
"lunarphp/meilisearch": "*",
|
"lunarphp/meilisearch": "*",
|
||||||
"spatie/laravel-translation-loader": "^2.8"
|
"spatie/laravel-translation-loader": "^2.8",
|
||||||
|
"lunarphp/stripe": "1.3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Checkout\Contracts;
|
||||||
|
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boboko-owned payment driver — wraps a payment gateway's own confirmation
|
||||||
|
* mechanics (Stripe's synchronous authorize() call, a redirect-based
|
||||||
|
* provider's async callback/webhook, anything else) behind one uniform
|
||||||
|
* moment: "payment is confirmed, place the order."
|
||||||
|
*
|
||||||
|
* confirm() is the only thing a driver is required to do: once it has,
|
||||||
|
* by whatever mechanism is native to that gateway, independently decided
|
||||||
|
* the payment succeeded, it calls Modules\Core\Checkout\Services\
|
||||||
|
* CheckoutService::placeOrder($fingerprint) itself — no driver ever calls
|
||||||
|
* Lunar\Models\Cart::createOrder() directly. This is what lets the
|
||||||
|
* storefront checkout sequence stay uniform regardless of which provider is
|
||||||
|
* active: set addresses, select shipping, hand off to whichever driver is
|
||||||
|
* configured, and the driver decides when (or whether) the order actually
|
||||||
|
* gets created. See docs/checkout.md / docs/payments.md.
|
||||||
|
*/
|
||||||
|
interface PaymentDriver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* $data carries whatever the gateway needs to confirm this specific
|
||||||
|
* payment (Stripe: ['payment_intent' => $id], a redirect-based
|
||||||
|
* provider: its callback payload) — passed explicitly by the caller
|
||||||
|
* (a controller, a webhook job) rather than a driver reaching into the
|
||||||
|
* global request(), so confirm() works the same whether it's called
|
||||||
|
* from a synchronous HTTP request or an async webhook/job with no
|
||||||
|
* active request at all.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*
|
||||||
|
* @throws \Lunar\Exceptions\FingerprintMismatchException
|
||||||
|
* @throws \Lunar\Exceptions\Carts\CartException
|
||||||
|
*/
|
||||||
|
public function confirm(Cart $cart, string $fingerprint, array $data): Order;
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Drivers;
|
||||||
|
|
||||||
|
use Lunar\Exceptions\Carts\CartException;
|
||||||
|
use Lunar\Exceptions\DisallowMultipleCartOrdersException;
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
use Lunar\Stripe\Actions\UpdateOrderFromIntent;
|
||||||
|
use Lunar\Stripe\Facades\Stripe;
|
||||||
|
use Lunar\Stripe\Models\StripePaymentIntent;
|
||||||
|
use Modules\Core\Checkout\Contracts\PaymentDriver;
|
||||||
|
use Modules\Core\Checkout\Services\CheckoutService;
|
||||||
|
use Modules\Core\Payment\Exceptions\PaymentNotConfirmedException;
|
||||||
|
use Stripe\PaymentIntent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps Lunar\Stripe\StripePaymentType::authorize() to satisfy
|
||||||
|
* Modules\Core\Checkout\Contracts\PaymentDriver — calls
|
||||||
|
* CheckoutService::placeOrder($fingerprint) at the moment Stripe confirms
|
||||||
|
* payment, instead of the vendor's own Cart::createOrder() call.
|
||||||
|
*
|
||||||
|
* This is a fork, not a decoration: StripePaymentType::authorize() is
|
||||||
|
* `final` and calls Cart::createOrder() directly with no seam to redirect
|
||||||
|
* that one call — so this class reimplements authorize()'s logic (intent
|
||||||
|
* retrieval, capture-on-policy, status mapping via UpdateOrderFromIntent)
|
||||||
|
* rather than wrapping the vendor method. Kept deliberately close to the
|
||||||
|
* original so a lunarphp/stripe upgrade is easy to diff against. See
|
||||||
|
* docs/payments.md.
|
||||||
|
*/
|
||||||
|
class StripePaymentDriver implements PaymentDriver
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CheckoutService $checkout,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws PaymentNotConfirmedException if Stripe hasn't confirmed the
|
||||||
|
* payment intent (wrong intent id, already processed, order already
|
||||||
|
* placed, or the gateway call itself fails) — nothing here should be
|
||||||
|
* treated as "place the order anyway."
|
||||||
|
* @throws \Lunar\Exceptions\FingerprintMismatchException
|
||||||
|
* @throws CartException
|
||||||
|
*/
|
||||||
|
public function confirm(Cart $cart, string $fingerprint, array $data): Order
|
||||||
|
{
|
||||||
|
$paymentIntentId = $data['payment_intent'];
|
||||||
|
|
||||||
|
$paymentIntentModel = StripePaymentIntent::where('intent_id', $paymentIntentId)->first();
|
||||||
|
|
||||||
|
if ($paymentIntentModel && ! $paymentIntentModel->isActive()) {
|
||||||
|
throw new PaymentNotConfirmedException('Payment intent already processed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $paymentIntentModel) {
|
||||||
|
$paymentIntentModel = StripePaymentIntent::create([
|
||||||
|
'intent_id' => $paymentIntentId,
|
||||||
|
'cart_id' => $cart->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentIntentModel->update(['processing_at' => now()]);
|
||||||
|
|
||||||
|
$stripe = Stripe::getClient();
|
||||||
|
$paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId);
|
||||||
|
|
||||||
|
if (! $paymentIntent) {
|
||||||
|
throw new PaymentNotConfirmedException('Unable to locate payment intent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy = config('lunar.stripe.policy', 'automatic');
|
||||||
|
|
||||||
|
if ($paymentIntent->status === PaymentIntent::STATUS_REQUIRES_CAPTURE && $policy === 'automatic') {
|
||||||
|
$paymentIntent = $stripe->paymentIntents->capture($paymentIntentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($paymentIntent->status !== PaymentIntent::STATUS_SUCCEEDED) {
|
||||||
|
$paymentIntentModel->update(['status' => $paymentIntent->status]);
|
||||||
|
|
||||||
|
throw new PaymentNotConfirmedException(
|
||||||
|
$paymentIntent->last_payment_error->message ?? "Payment intent status: {$paymentIntent->status}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = $this->checkout->placeOrder($fingerprint);
|
||||||
|
} catch (DisallowMultipleCartOrdersException|CartException $e) {
|
||||||
|
throw new PaymentNotConfirmedException($e->getMessage(), previous: $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentIntentModel->order_id = $order->id;
|
||||||
|
$paymentIntentModel->status = $paymentIntent->status;
|
||||||
|
$paymentIntentModel->processed_at = now();
|
||||||
|
$paymentIntentModel->save();
|
||||||
|
|
||||||
|
UpdateOrderFromIntent::execute($order, $paymentIntent);
|
||||||
|
|
||||||
|
return $order->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by a Modules\Core\Checkout\Contracts\PaymentDriver when the
|
||||||
|
* gateway has not confirmed payment — wrong/expired intent, already
|
||||||
|
* processed, or the gateway itself rejects the confirmation. A driver
|
||||||
|
* throws this instead of silently placing the order: CheckoutService::
|
||||||
|
* placeOrder() must only ever be called once a driver has positively
|
||||||
|
* confirmed payment, never as a fallback.
|
||||||
|
*/
|
||||||
|
class PaymentNotConfirmedException extends RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(string $message, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, previous: $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user