Skip to content

Add team-shared extra usage with admin pool and per-member caps#443

Merged
ross0x01 merged 12 commits into
mainfrom
team-extra-usage
May 16, 2026
Merged

Add team-shared extra usage with admin pool and per-member caps#443
ross0x01 merged 12 commits into
mainfrom
team-extra-usage

Conversation

@ross0x01
Copy link
Copy Markdown
Contributor

@ross0x01 ross0x01 commented May 14, 2026

Summary

  • Team admins can fund a shared extra-usage pool billed to the org's existing Stripe customer; any team member draws from it once the team subscription bucket is exhausted.
  • Admin controls: team-wide monthly cap, per-member monthly $ cap, member-level disable switch, per-member spend visibility (modeled on Cursor's Enterprise admin controls, more granular than its standard Teams plan).
  • Hard cutoff on cap hit, auto-reload on the org customer, trust-tier ladder applied at the org level, prepaid model that reuses the existing Stripe Checkout + webhook idempotency machinery.

What changed

Backend

  • New Convex tables team_extra_usage (org pool) and team_member_usage (per-member caps + spend), keyed by organization_id.
  • New convex/teamExtraUsage.ts (queries/mutations) and convex/teamExtraUsageActions.ts (Stripe Checkout + auto-reload). Mirrors the per-user path but charges the org's Stripe customer.
  • Rate limiter (lib/rate-limit/token-bucket.ts) branches on subscription === "team" && organizationId for deduct, refund, and post-stream adjustments. Three new failure messages: team pool disabled, member disabled, member cap hit.
  • buildExtraUsageConfig reads team state for team users; their personal extra_usage_enabled flag is ignored.

API routes (admin-gated via WorkOS `role.slug === "admin"`)

  • `GET/POST /api/team/extra-usage` — read admin view + member list / update team-pool settings
  • `POST /api/team/extra-usage/purchase` — create Stripe Checkout against the org customer
  • `GET /api/team/extra-usage/confirm` — post-checkout credit (idempotent with webhook)
  • `POST /api/team/extra-usage/webhook` — Stripe webhook safety net
  • `PATCH /api/team/extra-usage/members/:userId` — per-member cap / disabled flag

UI

  • New `TeamExtraUsageSection` mounted inside the Team tab for admins: pool toggle, balance, monthly progress, team cap, auto-reload, buy credits, and a per-member limits list with inline edit dialog. Reuses the existing extra-usage dialog components.
  • Personal Extra Usage tab is already hidden for team subscriptions in `SettingsDialog` — no member-facing changes needed.

Plan: `~/.claude/plans/create-a-plan-for-magical-moonbeam.md`.

Test plan

  • `npx convex codegen` succeeds and new tables appear in Convex dashboard
  • Admin enables team extra usage, buys $20 via Stripe Checkout, balance reflects in team pool
  • As a team member, drain the $40/seat subscription bucket and verify the next request draws from the team pool; `team_member_usage.monthly_spent_points` increments
  • Set a $5 per-member cap on member A; their requests stop with the "team-set monthly limit" error after $5 spent, even with positive team balance
  • Set `disabled = true` on member B; they get the "team admin paused your access" error
  • Configure auto-reload at $5 / $20; drain below $5 and confirm Stripe invoice posts to the org's customer (not any user's)
  • Trigger a request failure on a team member drawing from the pool; team balance and member's monthly spent are restored
  • Replay a Stripe `checkout.session.completed` for a team purchase; balance credits exactly once
  • Existing personal extra-usage flow for Pro/Pro+/Ultra users is byte-identical to today

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Organizations can buy shared extra-usage credits via checkout with confirmation redirects.
    • Admins get an “Extra Usage” section to manage pool state, buy credits, set caps, enable auto-reload, and edit per-member limits/disable status.
    • Settings dialog adds a Members tab and shows Extra Usage only to team admins.
    • Team member management endpoints now use org-scoped admin/team authorization.
  • Bug Fixes / Reliability

    • Webhooks and purchases are idempotent; auto-reload, refunds, and deductions are more robust.
  • Tests

    • Comprehensive tests added for team credit/debit/refund flows and config logic.

Review Change Stack

Lets a team admin fund a shared overflow pool that any member can draw from
once the team subscription bucket is exhausted, with admin-set per-member
monthly caps, a member-level disable switch, a team-wide cap, and a spend
dashboard. Auto-reload bills the org's existing Stripe customer instead of
individual users so the team's billing stays unified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

Project Deployment Actions Updated (UTC)
hackerai Ready Ready Preview, Comment May 16, 2026 9:08pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a team-scoped extra-usage system: Convex schema and server logic for a shared team credit pool with per-member controls, Stripe Checkout purchase + webhook handlers, admin API/UI to manage pool/settings/members, and client/server wiring so rate-limited usage can debit/refund an organization pool with auto-reload support.

Changes

Team Extra Usage Pool & Admin System

Layer / File(s) Summary
Team Extra Usage Schema & Data Models
convex/schema.ts, convex/teamExtraUsage.ts
Adds team_extra_usage and team_member_usage tables, point↔dollar helpers, month-reset helpers, and model bootstrapping helpers.
Team Billing Core Mutations & Queries
convex/teamExtraUsage.ts
addTeamCredits, deductTeamPoints, refundTeamPoints, getTeamExtraUsageStateForBackend, getTeamExtraUsageAdminView, updateTeamExtraUsageSettings, updateTeamMemberUsage, and internal recordTeamAutoReloadOutcome.
Stripe Checkout & Auto-Reload Actions
convex/teamExtraUsageActions.ts
Lazy Stripe/WorkOS init, customer/payment-method helpers, createAutoReloadInvoice, createTeamPurchaseSession, and deductWithAutoReloadForTeam orchestrating charge→credit→deduct flows.
Admin Request Guards
app/api/team/team-auth.ts
requireTeamOrg and requireAdminOrg guards that enforce team subscription and admin membership via WorkOS, returning org/user context or NextResponse failures.
Admin API Routes
app/api/team/extra-usage/{route.ts,purchase/route.ts,confirm/route.ts,webhook/route.ts,members/[userId]/route.ts}
GET/POST /api/team/extra-usage (admin view/settings), POST /purchase (create Checkout session), GET /confirm (Checkout return landing + idempotent credit), POST /webhook (Stripe webhook processing), and PATCH /members/:userId (update per-member limits/disabled).
Team Library & Client Helpers
lib/extra-usage.ts, lib/api/chat-stream-helpers.ts, lib/api/chat-handler.ts
New TeamExtraUsageState, getTeamExtraUsageState, deductFromTeamBalance, refundToTeamBalance; buildExtraUsageConfig and request billing flows extended to accept and thread organizationId.
Rate Limiter Team Routing
lib/rate-limit/token-bucket.ts
Routes extra-usage deductions/refunds to team pool helpers when subscription is team, extends deductUsage/refundUsage signatures, and maps team-specific failures to distinct error reasons.
Refund Tracker Organization Context
lib/rate-limit/refund.ts
UsageRefundTracker.setUser now accepts optional organizationId and passes it into refund operations; tests updated accordingly.
Admin UI: Team Extra Usage Section
app/components/TeamExtraUsageSection.tsx
Client React admin UI to view/manage pool, purchase credits, adjust caps/auto-reload, and edit per-member limits/disabled via dialog.
Settings Dialog Integration
app/components/SettingsDialog.tsx
Adds isTeamAdmin check and conditionally renders the "Extra Usage" tab (admin-only) and a "Members" tab for team subscriptions.
Route Refactors to Guards
app/api/team/{invite,members,seats}/route.ts
Replaces inline subscription/membership checks with requireTeamOrg/requireAdminOrg to centralize auth and error shaping.
Backend & Config Tests
convex/__tests__/teamExtraUsage.test.ts, lib/api/__tests__/build-extra-usage-config.test.ts
Extensive tests for Convex team logic (deduct/refund/credits/idempotency/state) and buildExtraUsageConfig behavior across free, team, and individual subscriptions.

Sequence Diagram

sequenceDiagram
  participant AdminClient
  participant ConvexAction as convex.teamExtraUsageActions.createTeamPurchaseSession
  participant Stripe
  participant ConfirmRoute as /api/team/extra-usage/confirm
  participant Webhook as /api/team/extra-usage/webhook
  participant ConvexMut as convex.teamExtraUsage.addTeamCredits

  AdminClient->>ConvexAction: createTeamPurchaseSession(serviceKey, orgId, amount, baseUrl)
  ConvexAction->>Stripe: create Checkout session (metadata: orgId, amount, type)
  AdminClient->>Stripe: User completes Checkout
  Stripe->>ConfirmRoute: redirect to /confirm?session_id=cs_...
  ConfirmRoute->>ConvexMut: addTeamCredits(serviceKey, orgId, amount, idempotencyKey)
  Stripe->>Webhook: emits checkout.session.completed
  Webhook->>ConvexMut: addTeamCredits(CONVEX_SERVICE_ROLE_KEY, idempotencyKeys)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hackerai-tech/hackerai#396: Prior extra-usage checkout confirm/webhook idempotency and crediting flow similar to this PR’s confirm/webhook handlers.
  • hackerai-tech/hackerai#335: Introduced trust-cap fields/logic used by the team extra-usage effective cap computations.
  • hackerai-tech/hackerai#436: Overlapping changes to rate-limiter cap-hit reporting and token-bucket logic that intersect with this PR’s token-bucket modifications.

Poem

🐰 I hopped through Convex, Stripe, and code,

Shared credits tucked in a glowing node.
Admins click, webhooks hum, balances mend,
Caps and members all stitched end-to-end.
A rabbit cheers — purchase complete, my friend!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly describes the main feature being added: team-shared extra usage with admin-controlled pools and per-member spending caps.
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 team-extra-usage

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.

Tip

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

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

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

Built for teams:

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

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

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: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/team/extra-usage/members/`[userId]/route.ts:
- Around line 46-54: The PATCH handler currently passes unvalidated body fields
into convex.mutation(api.teamExtraUsage.updateTeamMemberUsage), causing 500s for
bad input; before calling that mutation validate req.json() output: confirm
body.monthlyLimitDollars is either undefined or a finite non-negative number (or
numeric string converted to number) and body.disabled is either undefined or a
boolean, otherwise return a 400 response; perform these checks in the same
route.ts function that computes guard.organizationId and targetUserId and only
call api.teamExtraUsage.updateTeamMemberUsage when the types/values are valid.

In `@app/api/team/extra-usage/purchase/route.ts`:
- Around line 22-27: The current amountDollars check only tests typeof number
but allows NaN/Infinity/zero/negative; update the validation in the route
handler to require Number.isFinite(amountDollars) && amountDollars > 0 and
return a 400 NextResponse.json({ error: "amountDollars must be a finite number
greater than 0" }, { status: 400 }) when it fails; locate the existing check
around the amountDollars variable and replace the condition and error message
accordingly before proceeding to checkout creation.

In `@app/api/team/extra-usage/route.ts`:
- Around line 83-93: The POST handler is sending unvalidated values to
convex.mutation (api.teamExtraUsage.updateTeamExtraUsageSettings); add runtime
validation of the parsed body from req.json() in the route.ts POST handler:
verify body.enabled and body.autoReloadEnabled are booleans, and
body.autoReloadThresholdDollars, body.autoReloadAmountDollars, and
body.monthlyCapDollars are numbers (and non-negative) before calling the
mutation with guard.organizationId; if validation fails, return a 400 response
with a clear error message instead of calling convex.mutation so invalid types
do not produce 500s.

In `@app/api/team/extra-usage/webhook/route.ts`:
- Around line 68-76: The webhook currently returns a 400 for invalid metadata
(checking organizationId and amountDollars) which causes Stripe to retry; update
the handler in route.ts so that when organizationId is missing or amountDollars
is NaN you still log the error (use the existing console.error message
referencing session.id) but return a 2xx acknowledgement via NextResponse.json
(e.g., success/ok) instead of the 400 error to stop Stripe redeliveries; keep
the rest of the flow unchanged.
- Around line 56-61: The webhook grants credits on checkout.session.completed
regardless of settlement; modify the event handling in the route's switch over
event.type to (1) check session.payment_status === "paid" before issuing credits
in the "checkout.session.completed" case and return early if not paid, and (2)
add a new case for "checkout.session.async_payment_succeeded" that performs the
same credit issuance flow for sessions with metadata.type ===
"team_extra_usage_purchase". Update the logic that reads session (currently cast
as Stripe.Checkout.Session) so both cases reuse the same credit-granting
function (e.g., grantTeamExtraUsageCredits or similar) to avoid duplication and
ensure credits are only issued when payment_status is "paid" or on
async_payment_succeeded.

In `@app/components/TeamExtraUsageSection.tsx`:
- Around line 79-100: updatePool and similar functions (updateMember) swallow
errors by catching them and not propagating the failure, so callers still treat
the operation as successful; change these functions to propagate failures
(either rethrow the caught error or return a rejected Promise/false) after
performing local cleanup/logging so callers can show error UI instead of success
toasts. Locate updatePool and updateMember in TeamExtraUsageSection.tsx and
ensure that in the catch block you do not silently absorb the error—after
logging/toasting, rethrow the error (or return Promise.reject(err)) so callers
that await updatePool/updateMember can detect failure and avoid showing success
dialogs/toasts.

In `@convex/teamExtraUsage.ts`:
- Around line 564-567: The query that builds members via
ctx.db.query("team_member_usage").withIndex("by_org", (q) =>
q.eq("organization_id", args.organizationId")).collect() returns an unbounded
collection; replace .collect() with a bounded call (e.g., .take(N)) or implement
pagination so the result is limited (or loop with cursors). Update the code
where members is assigned to use .take(...) or a paginated fetch and ensure
callers handle partial results accordingly.

In `@lib/api/chat-stream-helpers.ts`:
- Around line 875-895: The current team-path in chat-stream-helpers.ts treats a
null getTeamExtraUsageState(organizationId, userId) as an optimistic enabled
config, which can bypass admin/member hard cutoffs; change the fallback to a
conservative deny by returning undefined (or an explicit { enabled: false })
instead of an enabled object, keep the console.warn for observability, and
ensure the logic in the subscription === "team" branch (the
getTeamExtraUsageState handling and subsequent checks on
state.enabled/state.memberDisabled/state.balanceDollars/state.autoReloadEnabled)
treats null state as disallowing extra usage so overflow cannot occur when team
state is unavailable.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f037abf-1975-4919-bc92-17a9340e5f17

📥 Commits

Reviewing files that changed from the base of the PR and between 4e588fb and 0dcf113.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (17)
  • app/api/team/extra-usage/auth.ts
  • app/api/team/extra-usage/confirm/route.ts
  • app/api/team/extra-usage/members/[userId]/route.ts
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/extra-usage/route.ts
  • app/api/team/extra-usage/webhook/route.ts
  • app/components/TeamExtraUsageSection.tsx
  • app/components/TeamTab.tsx
  • convex/schema.ts
  • convex/teamExtraUsage.ts
  • convex/teamExtraUsageActions.ts
  • lib/api/chat-handler.ts
  • lib/api/chat-stream-helpers.ts
  • lib/extra-usage.ts
  • lib/rate-limit/__tests__/refund.test.ts
  • lib/rate-limit/refund.ts
  • lib/rate-limit/token-bucket.ts

Comment thread app/api/team/extra-usage/members/[userId]/route.ts Outdated
Comment thread app/api/team/extra-usage/purchase/route.ts Outdated
Comment thread app/api/team/extra-usage/route.ts Outdated
Comment thread app/api/team/extra-usage/webhook/route.ts
Comment thread app/api/team/extra-usage/webhook/route.ts Outdated
Comment thread app/components/TeamExtraUsageSection.tsx Outdated
Comment thread convex/teamExtraUsage.ts
Comment thread lib/api/chat-stream-helpers.ts
ross0x01 and others added 2 commits May 14, 2026 15:04
Backs the new team-pool state machine with unit tests covering deduct/refund
branches, addTeamCredits idempotency, and buildExtraUsageConfig's team
routing. Promotes the per-route requireAdminOrg helper into a shared
team-auth module and adopts it in /api/team/{members,invite,seats}, cutting
the duplicated WorkOS membership + admin-role lookups. Also moves the
TeamExtraUsageSection mount from inside the Team tab to its own "Extra
Usage" tab for team admins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Disambiguates from the new "Extra Usage" tab that team admins also see —
"Members" describes the contents (member list, invites, seat management)
more directly than the generic "Team".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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)
app/components/SettingsDialog.tsx (1)

49-60: 💤 Low value

Missing fetch cleanup could cause state updates on unmounted component.

The useEffect fetch doesn't use an AbortController for cleanup. If the dialog unmounts or subscription changes mid-flight, React will warn about setting state on an unmounted component.

♻️ Suggested fix with AbortController
  useEffect(() => {
    // Only consulted when subscription === "team"; tabs() condition gates
    // any other read, so a stale value after switching tier is harmless and
    // will be overwritten the next time the user becomes a team member.
    if (subscription !== "team") return;
+   const controller = new AbortController();
-   fetch("/api/team/members")
+   fetch("/api/team/members", { signal: controller.signal })
      .then((res) => res.json())
      .then((data) => setIsTeamAdmin(data.isAdmin ?? false))
      .catch(() => setIsTeamAdmin(false));
+   return () => controller.abort();
  }, [subscription]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/SettingsDialog.tsx` around lines 49 - 60, The effect that
fetches "/api/team/members" (inside the useEffect that reads/sets isTeamAdmin
via setIsTeamAdmin) needs an AbortController: create a controller, pass
controller.signal into fetch, and in the fetch's then/catch paths guard against
setting state if the request was aborted (or check signal.aborted) so you don't
call setIsTeamAdmin after unmount; return a cleanup from the effect that calls
controller.abort() to cancel in-flight requests when subscription changes or the
component unmounts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/team/team-auth.ts`:
- Around line 35-50: The code currently picks the first active membership from
workos.userManagement.listOrganizationMemberships (memberships.data[0]), which
is ambiguous for users in multiple orgs; update the handler that calls
listOrganizationMemberships (team-auth) to accept an organizationId parameter
from the request and use it to find the matching membership (e.g., find
membership where membership.organization.id === organizationId) and return a
400/404 if missing, or alternatively enforce/validate single-membership by
returning an explicit error if memberships.data.length > 1 and no organizationId
was provided; ensure responses and error messages reference the membership
selection requirement so callers can disambiguate.

In `@convex/__tests__/teamExtraUsage.test.ts`:
- Around line 379-381: The currentMonth computation calls new Date() multiple
times which can yield inconsistent results across UTC month boundaries; create a
single Date instance (e.g., const now = new Date()) and derive currentMonth from
that single variable instead of calling new Date() repeatedly so currentMonth is
stable (update the code that defines currentMonth to use now.getUTCFullYear()
and now.getUTCMonth()).
- Around line 42-43: The test currently sets SERVICE_KEY and mutates
process.env.CONVEX_SERVICE_ROLE_KEY at module scope which can leak into other
tests; capture the original value (e.g. const _ORIGINAL_CONVEX_SERVICE_ROLE_KEY
= process.env.CONVEX_SERVICE_ROLE_KEY), move the assignment of
process.env.CONVEX_SERVICE_ROLE_KEY = SERVICE_KEY into a beforeEach or beforeAll
block, and restore the original value in an afterEach or afterAll block (e.g.
process.env.CONVEX_SERVICE_ROLE_KEY = _ORIGINAL_CONVEX_SERVICE_ROLE_KEY) so
SERVICE_KEY and process.env.CONVEX_SERVICE_ROLE_KEY are cleaned up after the
test run.

---

Nitpick comments:
In `@app/components/SettingsDialog.tsx`:
- Around line 49-60: The effect that fetches "/api/team/members" (inside the
useEffect that reads/sets isTeamAdmin via setIsTeamAdmin) needs an
AbortController: create a controller, pass controller.signal into fetch, and in
the fetch's then/catch paths guard against setting state if the request was
aborted (or check signal.aborted) so you don't call setIsTeamAdmin after
unmount; return a cleanup from the effect that calls controller.abort() to
cancel in-flight requests when subscription changes or the component unmounts.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: fad4a6d9-d191-4b8e-a298-4840814ccc28

📥 Commits

Reviewing files that changed from the base of the PR and between 0dcf113 and 250fe8a.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (10)
  • app/api/team/extra-usage/members/[userId]/route.ts
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/extra-usage/route.ts
  • app/api/team/invite/route.ts
  • app/api/team/members/route.ts
  • app/api/team/seats/route.ts
  • app/api/team/team-auth.ts
  • app/components/SettingsDialog.tsx
  • convex/__tests__/teamExtraUsage.test.ts
  • lib/api/__tests__/build-extra-usage-config.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/api/team/extra-usage/members/[userId]/route.ts
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/extra-usage/route.ts

Comment thread app/api/team/team-auth.ts Outdated
Comment thread convex/__tests__/teamExtraUsage.test.ts Outdated
Comment thread convex/__tests__/teamExtraUsage.test.ts
- Reject NaN/Infinity/non-positive amountDollars in /purchase
- Ack malformed-metadata webhook events with 200 to stop Stripe retries
- Gate success toasts on updatePool/updateMember actually succeeding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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

♻️ Duplicate comments (1)
app/api/team/extra-usage/webhook/route.ts (1)

56-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate crediting on settled payment and handle async settlement event.

Credits are issued in checkout.session.completed only. For delayed payment methods, settlement can complete later; this should also handle checkout.session.async_payment_succeeded (or explicitly gate by payment state before crediting).

💡 Suggested fix
   switch (event.type) {
-    case "checkout.session.completed": {
+    case "checkout.session.completed":
+    case "checkout.session.async_payment_succeeded": {
       const session = event.data.object as Stripe.Checkout.Session;
       if (session.metadata?.type !== "team_extra_usage_purchase") {
         return NextResponse.json({ received: true });
       }
+      if (event.type === "checkout.session.completed" && session.payment_status !== "paid") {
+        return NextResponse.json({ received: true });
+      }
In Stripe Checkout webhook fulfillment, should handlers process both `checkout.session.completed` and `checkout.session.async_payment_succeeded`, and avoid fulfilling when `checkout.session.completed` has `payment_status = "processing"`?

Also applies to: 78-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/team/extra-usage/webhook/route.ts` around lines 56 - 58, Update the
webhook switch handling so credits are only issued when the Checkout Session is
settled: check both event.type values "checkout.session.completed" and
"checkout.session.async_payment_succeeded" (or explicitly check
session.payment_status !== "processing" inside the "checkout.session.completed"
case) before calling the crediting logic; use the existing session variable
(event.data.object as Stripe.Checkout.Session) to inspect payment_status and
move the credit issuance into a shared helper invoked only when settled to avoid
double-processing across the two event types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/team/extra-usage/purchase/route.ts`:
- Around line 19-20: Wrap the JSON parsing calls (the await req.json() on the
top-level request and the similar parse in the 53-58 block) in a try/catch that
treats JSON parse errors as client errors: catch the SyntaxError (or any JSON
parsing error) and return an HTTP 400 response with a brief message instead of
letting it bubble to the generic 500 handler; update the POST/route handler to
detect parse failures around the req.json() call(s) and return new Response(...)
with status 400 when parsing fails.

In `@app/components/TeamExtraUsageSection.tsx`:
- Around line 67-72: When load() fails you must stop relying on pool being null
to indicate loading — either set an explicit error/empty state or setPool to an
empty sentinel and update rendering; modify the catch block in load() to call
setLoading(false) (already done) and also call setPool([]) or setLoadError(true)
so the UI can detect failure, and update the render condition (the branch that
currently checks if (!pool) to show "Loading team usage…") to check if (!pool &&
loading) for the loading spinner and render an error/empty-with-retry when
(!pool && !loading). Ensure you reference and update the existing symbols
load(), pool, setPool, setLoading (or add setLoadError/getLoadError) and the
retry handler to re-run load().
- Around line 485-488: The component is rounding member.monthlyLimitDollars when
initializing limitInput which can unintentionally change decimal caps; update
the useState initializer in TeamExtraUsageSection (limitInput / setLimitInput)
to preserve the original numeric value instead of Math.round — e.g. convert
member.monthlyLimitDollars to a string directly
(String(member.monthlyLimitDollars) or member.monthlyLimitDollars.toString())
when it’s not null so opening/saving the edit dialog won’t alter decimal limits.

---

Duplicate comments:
In `@app/api/team/extra-usage/webhook/route.ts`:
- Around line 56-58: Update the webhook switch handling so credits are only
issued when the Checkout Session is settled: check both event.type values
"checkout.session.completed" and "checkout.session.async_payment_succeeded" (or
explicitly check session.payment_status !== "processing" inside the
"checkout.session.completed" case) before calling the crediting logic; use the
existing session variable (event.data.object as Stripe.Checkout.Session) to
inspect payment_status and move the credit issuance into a shared helper invoked
only when settled to avoid double-processing across the two event types.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1719f76-c5c3-4f52-9b40-fe3a1b4b2f17

📥 Commits

Reviewing files that changed from the base of the PR and between 250fe8a and c68eea9.

📒 Files selected for processing (3)
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/extra-usage/webhook/route.ts
  • app/components/TeamExtraUsageSection.tsx

Comment thread app/api/team/extra-usage/purchase/route.ts Outdated
Comment thread app/components/TeamExtraUsageSection.tsx
Comment thread app/components/TeamExtraUsageSection.tsx
ross0x01 and others added 2 commits May 14, 2026 15:22
- requireTeamOrg now uses session.organizationId instead of picking the
  first WorkOS membership, so multi-org users can't accidentally act on
  the wrong org
- Snapshot/restore CONVEX_SERVICE_ROLE_KEY in the team extra usage test
  file so it doesn't leak into other test files in the same Jest worker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ision

- Team extra usage routes now return 400 on invalid JSON instead of
  bubbling up as 500
- TeamExtraUsageSection shows a retry button when the initial fetch
  fails instead of leaving "Loading team usage…" stuck on screen
- The member limit edit dialog prefills with the exact stored value
  so re-saving doesn't silently round a decimal cap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: 1

🧹 Nitpick comments (1)
app/api/team/extra-usage/route.ts (1)

32-47: ⚖️ Poor tradeoff

N+1 API calls to fetch user details.

Each membership triggers a separate getUser call. WorkOS User Management API does not support batch getUser or listUsers by IDs, so individual calls are required. For teams with many members, consider caching user details or implementing a memoization layer to reduce repeated API calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/team/extra-usage/route.ts` around lines 32 - 47, The membership
mapping is making N calls via workos.userManagement.getUser inside
memberships.data.map causing an N+1 API problem; fix by introducing a user
cache/memoization (e.g., a Map keyed by userId) used in the members construction
so you only call workos.userManagement.getUser once per unique userId and reuse
the cached User for duplicate IDs, optionally batching parallel fetches for
unique IDs before mapping and then reference usageByUserId and the cached users
when building the members array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/team/extra-usage/route.ts`:
- Around line 24-28: The call to
workos.userManagement.listOrganizationMemberships returns paginated results so
the current code only uses the first page (memberships.data) and drops
subsequent pages; update the logic around listOrganizationMemberships to iterate
through all pages (using the SDK pagination fields such as next_cursor/has_more
or equivalent) and accumulate every page's members into a single array (replace
usages of memberships.data with the fully aggregated memberships array),
ensuring the function that consumes memberships (e.g., the variable currently
named memberships or memberships.data) receives the complete list.

---

Nitpick comments:
In `@app/api/team/extra-usage/route.ts`:
- Around line 32-47: The membership mapping is making N calls via
workos.userManagement.getUser inside memberships.data.map causing an N+1 API
problem; fix by introducing a user cache/memoization (e.g., a Map keyed by
userId) used in the members construction so you only call
workos.userManagement.getUser once per unique userId and reuse the cached User
for duplicate IDs, optionally batching parallel fetches for unique IDs before
mapping and then reference usageByUserId and the cached users when building the
members array.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36c1d5e5-c7d1-4453-8b14-f1e580a2192f

📥 Commits

Reviewing files that changed from the base of the PR and between c68eea9 and bec151b.

📒 Files selected for processing (6)
  • app/api/team/extra-usage/members/[userId]/route.ts
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/extra-usage/route.ts
  • app/api/team/team-auth.ts
  • app/components/TeamExtraUsageSection.tsx
  • convex/__tests__/teamExtraUsage.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/api/team/extra-usage/members/[userId]/route.ts
  • app/api/team/extra-usage/purchase/route.ts
  • app/api/team/team-auth.ts
  • app/components/TeamExtraUsageSection.tsx
  • convex/tests/teamExtraUsage.test.ts

Comment thread app/api/team/extra-usage/route.ts Outdated
The route only consumed the first page of listOrganizationMemberships
(default limit 10), silently dropping members from teams larger than the
page size. Use the SDK's autoPagination() to fetch all pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	convex/schema.ts
#	lib/api/chat-handler.ts
@ross0x01 ross0x01 merged commit e2390f9 into main May 16, 2026
5 checks passed
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