Skip to content

feat(api): implement subscription management portal endpoints#511

Open
ahmednahima0-beep wants to merge 12 commits intotestfrom
feat/subscriptions-portal-post
Open

feat(api): implement subscription management portal endpoints#511
ahmednahima0-beep wants to merge 12 commits intotestfrom
feat/subscriptions-portal-post

Conversation

@ahmednahima0-beep
Copy link
Copy Markdown
Collaborator

@ahmednahima0-beep ahmednahima0-beep commented May 4, 2026

  • Added a new route for managing subscription portals, including OPTIONS and POST handlers.
  • The OPTIONS handler responds with CORS headers for preflight requests.
  • The POST handler creates a subscription management session using Stripe, validating the request and returning the session ID and URL.
  • Introduced validation for incoming requests to ensure proper structure and authentication.
  • Added tests for both OPTIONS and POST handlers to verify functionality and error handling.
  • Implemented utility functions for creating billing portal sessions and validating requests.

Files added:

  • app/api/subscriptions/portal/route.ts
  • lib/stripe/createSubscriptionPortalHandler.ts
  • lib/stripe/validateCreateSubscriptionPortalRequest.ts
  • lib/stripe/createBillingPortalSession.ts
  • lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
  • Tests for the new functionality in tests directory.

This commit enhances the subscription management capabilities of the API, allowing users to manage their subscriptions effectively.


Summary by cubic

Adds a new /api/subscriptions/portal endpoint to create Stripe billing portal sessions and return { id, url }. Handles CORS preflight, strict JSON body and auth validation, checks for an active subscription, and disables caching.

  • New Features

    • OPTIONS returns 200 with CORS headers.
    • POST validates { returnUrl }, uses auth-derived account, checks subscription via getActiveSubscriptionDetails, creates the portal session, and returns { id, url }; returns 400 when no subscription or session URL, 500 on internal errors.
  • Refactors

    • Replaced request validator with validateCreateSubscriptionPortalBody; accountId now always comes from auth.
    • Switched from deprecated selectBillingCustomers to getActiveSubscriptionDetails and removed the old helper.

Written for commit 9e75112. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Users can now access a dedicated subscription management portal to view, update, and manage their subscription status, billing information, and payment methods.
    • Implemented secure authentication and comprehensive request validation to protect user data during portal access.
    • Added robust error handling to ensure reliable portal operation.

- Added a new route for managing subscription portals, including OPTIONS and POST handlers.
- The OPTIONS handler responds with CORS headers for preflight requests.
- The POST handler creates a subscription management session using Stripe, validating the request and returning the session ID and URL.
- Introduced validation for incoming requests to ensure proper structure and authentication.
- Added tests for both OPTIONS and POST handlers to verify functionality and error handling.
- Implemented utility functions for creating billing portal sessions and validating requests.

Files added:
- app/api/subscriptions/portal/route.ts
- lib/stripe/createSubscriptionPortalHandler.ts
- lib/stripe/validateCreateSubscriptionPortalRequest.ts
- lib/stripe/createBillingPortalSession.ts
- lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
- Tests for the new functionality in __tests__ directory.

This commit enhances the subscription management capabilities of the API, allowing users to manage their subscriptions effectively.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview May 6, 2026 7:00pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Adds a new subscription billing portal endpoint with CORS support. Includes route handlers for OPTIONS and POST requests, request body validation via Zod schema, integration with Stripe's billing portal API, and authentication context validation. All configured for dynamic execution with no caching.

Changes

Subscription Portal Endpoint

Layer / File(s) Summary
Request Validation & Types
lib/stripe/validateCreateSubscriptionPortalBody.ts
Defines Zod schema for portal request body (returnUrl required and URL-validated), exports validated response type, and implements validator function that parses JSON, validates schema, and extracts accountId from auth context.
Stripe Integration
lib/stripe/createBillingPortalSession.ts
Wraps Stripe's billingPortal.sessions.create API to create a portal session with given customer ID and return URL.
Handler Orchestration
lib/stripe/createSubscriptionPortalHandler.ts
Orchestrates the flow: validates request body, fetches active subscription via accountId, creates billing portal session, and returns portal URL or error responses with CORS headers.
Route & API Layer
app/api/subscriptions/portal/route.ts
Exports OPTIONS handler for CORS preflight and POST handler that delegates to createSubscriptionPortalHandler. Configured for forced dynamic execution with no fetch caching.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as Portal Route
    participant Validator as Request Validator
    participant Handler as Portal Handler
    participant Auth as Auth Context
    participant DB as Subscription DB
    participant Stripe as Stripe API
    
    Client->>Route: POST /api/subscriptions/portal + returnUrl
    Route->>Handler: createSubscriptionPortalHandler(request)
    Handler->>Validator: validateCreateSubscriptionPortalBody(request)
    Validator->>Validator: Parse JSON & validate schema
    Validator->>Auth: validateAuthContext(request)
    Auth-->>Validator: accountId (or error)
    Validator-->>Handler: { accountId, returnUrl }
    Handler->>DB: getActiveSubscriptionDetails(accountId)
    DB-->>Handler: { customer: customerId, ... }
    Handler->>Stripe: createBillingPortalSession(customerId, returnUrl)
    Stripe-->>Handler: { id, url, ... }
    Handler-->>Route: 200 { id, url } + CORS headers
    Route-->>Client: Portal session created
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • recoupable/api#506: Introduces getActiveSubscriptionDetails which is called by the handler in this PR to fetch subscription context before creating the portal session.

Suggested reviewers

  • sweetmantech

🌟 A portal takes shape, with validation's grace,
Stripe sessions bloom in their rightful place,
CORS preflight waves, auth guards stand tall,
Billing flows smoothly—one endpoint for all! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning DRY violation: JSON parsing logic duplicated in validators. SRP issue: validateCreateSubscriptionPortalBody bundles schema, types, and validator together, unlike the session pattern. Extract shared JSON parsing utility. Move schema to separate file to align with existing session pattern.
✅ Passed checks (2 passed)
Check name Status Explanation
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 feat/subscriptions-portal-post

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@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.

4 issues found across 13 files

Confidence score: 3/5

  • There is a concrete regression risk in lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts: returning null on Supabase errors can misclassify database failures as “not found” and surface a 400 instead of a 500 to clients.
  • The remaining findings are mostly maintainability/test-quality concerns (over-100-line test files in app/api/subscriptions/portal/__tests__/route.post.outcomes.test.ts and lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.test.ts), which are less likely to break runtime behavior.
  • Test coverage signal is a bit weak because app/api/subscriptions/portal/__tests__/route.test.ts validates exports only and does not verify API behavior added by this PR.
  • Pay close attention to lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts, app/api/subscriptions/portal/__tests__/route.test.ts - error-path handling should throw on DB failures, and route tests should validate real API outcomes.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts">

<violation number="1" location="lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts:19">
P1: Do not return `null` on Supabase errors here; it makes DB failures look like "Billing customer not found" (400). Throw instead so the handler returns a 500.</violation>
</file>

<file name="app/api/subscriptions/portal/__tests__/route.post.outcomes.test.ts">

<violation number="1" location="app/api/subscriptions/portal/__tests__/route.post.outcomes.test.ts:12">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**

Test file exceeds the repository's 100-line limit.</violation>
</file>

<file name="app/api/subscriptions/portal/__tests__/route.test.ts">

<violation number="1" location="app/api/subscriptions/portal/__tests__/route.test.ts:8">
P3: Custom agent: **Flag AI Slop and Fabricated Changes**

This test only checks that the route exports handler functions; it does not exercise or verify any API behavior introduced by the PR.</violation>
</file>

<file name="lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.test.ts">

<violation number="1" location="lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.test.ts:1">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**

This new test file exceeds the repository’s 100-line limit for maintainability.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant API as API Route Handler
    participant Auth as Auth Service
    participant DB as Supabase (DB)
    participant Stripe as Stripe API

    Note over Client,Stripe: NEW: Subscription Portal Management Flow

    Client->>API: OPTIONS /api/subscriptions/portal
    API-->>Client: 200 OK (CORS Headers)

    Client->>API: POST /api/subscriptions/portal (body: returnUrl, accountId?)
    API->>API: NEW: validateCreateSubscriptionPortalRequest()
    
    alt Invalid JSON or Schema
        API-->>Client: 400 Bad Request (Validation Error)
    else Valid Request Structure
        API->>Auth: NEW: validateAuthContext(request, accountId)
        Note right of Auth: Checks x-api-key or Authorization header
        
        alt Auth Failed
            Auth-->>API: 401 Unauthorized
            API-->>Client: 401 Unauthorized
        else Auth Success
            Auth-->>API: Return accountId
            
            API->>DB: NEW: selectStripeBillingCustomerByAccountId(accountId)
            DB-->>API: Return customer record
            
            alt Customer Not Found
                API-->>Client: 400 Bad Request (Customer not found)
            else Customer Found
                API->>Stripe: NEW: createBillingPortalSession(customer_id, returnUrl)
                
                alt Stripe Success
                    Stripe-->>API: Session (id, url)
                    API-->>Client: 200 OK { id, url }
                else Stripe Error / Missing URL
                    Stripe-->>API: Error
                    API-->>Client: 500 Internal Server Error
                end
            end
        end
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts Outdated
@@ -0,0 +1,120 @@
import "./routeTestMocks";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Custom agent: Enforce Clear Code Style and Maintainability Practices

Test file exceeds the repository's 100-line limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/subscriptions/portal/__tests__/route.post.outcomes.test.ts, line 12:

<comment>Test file exceeds the repository's 100-line limit.</comment>

