Skip to content

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

Merged
sweetmantech merged 14 commits intotestfrom
feature/api-subscriptions-status-get
May 6, 2026
Merged

feat(api): add GET /api/subscriptions/status#506
sweetmantech merged 14 commits intotestfrom
feature/api-subscriptions-status-get

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

Added GET /api/accounts/{id}/subscription to return an account’s subscription status and source, replacing the old query-param endpoint. Returns { isPro, status, plan, source } based on the account’s own or org-backed subscription.

  • New Features

    • GET and OPTIONS handlers with CORS.
    • Path param id (UUID) and x-api-key or Bearer auth; forwards { error } on failure.
    • Chooses the first active subscription: prefer account, else organization; active = active or trialing without canceled_at.
    • Stripe lookups: paginates 100/page with no fixed cap; stops on first page with a match or if the cursor is stuck; org scan stops at first match.
    • Tests for route/handler/validator and Stripe helpers, including pagination, canceled trials, and unsupported statuses.
  • Refactors

    • Removed unused stripeCustomerId from subscription lookups.
    • Extracted toStatus to normalize Stripe statuses to the API enum.

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

Summary by CodeRabbit

  • New Features
    • Added an API endpoint to fetch real-time subscription status and determine pro account eligibility.
    • Checks both direct account subscriptions and linked organization subscriptions for active plans.
    • Includes request validation and authentication checks to ensure secure access.
    • Improved error handling and consistent JSON responses for subscription queries.

…x v2.0.0-beta.11

This commit merges the latest changes from the main branch and ensures compatibility with the updated @vercel/sandbox version. The API has been adjusted to reflect the renaming of Sandbox.sandboxId to Sandbox.name, with corresponding updates to method parameters. All relevant tests have been updated to mock the new Sandbox structure and verify functionality.

Verification steps have been executed successfully, confirming no issues with installation, type checking, linting, or 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 Ready Ready Preview May 6, 2026 1:53pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 9 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ 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: 5123674d-a29d-4bc6-ac14-268fd28422a7

📥 Commits

Reviewing files that changed from the base of the PR and between 202fa58 and c1ebf26.

