2026-08-31 12:50:29 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Payment\Drivers;
|
|
|
|
|
|
|
|
|
|
use Lunar\Models\Cart;
|
|
|
|
|
use Lunar\Stripe\Actions\UpdateOrderFromIntent;
|
|
|
|
|
use Lunar\Stripe\Facades\Stripe;
|
|
|
|
|
use Lunar\Stripe\Models\StripePaymentIntent;
|
2026-09-02 16:14:52 +03:00
|
|
|
use Modules\Core\Checkout\Events\OrderPlaced;
|
|
|
|
|
use Modules\Core\Checkout\Events\PaymentConfirmed;
|
|
|
|
|
use Modules\Core\Payment\Contracts\PaymentDriver;
|
2026-08-31 12:50:29 +03:00
|
|
|
use Modules\Core\Payment\Exceptions\PaymentNotConfirmedException;
|
|
|
|
|
use Stripe\PaymentIntent;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wraps Lunar\Stripe\StripePaymentType::authorize() to satisfy
|
2026-09-02 16:14:52 +03:00
|
|
|
* Modules\Core\Payment\Contracts\PaymentDriver — dispatches
|
|
|
|
|
* PaymentConfirmed at the moment Stripe confirms payment, instead of the
|
|
|
|
|
* vendor's own Cart::createOrder() call.
|
2026-08-31 12:50:29 +03:00
|
|
|
*
|
|
|
|
|
* 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
|
2026-09-02 16:14:52 +03:00
|
|
|
* retrieval, capture-on-policy) 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.
|
|
|
|
|
*
|
|
|
|
|
* The status-mapping step (UpdateOrderFromIntent) this driver used to do
|
|
|
|
|
* inline right after placeOrder() returned now happens in onOrderPlaced()
|
|
|
|
|
* below instead — see PaymentDriver's docblock for why a driver can no
|
|
|
|
|
* longer rely on placeOrder()'s return value. Since that step needs the
|
|
|
|
|
* live Stripe PaymentIntent, not just the Order, onOrderPlaced() re-fetches
|
|
|
|
|
* it from Stripe via the StripePaymentIntent row this method already wrote
|
|
|
|
|
* (keyed by the order's cart_id) rather than carrying the PaymentIntent
|
|
|
|
|
* object across the event boundary itself.
|
2026-08-31 12:50:29 +03:00
|
|
|
*/
|
|
|
|
|
class StripePaymentDriver implements PaymentDriver
|
|
|
|
|
{
|
2026-08-31 14:12:21 +03:00
|
|
|
/**
|
|
|
|
|
* Same key lunarphp/stripe's own StripeManager reads its API key from
|
|
|
|
|
* (Stripe::setApiKey(config('services.stripe.key')) in
|
|
|
|
|
* StripeManager::__construct()) — no key, no usable driver.
|
|
|
|
|
*/
|
|
|
|
|
public function isConfigured(): bool
|
|
|
|
|
{
|
|
|
|
|
return filled(config('services.stripe.key'));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-31 12:50:29 +03:00
|
|
|
/**
|
|
|
|
|
* @throws PaymentNotConfirmedException if Stripe hasn't confirmed the
|
2026-09-02 16:14:52 +03:00
|
|
|
* payment intent (wrong intent id, already processed, or the gateway
|
|
|
|
|
* call itself fails) — nothing here should be treated as "confirm
|
|
|
|
|
* anyway."
|
2026-08-31 12:50:29 +03:00
|
|
|
*/
|
2026-09-02 16:14:52 +03:00
|
|
|
public function confirm(Cart $cart, string $type, string $fingerprint, array $data): void
|
2026-08-31 12:50:29 +03:00
|
|
|
{
|
|
|
|
|
$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}."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 16:14:52 +03:00
|
|
|
$paymentIntentModel->status = $paymentIntent->status;
|
|
|
|
|
$paymentIntentModel->save();
|
|
|
|
|
|
|
|
|
|
PaymentConfirmed::dispatch($cart, $type, $fingerprint, $data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registered in PaymentServiceProvider. Matches via the order's
|
|
|
|
|
* cart_id against the StripePaymentIntent row confirm() wrote, so a
|
|
|
|
|
* non-Stripe OrderPlaced (offline types fire the same event) is
|
|
|
|
|
* ignored rather than acted on.
|
|
|
|
|
*/
|
|
|
|
|
public function onOrderPlaced(OrderPlaced $event): void
|
|
|
|
|
{
|
|
|
|
|
$order = $event->order;
|
|
|
|
|
|
|
|
|
|
$paymentIntentModel = StripePaymentIntent::where('cart_id', $order->cart_id)->first();
|
|
|
|
|
|
|
|
|
|
if (! $paymentIntentModel) {
|
|
|
|
|
return;
|
2026-08-31 12:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$paymentIntentModel->order_id = $order->id;
|
|
|
|
|
$paymentIntentModel->processed_at = now();
|
|
|
|
|
$paymentIntentModel->save();
|
|
|
|
|
|
2026-09-02 16:14:52 +03:00
|
|
|
$paymentIntent = Stripe::getClient()->paymentIntents->retrieve($paymentIntentModel->intent_id);
|
2026-08-31 12:50:29 +03:00
|
|
|
|
2026-09-02 16:14:52 +03:00
|
|
|
UpdateOrderFromIntent::execute($order, $paymentIntent);
|
2026-08-31 12:50:29 +03:00
|
|
|
}
|
|
|
|
|
}
|