feat(api): implement subscription management portal endpoints#511
feat(api): implement subscription management portal endpoints#511ahmednahima0-beep wants to merge 12 commits intotestfrom
Conversation
- 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 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. ChangesSubscription Portal Endpoint
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
4 issues found across 13 files
Confidence score: 3/5
- There is a concrete regression risk in
lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts: returningnullon 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.tsandlib/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.tsvalidates 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
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| @@ -0,0 +1,120 @@ | |||
| import "./routeTestMocks"; | |||
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
already implemented.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (7)
app/api/subscriptions/portal/__tests__/route.options.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.outcomes.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.validation.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/routeTestMocks.tsis excluded by!**/__tests__/**and included byapp/**lib/stripe/__tests__/createSubscriptionPortalHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (6)
app/api/subscriptions/portal/route.tslib/stripe/createBillingPortalSession.tslib/stripe/createSubscriptionPortalHandler.tslib/stripe/createSubscriptionPortalSchemas.tslib/stripe/validateCreateSubscriptionPortalRequest.tslib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
- 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.
… 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.
- 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/stripe/createSubscriptionPortalSchemas.ts (1)
5-5: ⚡ Quick winUse Zod v4's preferred
z.url()top-level schema and object error params.Two deprecations in one line:
z.string().url()– the method chain form is deprecated in Zod v4. The recommended replacement is the standalonez.url()top-level schema..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❌ deprecatedandz.url()as the✅replacement. Additionally, the oldmessageparameter 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
⛔ Files ignored due to path filters (5)
app/api/subscriptions/portal/__tests__/route.post.outcomes.early.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.errors.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.success.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.auth.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateCreateSubscriptionPortalRequest.body.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (3)
lib/stripe/createSubscriptionPortalSchemas.tslib/stripe/validateCreateSubscriptionPortalRequest.tslib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/supabase/billing_customers/selectStripeBillingCustomerByAccountId.ts
| export const createSubscriptionPortalBodySchema = z | ||
| .object({ | ||
| returnUrl: z.string().min(1, "returnUrl is required").url("returnUrl must be a valid URL"), | ||
| }) | ||
| .strict(); |
There was a problem hiding this comment.
🛠️ 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.
| export async function validateCreateSubscriptionPortalRequest( | ||
| request: NextRequest, | ||
| ): Promise<NextResponse | ValidatedCreateSubscriptionPortalRequest> { |
There was a problem hiding this comment.
🛠️ 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.
…ecoupable/api into feat/subscriptions-portal-post
…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.
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.
Bug: portal endpoint can't find the Stripe customer for valid subscribersRepro on the preview deploymentTested against 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 causeThis handler resolves the Stripe customer ID via a Supabase // 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 implementationThe production chat repo ( 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 fixThe api repo already has // 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:
Confirmed working (no change needed)
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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/stripe/createSubscriptionPortalHandler.ts (1)
22-25: ⚡ Quick winRemove or revise the unsafe casting concern—the
customerfield is never expanded.The Stripe subscription fetched via
getActiveSubscriptionsdoes not use anexpandparameter (seegetActiveSubscriptions.tslines 19–22). Without expansion,subscription.customeris always a string ID, making theas stringcast 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
⛔ Files ignored due to path filters (8)
app/api/subscriptions/portal/__tests__/route.post.outcomes.early.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.errors.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.outcomes.portal.success.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/route.post.validation.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**app/api/subscriptions/portal/__tests__/routeTestMocks.tsis excluded by!**/__tests__/**and included byapp/**lib/stripe/__tests__/createSubscriptionPortalHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateCreateSubscriptionPortalBody.auth.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateCreateSubscriptionPortalBody.body.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (2)
lib/stripe/createSubscriptionPortalHandler.tslib/stripe/validateCreateSubscriptionPortalBody.ts
Files added:
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/portalendpoint 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
{ returnUrl }, uses auth-derived account, checks subscription viagetActiveSubscriptionDetails, creates the portal session, and returns{ id, url }; returns 400 when no subscription or session URL, 500 on internal errors.Refactors
validateCreateSubscriptionPortalBody;accountIdnow always comes from auth.selectBillingCustomerstogetActiveSubscriptionDetailsand removed the old helper.Written for commit 9e75112. Summary will update on new commits.
Summary by CodeRabbit