⛔ Files ignored due to path filters (8)
  • app/api/accounts/[id]/subscription/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/accounts/[id]/subscription/__tests__/routeTestMocks.ts is excluded by !**/__tests__/** and included by app/**
  • lib/stripe/__tests__/buildSubscriptionResponse.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/getAccountSubscriptionHandler.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__/getActiveSubscriptionsTestHelpers.ts is excluded by !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/toStatus.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/stripe/__tests__/validateAccountSubscriptionParams.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • app/api/accounts/[id]/subscription/route.ts
  • lib/stripe/buildSubscriptionResponse.ts
  • lib/stripe/getAccountSubscriptionHandler.ts
  • lib/stripe/getActiveSubscriptions.ts
  • lib/stripe/toStatus.ts
  • lib/stripe/validateAccountSubscriptionParams.ts
📝 Walkthrough

Walkthrough

Adds a new Next.js API route and supporting server utilities to validate requests, query Stripe for active subscriptions (account and organization), determine pro status, and return JSON with CORS headers. Routes are forced dynamic and caching is disabled.

Changes

Subscription status flow

Layer / File(s) Summary
Request Validation
lib/stripe/validateGetSubscriptionStatusRequest.ts
Parses accountId from query params, validates as UUID via zod, calls validateAuthContext, and returns either a typed { accountId } or a NextResponse error with CORS headers.
Active-subscription query
lib/stripe/getActiveSubscriptions.ts
Paginates Stripe subscriptions (limit 100), filters by current_period_end > now and metadata?.accountId === accountId, accumulates matching subscriptions, returns array or [] on error.
Account-level helper
lib/stripe/getActiveSubscriptionDetails.ts
Calls getActiveSubscriptions(accountId) and returns the first subscription or null; logs and returns null on error.
Organization-level lookup
lib/stripe/getOrgSubscription.ts
Fetches linked organizations for an account, iterates organization IDs sequentially, calls getActiveSubscriptionDetails(orgId) and returns the first non-null subscription or null.
Subscription status check
lib/stripe/isActiveSubscription.ts
Determines whether a subscription is active: returns true for status === "active", true for trialing when not canceled, otherwise false.
Handler orchestration
lib/stripe/getSubscriptionStatusHandler.ts
Validates request, concurrently fetches account and org subscription details, computes isPro from either source, and returns { isPro } JSON with CORS headers; catches errors and returns 500 with CORS.
API route
app/api/subscriptions/status/route.ts
Exports OPTIONS() returning 200 with CORS headers, GET(request) delegating to getSubscriptionStatusHandler, and route config: dynamic = "force-dynamic", fetchCache = "force-no-store", revalidate = 0.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Route as /api/subscriptions/status
    participant Validator as validateGetSubscriptionStatusRequest
    participant Auth as validateAuthContext
    participant Handler as getSubscriptionStatusHandler
    participant AcctSub as getActiveSubscriptionDetails
    participant OrgSub as getOrgSubscription
    participant Stripe as Stripe API

    Client->>Route: GET ?accountId=...
    Route->>Validator: validate request (query + auth)
    Validator->>Auth: check auth context
    alt auth fails
        Auth-->>Validator: failure response
        Validator-->>Route: 400/401 NextResponse (CORS)
        Route-->>Client: error JSON
    else auth succeeds
        Validator-->>Route: { accountId }
        Route->>Handler: handle request
        Handler->>AcctSub: fetch account subscription (concurrent)
        Handler->>OrgSub: fetch org subscription (concurrent)
        AcctSub->>Stripe: list subscriptions (paginated)
        OrgSub->>Stripe: list subscriptions (paginated)
        Stripe-->>AcctSub: subscriptions
        Stripe-->>OrgSub: subscriptions
        AcctSub-->>Handler: first active or null
        OrgSub-->>Handler: first active or null
        Handler->>Handler: compute isPro
        Handler-->>Route: 200 { isPro } (CORS)
        Route-->>Client: 200 JSON { isPro }
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • sweetmantech

Poem

✨ A tiny route sends queries out to Stripe,
Checks accounts and orgs to see who’s in the light,
Validation whispers, CORS waves hello,
isPro returns true or false in JSON flow—
Server-side checks making billing right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning getOrgSubscription.ts lacks try-catch error handling for async getAccountOrganizations() call, while getActiveSubscriptionDetails.ts properly wraps async calls—creating inconsistent error-handling patterns that reduce robustness and violate SOLID principles. Add try-catch wrapping around getAccountOrganizations() in getOrgSubscription.ts with console.error logging and null return on error, matching getActiveSubscriptionDetails.ts pattern. Extract duplicate schema validation error-response code into shared utility function handleZodValidationError.ts.
✅ 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-subscriptions-status-get

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

❤️ Share

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

4 issues found across 11 files

Confidence score: 2/5

  • There is a high-confidence logic bug in lib/stripe/isActiveSubscription.ts: isActiveSubscription can return true for inactive non-trial statuses (like canceled), which creates clear user-facing subscription state errors.
  • lib/stripe/getActiveSubscriptions.ts only reads the first Stripe page (limit: 100), so organizations with subscriptions beyond page 1 may be treated as unsubscribed; this is a concrete regression risk in production data.
  • Risk is elevated further because app/api/subscriptions/status/__tests__/route.test.ts currently adds only a handler-export smoke test, so the new endpoint behavior is not strongly validated, and lib/stripe/getOrgSubscription.ts adds avoidable parallel API load/latency.
  • Pay close attention to lib/stripe/isActiveSubscription.ts, lib/stripe/getActiveSubscriptions.ts, app/api/subscriptions/status/__tests__/route.test.ts - incorrect active-state logic, incomplete pagination, and weak behavioral test coverage are the main merge risks.
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/getOrgSubscription.ts">

<violation number="1" location="lib/stripe/getOrgSubscription.ts:18">
P2: Avoid fetching subscriptions for all orgs in parallel when only the first non-null result is needed; this causes unnecessary Stripe API calls and extra latency.</violation>
</file>

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

<violation number="1" location="lib/stripe/isActiveSubscription.ts:7">
P1: `isActiveSubscription` returns `true` for non-trial inactive statuses (e.g. canceled) because it only negates `isCanceledTrial` instead of checking allowed active statuses.</violation>
</file>

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

<violation number="1" location="lib/stripe/getActiveSubscriptions.ts:6">
P1: This only checks the first page of Stripe subscriptions (`limit: 100`), so valid subscriptions beyond page 1 can be missed. Paginate through all pages (or use Stripe auto-pagination) before filtering by `metadata.accountId`.</violation>
</file>

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

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

New test is only a handler-export smoke test and does not validate the endpoint behavior introduced by the PR.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant Handler as Subscription Handler
    participant Auth as Auth Service
    participant DB as Supabase
    participant Stripe as Stripe API

    Note over Client,Stripe: GET /api/subscriptions/status?accountId={uuid}

    Client->>Handler: NEW: Request with x-api-key or Bearer token
    
    rect rgb(240, 240, 240)
    Note right of Handler: Validation Phase
    Handler->>Handler: Validate accountId is UUID
    Handler->>Auth: NEW: validateAuthContext(request, accountId)
    
    alt Auth Success
        Auth-->>Handler: Return AuthContext (accountId, orgId, token)
    else Auth Failure
        Auth-->>Handler: Return 401/403 NextResponse
        Handler-->>Client: Error JSON (mapped status)
    end
    end

    rect rgb(230, 245, 255)
    Note right of Handler: Subscription Lookup (Concurrent)
    par Personal Check
        Handler->>Stripe: NEW: list({ metadata: { accountId } })
        Stripe-->>Handler: Personal subscriptions
    and Organization Check
        Handler->>DB: NEW: getAccountOrganizations({ accountId })
        DB-->>Handler: List of organization_ids
        loop For each Org
            Handler->>Stripe: NEW: list({ metadata: { accountId: orgId } })
            Stripe-->>Handler: Org subscriptions
        end
    end
    end

    rect rgb(240, 240, 240)
    Note right of Handler: Business Logic
    Handler->>Handler: NEW: isActiveSubscription()
    Note over Handler: Check status (active/trialing) && !canceled_trial
    
    alt Is Pro (Personal or Org)
        Handler-->>Client: 200 OK { isPro: true } + CORS Headers
    else Not Pro
        Handler-->>Client: 200 OK { isPro: false } + CORS Headers
    end
    end

    opt Unhandled Exception
        Handler-->>Client: 500 Internal Server Error
    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 Outdated
Comment thread lib/stripe/getActiveSubscriptions.ts Outdated
Comment thread lib/stripe/getOrgSubscription.ts Outdated
Comment thread app/api/subscriptions/status/__tests__/route.test.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
app/api/subscriptions/status/route.ts (1)

1-29: Route integration tests are minimal; consider expanding them.

Tests do exist for the handler and validation layer (getSubscriptionStatusHandler.test.ts and validateGetSubscriptionStatusRequest.test.ts), and they provide good coverage of the subscription logic. However, app/api/subscriptions/status/__tests__/route.test.ts only verifies that handlers are exported—it doesn't invoke them with actual NextRequest objects.

Consider adding route-level integration tests that call GET and OPTIONS with real requests to catch any edge cases at the API boundary (request parsing, response formatting, CORS headers). This complements the existing handler tests and strengthens the overall test suite.

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

In `@app/api/subscriptions/status/route.ts` around lines 1 - 29, The route-level
tests currently only check exports; add integration tests that call the exported
OPTIONS and GET functions with real NextRequest objects to validate request
parsing, CORS headers, and response formatting: for OPTIONS invoke OPTIONS() and
assert status 200 and headers match getCorsHeaders(), and for GET construct
NextRequest instances (including query/accountId scenarios and error cases) and
assert the JSON body/status returned by GET(request) matches expected outputs
(you can reuse/match behavior covered by getSubscriptionStatusHandler); if
needed, mock or stub dependencies used by getSubscriptionStatusHandler to
simulate paid/unpaid/org cases so the GET route-level tests exercise the full
API boundary.
🤖 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-15: The current call to stripeClient.subscriptions.list in
getActiveSubscriptions.ts fetches up to 100 global subscriptions and can miss
the target account’s subscription; replace the naive list call with a targeted
search (use stripeClient.subscriptions.search with a query on
metadata.accountId:"{accountId}" and status:"active") so you only return
subscriptions for the given accountId, and if your Stripe SDK/version lacks
subscriptions.search fallback to listing by customer
(stripeClient.subscriptions.list with customer: stripeCustomerId and status:
"active") and then filter by metadata.accountId before producing
activeSubscriptions.

In `@lib/stripe/getOrgSubscription.ts`:
- Around line 18-20: getOrgSubscription currently always returns null because
getActiveSubscriptions filters on subscription.metadata?.accountId but
createStripeSession only sets metadata.accountId to a billing account ID, not an
organization ID; fix by creating an org-specific session creation and lookup:
add a new createOrgStripeSession (or extend createStripeSession with an org
flag) that sets metadata.organizationId (or metadata.orgId) when creating org
subscriptions, and update getActiveSubscriptions and
getActiveSubscriptionDetails (and getOrgSubscription) to look for
subscription.metadata.organizationId (or orgId) when resolving organization
subscriptions so org lookups can actually match real subscription metadata.

In `@lib/stripe/isActiveSubscription.ts`:
- Around line 3-9: The isActiveSubscription function currently treats any
non-trial, non-canceled subscription as active (returning true for statuses like
"past_due", "unpaid", "paused", etc.); update isActiveSubscription to explicitly
check subscription.status against the set of allowed active statuses (e.g.,
"active" and "trialing", and include "past_due" only if you intend to grant pro
access during retry windows) instead of relying on the isTrial/isCanceledTrial
logic—use subscription.status comparisons to decide subscriptionActive and
return that result (refer to isActiveSubscription, subscription.status, isTrial,
isCanceledTrial, subscriptionActive to locate and modify the logic).

In `@lib/stripe/validateGetSubscriptionStatusRequest.ts`:
- Around line 16-17: Rename the file
lib/stripe/validateGetSubscriptionStatusRequest.ts to
lib/stripe/validateGetSubscriptionStatusQuery.ts and rename the exported
function validateGetSubscriptionStatusRequest to
validateGetSubscriptionStatusQuery; update all imports/usages to reference
validateGetSubscriptionStatusQuery so callers load the new module name and
exported identifier, and ensure the file now follows the
validate<EndpointName>Query.ts convention for query-parameter validators.
- Around line 8-14: Export the existing querySchema and replace the hand-written
ValidatedGetSubscriptionStatusRequest with an exported type derived from the Zod
schema using z.infer; specifically make querySchema exported, remove the manual
type declaration, and add export type ValidatedGetSubscriptionStatusRequest =
z.infer<typeof querySchema> so the type always matches the schema (referencing
querySchema and ValidatedGetSubscriptionStatusRequest).

---

Nitpick comments:
In `@app/api/subscriptions/status/route.ts`:
- Around line 1-29: The route-level tests currently only check exports; add
integration tests that call the exported OPTIONS and GET functions with real
NextRequest objects to validate request parsing, CORS headers, and response
formatting: for OPTIONS invoke OPTIONS() and assert status 200 and headers match
getCorsHeaders(), and for GET construct NextRequest instances (including
query/accountId scenarios and error cases) and assert the JSON body/status
returned by GET(request) matches expected outputs (you can reuse/match behavior
covered by getSubscriptionStatusHandler); if needed, mock or stub dependencies
used by getSubscriptionStatusHandler to simulate paid/unpaid/org cases so the
GET route-level tests exercise the full API boundary.
🪄 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: 7c94c870-dbab-4042-9d7d-6389d3ee4cd1

📥 Commits

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

⛔ Files ignored due to path filters (4)
  • app/api/subscriptions/status/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/subscriptions/status/__tests__/routeTestMocks.ts is excluded by !**/__tests__/** and included by app/**
  • 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 (7)
  • app/api/subscriptions/status/route.ts
  • lib/stripe/getActiveSubscriptionDetails.ts
  • lib/stripe/getActiveSubscriptions.ts
  • lib/stripe/getOrgSubscription.ts
  • lib/stripe/getSubscriptionStatusHandler.ts
  • lib/stripe/isActiveSubscription.ts
  • lib/stripe/validateGetSubscriptionStatusRequest.ts

Comment thread lib/stripe/getActiveSubscriptions.ts Outdated
Comment thread lib/stripe/getOrgSubscription.ts Outdated
Comment on lines +18 to +20
const subscriptions = await Promise.all(orgIds.map(orgId => getActiveSubscriptionDetails(orgId)));

return subscriptions.find(sub => sub !== null) ?? null;
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:

#!/bin/bash
# Find any code that creates a Stripe subscription/session with an organizationId or orgId
# in the metadata, to verify whether org subscriptions can be found by getOrgSubscription.
rg -n --type=ts -C4 'metadata.*org' 
rg -n --type=ts -C4 'subscription_data.*org'
rg -n --type=ts -C4 'createStripeSession\|checkout.*session.*create' | grep -i 'org'

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

fd -t f -n 'getOrgSubscription|getActiveSubscriptionDetails|getActiveSubscriptions|createStripeSession' lib/stripe/

Repository: recoupable/api

Length of output: 289


🏁 Script executed:

cat -n lib/stripe/getOrgSubscription.ts

Repository: recoupable/api

Length of output: 1039


🏁 Script executed:

rg -n 'getOrgSubscription' --type=ts -B2 -A2

Repository: recoupable/api

Length of output: 4446


🏁 Script executed:

fd -t f 'getActiveSubscriptionDetails' lib/stripe/ && cat -n lib/stripe/getActiveSubscriptionDetails.ts

Repository: recoupable/api

Length of output: 579


🏁 Script executed:

fd -t f 'getActiveSubscriptions' lib/stripe/ && cat -n lib/stripe/getActiveSubscriptions.ts

Repository: recoupable/api

Length of output: 899


🏁 Script executed:

fd -t f 'createStripeSession' lib/stripe/ && head -100 lib/stripe/createStripeSession.ts

Repository: recoupable/api

Length of output: 950


getOrgSubscription silently returns null because subscriptions are never created with organization IDs in metadata.

getActiveSubscriptionDetails(orgId) calls getActiveSubscriptions(orgId), which filters subscriptions by subscription.metadata?.accountId === orgId (line 14 of getActiveSubscriptions.ts). However, the only subscription creation flow in the codebase (createStripeSession) sets metadata.accountId to the billing account ID, never an organization ID.

When getOrgSubscription searches for organization subscriptions, the filter in getActiveSubscriptions never matches because no subscription is created with an organization ID in metadata.accountId. This makes the org subscription path (used in getSubscriptionStatusHandler.ts) non-functional—it will always return null.

To enable organization subscriptions, either:

  1. Create a separate function (e.g., createOrgStripeSession) that explicitly sets metadata.organizationId or metadata.orgId, and update getOrgSubscription to search by that field; or
  2. Refactor getOrgSubscription to use an alternative lookup mechanism.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/stripe/getOrgSubscription.ts` around lines 18 - 20, getOrgSubscription
currently always returns null because getActiveSubscriptions filters on
subscription.metadata?.accountId but createStripeSession only sets
metadata.accountId to a billing account ID, not an organization ID; fix by
creating an org-specific session creation and lookup: add a new
createOrgStripeSession (or extend createStripeSession with an org flag) that
sets metadata.organizationId (or metadata.orgId) when creating org
subscriptions, and update getActiveSubscriptions and
getActiveSubscriptionDetails (and getOrgSubscription) to look for
subscription.metadata.organizationId (or orgId) when resolving organization
subscriptions so org lookups can actually match real subscription metadata.

Comment thread lib/stripe/isActiveSubscription.ts
Comment thread lib/stripe/validateGetSubscriptionStatusRequest.ts Outdated
Comment thread lib/stripe/validateGetSubscriptionStatusRequest.ts Outdated
- Refactored the isActiveSubscription function to improve clarity and efficiency in determining subscription status.
- Added comprehensive unit tests for isActiveSubscription to cover various subscription states, including active, trialing, and canceled scenarios.
- Ensured that the function correctly handles null or undefined inputs, returning false as expected.
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 2 files (changes from recent commits).

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

… enhance filtering

- Refactored getActiveSubscriptions to implement pagination for fetching active subscriptions from Stripe, allowing for more than 100 results.
- Introduced a constant PAGE_LIMIT for better maintainability of the subscription listing limit.
- Enhanced filtering logic to ensure only subscriptions matching the specified accountId are returned.
- Improved error handling to log issues encountered during the subscription fetching process.
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 2 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:13">
P1: Unbounded pagination through all Stripe subscriptions without a safety limit or customer filter. This loop will make N/100 sequential API calls for every subscription in the entire Stripe account. Consider adding a `customer` param to scope the query, or at minimum a max-pages safeguard to prevent runaway API calls and request timeouts.</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
…subscription

- Changed the implementation of getOrgSubscription to iterate through organization IDs and return the first active subscription found, improving efficiency by eliminating unnecessary parallel requests.
- Removed the previous logic that collected all subscriptions and filtered them, simplifying the function's flow.
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 2 files (changes from recent commits).

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

- Added tests for OPTIONS handler to verify it returns 200 status with CORS headers.
- Implemented tests for GET handler to ensure it correctly forwards requests to getSubscriptionStatusHandler and returns the expected response.
- Introduced beforeEach hook to clear mocks before each test, improving test isolation.
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

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

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

- Exported the querySchema for use in other modules, enhancing reusability.
- Simplified the ValidatedGetSubscriptionStatusRequest type by inferring it directly from querySchema, improving type safety and maintainability.
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

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

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

…quest validation

- Renamed `validateGetSubscriptionStatusRequest` to `validateGetSubscriptionStatusQuery` for clarity and consistency.
- Updated references in `getSubscriptionStatusHandler` and related tests to use the new validation function.
- Removed the deprecated `validateGetSubscriptionStatusRequest` file and its 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.

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

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

…improve filtering

- Introduced a maximum page limit for `subscriptions.list` calls to prevent excessive latency on large accounts.
- Updated the function to stop fetching after the first page that contains a matching subscription, optimizing performance.
- Added support for an optional `stripeCustomerId` parameter to scope the subscription list to a specific customer.
- Enhanced unit tests to cover new functionality, including early termination on matches and pagination limits.
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 2 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:22">
P1: The hard 50-page cap can incorrectly return no subscription when matches exist beyond the cutoff.</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
…or improved match retrieval

- Eliminated the fixed page limit for `subscriptions.list` calls, allowing the function to paginate until all matches are found.
- Updated the logic to break pagination if the cursor does not advance, preventing infinite loops.
- Enhanced unit tests to verify behavior with no artificial page limits and to ensure correct handling of pagination scenarios.
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 2 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/__tests__/getActiveSubscriptions.test.ts">

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

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

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

Comment thread lib/stripe/__tests__/getActiveSubscriptions.test.ts
…mock handling

- Simplified test setup by consolidating mock definitions for `stripeClient.subscriptions.list`.
- Enhanced readability and maintainability of tests by using helper functions for mock data generation.
- Ensured consistent behavior across tests by standardizing the way mock responses are defined and utilized.
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 2 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/__tests__/getActiveSubscriptionsTestHelpers.ts">

<violation number="1" location="lib/stripe/__tests__/getActiveSubscriptionsTestHelpers.ts:5">
P2: Custom agent: **Module should export a single primary function whose name matches the filename**

Module exports multiple top-level functions and none matches the filename.</violation>
</file>

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

Comment thread lib/stripe/__tests__/getActiveSubscriptionsTestHelpers.ts Outdated
…improved clarity

- Consolidated subscription-related helper functions into a single `getActiveSubscriptionsTestHelpers` function.
- Enhanced test readability by using destructured imports for mock data generation.
- Improved maintainability of tests by centralizing mock definitions and reducing redundancy.
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 2 files (changes from recent commits).

Requires human review: This PR introduces a new feature with non-trivial business logic for determining subscription status, involving Stripe API pagination and organization lookups.

…scription

Aligns the implementation with recoupable/docs#183: documents subscription
status as a resource nested under the account it belongs to, identifies the
account via path param, and returns the documented response shape.

- New route: app/api/accounts/[id]/subscription
- New response: { isPro, status, plan, source } (was { isPro })
- New handler/validator/mapper with unit tests covering account-active,
  org-active, neither-active, trialing-with-canceled_at, and unsupported
  Stripe statuses
- Deletes the old query-param endpoint and helpers
@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented May 6, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Comment thread lib/stripe/getActiveSubscriptions.ts Outdated
* When `stripeCustomerId` is set, scopes the Stripe list to that customer.
* Paginates until Stripe reports no more pages (no fixed page cap — avoids missing matches deep in the list).
*/
export const getActiveSubscriptions = async (accountId: string, stripeCustomerId?: string) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

YAGNI

  • actually: unused stripeCustomerId is included in getActiveSubscriptions function.
  • required: remove the unused stripeCustomerId to follow YAGNI principle.

Comment thread lib/stripe/buildSubscriptionResponse.ts Outdated
"past_due",
]);

function toStatus(stripeStatus: Stripe.Subscription.Status): SubscriptionStatus {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SRP - new lib file for toStatus.ts

…ts own file

- YAGNI: getActiveSubscriptions no longer accepts the unused stripeCustomerId
  parameter; corresponding test removed.
- SRP: toStatus (Stripe status → SubscriptionStatus enum) lives in its own
  module with focused unit tests; buildSubscriptionResponse now imports it.
@sweetmantech
Copy link
Copy Markdown
Contributor

Manual preview testing

Preview deployment: https://api-ngnpd6vm8-recoup.vercel.app
Commit tested: c1ebf265 (HEAD as of testing)

Setup

Two API keys were used to exercise both branches of the response mapping:

  1. Inactive account — created on the preview via the unauthenticated agent signup flow (POST /api/agents/signup), per https://developers.recoupable.com.
  2. Active subscription account — used a key with an active Stripe trial.

accountId for each was resolved via GET /api/accounts/id so reviewers can reproduce without leaking keys.

Test matrix

# Case Method/Path Expected Actual
1 Active direct subscription GET /api/accounts/{id}/subscription 200 200 {"isPro":true,"status":"trialing","plan":"pro","source":"account"}
2 No subscription GET /api/accounts/{id}/subscription 200 200 {"isPro":false,"status":"none","plan":null,"source":null}
3 Invalid UUID path param GET /api/accounts/not-a-uuid/subscription 400 400 {"error":"id must be a valid UUID"}
4 Missing auth GET /api/accounts/{id}/subscription (no headers) 401 401 {"error":"Exactly one of x-api-key or Authorization must be provided"}
5 Other account's UUID (not in caller's org) GET /api/accounts/00000000-0000-0000-0000-000000000000/subscription 403 403 {"error":"Access denied to specified account_id"}
6 CORS preflight OPTIONS /api/accounts/{id}/subscription 200 + CORS 200 with Access-Control-Allow-{Origin,Methods,Headers}
7 Old query-param endpoint removed GET /api/subscriptions/status?accountId=... 404 404