<file context>
@@ -0,0 +1,120 @@
+
+const ACCOUNT = "123e4567-e89b-12d3-a456-426614174001";
+
+describe("POST /api/subscriptions/portal (handler outcomes)", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

already implemented.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the update!

Comment thread app/api/subscriptions/portal/__tests__/route.test.ts Outdated
Copy link
Copy Markdown

@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

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

Inline comments:
In `@lib/stripe/createSubscriptionPortalSchemas.ts`:
- Around line 3-8: Remove the accountId field from the request body schema
defined by createSubscriptionPortalBodySchema so clients cannot supply an
account selector; update createSubscriptionPortalBodySchema to only include
returnUrl and remain .strict(). Then find any places that read body.accountId
(or expect accountId from this schema) and instead derive the account via
validateAuthContext()/the request auth middleware, passing that derived
accountId into any downstream calls that previously used body.accountId; also
update any related types/interfaces that referenced accountId from this schema.

In `@lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts`:
- Around line 17-20: The current error handling in
selectStripeBillingCustomerByAccountId swallows Supabase errors and returns
null; change the block that checks for the Supabase error (the if (error) branch
in selectStripeBillingCustomerByAccountId) to log the error and rethrow it (or
throw a new Error that includes the original error) instead of returning null so
callers (e.g., createSubscriptionPortalHandler) can surface a 500 for real
DB/permission failures; continue returning null only when the query succeeds but
no rows are found.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c873670-c3f0-4522-bd17-a21a72b693c4

📥 Commits

Reviewing files that changed from the base of the PR and between e2c3167 and 765789e.

⛔ Files ignored due to path filters (7)
  • app/api/subscriptions/portal/__tests__/route.options.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.validation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/routeTestMocks.ts is excluded by !**/__tests__/** and included by app/**
  • lib/stripe/__tests__/createSubscriptionPortalHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • app/api/subscriptions/portal/route.ts
  • lib/stripe/createBillingPortalSession.ts
  • lib/stripe/createSubscriptionPortalHandler.ts
  • lib/stripe/createSubscriptionPortalSchemas.ts
  • lib/stripe/validateCreateSubscriptionPortalRequest.ts
  • lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts

Comment thread lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts Outdated
- Deleted test files for the subscription portal outcomes and validation request, as they are no longer needed.
- Updated the `selectStripeBillingCustomerByAccountId` function to throw an error instead of returning null on failure, improving error handling.

This cleanup enhances the codebase by removing obsolete tests and refining error management in the billing customer selection process.
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.

0 issues found across 5 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

… validation

- Deleted the test files for the subscription portal outcomes and the main route, as they are no longer relevant to the current implementation.
- This cleanup helps streamline the test suite and maintain focus on active tests.
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.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

- Removed the optional `accountId` field from the `createSubscriptionPortalBodySchema` as it is no longer required.
- Updated the `validateCreateSubscriptionPortalRequest` function to no longer pass `accountId` to `validateAuthContext`, simplifying the authentication context validation.
- Adjusted related tests to reflect the changes in the schema and validation logic, ensuring they accurately verify the expected behavior without the `accountId` dependency.
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.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Copy link
Copy Markdown

@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)
lib/stripe/createSubscriptionPortalSchemas.ts (1)

5-5: ⚡ Quick win

Use Zod v4's preferred z.url() top-level schema and object error params.

Two deprecations in one line:

  1. z.string().url() – the method chain form is deprecated in Zod v4. The recommended replacement is the standalone z.url() top-level schema.
  2. .min(1, "returnUrl is required") and .url("returnUrl must be a valid URL") – the bare string shorthand is deprecated; prefer { error: "..." }.

Since z.url() already rejects empty strings as invalid URLs, the .min(1) guard is redundant and the chain simplifies to a single validator. If you want a distinct "required" message for the empty-string case you can pipe: z.string().min(1, { error: "returnUrl is required" }).pipe(z.url({ error: "returnUrl must be a valid URL" })).

♻️ Proposed refactor (simple form)
 export const createSubscriptionPortalBodySchema = z
   .object({
-    returnUrl: z.string().min(1, "returnUrl is required").url("returnUrl must be a valid URL"),
+    returnUrl: z.url({ error: "returnUrl must be a valid URL" }),
   })
   .strict();

The Zod v4 changelog confirms: "The method forms (z.string().email()) still exist and work as before, but are now deprecated." The changelog shows z.string().url() as ❌ deprecated and z.url() as the replacement. Additionally, the old message parameter is still supported but deprecated. The current docs also confirm that the string shorthand (e.g. z.string().min(5, "Too short!")) still works, but the preferred form uses an object { error: "..." } instead.

🤖 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 `@lib/stripe/createSubscriptionPortalSchemas.ts` at line 5, The returnUrl
schema uses the deprecated chained form z.string().min(...).url(...); update the
returnUrl entry in the schema (in createSubscriptionPortalSchemas) to use Zod
v4's top-level z.url() with object error params—i.e., replace the chained form
with z.url({ error: "returnUrl must be a valid URL" }) or, if you need a
distinct empty-string message, use z.string().min(1, { error: "returnUrl is
required" }).pipe(z.url({ error: "returnUrl must be a valid URL" })) so the
schema uses non-deprecated APIs and proper { error: "..."} messages.
🤖 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 `@lib/stripe/createSubscriptionPortalSchemas.ts`:
- Around line 3-7: The file currently defines and exports
createSubscriptionPortalBodySchema but is named
createSubscriptionPortalSchemas.ts; rename the file to
createSubscriptionPortalBodySchema.ts and move the current content there so the
filename matches the primary export (createSubscriptionPortalBodySchema), then
update any imports that reference createSubscriptionPortalSchemas to point to
the new filename.

In `@lib/stripe/validateCreateSubscriptionPortalRequest.ts`:
- Around line 12-14: Rename the file to validateCreateSubscriptionPortalBody.ts
and rename the exported function validateCreateSubscriptionPortalRequest to
validateCreateSubscriptionPortalBody, and the result type
ValidatedCreateSubscriptionPortalRequest to
ValidatedCreateSubscriptionPortalBody; co-locate the Zod schema currently in
createSubscriptionPortalSchemas.ts into this new file, export the schema (e.g.,
createSubscriptionPortalSchema), export the inferred TypeScript type using
z.infer<typeof createSubscriptionPortalSchema>, and export the validated result
type alongside the function (ValidatedCreateSubscriptionPortalBody). Update all
imports/usages to reference the new file and renamed symbols
(validateCreateSubscriptionPortalBody, createSubscriptionPortalSchema, and
ValidatedCreateSubscriptionPortalBody). Ensure the function still returns
Promise<NextResponse | ValidatedCreateSubscriptionPortalBody> and uses the
co-located schema for validation.

---

Nitpick comments:
In `@lib/stripe/createSubscriptionPortalSchemas.ts`:
- Line 5: The returnUrl schema uses the deprecated chained form
z.string().min(...).url(...); update the returnUrl entry in the schema (in
createSubscriptionPortalSchemas) to use Zod v4's top-level z.url() with object
error params—i.e., replace the chained form with z.url({ error: "returnUrl must
be a valid URL" }) or, if you need a distinct empty-string message, use
z.string().min(1, { error: "returnUrl is required" }).pipe(z.url({ error:
"returnUrl must be a valid URL" })) so the schema uses non-deprecated APIs and
proper { error: "..."} messages.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2397761a-37d8-4479-ba7d-e388bae8e18a

📥 Commits

Reviewing files that changed from the base of the PR and between 765789e and fcd6416.

⛔ Files ignored due to path filters (5)
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.early.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.errors.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.success.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.auth.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.body.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • lib/stripe/createSubscriptionPortalSchemas.ts
  • lib/stripe/validateCreateSubscriptionPortalRequest.ts
  • lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts

Comment on lines +3 to +7
export const createSubscriptionPortalBodySchema = z
.object({
returnUrl: z.string().min(1, "returnUrl is required").url("returnUrl must be a valid URL"),
})
.strict();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Rename file to match the exported name per guidelines.

The file is createSubscriptionPortalSchemas.ts but its sole export is createSubscriptionPortalBodySchema. Per the coding guidelines, the file name must match the primary exported entity — create createSubscriptionPortalBodySchema.ts and move this content there.

As per coding guidelines: "The file name MUST match the exported function name. If a new function is defined in a file whose name does not match, leave a review comment telling the developer to create a new file named after the function."

🤖 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 `@lib/stripe/createSubscriptionPortalSchemas.ts` around lines 3 - 7, The file
currently defines and exports createSubscriptionPortalBodySchema but is named
createSubscriptionPortalSchemas.ts; rename the file to
createSubscriptionPortalBodySchema.ts and move the current content there so the
filename matches the primary export (createSubscriptionPortalBodySchema), then
update any imports that reference createSubscriptionPortalSchemas to point to
the new filename.

Comment on lines +12 to +14
export async function validateCreateSubscriptionPortalRequest(
request: NextRequest,
): Promise<NextResponse | ValidatedCreateSubscriptionPortalRequest> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Rename file (and function) to follow the validate<EndpointName>Body.ts convention.

The lib/**/validate*.ts guideline requires naming files validate<EndpointName>Body.ts or validate<EndpointName>Query.ts. This file should be validateCreateSubscriptionPortalBody.ts, with the exported function renamed to validateCreateSubscriptionPortalBody accordingly (and the exported type renamed to match, e.g. ValidatedCreateSubscriptionPortalBody).

The guideline also requires the file to export both the Zod schema and the inferred TypeScript type alongside the function. Currently the schema lives in a separate createSubscriptionPortalSchemas.ts file. Co-locating the schema, z.infer<> type, and the validated result type in one validateCreateSubscriptionPortalBody.ts would make the module fully self-contained and guideline-compliant.

As per coding guidelines: "Create validate functions in validate<EndpointName>Body.ts or validate<EndpointName>Query.ts files that export both the schema and inferred TypeScript type."

🤖 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 `@lib/stripe/validateCreateSubscriptionPortalRequest.ts` around lines 12 - 14,
Rename the file to validateCreateSubscriptionPortalBody.ts and rename the
exported function validateCreateSubscriptionPortalRequest to
validateCreateSubscriptionPortalBody, and the result type
ValidatedCreateSubscriptionPortalRequest to
ValidatedCreateSubscriptionPortalBody; co-locate the Zod schema currently in
createSubscriptionPortalSchemas.ts into this new file, export the schema (e.g.,
createSubscriptionPortalSchema), export the inferred TypeScript type using
z.infer<typeof createSubscriptionPortalSchema>, and export the validated result
type alongside the function (ValidatedCreateSubscriptionPortalBody). Update all
imports/usages to reference the new file and renamed symbols
(validateCreateSubscriptionPortalBody, createSubscriptionPortalSchema, and
ValidatedCreateSubscriptionPortalBody). Ensure the function still returns
Promise<NextResponse | ValidatedCreateSubscriptionPortalBody> and uses the
co-located schema for validation.

…nction

- Replaced instances of `validateCreateSubscriptionPortalRequest` with `validateCreateSubscriptionPortalBody` across multiple test files and the handler.
- Updated related test mocks and implementations to align with the new validation function.
- Removed obsolete validation request and schema files, streamlining the codebase and improving clarity in the subscription portal handling logic.
…selectBillingCustomers

Match the repo's select<TableName> naming convention. The function now takes
optional { accountId, provider } filters and returns an array, matching
sibling helpers like selectAccounts. Handler picks the first row.
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.

0 issues found across 8 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Vercel build failed because Supabase types `billing_customers.provider` as
the strict enum union, not string. Local pnpm test passes because vitest
doesn't typecheck — only `next build` does, which is what runs on Vercel.
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.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

@sweetmantech
Copy link
Copy Markdown
Contributor

Bug: portal endpoint can't find the Stripe customer for valid subscribers

Repro on the preview deployment

Tested against https://api-5cbd6u2zd-recoup.vercel.app (commit 58eed67c) with an API key for an account that has a confirmed active Stripe subscription (verified via GET /api/accounts/{id}/subscription returning {"isPro":true,"status":"trialing","plan":"pro","source":"account"}).

curl -X POST "$PREVIEW/api/subscriptions/portal" \
  -H "x-api-key: <KEY>" -H "Content-Type: application/json" \
  -d '{"returnUrl":"https://chat.recoupable.com/billing"}'
# -> 400 {"error":"Billing customer not found"}

Same flow against production chat for the same account succeeds and returns a usable portal URL. So the user has a valid subscription; the api is just looking in the wrong place.

Root cause

This handler resolves the Stripe customer ID via a Supabase billing_customers table:

// lib/stripe/createSubscriptionPortalHandler.ts
const [billingCustomer] = await selectBillingCustomers({
  accountId: validated.accountId,
  provider: "stripe",
});
if (!billingCustomer) {
  return NextResponse.json({ error: "Billing customer not found" }, { status: 400, ... });
}

That table isn't reliably populated for accounts that have working subscriptions — my account has no row in it, despite Stripe knowing about my active subscription.

The reference implementation

The production chat repo (chat/lib/stripe/createBillingPortalSession.ts) doesn't go through Supabase at all. It derives the Stripe customer from the active subscription's customer field, asking Stripe directly:

const activeSubscription = await getActiveSubscriptionDetails(accountId);
if (!activeSubscription) throw new Error("No active subscription found for this account");
const portalSession = await stripeClient.billingPortal.sessions.create({
  customer: activeSubscription.customer as string,
  return_url: returnUrl,
});

This is the pattern that's working in production today.

Suggested fix

The api repo already has getActiveSubscriptionDetails (it shipped with #506 and is what /api/accounts/[id]/subscription uses). Swap the Supabase lookup for it:

// inside createSubscriptionPortalHandler
const subscription = await getActiveSubscriptionDetails(validated.accountId);
if (!subscription) {
  return NextResponse.json({ error: "No active subscription found" }, { status: 400, ... });
}

const session = await createBillingPortalSession(
  subscription.customer as string,
  validated.returnUrl,
);

Side effects:

  • lib/supabase/billing_customers/selectBillingCustomers.ts becomes unused — safe to delete (this handler was its only caller).
  • Test mocks for selectBillingCustomers should be replaced with mocks for getActiveSubscriptionDetails.
  • The error message changes from "Billing customer not found" → "No active subscription found", which is the more accurate signal.

Confirmed working (no change needed)

# Case Status Body
2 Missing returnUrl 400 Invalid input: expected string, received undefined
3 No auth 401 Exactly one of x-api-key or Authorization must be provided
4 Invalid JSON 400 Invalid JSON body
5 OPTIONS preflight 200 CORS headers correct
6 Unknown body key 400 Unrecognized key: "extra" (strict schema works)

All error paths and validation behave correctly. The fix is isolated to the customer-lookup step.

…eSubscriptionDetails

This commit updates the subscription portal handler to utilize the new `getActiveSubscriptionDetails` function instead of the deprecated `selectBillingCustomers`. The changes include updating test cases to reflect the new logic, modifying error messages for clarity, and ensuring that the handler correctly checks for active subscriptions. Additionally, the `selectBillingCustomers` function has been removed from the codebase.
Copy link
Copy Markdown

@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)
lib/stripe/createSubscriptionPortalHandler.ts (1)

22-25: ⚡ Quick win

Remove or revise the unsafe casting concern—the customer field is never expanded.

The Stripe subscription fetched via getActiveSubscriptions does not use an expand parameter (see getActiveSubscriptions.ts lines 19–22). Without expansion, subscription.customer is always a string ID, making the as string cast safe. The hypothetical risk of passing a customer object to the Stripe API does not apply here.

If defensive programming is desired, a type guard could still be added as a safeguard against future changes, but this is not a correctness issue in the current implementation.

🤖 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 `@lib/stripe/createSubscriptionPortalHandler.ts` around lines 22 - 25, The cast
"subscription.customer as string" is unnecessary because getActiveSubscriptions
does not expand customer (so subscription.customer is already a string); remove
the unsafe cast or replace it with a simple type guard to assert it's a string
before calling createBillingPortalSession. Locate the call in
createSubscriptionPortalHandler.ts (the session creation that calls
createBillingPortalSession) and either drop the "as string" cast or add a small
runtime check (typeof subscription.customer === "string") that throws a clear
error if not, then pass subscription.customer into createBillingPortalSession;
also reference getActiveSubscriptions to note why the cast is safe if you remove
it.
🤖 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 `@lib/stripe/createSubscriptionPortalHandler.ts`:
- Around line 22-25: The cast "subscription.customer as string" is unnecessary
because getActiveSubscriptions does not expand customer (so
subscription.customer is already a string); remove the unsafe cast or replace it
with a simple type guard to assert it's a string before calling
createBillingPortalSession. Locate the call in
createSubscriptionPortalHandler.ts (the session creation that calls
createBillingPortalSession) and either drop the "as string" cast or add a small
runtime check (typeof subscription.customer === "string") that throws a clear
error if not, then pass subscription.customer into createBillingPortalSession;
also reference getActiveSubscriptions to note why the cast is safe if you remove
it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fa24033a-6388-4693-a6f4-13f3ecac095d

📥 Commits

Reviewing files that changed from the base of the PR and between fcd6416 and 9e75112.

⛔ Files ignored due to path filters (8)
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.early.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.errors.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.success.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/route.post.validation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/portal/__tests__/routeTestMocks.ts is excluded by !**/__tests__/** and included by app/**
  • lib/stripe/__tests__/createSubscriptionPortalHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateCreateSubscriptionPortalBody.auth.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateCreateSubscriptionPortalBody.body.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/stripe/createSubscriptionPortalHandler.ts
  • lib/stripe/validateCreateSubscriptionPortalBody.ts

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.

0 issues found across 7 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

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.

2 participants