Skip to content

[codex] Support Stripe emulator credit top-ups#1882

Open
riderx wants to merge 6 commits intomainfrom
codex/stripe-emulator-credits-flow
Open

[codex] Support Stripe emulator credit top-ups#1882
riderx wants to merge 6 commits intomainfrom
codex/stripe-emulator-credits-flow

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 1, 2026

Summary (AI generated)

  • add a local Stripe emulator launcher and backend Playwright launcher so Capgo can run credit checkout flows against vercel-labs/emulate
  • teach the backend credit top-up flow to work with the emulator by using configurable Stripe API hosts, list-based price lookup, metadata-based checkout recovery, and emulator-compatible checkout session resolution
  • add a headless Playwright E2E that buys credits through the emulator checkout UI and verifies the resulting purchase transaction

Motivation (AI generated)

Capgo needs a reliable local test path for Stripe-backed credit purchases without depending on live Stripe infrastructure. The upstream emulator covers enough of the one-time checkout path to validate credit purchases, but Capgo needed compatibility work around session lookup, line-item recovery, local function env wiring, and end-to-end test orchestration.

Business Impact (AI generated)

This lowers the cost and friction of validating billing-critical credit purchases, reduces regressions in checkout completion, and gives the team a repeatable local flow for testing a user buying credits from start to finish. That improves confidence in a revenue path without waiting on live Stripe or incomplete subscription emulation support.

Test Plan (AI generated)

  • bun lint
  • bun lint:backend
  • bun typecheck
  • bunx vitest run tests/stripe-redirects.unit.test.ts
  • SKIP_BACKEND_START=1 SKIP_STRIPE_EMULATOR_START=1 bunx playwright test playwright/e2e/credits-top-up.spec.ts --project=chromium

Generated with AI

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved credit checkout session validation and fallback handling when session ID is unavailable or invalid.
    • Enhanced Stripe integration to support both live and test environments with automatic fallback mechanisms.
  • Tests

    • Added comprehensive E2E test coverage for credit top-up and subscription checkout workflows using Stripe test emulator.
    • Expanded test infrastructure with improved session handling and local Supabase integration support.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces Stripe emulator infrastructure for E2E testing and enhances the credit top-up flow to support optional session resolution. New npm scripts and vendor packages enable Playwright to run with a local Stripe emulator, while backend utilities and endpoints are updated to handle missing session IDs by inferring them from recent Stripe customer checkout data.

Changes

Cohort / File(s) Summary
Stripe Emulator Infrastructure
scripts/vendor/emulate-core/..., scripts/vendor/emulate-stripe/..., scripts/serve-stripe-emulator.ts, package.json
Added vendor shims re-exporting @emulators/core and @emulators/stripe from emulate package, a new stripe:emulator npm script to spawn the emulator server on configurable port, and dev dependencies (@emulators/core, @emulators/stripe, emulate, @hono/node-server).
Playwright Configuration & Backend
playwright.config.ts, scripts/serve-backend-playwright.ts, scripts/playwright-stripe.ts
Forced headless mode in Playwright, added conditional Stripe emulator webServer startup with skippable control, updated backend command to inject Stripe/Webapp environment variables and use Supabase functions "ok" endpoint for readiness checking. Added helper functions to parse emulator port and construct Stripe API base URL.
E2E Tests
playwright/e2e/credits-top-up.spec.ts, playwright/e2e/subscription-checkout.spec.ts
Added comprehensive E2E tests exercising credit top-up and subscription plan upgrade flows against local Stripe emulator, including Supabase provisioning, payment completion, and transaction verification.
Backend Stripe Utilities
supabase/functions/_backend/utils/stripe.ts
Added URL validation for STRIPE_API_BASE_URL, emulator detection, dynamic Stripe client configuration, price listing fallback with database lookup, and new getCreditCheckoutDetails() helper with emulator metadata fallback path.
Backend Credits Handling
supabase/functions/_backend/private/credits.ts, supabase/functions/_backend/triggers/stripe_event.ts
Made sessionId optional in credit completion request, added resolveCheckoutSession() logic to derive missing session IDs from recent customer checkout history, introduced hasProcessedCreditTopUp() idempotency check, and refactored line-item aggregation to use centralized helper.
Frontend Credits & Plans UI
src/pages/settings/organization/Credits.vue, src/pages/settings/organization/Plans.vue, src/services/stripe.ts
Updated credits form to accept optional/null sessionId, refined session ID regex pattern to allow hyphens, added data-test attributes for E2E test selectors on form inputs and plan cards.
Test Utilities
tests/test-utils.ts, tests/stripe-redirects.unit.test.ts
Enhanced test infrastructure with local Supabase status inspection and environment hydration helpers, refactored Stripe mock setup, and added emulator-specific test cases for client configuration, checkout fallback behavior, and metadata-driven price resolution.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Playwright as Playwright Client
    participant Frontend as Frontend (Vue)
    participant Backend as Backend<br/>(Supabase Functions)
    participant Stripe as Stripe Emulator
    participant DB as Supabase DB
    
    User->>Playwright: 1. Test initiates<br/>Setup org/app/customer
    Playwright->>Stripe: 2. Create customer,<br/>product, price
    Playwright->>DB: 3. Insert plan record<br/>with stripe_id
    Playwright->>Frontend: 4. Load /settings/credits
    User->>Frontend: 5. Select $50 top-up<br/>Submit form
    Frontend->>Backend: 6. POST /checkout<br/>(orgId, quantity)
    Backend->>Stripe: 7. Create checkout<br/>session
    Stripe-->>Frontend: 8. Redirect to<br/>checkout URL
    User->>Stripe: 9. Click "Pay &<br/>Complete"
    Stripe-->>Frontend: 10. Redirect to<br/>credits page
    Frontend->>Backend: 11. POST /complete-top-up<br/>(orgId, sessionId)
    Backend->>Stripe: 12. Retrieve session<br/>details
    Backend->>DB: 13. Query recent<br/>sessions (if needed)
    Backend->>DB: 14. Check idempotency<br/>(hasProcessedCreditTopUp)
    Backend->>DB: 15. Grant credits via<br/>RPC top_up_usage_credits
    Backend->>DB: 16. Insert transaction<br/>record
    Backend-->>Frontend: 17. Success response
    Frontend->>User: 18. Display success<br/>toast
