feat(api): add GET /api/subscriptions/status#505
feat(api): add GET /api/subscriptions/status#505ahmednahima0-beep wants to merge 5 commits intotestfrom
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis 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 ChangesSubscription Status API
Sandbox ID Identifier Update
Sequence DiagramsequenceDiagram
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
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
Poem
🚥 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 unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 41 minutes and 49 seconds.Comment |
There was a problem hiding this comment.
2 issues found across 10 files
Confidence score: 2/5
- There is a high-confidence, high-severity logic issue in
lib/stripe/isActiveSubscription.ts:isActiveSubscriptionappears 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.tsis low severity (maintainability/style only) and not merge-blocking by itself. - Pay close attention to
lib/stripe/isActiveSubscription.tsandlib/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
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
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-levelz.uuid()standalone.In Zod v4, string formats like
uuidwere promoted to top-levelznamespace functions, andz.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 be10). If any existing account IDs were generated with a non-compliant variant, considerz.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
⛔ Files ignored due to path filters (2)
lib/stripe/__tests__/getSubscriptionStatusHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateGetSubscriptionStatusRequest.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (8)
app/api/subscriptions/status/route.tslib/stripe/getActiveSubscriptionDetails.tslib/stripe/getActiveSubscriptions.tslib/stripe/getOrgSubscription.tslib/stripe/getSubscriptionIsPro.tslib/stripe/getSubscriptionStatusHandler.tslib/stripe/isActiveSubscription.tslib/stripe/validateGetSubscriptionStatusRequest.ts
| export type ValidatedGetSubscriptionStatusRequest = { | ||
| accountId: string; | ||
| }; |
There was a problem hiding this comment.
🛠️ 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:
- File naming: The guideline requires
validate<EndpointName>Query.ts. This file should be namedvalidateSubscriptionStatusQuery.ts(and the function renamed to match). - Schema export: The guideline requires exporting both the Zod schema and the inferred TypeScript type. Currently only
ValidatedGetSubscriptionStatusRequestis 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 unchangedAs 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.
There was a problem hiding this comment.
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.
- 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.
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/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
⛔ Files ignored due to path filters (10)
lib/sandbox/__tests__/createSandbox.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/sandbox/__tests__/createSandboxFromSnapshot.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/sandbox/__tests__/getActiveSandbox.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/sandbox/__tests__/getOrCreateSandbox.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/sandbox/__tests__/getSandboxStatus.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/sandbox/__tests__/processCreateSandbox.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/getActiveSubscriptions.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/getSubscriptionStatusHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/isActiveSubscription.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/stripe/__tests__/validateSubscriptionStatusQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (10)
lib/sandbox/createSandbox.tslib/sandbox/createSandboxFromSnapshot.tslib/sandbox/getActiveSandbox.tslib/sandbox/getOrCreateSandbox.tslib/sandbox/getSandboxStatus.tslib/sandbox/processCreateSandbox.tslib/stripe/getActiveSubscriptions.tslib/stripe/getSubscriptionStatusHandler.tslib/stripe/isActiveSubscription.tslib/stripe/validateSubscriptionStatusQuery.ts
✅ Files skipped from review due to trivial changes (1)
- lib/stripe/getActiveSubscriptions.ts
| await insertAccountSandbox({ | ||
| account_id: accountId, | ||
| sandbox_id: sandbox.name, | ||
| sandbox_id: sandbox.sandboxId, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| export const subscriptionStatusQuerySchema = z.object({ | ||
| accountId: z | ||
| .string({ message: "accountId is required" }) | ||
| .min(1, "accountId is required") | ||
| .uuid("accountId must be a valid UUID"), | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "package.json" -type f | head -5Repository: 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.tsRepository: 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:
- 1: https://v4.zod.dev/error-customization
- 2: https://v4.zod.dev/v4/changelog?id=error-customization
- 3: https://zod.dev/error-customization
- 4: [v4] error behaves different than errorMap no longer give us a default error message colinhacks/zod#4893
- 5: https://v4.zod.dev/v4?id=simplified-error-customization
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>
| 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 }); |
There was a problem hiding this comment.
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>
| 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.
There was a problem hiding this comment.
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.
| 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, | ||
| ) ?? []; |
There was a problem hiding this comment.
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>
| 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.
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
Stripesubscriptions for the account or its orgs, with strict auth and CORS.New Features
Next.jsroute and handler with CORS preflight.accountIdUUID,x-api-keyorAuthorization: Bearer, and account override rules.isProfrom activeStripesubs (account or org): filters bymetadata.accountIdandcurrent_period_end > now; active = not a canceled trial. Returns 400/401/403/500. Tests added.Refactors
sandboxIdacross code/tests;Sandbox.get({ sandboxId }).Written for commit 24a5b5a. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Updates