Repository navigation
[Payments]: Stripe Checkout Sessions gateway — create session, callback, record payment #651
Description
Port the v1 Stripe integration to v2: an embedded Stripe Checkout Session is created for a payable invoice, and a callback endpoint verifies the session, records a Payment, and logs the gateway transaction. v1 behavioral spec: application/modules/guest/controllers/gateways/Stripe.php (branch prep/v180).
Specific
Add stripe/stripe-php and implement two endpoints in Modules/Payments:
POST /pay/{urlKey}/stripe/session— creates a Checkout Session (mode=payment,ui_mode=embedded) for the invoice identified byurl_key, withclient_reference_id = url_key,unit_amount = invoice balance × 100, currency fromsettings.stripe_currency, product nameInvoice #<number>,return_url = <app>/pay/stripe/callback/{CHECKOUT_SESSION_ID}; returns{"clientSecret": ...}JSON (v1:Stripe.php:43-85).GET /pay/stripe/callback/{checkoutSessionId}— retrieves the session, and whenpayment_status === 'paid': dedup-checkspayment_intentagainstpayments.payment_external_id, rejects if invoice balance ≤ 0, rejects on currency mismatch vssettings.stripe_currency, rejects ifamount_total/100 + 0.0001 < balance, otherwise creates aPayment(date=today, amount=amount_total/100, method fromsettings.stripe_online_payment_method,payment_external_id = payment_intent). Every attempt — success, rejection, exception — writes aGatewayTransactionrecord ([Payments]: Online Payments Journal #144). Redirects back to the guest invoice view with a success/info/error flash (v1:Stripe.php:95-211).
Secret keysettings.api_keymust be stored encrypted (v1 usedcrypt->decode,Stripe.php:32); rename tosettings.stripe_api_key/settings.stripe_publishable_keyfor consistency with the PayPal keys.
Measurable
- Session-create endpoint returns a client secret for a payable, guest-visible invoice; 404 for unknown/hidden url_key; redirect with
invoice_already_paidwhen balance ≤ 0; 404 on non-POST. - Callback records exactly one
Paymentperpayment_intent(second callback with the same intent creates no payment and shows "already processed"). - Currency or amount mismatch creates no payment and logs a failed
GatewayTransaction. - A
GatewayTransactionrow exists after every callback invocation, including exception paths.
Achievable
- New service
Modules/Payments/Services/Gateways/StripeService.phpwrapping\Stripe\StripeClient(session create/retrieve only). - New controller
Modules/Payments/Http/Controllers/StripeController.php; routes inModules/Payments/routes/web.php(currently empty). - Migration adding
payment_external_id(string 255, nullable, unique) topayments— shared with the PayPal issue; see[Payments]: Guest online-payment page & shared gateway pipeline. - Depends on: shared-pipeline issue (guest invoice resolution by
url_key, validation helpers) and [Payments]: Online Payments Journal #144 (GatewayTransactionmodel).
Relevant
Stripe is one of only two active gateways in v1.8 (application/config/payment_gateways.php:445-454); v2 currently persists Stripe settings (Modules/Core/Filament/Admin/Pages/Settings.php:629-657) that nothing consumes.
Time-Bound
After the shared-pipeline issue and #144; same sprint as the PayPal gateway issue.
Arrange
Invoice::factory()->for($company)->create(['balance' => 100, 'url_key' => 'abc123']); settings seeded: stripe_enabled=1, stripe_currency=EUR, encrypted stripe_api_key; Stripe HTTP layer faked (session create returns client_secret, retrieve returns payment_status=paid, payment_intent=pi_123, currency=eur, amount_total=10000, client_reference_id=abc123).
Act
POST the session endpoint; then GET the callback with the fake session id; then GET the callback again with the same session id.
Assert
- Session response is JSON containing
clientSecret. - First callback creates a
paymentsrow withpayment_external_id = pi_123, amount 100.00, and the configured payment method. - Second callback creates no additional payment and flashes "already processed".
-
gateway_transactionshas one row per callback invocation with correct status. - Callback with
amount_total=5000(underpayment) orcurrency=usdcreates no payment and logs statusfailed. - GET session-create and any request for a non-guest-visible invoice return 404.
-
settings.stripe_api_keyis stored encrypted (raw DB value is not the plaintext key).
Activity
arjunkshah commentedon Jul 18, 2026
Payment-safety acceptance detail that is easy to miss: make the callback a server-authoritative state transition, not the browser success redirect. Persist a deterministic attempt record before calling Stripe, key idempotency on payment_intent plus invoice id, and treat paid-but-under/over/currency-mismatched sessions as rejected with a GatewayTransaction row and no Payment. Test a stale callback after the invoice is paid, duplicate callbacks with the same payment_intent, Stripe timeout after session creation, and a refresh of the callback URL. Also assert the session-create response never exposes the secret key and that the guest invoice cannot be paid after balance reaches zero. Those cases catch the revenue-risk failures before wiring the UI.