Loading
sequenceDiagram
    actor Playwright as Playwright Test
    participant Backend as Backend<br/>(Supabase Functions)
    participant Stripe as Stripe Emulator
    participant DB as Supabase DB
    
    Playwright->>Stripe: 1. Create session via<br/>emulator POST
    Playwright->>DB: 2. Await session<br/>list completion
    Playwright->>Backend: 3. POST /complete-top-up<br/>(orgId only,<br/>no sessionId)
    Backend->>Stripe: 4. List customer<br/>checkout sessions
    Backend->>DB: 5. Filter completed<br/>sessions by client_ref_id
    Backend->>DB: 6. Exclude already-<br/>processed transactions
    alt Session Found
        Backend->>Stripe: 7a. Retrieve session<br/>line items
    else Emulator Fallback
        Backend->>DB: 7b. Use session.metadata<br/>(productId, quantity)
    end
    Backend->>DB: 8. Grant credits &<br/>record transaction
    Backend-->>Playwright: 9. Success with<br/>resolved sessionId
    Playwright->>DB: 10. Poll usage_credit_<br/>transactions for<br/>latest purchase
    Playwright->>Playwright: 11. Assert amount=50<br/>& sessionId prefix
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

DO NOT MERGE

Poem

🐰 Hoppy hops through emulator lands,
Stripe coins dancing in test commands,
Playwright weaves the credits dream,
Sessions flowing like a stream—
"Charge complete!" the bunny cries,
With E2E tests in the skies! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete against the template; it lacks a formal 'Summary' section, omits the structured 'Test plan' section with specific reproduction steps, provides no screenshots, and the checklist is missing. Reformat the description to follow the template structure: add a 'Summary' section, provide detailed 'Test plan' with reproduction steps, add a 'Screenshots' section (or note N/A), and include the completed checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding Stripe emulator support for credit top-ups, which aligns with the core objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/stripe-emulator-credits-flow

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/stripe-emulator-credits-flow (f765e65) with main (d2f80aa)

Open in CodSpeed

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 1, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​hono/​node-server@​1.19.1210010010095100

View full report

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 1, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3d153e73f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/stripe-redirects.unit.test.ts (1)

4-13: Keep env/mock state local to each case.

mockedEnv plus the file-wide Stripe constructor mutations make the new tests depend on shared global state. That blocks the repo’s it.concurrent() convention and makes future cases easy to leak across one another; a per-test env factory / module reset would keep these parallelizable.

As per coding guidelines, tests/**/*.{test,spec}.{ts,js}: Use it.concurrent() instead of it() in test files to maximize test parallelism.

