-
Notifications
You must be signed in to change notification settings - Fork 11.6k
feat: Billing page redesign plus credits #23908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughUpdates the organizations Billing settings UI and server-side credit model. UI changes: removes the shell header border on the billing page; replaces CtaRow sections with nested/rounded cards; rewrites BillingCredits into a CreditRow-driven layout with org-aware labels/links, a remaining-credits progress bar, revised buy/add-credits input, expense-log download UI, and a new "Credit Worth" section plus matching skeleton; updates translations. Server changes: getAllCredits/_getAllCreditsForTeam now expose Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/features/ee/billing/credit-service.test.ts (1)
406-424: Test no longer validates intended policy; passes accidentally.This asserts “0 when not active,” but passes only because STRIPE_TEAM_MONTHLY_PRICE_ID isn’t set—not because of subscription gating (now removed). Either restore gating (preferred) or update the test to reflect the new policy by explicitly setting a priceId and expecting non‑zero for “trialing,” or delete the test.
Example (if policy is to allow credits while trialing):
- it("should return 0 if subscription is not active", async () => { + it("should compute credits even when subscription is trialing (policy change)", async () => { + vi.stubEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "price_team_monthly"); // ...mock team + 1 accepted member - // subscriptionStatus: "trialing" - expect(result).toBe(0); + // subscriptionStatus: "trialing" + // mock getPrice to 1000 + expect(result).toBe(500); });
🧹 Nitpick comments (10)
packages/features/ee/billing/credit-service.ts (3)
676-680: Avoid the second round‑trip: fetch all logs once and split in memory.Two nearly identical queries per call add latency and DB load in a frequently used path. Fetch once (no creditType filter) and compute monthly/additional usages locally.
Example:
- const creditBalance = await CreditsRepository.findCreditBalanceWithExpenseLogs( - { teamId, creditType: CreditType.MONTHLY }, - tx - ); - - const additionalCreditBalance = await CreditsRepository.findCreditBalanceWithExpenseLogs( - { teamId, creditType: CreditType.ADDITIONAL }, - tx - ); + const creditBalance = await CreditsRepository.findCreditBalanceWithExpenseLogs({ teamId }, tx);Then:
const monthlyLogs = creditBalance?.expenseLogs.filter(l => l.creditType === CreditType.MONTHLY) ?? []; const additionalLogs = creditBalance?.expenseLogs.filter(l => l.creditType === CreditType.ADDITIONAL) ?? []; const totalMonthlyCreditsUsed = monthlyLogs.reduce((s, l) => s + (l.credits ?? 0), 0); const totalAdditionalCreditsUsed = additionalLogs.reduce((s, l) => s + (l.credits ?? 0), 0); const additionalCredits = creditBalance?.additionalCredits ?? 0;
685-691: Nit: keep source ofadditionalCreditsconsistent with usage-scope.You read
additionalCreditsoff the MONTHLY-scoped query variable. While it currently works (field lives on creditBalance), it’s clearer to read it from the same object used for logs (or after the single-query refactor above).- const additionalCredits = creditBalance?.additionalCredits ?? 0; + const additionalCredits = (creditBalance /* or additionalCreditBalance */)?.additionalCredits ?? 0;
412-431: Low-balance threshold still based only on monthly credits. Confirm behavior.Now that availability and remaining computations combine monthly+additional, consider whether
warningLimitshould also be derived from the combined pool to avoid premature/late warnings.Would you like me to prototype using
totalMonthlyCredits + additionalCredits(or a tunable mix) here?apps/web/public/static/locales/en/common.json (1)
3238-3243: Make team credits tip dynamic to avoid mismatch with backend.Backend computes team credits per seat as 50% of Stripe price (unit_amount) in credits. Hardcoding “750” risks drift if price changes. Prefer a variable placeholder.
- "credits_per_tip_teams": "You receive 750 credits per month, per team member", + "credits_per_tip_teams": "You receive {{creditsPerSeat}} credits per month, per team member"Then pass
creditsPerSeatfrom UI based onStripeBillingService.getPrice/env.apps/web/modules/settings/billing/billing-view.tsx (2)
68-71: Encode query params in billingHref to avoid malformed/unsafe URLsreturnTo (and teamId) should be URL-encoded.
Apply:
- const billingHref = teamId - ? `/api/integrations/stripepayment/portal?teamId=${teamId}&returnTo=${WEBAPP_URL}${returnTo}` - : `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`; + const billingHref = teamId + ? `/api/integrations/stripepayment/portal?teamId=${encodeURIComponent( + teamId + )}&returnTo=${encodeURIComponent(`${WEBAPP_URL}${returnTo}`)}` + : `/api/integrations/stripepayment/portal?returnTo=${encodeURIComponent( + `${WEBAPP_URL}${returnTo}` + )}`;
78-79: Remove commented props noiseInline commented props add churn with no effect.
- // title={t("view_and_manage_billing_details")} - // description={t("view_and_edit_billing_details")}>apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx (1)
31-33: Use theme token for skeleton bar colorHard-coded bg-gray-200 may not match themes. Prefer a tokened class used elsewhere for skeletons.
- <div className="h-2 w-full rounded-md bg-gray-200" /> + <div className="h-2 w-full rounded-md bg-subtle" />apps/web/modules/settings/billing/components/BillingCredits.tsx (3)
194-198: Use correct remaining field with fallbackDisplay remaining for the same “forMonth” model you use above, falling back for older data.
- <CreditRow - label={t("remaining")} - value={creditsData.credits.totalRemainingMonthlyCredits} - isDashed={creditsData.credits.additionalCredits > 0} - /> + <CreditRow + label={t("remaining")} + value={ + creditsData.credits.totalRemainingCreditsForMonth ?? + creditsData.credits.totalRemainingMonthlyCredits ?? + 0 + } + isDashed={creditsData.credits.additionalCredits > 0} + />
141-143: Sanitize filename for spaces and multiple whitespaceReplace all spaces, not just the first.
- const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(" ", "-")}.csv`; + const filename = `credit-expense-log-${selectedMonth.value + .toLowerCase() + .replace(/\s+/g, "-")}.csv`;
253-255: Remove target on submit buttontarget has no effect with JS submit + router navigation and can confuse. Drop it.
- <Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits"> + <Button color="secondary" size="sm" type="submit" data-testid="buy-credits">
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx(1 hunks)apps/web/modules/settings/billing/billing-view.tsx(1 hunks)apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx(1 hunks)apps/web/public/static/locales/en/common.json(3 hunks)packages/features/ee/billing/credit-service.test.ts(10 hunks)packages/features/ee/billing/credit-service.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsxapps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsxapps/web/modules/settings/billing/billing-view.tsxapps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsxpackages/features/ee/billing/credit-service.test.tsapps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsxpackages/features/ee/billing/credit-service.tsapps/web/modules/settings/billing/billing-view.tsxapps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsxpackages/features/ee/billing/credit-service.test.tsapps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsxpackages/features/ee/billing/credit-service.tsapps/web/modules/settings/billing/billing-view.tsxapps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/billing/credit-service.test.tspackages/features/ee/billing/credit-service.ts
🧠 Learnings (2)
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality in CallService already has proper credits validation via validateCreditsForTestCall method which uses CreditService.hasAvailableCredits and throws 403 HttpError when credits are insufficient. The credits enforcement is properly implemented on the backend for test calls.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality already has proper credits validation implemented in CallService.validateCreditsForTestCall() method. This method is called early in createTestCall flow and uses CreditService.hasAvailableCredits() to check credits, throwing HttpError 403 when insufficient. The credits enforcement for test calls is already correctly implemented on the backend.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
🧬 Code graph analysis (3)
packages/features/ee/billing/credit-service.test.ts (2)
packages/lib/server/repository/credits.ts (1)
CreditsRepository(6-242)packages/features/ee/billing/credit-service.ts (1)
CreditService(55-701)
apps/web/modules/settings/billing/billing-view.tsx (2)
packages/ui/components/button/Button.tsx (1)
Button(221-349)apps/web/modules/settings/billing/components/BillingCredits.tsx (1)
BillingCredits(78-305)
apps/web/modules/settings/billing/components/BillingCredits.tsx (3)
packages/features/auth/lib/next-auth-options.ts (1)
session(746-771)packages/ui/components/progress-bar/ProgressBar.tsx (1)
ProgressBar(27-38)packages/ui/components/button/Button.tsx (1)
Button(221-349)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (10)
packages/features/ee/billing/credit-service.test.ts (3)
500-527: LGTM: Combined totals math is correct and well covered.Aggregation of monthly/additional usages and remaining totals matches production logic and covers edge cases.
531-552: LGTM: Handles no-expense scenarios cleanly (zeros).Good defaults and expectations.
554-607: LGTM: Additional scenarios (non‑zero/zero additional credits) are covered.Nice coverage for varying additional balances.
apps/web/public/static/locales/en/common.json (2)
3428-3429: LGTM: New generic labels.“Remaining” and “Total” keys look good and reusable.
903-904: LGTM: Copy tweak without ellipsis.Key name and value are clear.
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx (1)
29-29: LGTM: Removing the shell header border fits the redesign.No functional impact; translations are used correctly.
apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx (1)
6-77: Skeleton structure aligns with new credits UILayout and sections mirror the live component well. Looks good.
apps/web/modules/settings/billing/components/BillingCredits.tsx (2)
56-76: Month options generation looks goodUses dayjs.utc and caps at 12 months; non-hot path. No issues.
292-299: Localize “Learn more” and add rel for external linkReplace the hardcoded string with t("learn_more") and add rel="noopener noreferrer" (the learn_more key already exists under apps/web/public/static/locales/*/common.json).
- <Link + <Link key="Credit System" className="underline underline-offset-2" - target="_blank" + target="_blank" + rel="noopener noreferrer" href="https://cal.com/help/billing-and-usage/messaging-credits"> - Learn more + {t("learn_more")} </Link>,apps/web/modules/settings/billing/billing-view.tsx (1)
29-41: Do not remove CtaRow — it's imported and usedImported at apps/web/modules/settings/platform/billing/billing-view.tsx:19 and used at lines 91, 101, 111, 123; keep it (or refactor to a shared component if desired).
Likely an incorrect or invalid review comment.
| <Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link"> | ||
| {t("billing_portal")} | ||
| </Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add rel="noopener noreferrer" to external link
Prevents tabnabbing and drops referrer.
- <Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
+ <Button color="primary" href={billingHref} target="_blank" rel="noopener noreferrer" size="sm" EndIcon="external-link">📝 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.
| <Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link"> | |
| {t("billing_portal")} | |
| </Button> | |
| <Button color="primary" href={billingHref} target="_blank" rel="noopener noreferrer" size="sm" EndIcon="external-link"> | |
| {t("billing_portal")} | |
| </Button> |
🤖 Prompt for AI Agents
In apps/web/modules/settings/billing/billing-view.tsx around lines 90 to 92, the
external link Button is missing rel="noopener noreferrer"; add rel="noopener
noreferrer" to the Button so the rendered anchor includes it (if the Button
component doesn't forward rel, replace it with an <a> tag or set component="a"
and pass rel, href and target accordingly) to prevent tabnabbing and drop the
referrer.
apps/web/modules/settings/billing/components/BillingCredits.tsx
Outdated
Show resolved
Hide resolved
| // if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") { | ||
| // return 0; | ||
| // } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-introduce subscription gating or guard behind a feature flag.
By commenting out the subscription-status check, trialing/canceled teams will accrue monthly credits if STRIPE_TEAM_MONTHLY_PRICE_ID/ORG_MONTHLY_CREDITS are set. That’s a material policy change with revenue/abuse risk. If intentional, gate it behind an env flag and default to the previous behavior.
Apply:
- // if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") {
- // return 0;
- // }
+ const IGNORE_SUB_STATUS =
+ (process.env.CREDITS_IGNORE_SUBSCRIPTION_STATUS ?? "false").toLowerCase() === "true";
+ if (!IGNORE_SUB_STATUS && subscriptionStatus !== "active" && subscriptionStatus !== "past_due") {
+ return 0;
+ }📝 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.
| // if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") { | |
| // return 0; | |
| // } | |
| const IGNORE_SUB_STATUS = | |
| (process.env.CREDITS_IGNORE_SUBSCRIPTION_STATUS ?? "false").toLowerCase() === "true"; | |
| if (!IGNORE_SUB_STATUS && subscriptionStatus !== "active" && subscriptionStatus !== "past_due") { | |
| return 0; | |
| } |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this 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)
packages/features/ee/billing/credit-service.ts (1)
676-679: Reduce DB round‑trips: fetch once and split in code.Two calls to findCreditBalanceWithExpenseLogs hit the same row; only logs differ by creditType. Prefer a single call without creditType and partition logs in memory to cut one query per request.
If helpful, I can provide a concrete refactor that fetches all logs once and derives monthly/additional usage locally.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/features/ee/billing/credit-service.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/billing/credit-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/ee/billing/credit-service.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/features/ee/billing/credit-service.ts
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Type check / check-types
- GitHub Check: Tests / Unit
- GitHub Check: Linters / lint
| const totalAdditionalCreditsUsed = | ||
| additionalCreditBalance?.expenseLogs.reduce((sum, log) => sum + (log?.credits ?? 0), 0) || 0; | ||
|
|
||
| const additionalCredits = creditBalance?.additionalCredits ?? 0; | ||
| const totalCreditsForMonth = totalMonthlyCredits + additionalCredits; | ||
| const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed + totalAdditionalCreditsUsed; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix combined totals: double-subtracts additional usage → wrong “remaining” and progress.
You add additionalCredits (a current balance) to total, and also subtract additional usage this month. Because the balance already reflects usage, this undercounts remaining. Include additional used in the total to keep: remaining = monthlyRemaining + additionalBalance, and progress = (monthlyUsed + additionalUsed)/total.
Apply:
- const additionalCredits = creditBalance?.additionalCredits ?? 0;
- const totalCreditsForMonth = totalMonthlyCredits + additionalCredits;
- const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed + totalAdditionalCreditsUsed;
+ const additionalCredits = additionalCreditBalance?.additionalCredits ?? 0;
+ // Include additional usage in the total to avoid double‑subtracting it in "used".
+ const totalCreditsForMonth = totalMonthlyCredits + additionalCredits + totalAdditionalCreditsUsed;
+ const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed + totalAdditionalCreditsUsed;
return {
totalMonthlyCredits,
totalRemainingMonthlyCredits: Math.max(totalMonthlyCredits - totalMonthlyCreditsUsed, 0),
additionalCredits,
totalCreditsForMonth,
totalCreditsUsedThisMonth,
totalRemainingCreditsForMonth: Math.max(totalCreditsForMonth - totalCreditsUsedThisMonth, 0),
};Also applies to: 695-699
🤖 Prompt for AI Agents
In packages/features/ee/billing/credit-service.ts around lines 685-691 (and
similarly lines 695-699), the combined totals double-subtract additional usage
by adding additionalCredits (current balance) to totalCreditsForMonth while also
subtracting totalAdditionalCreditsUsed when computing remaining/progress;
instead compute totals such that remaining = monthlyRemaining +
additionalCredits and progress uses numerator (monthlyUsed +
totalAdditionalCreditsUsed) and denominator (monthlyAllowance +
additionalCredits), i.e., stop subtracting additional usage twice—include
additionalUsed only in the used side and keep additionalCredits as the balance
side when calculating remaining and progress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
packages/features/ee/billing/credit-service.ts (2)
594-596: Re-evaluate removing subscription status check or document the business decision.The subscription status check is commented out but the code still includes past_due in the condition. This creates confusion about the intended behavior.
If allowing inactive subscriptions to accrue credits is intentional, document it clearly and consider using a feature flag. Otherwise, uncomment the check:
- if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") { - return 0; - } + // Allow credits for active and past_due subscriptions only + if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") { + return 0; + }
694-696: Fix incorrect credit totals calculation causing wrong remaining balance.The calculation double-counts usage from additional credits. The
additionalCreditsfield represents the current balance (already reduced by usage), but you're also subtractingtotalAdditionalCreditsUsed. This leads to incorrect remaining credits calculation.The correct approach should be:
totalCreditsForMonth= total monthly allowance + (initial additional credits for the month)totalCreditsUsedThisMonth= monthly usage + additional usagetotalRemainingCreditsForMonth= remaining monthly + current additional balanceApply this fix:
- const additionalCredits = creditBalance?.additionalCredits ?? 0; - const totalCreditsForMonth = totalMonthlyCredits + additionalCredits; + const additionalCredits = creditBalance?.additionalCredits ?? 0; + // To get the total credits available for the month, we need to add back the used additional credits + // since additionalCredits is the current balance (already reduced by usage) + const totalCreditsForMonth = totalMonthlyCredits + additionalCredits + totalAdditionalCreditsUsed; const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed + totalAdditionalCreditsUsed;
🧹 Nitpick comments (1)
packages/features/ee/billing/credit-service.ts (1)
654-656: Initialize combined credit fields with actual values for users.For user-level credits, the combined totals are hardcoded to 0, but
additionalCreditshas the actual balance. The totals should reflect the additional credits available.Apply this diff to properly calculate user-level totals:
return { totalMonthlyCredits: 0, totalRemainingMonthlyCredits: 0, additionalCredits: creditBalance?.additionalCredits ?? 0, - totalCreditsForMonth: 0, + totalCreditsForMonth: creditBalance?.additionalCredits ?? 0, totalCreditsUsedThisMonth: 0, - totalRemainingCreditsForMonth: 0, + totalRemainingCreditsForMonth: creditBalance?.additionalCredits ?? 0, };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
apps/web/modules/settings/billing/billing-view.tsx(1 hunks)apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)packages/features/ee/billing/credit-service.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/modules/settings/billing/components/BillingCredits.tsx
- apps/web/modules/settings/billing/billing-view.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/billing/credit-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/ee/billing/credit-service.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/features/ee/billing/credit-service.ts
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Tests / Unit
- GitHub Check: Linters / lint
- GitHub Check: Type check / check-types
🔇 Additional comments (1)
packages/features/ee/billing/credit-service.ts (1)
682-685: Incorrect — the fetch is fine; additionalCredits lives on the single CreditBalance rowThe Prisma model shows additionalCredits is a field on CreditBalance and findCreditBalanceWithExpenseLogs({ creditType: ADDITIONAL }) is only fetching expenseLogs filtered by type — reading creditBalance?.additionalCredits is correct. Suggest optionally using additionalCreditBalance?.additionalCredits or renaming variables to avoid confusion.
Likely an incorrect or invalid review comment.
There was a problem hiding this 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
♻️ Duplicate comments (2)
apps/web/modules/settings/billing/components/BillingCredits.tsx (2)
33-38: Fix type mismatch for CreditRow (isDashed) and clean up class duplicationisDashed is used but not declared in props; also "mt-1" is duplicated in the class list.
-type CreditRowProps = { - label: string; - value: number; - isBold?: boolean; - className?: string; -}; +type CreditRowProps = { + label: string; + value: number; + isBold?: boolean; + isDashed?: boolean; + className?: string; +}; -const CreditRow = ({ label, value, isBold = false, isDashed, className = "" }: CreditRowProps) => { +const CreditRow = ({ label, value, isBold = false, isDashed = false, className = "" }: CreditRowProps) => { const numberFormatter = new Intl.NumberFormat(); return ( - <div - className={classNames( - `mt-1 flex justify-between`, - isDashed ? "border-subtle border-t border-dashed" : "mt-1", - className - )}> + <div + className={classNames( + "flex justify-between", + isDashed ? "mt-2 border-t border-dashed border-subtle pt-1" : "mt-1", + className + )}> <span className={classNames("text-sm", isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}> {label} </span> <span className={classNames(`text-sm`, isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}> {numberFormatter.format(value)} </span> </div> ); };Optional nit: memoize the formatter to avoid recreating per row render:
- const numberFormatter = new Intl.NumberFormat(); + const numberFormatter = useMemo(() => new Intl.NumberFormat(), []);Also applies to: 40-57
165-171: Respect zero values; remove boolean OR fallbacks(x ?? 0) || y incorrectly treats 0 as falsy and falls back to y. Use nullish coalescing only.
- const totalCredits = - (creditsData.credits.totalCreditsForMonth ?? 0) || - creditsData.credits.totalMonthlyCredits + creditsData.credits.additionalCredits; - const totalUsed = - (creditsData.credits.totalCreditsUsedThisMonth ?? 0) || - totalCredits - (creditsData.credits.totalRemainingCreditsForMonth ?? 0); + const totalCredits = + creditsData.credits.totalCreditsForMonth ?? + ((creditsData.credits.totalMonthlyCredits ?? 0) + (creditsData.credits.additionalCredits ?? 0)); + const totalUsed = + creditsData.credits.totalCreditsUsedThisMonth ?? + (totalCredits - (creditsData.credits.totalRemainingCreditsForMonth ?? 0));
🧹 Nitpick comments (4)
apps/web/modules/settings/billing/components/BillingCredits.tsx (4)
146-147: Make filename sanitization robust (replace all whitespace)Replace single-space replacement with global whitespace replacement.
- const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(" ", "-")}.csv`; + const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(/\s+/g, "-")}.csv`;
200-203: Coalesce remaining credits to a numberPrevent NaN/undefined rendering and keep types consistent with CreditRowProps.
- <CreditRow - label={t("remaining")} - value={creditsData.credits.totalRemainingCreditsForMonth} - isDashed={creditsData.credits.additionalCredits > 0} - /> + <CreditRow + label={t("remaining")} + value={creditsData.credits.totalRemainingCreditsForMonth ?? 0} + isDashed={(creditsData.credits.additionalCredits ?? 0) > 0} + />
240-254: Avoid mixing RHF register with manual setValue on onChangeYou already use valueAsNumber; RHF will parse to number. The extra onChange risks double state writes and validation quirks.
<TextField required type="number" {...register("quantity", { required: t("error_required_field"), min: { value: 50, message: t("minimum_of_credits_required") }, valueAsNumber: true, })} label="" containerClassName="w-full -mt-1" size="sm" - onChange={(e) => setValue("quantity", Number(e.target.value))} min={50} addOnSuffix={<>{t("credits")}</>} />
258-260: Remove invalid target attribute on a buttontarget has no effect on a and adds noise.
- <Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits"> + <Button color="secondary" size="sm" type="submit" data-testid="buy-credits"> {t("buy")} </Button>
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
🧬 Code graph analysis (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (3)
packages/features/auth/lib/next-auth-options.ts (1)
session(746-771)packages/ui/components/progress-bar/ProgressBar.tsx (1)
ProgressBar(27-38)packages/ui/components/button/Button.tsx (1)
Button(221-349)
🔇 Additional comments (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (1)
119-123: No change required — teamId is optional and server handles undefined.
ZGetAllCreditsSchema declares teamId as optional and getAllCredits.handler enforces admin when teamId is provided and correctly handles the undefined case (membership check / nullable teamId), so calling useQuery({ teamId }, { enabled: shouldRender }) is safe.
| <ServerTrans | ||
| t={t} | ||
| i18nKey="credit_worth_description" | ||
| components={[ | ||
| <Link | ||
| key="Credit System" | ||
| className="underline underline-offset-2" | ||
| target="_blank" | ||
| href="https://cal.com/help/billing-and-usage/messaging-credits"> | ||
| Learn more | ||
| </Link>, | ||
| ]} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add rel for external link security and localize link label
target="_blank" should include rel="noopener noreferrer". Also replace literal "Learn more" with a localized string.
<ServerTrans
t={t}
i18nKey="credit_worth_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
- target="_blank"
- href="https://cal.com/help/billing-and-usage/messaging-credits">
- Learn more
+ target="_blank"
+ rel="noopener noreferrer"
+ href="https://cal.com/help/billing-and-usage/messaging-credits">
+ {t("learn_more")}
</Link>,
]}
/>📝 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.
| <ServerTrans | |
| t={t} | |
| i18nKey="credit_worth_description" | |
| components={[ | |
| <Link | |
| key="Credit System" | |
| className="underline underline-offset-2" | |
| target="_blank" | |
| href="https://cal.com/help/billing-and-usage/messaging-credits"> | |
| Learn more | |
| </Link>, | |
| ]} | |
| /> | |
| <ServerTrans | |
| t={t} | |
| i18nKey="credit_worth_description" | |
| components={[ | |
| <Link | |
| key="Credit System" | |
| className="underline underline-offset-2" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| href="https://cal.com/help/billing-and-usage/messaging-credits"> | |
| {t("learn_more")} | |
| </Link>, | |
| ]} | |
| /> |
🤖 Prompt for AI Agents
In apps/web/modules/settings/billing/components/BillingCredits.tsx around lines
293 to 305, the external Link opens in a new tab but is missing rel="noopener
noreferrer" and uses a hardcoded "Learn more" label; update the Link props to
include rel="noopener noreferrer" for security and replace the literal label
with a localized string via the existing i18n function (e.g. use t('learn_more')
or the appropriate translation key) so the link text is localized.
There was a problem hiding this 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
♻️ Duplicate comments (1)
apps/web/public/static/locales/en/common.json (1)
903-904: Typo fix applied; ensure cross-file consistency.add_members_no_ellipsis looks correct and replaces the previous misspelling. Please confirm all usages and non‑EN locale keys were updated too.
#!/bin/bash rg -nP '\badd_members_no_elipsis\b' -C2 rg -nP '\badd_members_no_ellipsis\b' -C2 fd common.json apps/**/public/static/locales | xargs -I{} rg -nP '"add_members_no_ellipsis"' {}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)apps/web/public/static/locales/en/common.json(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/modules/settings/billing/components/BillingCredits.tsx
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Production builds / Build API v2
- GitHub Check: Production builds / Build Web App
- GitHub Check: Production builds / Build Atoms
- GitHub Check: Tests / Unit
- GitHub Check: Production builds / Build API v1
- GitHub Check: Type check / check-types
🔇 Additional comments (1)
apps/web/public/static/locales/en/common.json (1)
3431-3432: Verify scope of generic keys "remaining" and "total".Both keys are defined only in apps/web/public/static/locales/en/common.json (lines 3431–3432) and no usages or same keys were found elsewhere in the repo; confirm intended global use — if they’re credits-specific, namespace them (e.g., credits.remaining or total_credits_remaining).
| "credits_used": "Credits used", | ||
| "total_credits_remaining": "Total remaining", | ||
| "credits_per_tip_org": "You receive 1000 credits per month, per team member", | ||
| "credits_per_tip_teams": "You receive 750 credits per month, per team member", | ||
| "view_and_manage_credits": "View and manage credits for sending SMS messages", | ||
| "view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>", | ||
| "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more</0>", | ||
| "buy_additional_credits": "Buy additional credits ($0.01 per credit)", | ||
| "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log", | ||
| "overview": "Overview", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Credits copy: avoid hard-coded pricing and duplicated “credit worth” sentence.
- Don’t hard-code USD amounts; use ICU currency placeholders.
- credit_worth_description duplicates the “One credit is worth 1¢ …” sentence from view_and_manage_credits_description — keep it in one place.
- Consider neutralizing “for sending SMS messages” if credits also cover web calls; otherwise, ignore this note.
Apply this diff to de-duplicate and parameterize currency, plus minor capitalization tweak:
- "view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>",
+ "view_and_manage_credits_description": "View and manage credits for sending SMS messages. <0>Learn more</0>",
- "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more</0>",
+ "credit_worth_description": "One credit is worth {{pricePerCredit, currency}} (USD). <0>Learn more</0>",
- "buy_additional_credits": "Buy additional credits ($0.01 per credit)",
+ "buy_additional_credits": "Buy additional credits ({{pricePerCredit, currency}} per credit)",
- "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log",
+ "view_additional_credits_expense_tip": "You can view additional credit spending in your Expense Log"Optional (future): replace the plan-specific strings
- credits_per_tip_org
- credits_per_tip_teams
with a single parameterized string:
"You receive {{creditsPerMember}} credits per month, per team member".
🏁 Script executed:
#!/bin/bash
# Check if credits are used for web calls and align copy if needed
rg -nP 'web_call_credits_info|web_call_no_credits|credits_required' -C2Length of output: 72742
Credits copy: de-duplicate and parameterize per-credit price
- Don’t hard-code USD amounts; use an ICU currency placeholder (e.g. {{pricePerCredit, currency}}).
- Remove the duplicated “One credit is worth …” sentence from view_and_manage_credits_description and keep it only in credit_worth_description.
- Neutralize “for sending SMS messages” because credits are also consumed by web calls (see web_call_credits_info keys).
- Minor capitalization: unify "expense log" / "Expense Log" as product style requires.
Apply this diff to apps/web/public/static/locales/en/common.json:
- "view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>",
+ "view_and_manage_credits_description": "View and manage credits for credit-consuming features. <0>Learn more</0>",
- "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more</0>",
+ "credit_worth_description": "One credit is worth {{pricePerCredit, currency}}. <0>Learn more</0>",
- "buy_additional_credits": "Buy additional credits ($0.01 per credit)",
+ "buy_additional_credits": "Buy additional credits ({{pricePerCredit, currency}} per credit)",
- "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log",
+ "view_additional_credits_expense_tip": "You can view additional credit spending in your Expense Log"Optional: collapse credits_per_tip_org / credits_per_tip_teams into one parameterized string ("You receive {{creditsPerMember}} credits per month, per team member").
📝 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.
| "credits_used": "Credits used", | |
| "total_credits_remaining": "Total remaining", | |
| "credits_per_tip_org": "You receive 1000 credits per month, per team member", | |
| "credits_per_tip_teams": "You receive 750 credits per month, per team member", | |
| "view_and_manage_credits": "View and manage credits for sending SMS messages", | |
| "view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>", | |
| "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more</0>", | |
| "buy_additional_credits": "Buy additional credits ($0.01 per credit)", | |
| "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log", | |
| "overview": "Overview", | |
| "credits_used": "Credits used", | |
| "total_credits_remaining": "Total remaining", | |
| "credits_per_tip_org": "You receive 1000 credits per month, per team member", | |
| "credits_per_tip_teams": "You receive 750 credits per month, per team member", | |
| "view_and_manage_credits": "View and manage credits for sending SMS messages", | |
| "view_and_manage_credits_description": "View and manage credits for credit-consuming features. <0>Learn more</0>", | |
| "credit_worth_description": "One credit is worth {{pricePerCredit, currency}}. <0>Learn more</0>", | |
| "buy_additional_credits": "Buy additional credits ({{pricePerCredit, currency}} per credit)", | |
| "view_additional_credits_expense_tip": "You can view additional credit spending in your Expense Log", | |
| "overview": "Overview", |
🤖 Prompt for AI Agents
In apps/web/public/static/locales/en/common.json around lines 3238 to 3247, the
credits copy hard-codes a USD amount, duplicates the "One credit is worth…"
sentence, and ties credits specifically to "sending SMS messages" plus
inconsistent capitalization of "expense log"; replace hard-coded "$0.01 per
credit" with an ICU currency placeholder like {{pricePerCredit, currency}},
remove the duplicated sentence from view_and_manage_credits_description so it
only remains in credit_worth_description, neutralize the SMS-specific phrase in
view_and_manage_credits (e.g., remove "for sending SMS messages" or replace with
a neutral phrase like "for certain actions"), normalize "expense log"
capitalization to the product style (use "expense log"), and optionally
consolidate credits_per_tip_org and credits_per_tip_teams into a single
parameterized key such as "You receive {{creditsPerMember}} credits per month,
per team member".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
apps/web/modules/settings/billing/components/BillingCredits.tsx (2)
172-178: Respect 0 values; replace || with nullish coalescing and safe addendsCurrent logic wrongly falls back when totals are 0; also risks NaN if a part is undefined.
- const totalCredits = - (creditsData.credits.totalCreditsForMonth ?? 0) || - creditsData.credits.totalMonthlyCredits + creditsData.credits.additionalCredits; - const totalUsed = - (creditsData.credits.totalCreditsUsedThisMonth ?? 0) || - totalCredits - (creditsData.credits.totalRemainingCreditsForMonth ?? 0); + const totalCredits = + creditsData.credits.totalCreditsForMonth ?? + ((creditsData.credits.totalMonthlyCredits ?? 0) + (creditsData.credits.additionalCredits ?? 0)); + const totalUsed = + creditsData.credits.totalCreditsUsedThisMonth ?? + (totalCredits - (creditsData.credits.totalRemainingCreditsForMonth ?? 0));
303-318: Add rel to external link and localize label (per guidelines)Security: add rel="noopener noreferrer". i18n: replace literal "Learn more" with t("learn_more").
<Link key="Credit System" className="underline underline-offset-2" - target="_blank" - href="https://cal.com/help/billing-and-usage/messaging-credits"> - Learn more + target="_blank" + rel="noopener noreferrer" + href="https://cal.com/help/billing-and-usage/messaging-credits"> + {t("learn_more")} </Link>,
🧹 Nitpick comments (3)
apps/web/modules/settings/billing/components/BillingCredits.tsx (3)
43-66: Tighten CreditRow underline/margin classesAvoid redundant margins (
my-1plus fallbackmt-1). Simplify class logic.- <div - className={classNames( - `my-1 flex justify-between`, - underline === "dashed" - ? "border-subtle border-b border-dashed" - : underline === "solid" - ? "border-subtle border-b border-solid" - : "mt-1", - className - )}> + <div + className={classNames( + "flex justify-between", + underline ? "my-1 border-subtle border-b" : "my-1", + underline === "dashed" ? "border-dashed" : underline === "solid" ? "border-solid" : "", + className + )}>
200-204: Coalesce remaining credits to a numberEnsure formatter always receives a number.
- <CreditRow - label={t("total_credits_remaining")} - value={creditsData.credits.totalRemainingCreditsForMonth} - /> + <CreditRow + label={t("total_credits_remaining")} + value={creditsData.credits.totalRemainingCreditsForMonth ?? 0} + />
268-270: Remove target on a button-without-hreftarget has no effect on a and adds noise.
- <Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits"> + <Button color="secondary" size="sm" type="submit" data-testid="buy-credits">
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)apps/web/public/static/locales/en/common.json(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/public/static/locales/en/common.json
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
🧬 Code graph analysis (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (3)
packages/features/auth/lib/next-auth-options.ts (1)
session(746-771)packages/ui/components/progress-bar/ProgressBar.tsx (1)
ProgressBar(27-38)packages/ui/components/button/Button.tsx (1)
Button(221-349)
🔇 Additional comments (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (1)
214-219: Guard teams route when teamId is undefinedAvoid generating /settings/teams/undefined/members.
- : `/settings/teams/${teamId}/members` + : teamId + ? `/settings/teams/${teamId}/members` + : "/settings/teams"
| <p className="text-subtle text-sm font-medium leading-tight"> | ||
| {orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")} | ||
| </p> | ||
| <Button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CarinaWolli I think this is out of scope right now - alright if i do this in a follow up?
| <Label>{t("additional_credits")}</Label> | ||
| <div className="flex items-center gap-1"> | ||
| <p className="text-sm font-semibold leading-none"> | ||
| <span className="text-subtle font-medium">{t("current_balance")}</span>{" "} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <CreditRow label={t("credits_used")} value={totalUsed} underline="solid" /> | ||
| <CreditRow | ||
| label={t("total_credits_remaining")} | ||
| value={creditsData.credits.totalRemainingCreditsForMonth} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
packages/features/ee/billing/credit-service.ts (3)
654-657: User path: clarify that monthly fields are intentionally zero.Returning zeros for totalCreditsForMonth/Used/Remaining on the user path is fine, but it’s easy to misinterpret downstream. Please document this behavior to prevent UI regressions.
Add a short JSDoc above
_getAllCreditsnoting that for userId (no team), monthly fields are always 0 and only additionalCredits apply. If you prefer inline context, add a brief comment next to these properties.
664-667: Default (no userId/teamId): confirm null-object semantics.Zeroing monthly fields here is OK as a null-object response. Consider a brief comment to make this explicit for callers.
686-689: Monthly-only totals: API naming may confuse consumers.The new totals intentionally exclude additional credits (good: avoids double-subtract). To reduce ambiguity, consider renaming or documenting:
- totalCreditsForMonth → totalMonthlyPlanCredits
- totalCreditsUsedThisMonth → totalMonthlyUsed
- totalRemainingCreditsForMonth → totalMonthlyRemaining
If renaming is too disruptive, add JSDoc on
_getAllCreditsForTeamstating these are plan-only (exclude additionalCredits), while availableCredits elsewhere equals totalRemainingMonthlyCredits + additionalCredits.Also applies to: 693-697
packages/features/ee/billing/credit-service.test.ts (3)
67-74: Top-level instance spy won’t affect new instances created in beforeEach.You create a new CreditService in beforeEach, so this spy on the top-level instance is effectively dead. Switch to a prototype spy or move into beforeEach.
Apply:
-vi.spyOn(creditService, "_getAllCreditsForTeam").mockResolvedValue({ +vi.spyOn(CreditService.prototype, "_getAllCreditsForTeam").mockResolvedValue({ totalMonthlyCredits: 10, totalRemainingMonthlyCredits: 5, additionalCredits: 0, totalCreditsForMonth: 10, totalCreditsUsedThisMonth: 5, totalRemainingCreditsForMonth: 5, });
502-513: Duplicate mockResolvedValueOnce calls add noise.Each test path calls findCreditBalanceWithExpenseLogs once; the second mockResolvedValueOnce is unused. Simplify to a single mockResolvedValueOnce (or mockResolvedValue if reused).
Example:
-vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) - .mockResolvedValueOnce({ additionalCredits: 100, expenseLogs: [...] }) - .mockResolvedValueOnce({ additionalCredits: 100, expenseLogs: [...] }); +vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) + .mockResolvedValueOnce({ additionalCredits: 100, expenseLogs: [...] });Also applies to: 528-537, 553-567, 583-591
552-580: Test name no longer matches behavior.This test asserts that monthly totals exclude additional credits. Rename for clarity.
-it("should calculate total credits including additional credits for the month", async () => { +it("should keep additional credits separate from monthly totals", async () => {
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
apps/web/modules/settings/billing/components/BillingCredits.tsx(4 hunks)packages/features/ee/billing/credit-service.test.ts(10 hunks)packages/features/ee/billing/credit-service.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/modules/settings/billing/components/BillingCredits.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/billing/credit-service.test.tspackages/features/ee/billing/credit-service.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/ee/billing/credit-service.test.tspackages/features/ee/billing/credit-service.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/features/ee/billing/credit-service.test.tspackages/features/ee/billing/credit-service.ts
🧠 Learnings (2)
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality in CallService already has proper credits validation via validateCreditsForTestCall method which uses CreditService.hasAvailableCredits and throws 403 HttpError when credits are insufficient. The credits enforcement is properly implemented on the backend for test calls.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality already has proper credits validation implemented in CallService.validateCreditsForTestCall() method. This method is called early in createTestCall flow and uses CreditService.hasAvailableCredits() to check credits, throwing HttpError 403 when insufficient. The credits enforcement for test calls is already correctly implemented on the backend.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
🧬 Code graph analysis (1)
packages/features/ee/billing/credit-service.test.ts (2)
packages/lib/server/repository/credits.ts (1)
CreditsRepository(6-242)packages/features/ee/billing/credit-service.ts (1)
CreditService(55-699)
🔇 Additional comments (1)
packages/features/ee/billing/credit-service.test.ts (1)
240-243: Consistency check: new monthly fields in stubs.Stubs include totalCreditsForMonth/Used/Remaining consistent with plan-only semantics—LGTM. Ensure any UI relying on these does not add additionalCredits again when computing progress.
Also applies to: 301-304, 334-337, 356-359, 793-796, 919-922
- Replace Button href with onClick handler to open MemberInvitationModal - Add MemberInvitationModalWithoutMembers import and state management - Maintain existing team/org context support - Follow established modal usage patterns from other components - Fix lint error by using undefined instead of empty arrow function Co-Authored-By: [email protected] <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
packages/features/ee/billing/credit-service.test.ts (6)
67-81: Shadowed instance spies — switch to prototype spiesThese spies target a top-level CreditService instance that isn’t used (a new instance is created in beforeEach). Spy on the prototype to affect all instances.
Apply:
-vi.spyOn(creditService, "_getAllCreditsForTeam").mockResolvedValue({ +vi.spyOn(CreditService.prototype, "_getAllCreditsForTeam").mockResolvedValue({ totalMonthlyCredits: 10, totalRemainingMonthlyCredits: 5, additionalCredits: 0, totalCreditsUsedThisMonth: 5, }); -vi.spyOn(creditService, "_getTeamWithAvailableCredits").mockResolvedValue({ +vi.spyOn(CreditService.prototype, "_getTeamWithAvailableCredits").mockResolvedValue({ availableCredits: 3, }); -vi.spyOn(creditService, "_getAllCredits").mockResolvedValue({ +vi.spyOn(CreditService.prototype, "_getAllCredits").mockResolvedValue({ additionalCredits: 1, });
293-299: Keep remaining monthly credits non-negative (mirror prod invariant)prod clamps totalRemainingMonthlyCredits with Math.max(..., 0). Avoid returning -1 in tests to prevent misleading branches.
vi.spyOn(CreditService.prototype, "_getAllCreditsForTeam").mockResolvedValue({ totalMonthlyCredits: 500, - totalRemainingMonthlyCredits: -1, + totalRemainingMonthlyCredits: 0, additionalCredits: 0, totalCreditsUsedThisMonth: 501, });
492-504: Disambiguate batched logs by creditType in mocksTwo consecutive mocks likely represent MONTHLY then ADDITIONAL batches. Set creditType to reduce ambiguity and future breakage if aggregation changes.
vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) .mockResolvedValueOnce({ additionalCredits: 100, expenseLogs: [ - { credits: 50, date: new Date() }, - { credits: 30, date: new Date() }, + { credits: 50, date: new Date(), creditType: CreditType.MONTHLY }, + { credits: 30, date: new Date(), creditType: CreditType.MONTHLY }, ], }) .mockResolvedValueOnce({ additionalCredits: 100, - expenseLogs: [{ credits: 80, date: new Date() }], + expenseLogs: [{ credits: 80, date: new Date(), creditType: CreditType.ADDITIONAL }], });
517-526: Type-complete empty-log mocksInclude creditType even when arrays are empty to keep mocks structurally consistent with repository select.
.mockResolvedValueOnce({ additionalCredits: 100, - expenseLogs: [], + expenseLogs: [], }) .mockResolvedValueOnce({ additionalCredits: 100, - expenseLogs: [], + expenseLogs: [], }); // (Optional) if you keep a placeholder entry for structure: -// expenseLogs: [{ credits: 0, date: new Date(), creditType: CreditType.MONTHLY }], +// expenseLogs: [{ credits: 0, date: new Date(), creditType: CreditType.MONTHLY }],Also applies to: 535-536
538-565: Rename for intent + add creditType to logsName suggests “including additional credits”; assertions only reflect MONTHLY usage/remaining. Clarify name and annotate logs.
-it("should calculate total credits including additional credits for the month", async () => { +it("should surface additionalCredits while monthly usage/remaining reflect MONTHLY logs", async () => { vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) .mockResolvedValueOnce({ additionalCredits: 150, expenseLogs: [ - { credits: 80, date: new Date() }, - { credits: 40, date: new Date() }, + { credits: 80, date: new Date(), creditType: CreditType.MONTHLY }, + { credits: 40, date: new Date(), creditType: CreditType.MONTHLY }, ], }) .mockResolvedValueOnce({ additionalCredits: 150, expenseLogs: [ - { credits: 25, date: new Date() }, - { credits: 15, date: new Date() }, + { credits: 25, date: new Date(), creditType: CreditType.ADDITIONAL }, + { credits: 15, date: new Date(), creditType: CreditType.ADDITIONAL }, ], });
567-585: Add creditType to zero-additional-credits logsSmall consistency win for test readability.
.mockResolvedValueOnce({ additionalCredits: 0, - expenseLogs: [{ credits: 100, date: new Date() }], + expenseLogs: [{ credits: 100, date: new Date(), creditType: CreditType.MONTHLY }], }) .mockResolvedValueOnce({ additionalCredits: 0, - expenseLogs: [], + expenseLogs: [], });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
apps/web/modules/settings/billing/components/BillingCredits.tsx(5 hunks)packages/features/ee/billing/credit-service.test.ts(10 hunks)packages/features/ee/billing/credit-service.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/modules/settings/billing/components/BillingCredits.tsx
- packages/features/ee/billing/credit-service.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/features/ee/billing/credit-service.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/features/ee/billing/credit-service.test.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/features/ee/billing/credit-service.test.ts
🧠 Learnings (2)
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality in CallService already has proper credits validation via validateCreditsForTestCall method which uses CreditService.hasAvailableCredits and throws 403 HttpError when credits are insufficient. The credits enforcement is properly implemented on the backend for test calls.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
📚 Learning: 2025-08-26T20:20:48.989Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:589-597
Timestamp: 2025-08-26T20:20:48.989Z
Learning: In calcom/cal.com PR #22995, the test call functionality already has proper credits validation implemented in CallService.validateCreditsForTestCall() method. This method is called early in createTestCall flow and uses CreditService.hasAvailableCredits() to check credits, throwing HttpError 403 when insufficient. The credits enforcement for test calls is already correctly implemented on the backend.
Applied to files:
packages/features/ee/billing/credit-service.test.ts
🧬 Code graph analysis (1)
packages/features/ee/billing/credit-service.test.ts (2)
packages/features/ee/billing/credit-service.ts (1)
CreditService(55-692)packages/lib/server/repository/credits.ts (1)
CreditsRepository(6-242)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (3)
packages/features/ee/billing/credit-service.test.ts (3)
61-63: LGTM: reminder scheduler mockAsync mock resolves to undefined; safe and explicit.
771-776: LGTM: availableCredits aggregates remaining monthly + additional300 matches 200 monthly remaining + 100 additional.
895-900: LGTM: idempotency path stays in MONTHLY laneMonthly usage mock aligns with expected expense log creation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (1)
300-306: Add rel for external link and localize labelSecurity + i18n. This was flagged earlier; echoing to ensure it lands.
- <Link + <Link key="Credit System" className="underline underline-offset-2" target="_blank" + rel="noopener noreferrer" href="https://cal.com/help/billing-and-usage/messaging-credits"> - Learn more + {t("learn_more")} </Link>
🧹 Nitpick comments (6)
apps/web/modules/settings/billing/components/BillingCredits.tsx (6)
36-67: CreditRow: remove redundant margin class; confirm border side; optional formatter hoist
- "my-1" is always applied and the else branch adds "mt-1" again; drop the else to avoid dup margins.
- Design check: you’re drawing a bottom border; earlier specs/screens hinted at a top divider for dashed rows. Confirm if border should be top or bottom.
- Minor: you create a new Intl.NumberFormat per row; consider hoisting to a module- or component-level constant to avoid churn.
- className={classNames( - `my-1 flex justify-between`, + className={classNames( + "my-1 flex justify-between", underline === "dashed" ? "border-subtle border-b border-dashed" : underline === "solid" ? "border-subtle border-b border-solid" - : "mt-1", + : "", className )}
177-205: Clamp progress percentage to [0, 100]Protect against rounding or API drift that could push percentage outside bounds.
-const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0; +const rawPctUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0; +const teamCreditsPercentageUsed = Math.min(100, Math.max(0, rawPctUsed));
155-156: Make filename replacement global.replace(" ", "-") only replaces the first space.
-const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(" ", "-")}.csv`; +const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(/\s+/g, "-")}.csv`;
259-261: Remove invalid target on Button; decide tab behavior in onSuccesstarget is not a valid attribute on a button element. If you intend a new tab for checkout, open it when sessionUrl arrives; otherwise just remove target.
-<Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits"> +<Button color="secondary" size="sm" type="submit" data-testid="buy-credits">Optional (if new tab desired instead of router.push):
- onSuccess: (data) => { - if (data.sessionUrl) { - router.push(data.sessionUrl); - } - }, + onSuccess: (data) => { + if (data.sessionUrl) { + window.open(data.sessionUrl, "_blank", "noopener,noreferrer"); + } + },
244-256: react-hook-form: onChange override should trigger validation/dirty stateOverriding register’s onChange can bypass built-in flags. Ensure setValue updates form state.
- onChange={(e) => setValue("quantity", Number(e.target.value))} + onChange={(e) => + setValue("quantity", Number(e.target.value), { shouldDirty: true, shouldValidate: true }) + }If TextField forwards register handlers, prefer composing the handler instead of overriding it.
239-240: A11y: hide decorative icon from SRMark the info icon as decorative; Tooltip content already conveys meaning.
-<Icon name="info" className="text-emphasis h-3 w-3" /> +<Icon name="info" aria-hidden="true" className="text-emphasis h-3 w-3" />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx(5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
apps/web/modules/settings/billing/components/BillingCredits.tsx
🧬 Code graph analysis (1)
apps/web/modules/settings/billing/components/BillingCredits.tsx (2)
packages/features/auth/lib/next-auth-options.ts (1)
session(746-771)packages/features/ee/teams/components/MemberInvitationModal.tsx (1)
MemberInvitationModalWithoutMembers(471-557)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Production builds / Build Atoms
- GitHub Check: Production builds / Build API v2
- GitHub Check: Production builds / Build Web App
- GitHub Check: Production builds / Build API v1
- GitHub Check: Tests / Unit
- GitHub Check: Production builds / Build Docs
- GitHub Check: Linters / lint
- GitHub Check: Type check / check-types
🔇 Additional comments (4)
apps/web/modules/settings/billing/components/BillingCredits.tsx (4)
11-25: Imports look good and purposefulBrings in the modal and UI deps needed for the new UX without excess.
174-176: Monthly-only totals align with product directionUsing only monthly credits (excluding additional) matches the latest guidance. Nice.
211-214: Nice UX: Add members opens modal instead of redirectAddresses prior feedback and keeps users in context.
311-321: Modal gating by teamId looks correctProps match the modal’s API; noop onSettingsOpen is acceptable here.



What does this PR do?
PR Updates to the new designs for billing + credits
Image Demo (if applicable):