From 661e8b9a96ee5430bdec4fd6a613fac64982492d Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Mon, 31 Aug 2026 12:50:29 +0300 Subject: [PATCH] Feat: Adding Payment Drivers, configuring stripe --- composer.json | 3 +- src/Checkout/Contracts/PaymentDriver.php | 41 +++++++ src/Payment/Drivers/StripePaymentDriver.php | 100 ++++++++++++++++++ .../PaymentNotConfirmedException.php | 22 ++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/Checkout/Contracts/PaymentDriver.php create mode 100644 src/Payment/Drivers/StripePaymentDriver.php create mode 100644 src/Payment/Exceptions/PaymentNotConfirmedException.php diff --git a/composer.json b/composer.json index 2e2963d..0e69457 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "lunarphp/table-rate-shipping": "^1.3", "lunarphp/search": "*", "lunarphp/meilisearch": "*", - "spatie/laravel-translation-loader": "^2.8" + "spatie/laravel-translation-loader": "^2.8", + "lunarphp/stripe": "1.3.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/src/Checkout/Contracts/PaymentDriver.php b/src/Checkout/Contracts/PaymentDriver.php new file mode 100644 index 0000000..f8c56ac --- /dev/null +++ b/src/Checkout/Contracts/PaymentDriver.php @@ -0,0 +1,41 @@ + $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 $data + * + * @throws \Lunar\Exceptions\FingerprintMismatchException + * @throws \Lunar\Exceptions\Carts\CartException + */ + public function confirm(Cart $cart, string $fingerprint, array $data): Order; +} diff --git a/src/Payment/Drivers/StripePaymentDriver.php b/src/Payment/Drivers/StripePaymentDriver.php new file mode 100644 index 0000000..d705513 --- /dev/null +++ b/src/Payment/Drivers/StripePaymentDriver.php @@ -0,0 +1,100 @@ +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(); + } +} diff --git a/src/Payment/Exceptions/PaymentNotConfirmedException.php b/src/Payment/Exceptions/PaymentNotConfirmedException.php new file mode 100644 index 0000000..a5cef0d --- /dev/null +++ b/src/Payment/Exceptions/PaymentNotConfirmedException.php @@ -0,0 +1,22 @@ +