Also applies to: 43-45, 195-196, 218-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/stripe-redirects.unit.test.ts` around lines 4 - 13, The test currently
uses a file-scoped mockedEnv and a global Stripe constructor mutation
(mockedEnv, vi.mock('hono/adapter'), and the Stripe constructor changes) which
creates shared state between tests; change to create the env and mocks inside
each test (or beforeEach) and call vi.resetModules()/vi.clearAllMocks() between
tests so each it.concurrent() gets a fresh module graph; also convert tests to
use it.concurrent() instead of it() per the test guidelines so cases run in
parallel without leaking the mockedEnv/Stripe mutations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/serve-backend-playwright.ts`:
- Around line 10-12: The defaultStripeBaseUrl fallback uses 127.0.0.1 which
diverges from playwright.config.ts’s default host; update
scripts/serve-backend-playwright.ts so defaultStripeBaseUrl is derived from the
same shared helper or constant used by the Playwright launcher (or change the
fallback to host.docker.internal) and ensure stripeApiBaseUrl continues to
respect env.STRIPE_API_BASE_URL; locate the symbol defaultStripeBaseUrl (and
stripeApiBaseUrl) and replace its hardcoded fallback with the shared
helper/constant or the identical 'host.docker.internal' default so both
auto-start and SKIP_BACKEND_START=1 paths use the same network topology.

In `@supabase/functions/_backend/private/credits.ts`:
- Around line 177-198: The current fallback picks the newest completed session
(latestCompletedPaymentSession) without ensuring it hasn't already been
processed; change the logic to scan recent sessions.data (filtered by customer,
mode === 'payment', payment_status === 'paid', status === 'complete', and
matching orgId via session.client_reference_id or session.metadata?.orgId), sort
by created desc, then iterate and return the first candidate that is confirmed
unprocessed (use or add an idempotency/processing check function to detect if
that session's grant/top-up was already applied). If you find more than one
unprocessed candidate, throw a simpleError('multiple_unprocessed_sessions', ...)
instead of returning the newest; if none are unprocessed, throw
simpleError('session_not_found', ...). Ensure you replace the single
latestCompletedPaymentSession selection with this scan-and-validate flow.

---

Nitpick comments:
In `@tests/stripe-redirects.unit.test.ts`:
- Around line 4-13: The test currently uses a file-scoped mockedEnv and a global
Stripe constructor mutation (mockedEnv, vi.mock('hono/adapter'), and the Stripe
constructor changes) which creates shared state between tests; change to create
the env and mocks inside each test (or beforeEach) and call
vi.resetModules()/vi.clearAllMocks() between tests so each it.concurrent() gets
a fresh module graph; also convert tests to use it.concurrent() instead of it()
per the test guidelines so cases run in parallel without leaking the
mockedEnv/Stripe mutations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8973a5bf-460e-40f4-b23f-ab277be98f0e

📥 Commits

Reviewing files that changed from the base of the PR and between 9236ab8 and c3d153e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • package.json
  • playwright.config.ts
  • playwright/e2e/credits-top-up.spec.ts
  • scripts/serve-backend-playwright.ts
  • scripts/serve-stripe-emulator.ts
  • scripts/vendor/emulate-core/index.ts
  • scripts/vendor/emulate-core/package.json
  • scripts/vendor/emulate-stripe/index.ts
  • scripts/vendor/emulate-stripe/package.json
  • src/pages/settings/organization/Credits.vue
  • src/services/stripe.ts
  • supabase/functions/_backend/private/credits.ts
  • supabase/functions/_backend/triggers/stripe_event.ts
  • supabase/functions/_backend/utils/stripe.ts
  • tests/stripe-redirects.unit.test.ts
  • tests/test-utils.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1450f0445a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7d9ae9f83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@playwright/e2e/credits-top-up.spec.ts`:
- Around line 1-5: The import ordering fails linting because the type import
"SupabaseClient" from "@supabase/supabase-js" should appear before the builtin
import "randomUUID" from 'node:crypto'; reorder the imports so the type import
line "import type { SupabaseClient } from '@supabase/supabase-js'" (and the
"Database" type import if applicable) comes above the builtin import "import {
randomUUID } from 'node:crypto'", preserving the rest of the existing imports
(getSupabaseClient, resetAndSeedAppData, resetAppData, USER_ID, USER_PASSWORD,
and the test helpers) and keeping them in their current relative order.
- Around line 39-53: The afterEach cleanup currently swallows delete failures
and deletes orgs before any billing rows that may block the delete; update the
test.afterEach block so it first deletes dependent billing rows
(supabase.from('stripe_info').delete().eq('customer_id', customerId) and any
billing/grant/purchase tables you added) and then deletes org_users and orgs,
ensure each await is awaited and failures are not swallowed (either let
exceptions propagate or catch and rethrow/log) and keep calling
resetAppData(appId) as needed; reference the existing symbols test.afterEach,
resetAppData, supabase.from('org_users'), supabase.from('orgs'), 'stripe_info'
(customerId), and 'plans' (planStripeId) when making the ordering and
error-handling changes.

In `@playwright/e2e/subscription-checkout.spec.ts`:
- Around line 1-5: The import order fails perfectionist/sort-imports; move the
type import "import type { SupabaseClient } from '@supabase/supabase-js'" to
come before the builtin import "import { randomUUID } from 'node:crypto'".
Update the top of subscription-checkout.spec.ts so the sequence is:
module-scoped type imports (SupabaseClient, Database), then builtin/node imports
(randomUUID), then project/test imports (getSupabaseClient, resetAndSeedAppData,
resetAppData, USER_ID, USER_PASSWORD), and finally test helpers (expect, test)
to satisfy the linter.

In `@scripts/playwright-stripe.ts`:
- Around line 1-2: getStripeEmulatorPort currently uses Number.parseInt and can
return NaN for invalid env values; update the function to parse the value,
validate with Number.isNaN, and throw a clear error (matching the pattern used
in serve-stripe-emulator.ts) when STRIPE_EMULATOR_PORT is not a valid integer so
callers like webServer.port and getPlaywrightStripeApiBaseUrl receive a valid
port; locate getStripeEmulatorPort and replace the single-line return with logic
that reads processEnv.STRIPE_EMULATOR_PORT (or default '4510'), parses to a
number, checks Number.isNaN(parsed), and throws an informative Error if invalid,
otherwise returns the parsed port.

In `@scripts/serve-backend-playwright.ts`:
- Around line 57-71: The process hangs because our custom handlers installed by
forwardSignal (which calls process.on(signal, ...)) intercept the re-sent
signal; fix by removing those handlers before re-sending the child's signal. In
the child.on('exit') block where you detect a non-null signal, call
process.off/process.removeListener (or removeAllListeners) for the same signals
registered by forwardSignal (e.g., 'SIGINT' and 'SIGTERM') to unregister the
handlers that call child.kill, then call process.kill(process.pid, signal) to
allow default termination; ensure forwardSignal still installs handlers via
process.on so removal targets the same listeners.

In `@supabase/functions/_backend/private/credits.ts`:
- Around line 161-185: The fallback idempotency check in hasProcessedCreditTopUp
currently only checks source_ref.sessionId, causing paymentIntent-based ledger
deduplication to be missed; update hasProcessedCreditTopUp to accept an optional
paymentIntent argument and modify the Supabase query to match rows where
source_ref contains either { sessionId } OR { paymentIntent } (so the .contains
filter checks both keys or use an OR condition), and update callers (notably
resolveCheckoutSession when iterating candidate sessions) to pass the
candidate's payment_intent into hasProcessedCreditTopUp; apply the same change
for the duplicate logic referenced around the other occurrence (lines ~223-233).

In `@tests/test-utils.ts`:
- Around line 60-108: The module currently hydrates SUPABASE_URL and keys but
leaves the module-level POSTGRES_URL frozen at import time, so functions in
scripts/supabase-worktree.ts like getPostgresClient() and executeSQL() continue
to use the old DB (port 54322); change the module to derive the Postgres
connection string lazily by removing the module-level POSTGRES_URL constant and
instead call hydrateLocalSupabaseEnvFromStatus() (or ensure it has run) and
compute the Postgres URL inside getPostgresClient() (or a small helper used by
getPostgresClient()/executeSQL()), updating any references from POSTGRES_URL to
the new on-demand getter so the DB_URL used matches the hydrated SUPABASE_URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a04ded7-c233-4d2f-98cd-f93a7163825a

📥 Commits

Reviewing files that changed from the base of the PR and between 1450f04 and 14d2d74.

📒 Files selected for processing (11)
  • playwright.config.ts
  • playwright/e2e/credits-top-up.spec.ts
  • playwright/e2e/subscription-checkout.spec.ts
  • scripts/playwright-stripe.ts
  • scripts/serve-backend-playwright.ts
  • scripts/supabase-worktree.ts
  • src/pages/settings/organization/Plans.vue
  • supabase/functions/_backend/private/credits.ts
  • supabase/functions/_backend/utils/stripe.ts
  • tests/stripe-redirects.unit.test.ts
  • tests/test-utils.ts
✅ Files skipped from review due to trivial changes (1)
  • src/pages/settings/organization/Plans.vue
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/stripe-redirects.unit.test.ts
  • supabase/functions/_backend/utils/stripe.ts

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant