Skip to content

[Fix]: Can now switch subscriptions/ create checkout urls even when stripeAccountId is null with test mode#1519

Open
nams1570 wants to merge 6 commits into
devfrom
fix-payments-bugs
Open

[Fix]: Can now switch subscriptions/ create checkout urls even when stripeAccountId is null with test mode#1519
nams1570 wants to merge 6 commits into
devfrom
fix-payments-bugs

Conversation

@nams1570
Copy link
Copy Markdown
Collaborator

@nams1570 nams1570 commented May 29, 2026

Context

Remote/Local emulator allow one to set up payments configs without being provisioned stripe connected accounts. Previously, even if the user had test mode toggled, they couldn't create checkout urls or switch subscriptions. Now, they can thanks to the refactor.

Out of scope

The switch route still needs to be reworked for when we want to consider stripe-test mode or test mode - stripe switches. For now we should be good


Summary by cubic

Enable creating checkout URLs and switching subscriptions in test mode without a connected Stripe account (stripeAccountId can be null). Block switching a Stripe-backed subscription while the project is in test mode; cancel it or disable test mode first. Live mode still requires Stripe.

  • New Features

    • Create purchase URLs in test mode without Stripe onboarding; verification codes may include optional stripe_customer_id, stripe_account_id, and charges_enabled.
    • Allow plan switching in test mode by granting the target product directly when no Stripe subscription exists or when the existing subscription is DB-only.
    • Dashboard adds a dedicated test-mode bypass form and shows a “Payments not enabled” message in live mode when stripe_account_id is null.
  • Bug Fixes

    • Live purchase-session now rejects codes missing Stripe identifiers to prevent routing test-mode codes to the live endpoint.
    • Validation and OpenAPI updated: stripe_account_id and charges_enabled are nullable and no longer required in validate-code responses.
    • Explicitly prevent switching a Stripe-backed subscription while in test mode, with a clear error message.

Written for commit fe8433d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Payments test mode: create purchase URLs and complete purchases without Stripe onboarding; test-mode can grant products directly and allows plan switches when no live Stripe subscription exists; live-mode still enforces Stripe constraints.
  • UI

    • Checkout adds a test-mode bypass UI and clearer "Payments not enabled" handling.
  • Documentation

    • API schemas updated: some payment fields now nullable and response requirements adjusted.
  • Tests

    • Expanded e2e coverage for test vs live payment flows and switching behavior.

Review Change Stack

nams1570 added 2 commits May 27, 2026 17:26
This can happen now even when payments onboarding is incomplete/ they are using remote emulator.
Note that switching a stripe backed sub to a test sub or vice versa is still not possible rn. This is intentional, it will require some rethinking.
Copilot AI review requested due to automatic review settings May 29, 2026 01:31
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-auth-internal-tool Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-auth-mcp Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-auth-skills Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-backend Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-dashboard Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-demo Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-docs Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-preview-backend Ready Ready Preview, Comment May 29, 2026 8:58pm
stack-preview-dashboard Ready Ready Preview, Comment May 29, 2026 8:58pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5030384-fedd-49e6-9ddd-41c05d804766

📥 Commits

Reviewing files that changed from the base of the PR and between cf77318 and fe8433d.

📒 Files selected for processing (1)
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts

📝 Walkthrough

Walkthrough

Adds test-mode awareness across payments: Stripe initialization is conditional, verification codes and validate responses allow nullable Stripe fields, purchase-session validates Stripe identifiers, the dashboard shows a dedicated test-mode bypass UI, subscription switching short-circuits DB grants in test mode, and E2E tests exercise test vs live flows.

Changes

Test Mode Payment Flow

Layer / File(s) Summary
Verification Code Schema - Nullable Stripe Fields
apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx, apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts
stripeCustomerId, stripeAccountId, and chargesEnabled become optional in the verification code handler; validate-code response schema makes stripe_account_id and charges_enabled nullable and returns them via ?? null.
Purchase URL Creation - Test Mode Path
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
Gates Stripe initialization on testMode: when false, performs Stripe customer/account lookup/creation and reads chargesEnabled; when true, skips Stripe and passes undefined/null identifiers to verification code creation.
Purchase Session Guard for Stripe Identifiers
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx
Adds a pre-Stripe validation guard that throws a HexclaveAssertionError when validated purchase-code payload lacks required Stripe identifiers, directing test-mode codes to the internal test-mode session endpoint.
Subscription Switch - Test Mode Support
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts
Introduces testMode flag; immutability check for server-granted subscriptions is skipped in test mode; in test mode with no Stripe-backed subscription the endpoint grants the product via DB-only logic and returns success; switching away from live Stripe subscriptions is blocked in test mode.
Dashboard UI Components - Test Mode and Nullable Fields
apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx, apps/dashboard/src/components/payments/checkout.tsx
Adds TestModeBypassForm; updates CheckoutForm to accept chargesEnabled and isFree; makes ProductData.charges_enabled nullable; client rendering now shows test-mode bypass, a "Payments not enabled" message when stripe_account_id is null, or the Stripe checkout otherwise.
OpenAPI Documentation Updates
docs-mintlify/openapi/admin.json, docs-mintlify/openapi/client.json, docs-mintlify/openapi/server.json
Updates validate-code response schemas to reflect that stripe_account_id and charges_enabled are no longer required and may be null.
E2E Test Coverage - Test and Live Mode Scenarios
apps/e2e/tests/backend/endpoints/api/v1/payments/...
Renames/adjusts live-mode tests, adds test-mode purchase URL creation and switch tests, flips testMode flags in setups, routes internal test-mode purchase sessions to admin endpoints, and updates snapshots to expect nullable Stripe fields.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • BilalG1

"I'm a rabbit with a debug hat on,
Test-mode hops where Stripe is gone,
Codes turn nullable, UI sways,
Switches short-circuit in test days,
E2E proves both live and play." 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: enabling subscription switching and checkout URL creation in test mode when stripeAccountId is null.
Description check ✅ Passed The description provides clear context, explains the changes, and includes a detailed auto-generated summary of features and bug fixes. Required template sections are minimal but the provided description is substantive and informative.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-payments-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR allows payment checkout and subscription switching flows to work in test mode even when a project has no connected Stripe account, while preserving live-mode onboarding requirements.

Changes:

  • Skips Stripe account/customer lookup when creating test-mode purchase URLs and returns nullable Stripe fields from validation.
  • Adds a dashboard test-mode bypass path and live-mode “payments not enabled” state.
  • Adds backend switch-plan handling/tests for DB-only test-mode grants and updates OpenAPI docs.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts Makes Stripe setup optional for test-mode purchase URL creation.
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx Guards live purchase sessions against codes without Stripe identifiers.
apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx Allows purchase verification codes to omit Stripe metadata.
apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts Returns nullable Stripe metadata for validated purchase codes.
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts Adds test-mode DB grant path for switching non-Stripe subscriptions.
apps/dashboard/src/components/payments/checkout.tsx Extracts the test-mode bypass UI from the Stripe checkout form.
apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx Branches checkout rendering for test mode, missing Stripe account, and Stripe checkout.
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts Adds live/test-mode coverage for purchase URL creation without onboarding.
apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts Mirrors live-mode no-onboarding test updates for legacy naming.
apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts Adds switch-plan coverage for test-mode grants and live-mode rejection.
docs-mintlify/openapi/server.json Updates validate-code response required fields.
docs-mintlify/openapi/client.json Updates validate-code response required fields.
docs-mintlify/openapi/admin.json Updates validate-code response required fields.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs-mintlify/openapi/server.json
Comment thread docs-mintlify/openapi/client.json
Comment thread docs-mintlify/openapi/admin.json
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR enables creating purchase checkout URLs and switching subscriptions in test mode even when no Stripe connected account (stripeAccountId) is provisioned. It introduces a test-mode short-circuit in the switch route and conditionally skips Stripe customer/account setup in create-purchase-url.

  • Backend: create-purchase-url skips Stripe calls in test mode, leaving stripeCustomerId/stripeAccountId/chargesEnabled as undefined; purchase-session now guards against test-mode codes reaching it via an assertion error; the switch route adds a DB-only grantProductToCustomer path for test mode, and explicitly blocks switching a live Stripe-backed subscription while test mode is active.
  • Frontend: The CheckoutForm was refactored to extract a dedicated TestModeBypassForm component; page-client.tsx renders one of three branches: test-mode bypass, "Payments not enabled" (live mode, no Stripe), or the normal Stripe checkout.
  • Tests / Docs: E2E tests updated to explicitly set testMode: false when exercising live Stripe paths; old test snapshots updated to reflect nullable stripe_account_id and charges_enabled; OpenAPI required arrays updated accordingly.

Confidence Score: 5/5

Safe to merge. The test-mode and live-mode paths are cleanly separated; the switch route's mutually-exclusive condition branches are exhaustive and verified by the new E2E tests.

The core logic changes are well-structured, all live-mode behavior is unchanged, and the new E2E tests cover the four key scenarios. The one finding is a minor UI edge case affecting only products with no prices — an unusual configuration in practice.

No files require special attention.

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts Adds a test-mode short-circuit via grantProductToCustomer when no Stripe subscription exists; blocks switching a live Stripe-backed sub in test mode. Logic branches are mutually exclusive and exhaustive.
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts Conditionally skips Stripe customer creation in test mode; stripeCustomerId/stripeAccountId/chargesEnabled become undefined, stored as optional in the verification code.
apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx Adds an assertion-error guard rejecting null stripeAccountId/stripeCustomerId to prevent test-mode codes from reaching the live session path.
apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx Branches checkout render into three cases: test-mode bypass, 'Payments not enabled' (live + no stripeAccountId), and Stripe checkout. Missing a disabled guard for null selectedPriceId in the bypass form.
apps/dashboard/src/components/payments/checkout.tsx Extracts TestModeBypassForm component; removes onTestModeBypass prop from CheckoutForm. The async onClick handler on the bypass button still lacks error handling (previously flagged).
apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts Adds four new test cases covering test-mode switches (free-plan, existing grant, live-mode rejection) and updates existing tests to explicitly set testMode: false for live Stripe paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[create-purchase-url] --> B{testMode?}
    B -- Yes --> C[Skip Stripe\nstripeCustomerId/AccountId = undefined]
    B -- No --> D[Create Stripe customer\nFetch stripeAccountId & chargesEnabled]
    C --> E[Store verification code\nStripe fields optional]
    D --> E
    E --> F[validate-code]
    F --> G{testMode?}
    G -- Yes --> H[Return stripe_account_id: null\ncharges_enabled: null]
    G -- No --> I[Return stripe_account_id: string\ncharges_enabled: boolean]
    H --> J[UI: TestModeBypassForm]
    I --> K{stripe_account_id null?}
    K -- Yes --> L[UI: Payments not enabled]
    K -- No --> M[UI: Stripe CheckoutForm]
    J --> N[handleBypass -> /internal/test-mode-purchase-session]
    M --> O[purchase-session]
    N --> P{testMode active?}
    P -- No --> Q[403 Forbidden]
    P -- Yes --> R[grantProductToCustomer]
    O --> S{stripeAccountId/CustomerId null?}
    S -- Yes --> T[HexclaveAssertionError]
    S -- No --> U[Stripe payment flow]
    subgraph Switch Route
    V[switch/route] --> W{testMode?}
    W -- Yes, no Stripe sub --> X[grantProductToCustomer DB-only grant]
    W -- Yes, Stripe sub exists --> Y[400: Cannot switch live sub in test mode]
    W -- No --> Z[Stripe subscription update/create]
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx:397-400
The `disabled` prop on `TestModeBypassForm` doesn't guard against a null `selectedPriceId`. If a product is configured with no prices and a purchase URL is created for it in test mode, the button will be enabled — but `handleBypass` will send `price_id: null`, which fails the `yupString().defined()` validation on the backend (producing an unhandled error). Adding `!selectedPriceId` to the disable condition keeps the UX consistent with the live checkout path, which also requires a price to be selected before proceeding.

```suggestion
              <TestModeBypassForm
                onBypass={handleBypass}
                disabled={quantityNumber < 1 || isTooLarge || data.already_bought_non_stackable === true || !selectedPriceId}
              />
```

Reviews (2): Last reviewed commit: "chore: update tests" | Re-trigger Greptile

Comment thread apps/dashboard/src/components/payments/checkout.tsx
Copy link
Copy Markdown

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

In test mode, grantProductToCustomer fails when canceling a conflicting Stripe subscription because it calls getStripeForAccount which fails if Stripe is not configured

Fix on Vercel

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: 4

🧹 Nitpick comments (4)
apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts (1)

112-114: 💤 Low value

Escape customer_id when building the Stripe customers search query.

metadata['customerId']:'${req.body.customer_id}' interpolates customer_id directly into Stripe’s Search Query Language string; a value containing ' (and \) can break the query / enable query injection. Escape/sanitize the value before interpolation (e.g., replace \ with \\ and ' with \', or centralize this in a small helper).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts`
around lines 112 - 114, The Stripe customer search query interpolates
req.body.customer_id directly into the Search Query Language string in the
stripe.customers.search call (variable stripeCustomerSearch), which can break or
allow injection if the value contains backslashes or single quotes; before
building the query, sanitize/escape the customer_id (e.g., replace "\" with
"\\\\" and "'" with "\\'") or centralize this logic in a small helper
(escapeStripeSearchQuery or similar) and use that escaped value when
constructing `query: \`metadata['customerId']:'${...}'\`` so the generated query
string is safe.
docs-mintlify/openapi/server.json (1)

6744-6749: OpenAPI “required” update matches generator logic (nullable → non-required)

File: docs-mintlify/openapi/server.json (around lines 6744-6749)
The /payments/purchases/validate-code (POST, responses.200.content.application/json.schema) required list includes "test_mode" but excludes "stripe_account_id" and "charges_enabled".

This aligns with backend schema + generator behavior:

  • Backend: apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts
    • stripe_account_id: yupString().nullable().defined()
    • charges_enabled: yupBoolean().nullable().defined()
    • test_mode: yupBoolean().defined()
  • OpenAPI generator: apps/backend/src/lib/openapi.tsx marks a field as required only when it’s not optional and not nullable (required: !(field as any).optional && !(field as any).nullable && !!schema).

