Skip to content

[Payments]: Stripe Checkout Sessions gateway — create session, callback, record payment #651

Open

Description

@nielsdrost7

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:

  1. POST /pay/{urlKey}/stripe/session — creates a Checkout Session (mode=payment, ui_mode=embedded) for the invoice identified by url_key, with client_reference_id = url_key, unit_amount = invoice balance × 100, currency from settings.stripe_currency, product name Invoice #<number>, return_url = <app>/pay/stripe/callback/{CHECKOUT_SESSION_ID}; returns {"clientSecret": ...} JSON (v1: Stripe.php:43-85).
  2. GET /pay/stripe/callback/{checkoutSessionId} — retrieves the session, and when payment_status === 'paid': dedup-checks payment_intent against payments.payment_external_id, rejects if invoice balance ≤ 0, rejects on currency mismatch vs settings.stripe_currency, rejects if amount_total/100 + 0.0001 < balance, otherwise creates a Payment (date=today, amount=amount_total/100, method from settings.stripe_online_payment_method, payment_external_id = payment_intent). Every attempt — success, rejection, exception — writes a GatewayTransaction record ([Payments]: Online Payments Journal #144). Redirects back to the guest invoice view with a success/info/error flash (v1: Stripe.php:95-211).
    Secret key settings.api_key must be stored encrypted (v1 used crypt->decode, Stripe.php:32); rename to settings.stripe_api_key/settings.stripe_publishable_key for 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_paid when balance ≤ 0; 404 on non-POST.
  • Callback records exactly one Payment per payment_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 GatewayTransaction row exists after every callback invocation, including exception paths.

Achievable

  • New service Modules/Payments/Services/Gateways/StripeService.php wrapping \Stripe\StripeClient (session create/retrieve only).
  • New controller Modules/Payments/Http/Controllers/StripeController.php; routes in Modules/Payments/routes/web.php (currently empty).
  • Migration adding payment_external_id (string 255, nullable, unique) to payments — 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 (GatewayTransaction model).

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 payments row with payment_external_id = pi_123, amount 100.00, and the configured payment method.
  • Second callback creates no additional payment and flashes "already processed".
  • gateway_transactions has one row per callback invocation with correct status.
  • Callback with amount_total=5000 (underpayment) or currency=usd creates no payment and logs status failed.
  • GET session-create and any request for a non-guest-visible invoice return 404.
  • settings.stripe_api_key is stored encrypted (raw DB value is not the plaintext key).

Activity

  1. arjunkshah commented on Jul 18, 2026

    @arjunkshah

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions