fix(credits): seed new accounts at the right plan-aware balance#549
Conversation
Brand-new accounts used to receive exactly 25 credits regardless of plan, because insertCreditsUsage had a hard-coded DEFAULT_CREDITS=25 fallback that both call sites (agent signup + account create) relied on. The new credits-balance endpoint exposes this as "25 / 333 used 308" the moment an account is provisioned. Fix at the API layer, reusing what PR #547 already gave us: - New `lib/credits/getAccountSubscriptionState.ts` — single source of truth for "is this account pro?". Extracts the parallel getActiveSubscriptionDetails + getOrgSubscription lookup that checkAndResetCredits already did inline. - `checkAndResetCredits` now delegates to that helper. Behavior unchanged; 7 lines collapse to 2. - New `lib/credits/initializeAccountCredits.ts` — plan-aware seeder. Looks up the subscription state via the new helper, then calls insertCreditsUsage with PRO_CREDITS=1000 or DEFAULT_CREDITS=333 (the constants we already exported in PR #547). - Both call sites swap insertCreditsUsage(id) for initializeAccountCredits(id): - lib/agents/createAccountWithEmail.ts - lib/accounts/createAccountHandler.ts - Remove the booby-trap default from insertCreditsUsage. The remainingCredits parameter is now required, so any new caller that forgets to pick a plan-aware value gets a type error. TDD: 4 new tests for getAccountSubscriptionState, 3 for initializeAccountCredits, full checkAndResetCredits suite migrated to mock the new helper instead of three Stripe functions. 234 tests green across 39 files. lint clean. No typecheck regressions in changed files (pre-existing AI-SDK type drift in getCreditUsage.test and handleChatCredits.test is unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR refactors account creation and credit initialization to use a centralized subscription-state helper. It introduces ChangesCredits Initialization Refactor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
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 docstrings
🧪 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
lib/credits/getAccountSubscriptionState.ts (1)
31-31: 💤 Low valueConsider extracting the subscription selection logic for clarity.
The nested ternary on line 31 is correct but could be more readable. In "Chill" mode this is fine, but if you find yourself revisiting this code, consider a small helper or early-return pattern.
♻️ Optional: explicit conditional for clarity
- activeSubscription: hasAccountSub ? accountSub : hasOrgSub ? orgSub : null, + activeSubscription: hasAccountSub ? accountSub : (hasOrgSub ? orgSub : null),Or extract to a helper variable before the return:
const hasAccountSub = isActiveSubscription(accountSub); const hasOrgSub = isActiveSubscription(orgSub); + const activeSubscription = hasAccountSub ? accountSub : hasOrgSub ? orgSub : null; return { isPro: hasAccountSub || hasOrgSub, - activeSubscription: hasAccountSub ? accountSub : hasOrgSub ? orgSub : null, + activeSubscription, };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/credits/getAccountSubscriptionState.ts` at line 31, The nested ternary setting activeSubscription is hard to read; in getAccountSubscriptionState extract that logic into a small helper or local variable (e.g., computeActiveSubscription or activeSubscription variable) that takes hasAccountSub, accountSub, hasOrgSub, orgSub and returns accountSub if hasAccountSub, otherwise orgSub if hasOrgSub, otherwise null, then use that variable in the returned object to replace the inline ternary.lib/accounts/createAccountHandler.ts (1)
33-111: ⚖️ Poor tradeoffConsider breaking down this 79-line function.
The function exceeds the 50-line guideline for
lib/**/*.tsand handles multiple responsibilities: email lookup, wallet lookup, and new-account creation. Each path could be extracted into a focused helper (e.g.,findAccountByEmail,findAccountByWallet,createNewAccount).Given "Chill" mode and that the PR doesn't touch the overall structure, this is deferrable—but worth noting for future refactoring.
As per coding guidelines: "Keep functions under 50 lines" and "Keep functions small and focused."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/accounts/createAccountHandler.ts` around lines 33 - 111, createAccountHandler is too long and mixes responsibilities (email lookup, wallet lookup, creation); split it into focused helpers. Extract the email flow (use selectAccountByEmail, assignAccountToOrg, getAccountWithDetails) into findAccountByEmail, the wallet flow (selectAccountByWallet and the accountInfo/account_emails/account_wallets flattening) into findAccountByWallet, and the creation flow (insertAccount, insertAccountEmail, insertAccountWallet, initializeAccountCredits, assignAccountToOrg) into createNewAccount; then have createAccountHandler orchestrate these helpers and keep it under ~50 lines. Ensure the existing error handling and NextResponse usage remain the same and reuse the same types (CreateAccountBody, AccountDataResponse).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/accounts/createAccountHandler.ts`:
- Line 94: The call to initializeAccountCredits in createAccountHandler
currently ignores failures and can leave an account without a credits_usage row;
change the call to capture the return value (e.g., const credits = await
initializeAccountCredits(newAccount.id)) and if it is null/falsey throw an error
(or otherwise abort the handler) similar to createAccountWithEmail’s behavior so
the account creation does not silently proceed without initialized credits;
reference initializeAccountCredits and createAccountHandler (and mirror the
error handling used in createAccountWithEmail) to locate and implement the fix.
---
Nitpick comments:
In `@lib/accounts/createAccountHandler.ts`:
- Around line 33-111: createAccountHandler is too long and mixes
responsibilities (email lookup, wallet lookup, creation); split it into focused
helpers. Extract the email flow (use selectAccountByEmail, assignAccountToOrg,
getAccountWithDetails) into findAccountByEmail, the wallet flow
(selectAccountByWallet and the accountInfo/account_emails/account_wallets
flattening) into findAccountByWallet, and the creation flow (insertAccount,
insertAccountEmail, insertAccountWallet, initializeAccountCredits,
assignAccountToOrg) into createNewAccount; then have createAccountHandler
orchestrate these helpers and keep it under ~50 lines. Ensure the existing error
handling and NextResponse usage remain the same and reuse the same types
(CreateAccountBody, AccountDataResponse).
In `@lib/credits/getAccountSubscriptionState.ts`:
- Line 31: The nested ternary setting activeSubscription is hard to read; in
getAccountSubscriptionState extract that logic into a small helper or local
variable (e.g., computeActiveSubscription or activeSubscription variable) that
takes hasAccountSub, accountSub, hasOrgSub, orgSub and returns accountSub if
hasAccountSub, otherwise orgSub if hasOrgSub, otherwise null, then use that
variable in the returned object to replace the inline ternary.
🪄 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: 6616afb3-5b6a-4993-8871-4ff6a4e844fe
⛔ Files ignored due to path filters (6)
lib/accounts/__tests__/createAccountHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/agentSignupHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/createAccountWithEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/credits/__tests__/checkAndResetCredits.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/credits/__tests__/getAccountSubscriptionState.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/credits/__tests__/initializeAccountCredits.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (6)
lib/accounts/createAccountHandler.tslib/agents/createAccountWithEmail.tslib/credits/checkAndResetCredits.tslib/credits/getAccountSubscriptionState.tslib/credits/initializeAccountCredits.tslib/supabase/credits_usage/insertCreditsUsage.ts
| } | ||
|
|
||
| await insertCreditsUsage(newAccount.id); | ||
| await initializeAccountCredits(newAccount.id); |
There was a problem hiding this comment.
Add error handling for credit initialization.
Unlike createAccountWithEmail (which throws if initializeAccountCredits returns null), this handler silently proceeds when credit initialization fails. If initializeAccountCredits returns null, the account will exist without a credits_usage row, causing downstream read failures.
🛡️ Recommended fix
- await initializeAccountCredits(newAccount.id);
+ const credits = await initializeAccountCredits(newAccount.id);
+ if (!credits) {
+ throw new Error("Failed to initialize account credits");
+ }📝 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 initializeAccountCredits(newAccount.id); | |
| const credits = await initializeAccountCredits(newAccount.id); | |
| if (!credits) { | |
| throw new Error("Failed to initialize account credits"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/accounts/createAccountHandler.ts` at line 94, The call to
initializeAccountCredits in createAccountHandler currently ignores failures and
can leave an account without a credits_usage row; change the call to capture the
return value (e.g., const credits = await
initializeAccountCredits(newAccount.id)) and if it is null/falsey throw an error
(or otherwise abort the handler) similar to createAccountWithEmail’s behavior so
the account creation does not silently proceed without initialized credits;
reference initializeAccountCredits and createAccountHandler (and mirror the
error handling used in createAccountWithEmail) to locate and implement the fix.
There was a problem hiding this comment.
No issues found across 12 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant Client as Client (HTTP)
participant Handler as Account Creation Handler
participant InitCredits as initializeAccountCredits
participant SubState as getAccountSubscriptionState
participant Stripe as Stripe API
participant Insert as insertCreditsUsage (DB)
Note over Client,Insert: Account creation flow with plan-aware credit seeding
Client->>Handler: POST /api/agents/signup or POST /api/accounts
Handler->>Handler: Create account record
Handler->>InitCredits: initializeAccountCredits(accountId)
InitCredits->>SubState: getAccountSubscriptionState(accountId)
SubState->>Stripe: getActiveSubscriptionDetails(accountId)
SubState->>Stripe: getOrgSubscription(accountId)
Stripe-->>SubState: subscription details (or null)
SubState->>SubState: isActiveSubscription check on each
alt Account has active subscription
SubState-->>InitCredits: { isPro: true, activeSubscription: accountSub }
else No account sub but org has active sub
SubState-->>InitCredits: { isPro: true, activeSubscription: orgSub }
else Neither has active subscription
SubState-->>InitCredits: { isPro: false, activeSubscription: null }
end
alt isPro is true
InitCredits->>Insert: insertCreditsUsage(accountId, PRO_CREDITS=1000)
else isPro is false
InitCredits->>Insert: insertCreditsUsage(accountId, DEFAULT_CREDITS=333)
end
Insert->>Insert: INSERT into credits_usage
Insert-->>InitCredits: credits_usage record (or null)
alt Insert succeeds
InitCredits-->>Handler: credits_usage row
else Insert returns null
InitCredits-->>Handler: null
Handler->>Handler: Throw error (account creation aborted)
end
Handler-->>Client: New account response with correct credit balance
Requires human review: This PR refactors credit initialization by extracting subscription state logic, modifying account creation paths, and removing a default parameter from a database insert—changes that touch core business logic and carry moderate risk if a bug is introduced, so human review is warranted despite solid
Manual verification on the preview deploymentPreview URL: https://api-git-feat-plan-aware-credit-seeding-recoup.vercel.app 1. The fix — fresh agent now seeds at 333PREVIEW="https://api-git-feat-plan-aware-credit-seeding-recoup.vercel.app"
TS=$(date +%s)-$RANDOM
RESP=$(curl -s -X POST "$PREVIEW/api/agents/signup" \
-H "Content-Type: application/json" \
-d "{\"email\": \"agent+plan-aware-${TS}@recoupable.com\"}")
# → { account_id: "5fdaccfa-...", api_key: "recoup_sk_..." }
KEY=$(echo "$RESP" | jq -r .api_key)
ACCOUNT=$(echo "$RESP" | jq -r .account_id)
curl -s -H "x-api-key: $KEY" "$PREVIEW/api/accounts/$ACCOUNT/credits"{
"account_id": "5fdaccfa-7cd5-4d72-8c9c-443ad81fe604",
"remaining_credits": 333,
"total_credits": 333,
"used_credits": 0,
"is_pro": false,
"timestamp": "2026-05-11T21:06:23.51295"
}✅ 333 / 333 / used 0 — exactly what we want for a free-tier account on day one. Compare to the same 2. No read-path regression — pro account is unchangedcurl -s -H "x-api-key: $MAINTAINER_KEY" \
"$PREVIEW/api/accounts/fb678396-a68f-4294-ae50-b8cacf9ce77b/credits"{
"account_id": "fb678396-a68f-4294-ae50-b8cacf9ce77b",
"remaining_credits": 430,
"total_credits": 1000,
"used_credits": 570,
"is_pro": true,
"timestamp": "2026-04-24T17:50:43.475"
}✅ Identical to the same query on the #547 preview. The Summary
Both the write-path fix ( 🤖 Generated with Claude Code |
#550) Brand-new accounts used to receive exactly 25 credits regardless of plan, because insertCreditsUsage had a hard-coded DEFAULT_CREDITS=25 fallback that both call sites (agent signup + account create) relied on. The new credits-balance endpoint exposes this as "25 / 333 used 308" the moment an account is provisioned. Fix at the API layer, reusing what PR #547 already gave us: - New `lib/credits/getAccountSubscriptionState.ts` — single source of truth for "is this account pro?". Extracts the parallel getActiveSubscriptionDetails + getOrgSubscription lookup that checkAndResetCredits already did inline. - `checkAndResetCredits` now delegates to that helper. Behavior unchanged; 7 lines collapse to 2. - New `lib/credits/initializeAccountCredits.ts` — plan-aware seeder. Looks up the subscription state via the new helper, then calls insertCreditsUsage with PRO_CREDITS=1000 or DEFAULT_CREDITS=333 (the constants we already exported in PR #547). - Both call sites swap insertCreditsUsage(id) for initializeAccountCredits(id): - lib/agents/createAccountWithEmail.ts - lib/accounts/createAccountHandler.ts - Remove the booby-trap default from insertCreditsUsage. The remainingCredits parameter is now required, so any new caller that forgets to pick a plan-aware value gets a type error. TDD: 4 new tests for getAccountSubscriptionState, 3 for initializeAccountCredits, full checkAndResetCredits suite migrated to mock the new helper instead of three Stripe functions. 234 tests green across 39 files. lint clean. No typecheck regressions in changed files (pre-existing AI-SDK type drift in getCreditUsage.test and handleChatCredits.test is unchanged). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Follow-up to #547. Brand-new accounts were being seeded with exactly 25 credits regardless of plan, because
insertCreditsUsagehad a hardcoded fallback default of25. The new credit-balance endpoint surfaced this as "25 / 333 used 308" the moment an agent signup or account create call completes (see the screenshot in #547 — free-tier test account).This PR fixes it at the api layer, reusing the constants and Stripe-lookup logic shipped in #547 instead of duplicating them.
What changed
1. Extracted the shared plan-detection. PR #547's
checkAndResetCreditshadgetActiveSubscriptionDetails + getOrgSubscription + isActiveSubscriptionglue inline. Pulled it into a newlib/credits/getAccountSubscriptionState.tsso write paths and read paths share one truth.```ts
export async function getAccountSubscriptionState(accountId: string): Promise<{
isPro: boolean;
activeSubscription: Stripe.Subscription | null;
}>
```
2. Refactored `checkAndResetCredits` to use it. Behavior unchanged — 7 lines of inline subscription handling collapse to 2.
3. Added `lib/credits/initializeAccountCredits.ts` — plan-aware seeder. Uses the same `DEFAULT_CREDITS = 333` / `PRO_CREDITS = 1000` constants from #547 (`lib/credits/const.ts`), looks up subscription state via the new helper, then calls the lower-level insert.
```ts
export async function initializeAccountCredits(accountId: string) {
const { isPro } = await getAccountSubscriptionState(accountId);
return insertCreditsUsage(accountId, isPro ? PRO_CREDITS : DEFAULT_CREDITS);
}
```
4. Swapped call sites to use the seeder instead of the raw DB op:
5. Removed the booby-trap default from `insertCreditsUsage.ts`. The `remainingCredits` parameter is now required — any new caller that forgets to choose a plan-aware value gets a TypeScript error instead of silently inheriting 25.
DRY proof
After this PR:
TDD evidence
234 tests green across 39 files, lint clean, no typecheck regressions in changed files.
Out of scope
Test plan
🤖 Generated with Claude Code
Summary by cubic
New accounts now start with the correct plan-aware credits (333 free, 1000 pro) instead of a hardcoded 25. Centralized subscription detection and removed the fallback to fix the wrong initial balance shown by the credits endpoint.
Bug Fixes
lib/credits/initializeAccountCreditsand use it inlib/agents/createAccountWithEmailandlib/accounts/createAccountHandlerto seed plan-aware credits.insertCreditsUsage(accountId, remainingCredits)require an explicit balance; removed the 25-credit default.Refactors
lib/credits/getAccountSubscriptionStateas the single source of truth for plan status;checkAndResetCreditsnow uses it.Written for commit da69293. Summary will update on new commits.
Summary by CodeRabbit
New Features
Refactor