Still, ensure docs-mintlify/openapi/*.json was regenerated via the OpenAPI docs generation workflow so the committed artifact matches generator output (no manual JSON edits).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-mintlify/openapi/server.json` around lines 6744 - 6749, The OpenAPI
response schema for POST /payments/purchases/validate-code has `test_mode`
listed as required while `stripe_account_id` and `charges_enabled` are omitted
because the generator treats nullable fields as non-required; regenerate the
OpenAPI JSON artifacts (docs-mintlify/openapi/*.json) using the project's
OpenAPI docs generation workflow so the committed JSON matches the generator
output, and confirm the generator logic in openapi.tsx (the required: !(field as
any).optional && !(field as any).nullable && !!schema check) aligns with the
backend route schema in validate-code/route.ts (yupString().nullable().defined()
and yupBoolean().nullable().defined() vs yupBoolean().defined()) before
committing the regenerated files.
apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts (1)

320-329: 💤 Low value

Prefer inline snapshots for the success responses.

The status + body assertions on these switch responses can be collapsed into a single toMatchInlineSnapshot, which also captures any unexpected extra fields. The URL toMatch assertions elsewhere must stay as-is (random code), but these { success: true } responses are good snapshot candidates.

♻️ Example
-  expect(switchResponse.status).toBe(200);
-  expect(switchResponse.body).toEqual({ success: true });
+  expect(switchResponse).toMatchInlineSnapshot();

As per coding guidelines: "Prefer .toMatchInlineSnapshot over other selectors when writing tests".

Also applies to: 387-396

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts` around
lines 320 - 329, Replace the separate status and body assertions for the payment
plan switch response with a single inline snapshot assertion: capture the full
response object returned by niceBackendFetch (the switchResponse variable) using
expect(switchResponse).toMatchInlineSnapshot(...) so the test validates both
status and body (and any extra fields) in one assertion; do the same for the
other switch test that uses the same pattern (the other switchResponse usage
around the later test) and prefer toMatchInlineSnapshot over the split
expect(...status) and expect(...body) checks.
apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts (1)

174-209: 💤 Low value

Make test-mode reliance explicit.

This test asserts test-mode behavior but never sets payments.testMode, relying on the default being true. If that default ever changes, this test would silently start exercising live mode. Consider setting it explicitly to harden intent.

♻️ Suggested change
   await Project.updateConfig({
     payments: {
+      testMode: true,
       products: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts`
around lines 174 - 209, The test "should create purchase URL in test mode
without Stripe onboarding" relies on the default payments.testMode being true;
make the intent explicit by updating the Project.updateConfig call to include
payments.testMode: true so the test runs in test mode regardless of defaults —
modify the config object passed to Project.updateConfig (the same call that sets
payments.products) to include a payments.testMode = true flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/switch/route.ts:
- Around line 197-210: The test-mode short-circuit can leave the old
server-granted subscription (existingSub without stripeSubscriptionId) active
because the branch at route.ts does not cancel or update it before calling
grantProductToCustomer; inspect the grantProductToCustomer implementation and if
it does not already replace/cancel same-product-line subscriptions, modify this
branch to explicitly cancel or deactivate the prior existingSub (e.g., update
its status via Prisma or call the same internal cancel logic used for
Stripe-path updates) before calling grantProductToCustomer so the customer ends
up with a single active product; reference symbols: existingSub,
stripeSubscriptionId, grantProductToCustomer, and the test-mode branch in
route.ts.

In
`@apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx`:
- Around line 67-79: Replace the boolean coercions on Stripe IDs with explicit
nullish checks: change the guard in the route handler from `if
(!data.stripeAccountId || !data.stripeCustomerId)` to `if (data.stripeAccountId
== null || data.stripeCustomerId == null)` and update the diagnostic properties
`hasStripeAccountId` and `hasStripeCustomerId` to use `data.stripeAccountId !=
null` and `data.stripeCustomerId != null` respectively; keep the
HexclaveAssertionError construction (and other fields like tenancy.id,
data.customerId, tenancy.config.payments.testMode) unchanged.

In `@apps/dashboard/src/components/payments/checkout.tsx`:
- Around line 45-51: The TestModeBypassForm currently passes the
Promise-returning handler directly to the Button as onClick={onBypass}, so
rejections from handleBypass aren’t surfaced; import and use
runAsynchronouslyWithAlert to wrap the async handler and pass the wrapped
function to the Button (i.e., replace onClick={onBypass} with
onClick={runAsynchronouslyWithAlert(onBypass)}), ensuring the import for
runAsynchronouslyWithAlert is added and the symbols TestModeBypassForm, onBypass
and handleBypass are the targets for this change.

In `@docs-mintlify/openapi/admin.json`:
- Around line 6836-6841: The change touches a generated OpenAPI JSON (the
"required" array update) and must not be edited directly; update the source
schema generator (the Yup schema or OpenAPI metadata that defines the fields
like project_id, already_bought_non_stackable, conflicting_products, test_mode)
to mark the correct nullable/required settings, then run the “Regenerate OpenAPI
schemas” workflow to regenerate docs-mintlify/openapi/*.json so the generated
admin.json reflects the change; ensure the generator unit/metadata changes are
committed and the CI regeneration completes before pushing.

---

Nitpick comments:
In
`@apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts`:
- Around line 112-114: The Stripe customer search query interpolates
req.body.customer_id directly into the Search Query Language string in the
stripe.customers.search call (variable stripeCustomerSearch), which can break or
allow injection if the value contains backslashes or single quotes; before
building the query, sanitize/escape the customer_id (e.g., replace "\" with
"\\\\" and "'" with "\\'") or centralize this logic in a small helper
(escapeStripeSearchQuery or similar) and use that escaped value when
constructing `query: \`metadata['customerId']:'${...}'\`` so the generated query
string is safe.

In
`@apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts`:
- Around line 174-209: The test "should create purchase URL in test mode without
Stripe onboarding" relies on the default payments.testMode being true; make the
intent explicit by updating the Project.updateConfig call to include
payments.testMode: true so the test runs in test mode regardless of defaults —
modify the config object passed to Project.updateConfig (the same call that sets
payments.products) to include a payments.testMode = true flag.

In `@apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts`:
- Around line 320-329: Replace the separate status and body assertions for the
payment plan switch response with a single inline snapshot assertion: capture
the full response object returned by niceBackendFetch (the switchResponse
variable) using expect(switchResponse).toMatchInlineSnapshot(...) so the test
validates both status and body (and any extra fields) in one assertion; do the
same for the other switch test that uses the same pattern (the other
switchResponse usage around the later test) and prefer toMatchInlineSnapshot
over the split expect(...status) and expect(...body) checks.

In `@docs-mintlify/openapi/server.json`:
- Around line 6744-6749: The OpenAPI response schema for POST
/payments/purchases/validate-code has `test_mode` listed as required while
`stripe_account_id` and `charges_enabled` are omitted because the generator
treats nullable fields as non-required; regenerate the OpenAPI JSON artifacts
(docs-mintlify/openapi/*.json) using the project's OpenAPI docs generation
workflow so the committed JSON matches the generator output, and confirm the
generator logic in openapi.tsx (the required: !(field as any).optional &&
!(field as any).nullable && !!schema check) aligns with the backend route schema
in validate-code/route.ts (yupString().nullable().defined() and
yupBoolean().nullable().defined() vs yupBoolean().defined()) before committing
the regenerated files.
🪄 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: 006d2147-209e-46b4-a330-64a51d13a431

📥 Commits

Reviewing files that changed from the base of the PR and between bd61184 and 5b5bf07.

📒 Files selected for processing (13)
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts
  • apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts
  • apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx
  • apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts
  • apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx
  • apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx
  • apps/dashboard/src/components/payments/checkout.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--create-purchase-url.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/payments/switch-plans.test.ts
  • docs-mintlify/openapi/admin.json
  • docs-mintlify/openapi/client.json
  • docs-mintlify/openapi/server.json

Comment thread apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx Outdated
Comment thread apps/dashboard/src/components/payments/checkout.tsx
Comment thread docs-mintlify/openapi/admin.json
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 13 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread docs-mintlify/openapi/admin.json
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.

🧹 Nitpick comments (1)
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts (1)

212-219: 💤 Low value

Guard logic is correct; prefer explicit null checks per guidelines.

The new block is reachable and exhaustive: line 197 already returns for the !existingSub || !existingSub.stripeSubscriptionId test-mode case, so this complementary guard correctly blocks the live-Stripe-sub-in-test-mode path before falling into the Stripe flow at Line 221. StatusError(400) is appropriate here (user-actionable, no internal info leaked).

One nit on Line 213: the condition uses boolean truthiness rather than explicit null checks.

♻️ Use explicit null/undefinedness checks
-    if (testMode && existingSub && existingSub.stripeSubscriptionId) {
+    if (testMode && existingSub != null && existingSub.stripeSubscriptionId != null) {

As per coding guidelines: "Prefer explicit null/undefinedness checks over boolean checks (e.g., foo == null instead of !foo)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/switch/route.ts
around lines 212 - 219, Change the boolean truthiness checks in the guard to
explicit null/undefined checks: replace the condition that uses "existingSub"
and "existingSub.stripeSubscriptionId" with explicit null checks (e.g.,
existingSub != null and existingSub.stripeSubscriptionId != null) so the block
reads testMode && existingSub != null && existingSub.stripeSubscriptionId !=
null; keep the thrown StatusError(400) and message unchanged to preserve
behavior in route.ts where existingSub, stripeSubscriptionId, testMode and
StatusError are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/switch/route.ts:
- Around line 212-219: Change the boolean truthiness checks in the guard to
explicit null/undefined checks: replace the condition that uses "existingSub"
and "existingSub.stripeSubscriptionId" with explicit null checks (e.g.,
existingSub != null and existingSub.stripeSubscriptionId != null) so the block
reads testMode && existingSub != null && existingSub.stripeSubscriptionId !=
null; keep the thrown StatusError(400) and message unchanged to preserve
behavior in route.ts where existingSub, stripeSubscriptionId, testMode and
StatusError are used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0250772-575e-41db-87bd-e2fd1acacefd

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5bf07 and 89361f3.

📒 Files selected for processing (2)
  • apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts
  • apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx

These routes are now accessible in test mode so tests need to be updated to be more explicit.
@nams1570
Copy link
Copy Markdown
Collaborator Author

@greptileai please rereview

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 18 files

Re-trigger cubic

Comment on lines +67 to +68
if (data.stripeAccountId == null || data.stripeCustomerId == null) {
throw new HexclaveAssertionError(
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 May 29, 2026

Choose a reason for hiding this comment

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

should have an e2e test for this

Comment on lines +197 to +198
if (testMode && (existingSub == null || existingSub.stripeSubscriptionId == null)) {
await grantProductToCustomer({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In test mode, cancelling the old sub relies on the TimeFold-lagged ownedProducts read instead of the existingSub we already have, so two quick switches can leave two active subs in one line — cancel existingSub directly here.

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.

3 participants