Reproduce

PREVIEW=https://api-ngnpd6vm8-recoup.vercel.app

# Create a fresh agent + key (no auth required)
curl -sS -X POST "$PREVIEW/api/agents/signup" \
  -H 'Content-Type: application/json' \
  -d "{\"email\":\"agent+$(date +%s)@recoupable.com\"}"
# -> { account_id, api_key }

# Resolve account_id from a key (alternative)
curl -sS "$PREVIEW/api/accounts/id" -H "x-api-key: <KEY>"

# Inactive case
curl -sS "$PREVIEW/api/accounts/<ACCOUNT_ID>/subscription" -H "x-api-key: <KEY>"
# -> {"isPro":false,"status":"none","plan":null,"source":null}

# Active case — use a key with an active sub
curl -sS "$PREVIEW/api/accounts/<ACCOUNT_ID>/subscription" -H "x-api-key: <KEY>"
# -> {"isPro":true,"status":"trialing","plan":"pro","source":"account"}

Notes for reviewer

  • Response shape exactly matches recoupable/docs#183's SubscriptionResponse schema for both active and inactive states.
  • Stripe status flows through toStatus unchanged for supported values (active, trialing, canceled, past_due); unsupported Stripe statuses (incomplete, unpaid, etc.) collapse to "none" — covered by lib/stripe/__tests__/toStatus.test.ts.
  • source: "organization" was not exercised on preview (would require an account whose only active subscription is via an org). It is covered by unit tests in lib/stripe/__tests__/buildSubscriptionResponse.test.ts and lib/stripe/__tests__/getAccountSubscriptionHandler.test.ts.
  • Old /api/subscriptions/status route is fully removed (404), as expected.

