Skip to content

feat(api): add GET /api/subscriptions/status#505

Open
ahmednahima0-beep wants to merge 5 commits intotestfrom
feature/api-get-subscription-status
Open

feat(api): add GET /api/subscriptions/status#505
ahmednahima0-beep wants to merge 5 commits intotestfrom
feature/api-get-subscription-status

Conversation

@ahmednahima0-beep
Copy link
Copy Markdown
Collaborator

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

Implements OpenAPI subscription status: required query accountId (UUID), x-api-key or Bearer auth, validateAccountIdOverride, and isPro from Stripe subscription metadata plus org coverage. Adds route, handler, validation, helpers, and unit tests.


Summary by cubic

Adds a GET /api/subscriptions/status endpoint to check if an account has an active paid subscription. Returns { isPro } based on Stripe subscriptions for the account or its orgs, with strict auth and CORS.

  • New Features

    • Next.js route and handler with CORS preflight.
    • Validation: accountId UUID, x-api-key or Authorization: Bearer, and account override rules.
    • isPro from active Stripe subs (account or org): filters by metadata.accountId and current_period_end > now; active = not a canceled trial. Returns 400/401/403/500. Tests added.
  • Refactors

    • Standardized sandbox identifiers to sandboxId across code/tests; Sandbox.get({ sandboxId }).

Written for commit 24a5b5a. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added subscription status API endpoint with CORS support to check pro account eligibility per account.
  • Updates

    • Refined sandbox identifier resolution mechanism for improved sandbox management and retrieval.
    • Enhanced subscription status validation logic to accurately distinguish active billable subscriptions from trial accounts.

Implements OpenAPI subscription status: required query accountId (UUID), x-api-key or Bearer auth, validateAccountIdOverride, and isPro from Stripe subscription metadata plus org coverage. Adds route, handler, validation, helpers, and unit tests.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 3, 2026

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

Project Deployment Actions Updated (UTC)
api Error Error May 3, 2026 3:30pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@ahmednahima0-beep has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 49 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca6a81ab-0524-4559-9ee9-aae2277058b2

📥 Commits

Reviewing files that changed from the base of the PR and between 55c73ea and 24a5b5a.

⛔ Files ignored due to path filters (2)
  • lib/stripe/__tests__/getSubscriptionStatusHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (4)
  • lib/stripe/getActiveSubscriptions.ts
  • lib/stripe/getSubscriptionStatusHandler.ts
  • lib/stripe/isActiveSubscription.ts
  • lib/stripe/validateGetSubscriptionStatusRequest.ts
📝 Walkthrough

Walkthrough

This PR introduces a new subscription status API endpoint and supporting Stripe utility functions to check whether an account has an active pro subscription, while systematically updating sandbox operations to use sandboxId instead of name for identifying sandbox instances.

Changes

Subscription Status API

Layer / File(s) Summary
Data Validation
lib/stripe/validateSubscriptionStatusQuery.ts
Zod schema for validating accountId as a required non-empty UUID; validates auth context and account ID override, returning errors or a structured query object.
Core Subscription Queries
lib/stripe/getActiveSubscriptions.ts, lib/stripe/getActiveSubscriptionDetails.ts
Query Stripe for active subscriptions; filter by timestamp and metadata; wrap in error handling with logging.
Subscription Aggregation
lib/stripe/getOrgSubscription.ts, lib/stripe/getSubscriptionIsPro.ts
Fetch subscriptions across account and organization contexts; combine results to determine pro status.
Subscription Status Logic
lib/stripe/isActiveSubscription.ts
Refine pro subscription eligibility: "active" or uncanceled "trialing" subscriptions only.
Route Handler
lib/stripe/getSubscriptionStatusHandler.ts
Orchestrate validation, subscription checks, and JSON response with CORS headers; handle errors.
API Route
app/api/subscriptions/status/route.ts
Export OPTIONS for CORS preflight, GET delegating to handler, and cache configuration constants.

Sandbox ID Identifier Update

Layer / File(s) Summary
Response Type Updates
lib/sandbox/createSandbox.ts
Update SandboxCreatedResponse to use VercelSandbox["sandboxId"] instead of name.
Sandbox Creation & Snapshot
lib/sandbox/createSandbox.ts, lib/sandbox/createSandboxFromSnapshot.ts, lib/sandbox/processCreateSandbox.ts
Return and persist sandbox.sandboxId instead of sandbox.name in creation responses and database calls.
Sandbox Retrieval & Status
lib/sandbox/getActiveSandbox.ts, lib/sandbox/getOrCreateSandbox.ts, lib/sandbox/getSandboxStatus.ts
Use sandboxId parameter with Sandbox.get() and return sandbox.sandboxId instead of sandbox.name for all queries and responses.

Sequence Diagram

sequenceDiagram
    actor Client
    participant APIRoute as /api/subscriptions/status
    participant Handler as getSubscriptionStatusHandler
    participant Validator as validateSubscriptionStatusQuery
    participant Auth as Auth Context
    participant Stripe as Stripe API
    participant SubscriptionCheck as getSubscriptionIsPro

    Client->>APIRoute: GET /api/subscriptions/status?accountId=...
    APIRoute->>Handler: Forward request
    Handler->>Validator: Validate query & auth
    Validator->>Auth: Check auth context
    Auth-->>Validator: Auth valid
    Validator-->>Handler: { accountId: ... }
    Handler->>SubscriptionCheck: getSubscriptionIsPro(accountId)
    SubscriptionCheck->>Stripe: List active subscriptions (account)
    Stripe-->>SubscriptionCheck: Subscriptions
    SubscriptionCheck->>Stripe: List org subscriptions
    Stripe-->>SubscriptionCheck: Subscriptions
    SubscriptionCheck-->>Handler: isPro: boolean
    Handler-->>APIRoute: { isPro }
    APIRoute-->>Client: 200 + CORS headers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The subscription API introduces new validation and Stripe query logic requiring careful review of error handling and auth integration. The sandbox ID changes are repetitive and systematic but span nine files, demanding thorough verification that all references are converted consistently.

Possibly related PRs

Suggested reviewers

  • sweetmantech

Poem

🔐 Subscriptions now verified with stripe precision,
Checking pro status with mathematical decision—
Account or org, whichever rings true,
Sandbox IDs aligned, identity renewed! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Code contains two unresolved clean code violations: deprecated Zod v3 syntax with incorrect error handling in validateSubscriptionStatusQuery.ts, and silent swallowing of database failures in createSandboxFromSnapshot.ts. Update Zod schema to use unified error syntax and capture/propagate insertAccountSandbox() results to ensure proper error handling and data consistency.
✅ 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/api-get-subscription-status

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
Review rate limit: 0/1 reviews remaining, refill in 41 minutes and 49 seconds.

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.

2 issues found across 10 files

Confidence score: 2/5

  • There is a high-confidence, high-severity logic issue in lib/stripe/isActiveSubscription.ts: isActiveSubscription appears to classify many non-active statuses as active, which can cause real user-facing entitlement/billing behavior to be incorrect.
  • Given the 8/10 severity and 9/10 confidence on core subscription-state logic, this is a concrete regression risk rather than a minor uncertainty, so merge risk is elevated.
  • The issue in lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts is low severity (maintainability/style only) and not merge-blocking by itself.
  • Pay close attention to lib/stripe/isActiveSubscription.ts and lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts - fix active-status classification first, then address test file maintainability limits.
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/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts">

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

Test module exceeds the 100-line file limit required by the maintainability rule.</violation>
</file>

<file name="lib/stripe/isActiveSubscription.ts">

<violation number="1" location="lib/stripe/isActiveSubscription.ts:7">
P1: `isActiveSubscription` treats most non-active subscription statuses as active. The current logic only rejects trial subscriptions with `canceled_at`, so canceled or otherwise inactive subscriptions can be misclassified as active.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant Route as API Route (Subscription Status)
    participant Auth as Auth & Validation
    participant DB as Supabase (Account Orgs)
    participant Stripe as Stripe API

    Note over Client,Stripe: GET /api/subscriptions/status?accountId=UUID

    Client->>Route: GET request (Auth Headers + accountId)
    
    Route->>Auth: NEW: validateGetSubscriptionStatusRequest()
    Auth->>Auth: Validate UUID format
    
    Auth->>Auth: validateAuthContext() (Key or Bearer)
    alt Auth Failed
        Auth-->>Route: 401 Unauthorized
        Route-->>Client: Error Response
    end

    Auth->>Auth: NEW: validateAccountIdOverride()
    Note right of Auth: Checks if requester has access<br/>to target accountId
    alt Access Denied
        Auth-->>Route: 403 Forbidden
        Route-->>Client: Error Response
    end
    
    Auth-->>Route: Validated accountId

    Route->>Route: NEW: getSubscriptionIsPro(accountId)

    par Check Direct Subscription
        Route->>Stripe: NEW: getActiveSubscriptions(accountId)
        Stripe-->>Route: Subscription list
    and Check Organization Subscriptions
        Route->>DB: NEW: getAccountOrganizations(accountId)
        DB-->>Route: List of Organization IDs
        loop For each Org
            Route->>Stripe: NEW: getActiveSubscriptionDetails(orgId)
            Stripe-->>Route: Subscription details
        end
    end

    Route->>Route: NEW: isActiveSubscription()
    Note right of Route: Logic: Active if not a<br/>canceled trial

    alt isPro = true
        Route-->>Client: 200 OK { "isPro": true }
    else isPro = false
        Route-->>Client: 200 OK { "isPro": false }
    end
Loading

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

Comment thread lib/stripe/isActiveSubscription.ts
Comment thread lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts
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: 3

🧹 Nitpick comments (1)
lib/stripe/validateGetSubscriptionStatusRequest.ts (1)

27-31: ⚡ Quick win

z.string().uuid() is deprecated in Zod v4 — prefer the top-level z.uuid() standalone.

In Zod v4, string formats like uuid were promoted to top-level z namespace functions, and z.string().uuid() is now deprecated. It will be removed in the next major version.

♻️ Proposed fix
-  const parsedUuid = z.string().uuid("accountId must be a valid UUID").safeParse(raw);
+  const parsedUuid = z.uuid("accountId must be a valid UUID").safeParse(raw);

Note: z.uuid() in Zod v4 validates UUIDs strictly against RFC 9562/4122 (variant bits must be 10). If any existing account IDs were generated with a non-compliant variant, consider z.guid() for a more permissive UUID-like pattern instead.

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

In `@lib/stripe/validateGetSubscriptionStatusRequest.ts` around lines 27 - 31,
Replace the deprecated z.string().uuid(...) usage with the top-level z.uuid()
call: update the validation where parsedUuid is created in
validateGetSubscriptionStatusRequest (currently using z.string().uuid("accountId
must be a valid UUID")) to use z.uuid({ message: "accountId must be a valid
UUID" }) or z.guid(...) if you need permissive validation, then keep the same
.safeParse(raw) handling and error extraction (parsedUuid, parsedUuid.success,
parsedUuid.error.issues[0]) so downstream logic remains unchanged.
🤖 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/getActiveSubscriptions.ts`:
- Around line 6-11: The current call to stripeClient.subscriptions.list with
limit: 100 in getActiveSubscriptions.ts can miss matches because you filter by
metadata.accountId client-side; replace the single-page scan with Stripe
auto-pagination (e.g., use
stripeClient.subscriptions.list(...).autoPagingToArray() or autoPagingEach and
stop early when metadata.accountId matches) so you iterate all pages (or exit
once found) instead of only the first 100 results, or alternatively change the
logic to look up subscriptions by a stored subscription.id or customer.id in
your DB and fetch that specific subscription directly to avoid full scans.

In `@lib/stripe/isActiveSubscription.ts`:
- Around line 3-8: isActiveSubscription currently treats any non-trialing status
as active, which lets cancelled/unpaid/past_due/incomplete/etc. subscriptions
grant pro access; fix by returning true only for an explicit allowlist of truly
active statuses (e.g., check subscription.status is exactly "active" or
"trialing" and for "trialing" ensure subscription.canceled_at is falsy), and
return false for all other statuses (including "canceled", "unpaid", "past_due",
"incomplete", "incomplete_expired", "paused"); update the isActiveSubscription
function accordingly and ensure callers like getActiveSubscriptions rely on this
stricter status check rather than the period end alone.

In `@lib/stripe/validateGetSubscriptionStatusRequest.ts`:
- Around line 8-10: Rename the file and exported validator to match the pattern:
change lib/stripe/validateGetSubscriptionStatusRequest.ts and any exported
function to validateSubscriptionStatusQuery (file must be
validateSubscriptionStatusQuery.ts and the function name must match), and
refactor so the Zod schema is declared and exported as a named constant (e.g.,
subscriptionStatusQuerySchema) alongside an exported inferred type (e.g.,
SubscriptionStatusQuery) instead of inlining it inside the function; update
usages of ValidatedGetSubscriptionStatusRequest to the new exported inferred
type and ensure the validator function returns that inferred type.

---

Nitpick comments:
In `@lib/stripe/validateGetSubscriptionStatusRequest.ts`:
- Around line 27-31: Replace the deprecated z.string().uuid(...) usage with the
top-level z.uuid() call: update the validation where parsedUuid is created in
validateGetSubscriptionStatusRequest (currently using z.string().uuid("accountId
must be a valid UUID")) to use z.uuid({ message: "accountId must be a valid
UUID" }) or z.guid(...) if you need permissive validation, then keep the same
.safeParse(raw) handling and error extraction (parsedUuid, parsedUuid.success,
parsedUuid.error.issues[0]) so downstream logic remains unchanged.
🪄 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: 3a65257f-a3d0-4559-8426-6ec0d54dabe0

📥 Commits

Reviewing files that changed from the base of the PR and between a8bb14f and a617b68.

⛔ Files ignored due to path filters (2)
  • lib/stripe/__tests__/getSubscriptionStatusHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (8)
  • app/api/subscriptions/status/route.ts
  • lib/stripe/getActiveSubscriptionDetails.ts
  • lib/stripe/getActiveSubscriptions.ts
  • lib/stripe/getOrgSubscription.ts
  • lib/stripe/getSubscriptionIsPro.ts
  • lib/stripe/getSubscriptionStatusHandler.ts
  • lib/stripe/isActiveSubscription.ts
  • lib/stripe/validateGetSubscriptionStatusRequest.ts

Comment thread lib/stripe/getActiveSubscriptions.ts
Comment thread lib/stripe/isActiveSubscription.ts
Comment on lines +8 to +10
export type ValidatedGetSubscriptionStatusRequest = {
accountId: string;
};
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

Guideline violations: file name doesn't match validate<EndpointName>Query.ts pattern and the Zod schema is not exported.

Two coding guidelines are violated here:

  1. File naming: The guideline requires validate<EndpointName>Query.ts. This file should be named validateSubscriptionStatusQuery.ts (and the function renamed to match).
  2. Schema export: The guideline requires exporting both the Zod schema and the inferred TypeScript type. Currently only ValidatedGetSubscriptionStatusRequest is exported; the schema itself is inlined inside the function and not accessible to callers.
♻️ Proposed structure for the renamed file `validateSubscriptionStatusQuery.ts`
-export type ValidatedGetSubscriptionStatusRequest = {
-  accountId: string;
-};
+import { z } from "zod";
+
+export const subscriptionStatusQuerySchema = z.object({
+  accountId: z.uuid("accountId must be a valid UUID"),
+});
+
+export type ValidatedSubscriptionStatusQuery = z.infer<typeof subscriptionStatusQuerySchema>;
 
 /**
  * Validates GET /api/subscriptions/status: required query `accountId` (UUID),
  * auth, and access to the target account.
  */
-export async function validateGetSubscriptionStatusRequest(
+export async function validateSubscriptionStatusQuery(
   request: NextRequest,
-): Promise<ValidatedGetSubscriptionStatusRequest | NextResponse> {
+): Promise<ValidatedSubscriptionStatusQuery | NextResponse> {
   const raw = request.nextUrl.searchParams.get("accountId");
   if (raw === null || raw.trim() === "") {
     return NextResponse.json(
       { error: "accountId is required" },
       { status: 400, headers: getCorsHeaders() },
     );
   }
 
-  const parsedUuid = z.string().uuid("accountId must be a valid UUID").safeParse(raw);
+  const parsedUuid = subscriptionStatusQuerySchema.shape.accountId.safeParse(raw);
   // ... rest unchanged

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" and "The file name MUST match the exported function name."

Also applies to: 27-31

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

In `@lib/stripe/validateGetSubscriptionStatusRequest.ts` around lines 8 - 10,
Rename the file and exported validator to match the pattern: change
lib/stripe/validateGetSubscriptionStatusRequest.ts and any exported function to
validateSubscriptionStatusQuery (file must be validateSubscriptionStatusQuery.ts
and the function name must match), and refactor so the Zod schema is declared
and exported as a named constant (e.g., subscriptionStatusQuerySchema) alongside
an exported inferred type (e.g., SubscriptionStatusQuery) instead of inlining it
inside the function; update usages of ValidatedGetSubscriptionStatusRequest to
the new exported inferred type and ensure the validator function returns that
inferred type.

- Refactored `getActiveSubscriptions` to use `autoPagingEach` for improved performance and clarity in fetching active subscriptions.
- Renamed `validateGetSubscriptionStatusRequest` to `validateSubscriptionStatusQuery` for consistency with naming conventions.
- Enhanced `isActiveSubscription` logic to clarify the conditions under which a subscription is considered active.
- Removed obsolete validation file and associated tests, streamlining the codebase.
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 8 files (changes from recent commits).

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/stripe/getActiveSubscriptions.ts">

<violation number="1" location="lib/stripe/getActiveSubscriptions.ts:12">
P1: `autoPagingEach()` is used as an async iterable, but it returns `Promise<void>`. This loop will not iterate subscriptions correctly and can throw at runtime.</violation>
</file>

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

Comment thread lib/stripe/getActiveSubscriptions.ts Outdated
- Changed all instances of `sandbox.name` to `sandbox.sandboxId` across multiple files to align with the updated API structure.
- Updated the `SandboxCreatedResponse` interface to reflect the new property type for `sandboxId`.
- Adjusted related tests to ensure consistency with the new property naming.
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/sandbox/createSandboxFromSnapshot.ts`:
- Around line 24-27: The insertAccountSandbox call in createSandboxFromSnapshot
currently ignores its result so failures can be swallowed, causing an
unpersisted sandbox to be returned; update createSandboxFromSnapshot to
propagate insertAccountSandbox failures by checking its resolved value (or
allowing thrown errors to bubble) and throwing an error if the insert did not
succeed (e.g., if insertAccountSandbox returns false or null, throw a
descriptive Error), so callers (getActiveSandbox / getOrCreateSandbox) won't see
a live-but-unpersisted sandbox and create duplicates.

In `@lib/stripe/validateSubscriptionStatusQuery.ts`:
- Around line 8-13: The Zod schema uses deprecated message-style parameters;
update subscriptionStatusQuerySchema so accountId uses Zod v4's unified error
option instead of the old string({ message }) and string-style validator
messages—replace z.string({ message: "..." }) and the .min/.uuid string messages
with the v4 form that provides an error object (pass the unified error option to
z.string and to .min and .uuid for accountId) so the custom messages are applied
correctly.
🪄 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: 129174c2-2ae9-4e34-b170-0d0389f5c2df

📥 Commits

Reviewing files that changed from the base of the PR and between a617b68 and 55c73ea.

⛔ Files ignored due to path filters (10)
  • lib/sandbox/__tests__/createSandbox.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/createSandboxFromSnapshot.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/getActiveSandbox.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/getOrCreateSandbox.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/getSandboxStatus.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/processCreateSandbox.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/getActiveSubscriptions.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/getSubscriptionStatusHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/isActiveSubscription.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateSubscriptionStatusQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (10)
  • lib/sandbox/createSandbox.ts
  • lib/sandbox/createSandboxFromSnapshot.ts
  • lib/sandbox/getActiveSandbox.ts
  • lib/sandbox/getOrCreateSandbox.ts
  • lib/sandbox/getSandboxStatus.ts
  • lib/sandbox/processCreateSandbox.ts
  • lib/stripe/getActiveSubscriptions.ts
  • lib/stripe/getSubscriptionStatusHandler.ts
  • lib/stripe/isActiveSubscription.ts
  • lib/stripe/validateSubscriptionStatusQuery.ts
✅ Files skipped from review due to trivial changes (1)
  • lib/stripe/getActiveSubscriptions.ts

Comment on lines 24 to 27
await insertAccountSandbox({
account_id: accountId,
sandbox_id: sandbox.name,
sandbox_id: sandbox.sandboxId,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate account-sandbox insert failures.

insertAccountSandbox(...) is awaited, but its result is ignored. If that write fails, this function still returns a live sandbox with no persisted mapping, so getActiveSandbox / getOrCreateSandbox can miss it and create duplicates.

Suggested fix
-  await insertAccountSandbox({
+  const { error } = await insertAccountSandbox({
     account_id: accountId,
     sandbox_id: sandbox.sandboxId,
   });
+  if (error) {
+    throw error;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await insertAccountSandbox({
account_id: accountId,
sandbox_id: sandbox.name,
sandbox_id: sandbox.sandboxId,
});
const { error } = await insertAccountSandbox({
account_id: accountId,
sandbox_id: sandbox.sandboxId,
});
if (error) {
throw error;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandbox/createSandboxFromSnapshot.ts` around lines 24 - 27, The
insertAccountSandbox call in createSandboxFromSnapshot currently ignores its
result so failures can be swallowed, causing an unpersisted sandbox to be
returned; update createSandboxFromSnapshot to propagate insertAccountSandbox
failures by checking its resolved value (or allowing thrown errors to bubble)
and throwing an error if the insert did not succeed (e.g., if
insertAccountSandbox returns false or null, throw a descriptive Error), so
callers (getActiveSandbox / getOrCreateSandbox) won't see a live-but-unpersisted
sandbox and create duplicates.

Comment on lines +8 to +13
export const subscriptionStatusQuerySchema = z.object({
accountId: z
.string({ message: "accountId is required" })
.min(1, "accountId is required")
.uuid("accountId must be a valid UUID"),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "package.json" -type f | head -5

Repository: recoupable/api

Length of output: 73


🏁 Script executed:

fd -t f "validateSubscriptionStatusQuery.ts"

Repository: recoupable/api

Length of output: 104


🏁 Script executed:

cat package.json | grep -A 2 "zod"

Repository: recoupable/api

Length of output: 144


🏁 Script executed:

cat -n lib/stripe/validateSubscriptionStatusQuery.ts

Repository: recoupable/api

Length of output: 2146


🌐 Web query:

Zod v4 error message customization API syntax

💡 Result:

In Zod v4, you customize error messages via the unified error parameter (deprecated: message). You can pass either a string or a function (error map) wherever a validator accepts an error message. 1) Schema-level (per validation step) customization - As a string: - z.string({ error: "Not a string" }) - As a function (recommended when you want conditional logic by issue code): - z.string({ error: (iss) => iss.input === undefined ? "Field is required." : "Invalid input.", }) 2) Per-validation-method customization (e.g., min) - z.string.min(5, { error: (iss) => Password must have ${iss.minimum} characters or more, }) 3) Per-parse customization - Lower precedence than schema-level customizations: - const schema = z.string; - schema.parse(12, { error: iss => "per-parse custom error" }); 4) Global error customization - Use z.config({ customError }) (intended replacement for older global errorMap behavior): - z.config({ customError: (iss) => { if (iss.code === "invalid_type") return invalid type, expected ${iss.expected}; if (iss.code === "too_small") return minimum is ${iss.minimum}; // ... }, }); 5) Important notes about API syntax and precedence - error is the v4 unified API for customization; message is deprecated. - The error function is called with a context object (iss) that includes code, input, inst, and path (and validator-specific fields like minimum, maximum, etc.). - Returning undefined from an error function makes Zod fall back to the next error map in the precedence chain. - Precedence (highest to lowest): per-parse parse(..., { error }), then global error map (z.config({ customError })), then schema-level custom messages. If you share the specific validator call you’re using (e.g., z.string.min(...), .refine(...), z.object({ ... })), I can provide the exact error snippet for that case.

Citations:


Update Zod schema error customization to match Zod v4 API.

The code currently uses deprecated Zod syntax (message parameter and string-style validator messages). Zod v4 requires the unified error option. The custom error messages won't be applied correctly with the current code—validation will fall back to default Zod messages.

🔧 Proposed fix
 export const subscriptionStatusQuerySchema = z.object({
   accountId: z
-    .string({ message: "accountId is required" })
-    .min(1, "accountId is required")
-    .uuid("accountId must be a valid UUID"),
+    .string({ error: "accountId is required" })
+    .min(1, { error: "accountId is required" })
+    .uuid({ error: "accountId must be a valid UUID" }),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/stripe/validateSubscriptionStatusQuery.ts` around lines 8 - 13, The Zod
schema uses deprecated message-style parameters; update
subscriptionStatusQuerySchema so accountId uses Zod v4's unified error option
instead of the old string({ message }) and string-style validator
messages—replace z.string({ message: "..." }) and the .min/.uuid string messages
with the v4 form that provides an error object (pass the unified error option to
z.string and to .min and .uuid for accountId) so the custom messages are applied
correctly.

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.

2 issues found across 14 files (changes from recent commits).

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/sandbox/getSandboxStatus.ts">

<violation number="1" location="lib/sandbox/getSandboxStatus.ts:12">
P1: Use the beta SDK's `name` field here; this pinned version expects `Sandbox.get({ name })`.</violation>

<violation number="2" location="lib/sandbox/getSandboxStatus.ts:15">
P1: Read `sandbox.name` here; this beta SDK renamed the identifier field.</violation>
</file>

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


return {
sandboxId: sandbox.name,
sandboxId: sandbox.sandboxId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Read sandbox.name here; this beta SDK renamed the identifier field.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/getSandboxStatus.ts, line 15:

<comment>Read `sandbox.name` here; this beta SDK renamed the identifier field.</comment>

<file context>
@@ -9,10 +9,10 @@ import type { SandboxCreatedResponse } from "./createSandbox";
 
     return {
-      sandboxId: sandbox.name,
+      sandboxId: sandbox.sandboxId,
       sandboxStatus: sandbox.status,
       timeout: sandbox.timeout,
</file context>
Suggested change
sandboxId: sandbox.sandboxId,
sandboxId: sandbox.name,

export async function getSandboxStatus(sandboxId: string): Promise<SandboxCreatedResponse | null> {
try {
const sandbox = await Sandbox.get({ name: sandboxId });
const sandbox = await Sandbox.get({ sandboxId });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Use the beta SDK's name field here; this pinned version expects Sandbox.get({ name }).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/getSandboxStatus.ts, line 12:

<comment>Use the beta SDK's `name` field here; this pinned version expects `Sandbox.get({ name })`.</comment>

<file context>
@@ -9,10 +9,10 @@ import type { SandboxCreatedResponse } from "./createSandbox";
 export async function getSandboxStatus(sandboxId: string): Promise<SandboxCreatedResponse | null> {
   try {
-    const sandbox = await Sandbox.get({ name: sandboxId });
+    const sandbox = await Sandbox.get({ sandboxId });
 
     return {
</file context>
Suggested change
const sandbox = await Sandbox.get({ sandboxId });
const sandbox = await Sandbox.get({ name: sandboxId });

- Changed all instances of `sandbox.sandboxId` to `sandbox.name` across multiple files to align with the updated API structure.
- Updated related tests to ensure consistency with the new property naming.
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.

6 issues found across 17 files (changes from recent commits).

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/sandbox/__tests__/getSandboxStatus.test.ts">

<violation number="1">
P2: Keep the mock shape and assertion aligned with `getSandboxStatus`; this test was switched to `name`, but the code under test still uses `sandboxId`.</violation>
</file>

<file name="lib/sandbox/__tests__/getActiveSandbox.test.ts">

<violation number="1">
P3: Assert the actual `sandboxId` argument here; `name` does not match the implementation.</violation>
</file>

<file name="lib/sandbox/__tests__/processCreateSandbox.test.ts">

<violation number="1">
P2: Keep `sandboxId` on the mock as well; `processCreateSandbox` still reads that property when serializing the result.</violation>
</file>

<file name="lib/sandbox/__tests__/createSandbox.test.ts">

<violation number="1">
P2: Keep the mock aligned with `createSandbox()` and return `sandboxId` here, otherwise the test exercises `undefined` for the field it expects.</violation>
</file>

<file name="lib/stripe/getActiveSubscriptions.ts">

<violation number="1" location="lib/stripe/getActiveSubscriptions.ts:6">
P1: This change regresses from auto-pagination to a single-page Stripe query, so accounts can be incorrectly reported as having no active subscription when matches are past the first 100 records.</violation>
</file>

<file name="lib/sandbox/createSandboxFromSnapshot.ts">

<violation number="1">
P1: Persisting `sandbox.name` into `sandbox_id` is inconsistent with downstream reads that use `sandbox_id` as `sandboxId`, which can break sandbox lookups.</violation>
</file>

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

Comment on lines +6 to +16
const subscriptions = await stripeClient.subscriptions.list({
limit: 100,
current_period_end: {
gt: Math.floor(Date.now() / 1000),
},
});

const activeSubscriptions =
subscriptions?.data?.filter(
(subscription: Stripe.Subscription) => subscription.metadata?.accountId === accountId,
) ?? [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This change regresses from auto-pagination to a single-page Stripe query, so accounts can be incorrectly reported as having no active subscription when matches are past the first 100 records.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/stripe/getActiveSubscriptions.ts, line 6:

<comment>This change regresses from auto-pagination to a single-page Stripe query, so accounts can be incorrectly reported as having no active subscription when matches are past the first 100 records.</comment>

<file context>
@@ -3,19 +3,19 @@ import stripeClient from "@/lib/stripe/client";
-      current_period_end: { gt: now },
-    };
-    const matches: Stripe.Subscription[] = [];
+    const subscriptions = await stripeClient.subscriptions.list({
+      limit: 100,
+      current_period_end: {
</file context>
Suggested change
const subscriptions = await stripeClient.subscriptions.list({
limit: 100,
current_period_end: {
gt: Math.floor(Date.now() / 1000),
},
});
const activeSubscriptions =
subscriptions?.data?.filter(
(subscription: Stripe.Subscription) => subscription.metadata?.accountId === accountId,
) ?? [];
const activeSubscriptions: Stripe.Subscription[] = [];
for await (const subscription of stripeClient.subscriptions.list({
current_period_end: {
gt: Math.floor(Date.now() / 1000),
},
})) {
if (subscription.metadata?.accountId === accountId) {
activeSubscriptions.push(subscription);
}
}

- Updated all instances of `sandbox.name` to `sandbox.sandboxId` across multiple files to ensure consistency with the updated API structure.
- Adjusted related tests to reflect the new property naming for sandbox identifiers.
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 9 files (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues 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.

1 participant