@sweetmantech sweetmantech merged commit d0aa31d into test May 6, 2026
5 checks passed
@sweetmantech sweetmantech deleted the feature/api-subscriptions-status-get branch May 6, 2026 14:04
sweetmantech added a commit to recoupable/chat that referenced this pull request May 6, 2026
The Recoup API endpoint moved from GET /api/subscriptions/status?accountId=
to GET /api/accounts/{id}/subscription as part of recoupable/api#506 and
recoupable/docs#183. Hitting the old path now returns 404, which would leave
all paying users with isSubscribed=false. ProStatusResponse stays narrow
({ isPro }); the wider response shape from the api side is ignored by
structural typing.
sweetmantech added a commit to recoupable/chat that referenced this pull request May 6, 2026
…requests to Recoup API (#1729)

* refactor(subscription): update subscription status endpoint to proxy requests to Recoup API

- Enhanced the GET /api/subscription/status endpoint to proxy requests to the Recoup API, allowing for streamlined access to subscription status.
- Improved error handling and response formatting using NextResponse.
- Updated the fetch logic in the useProStatus hook to include authorization headers and handle access tokens via Privy authentication.
- Ensured that the query is only executed when the user is authenticated and has a valid account ID.

* fix(subscription): improve error handling in subscription status endpoint

- Added try-catch block around the fetch call to handle upstream fetch failures gracefully.
- Updated the response to return a 502 Bad Gateway status with an error message when the fetch fails.

* fix(subscription): enhance error logging and response handling in subscription status endpoint

- Updated error handling to log the specific error message when upstream fetch fails.
- Adjusted response logic to return null for 204 and 304 status codes, improving response consistency.

* refactor(subscription): remove deprecated subscription status endpoint

- Deleted the /api/subscription/status endpoint as it is no longer needed.
- Updated the useProStatus hook to directly fetch from the Recoup API, improving code clarity and reducing redundancy.

* fix(subscription): point useProStatus at the new account-scoped endpoint

The Recoup API endpoint moved from GET /api/subscriptions/status?accountId=
to GET /api/accounts/{id}/subscription as part of recoupable/api#506 and
recoupable/docs#183. Hitting the old path now returns 404, which would leave
all paying users with isSubscribed=false. ProStatusResponse stays narrow
({ isPro }); the wider response shape from the api side is ignored by
structural typing.

---------

Co-authored-by: Sweets Sweetman <sweetmantech@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants