Skip to content

Conversation

@hemantmm
Copy link
Contributor

closes: #21976

What does this PR do?

✅ Added comprehensive auto-recharge functionality to CreditService
✅ Created dedicated methods for threshold monitoring and payment processing
✅ Added user-configurable threshold and recharge amount settings
✅ Enhanced email notifications for different recharge scenarios
✅ Integrated seamlessly with existing Stripe billing service

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

@hemantmm hemantmm requested a review from a team as a code owner September 14, 2025 14:53
@vercel
Copy link

vercel bot commented Sep 14, 2025

@hemantmm is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds end-to-end auto-recharge for credit balances: database schema fields and purchase log changes; stores Stripe customer IDs on checkout completion; repository API refactors and new purchase/expense log methods; CreditService probes and processes auto-recharge, exposes get/update auto-recharge settings; StripeBillingService gains createAutoRechargePaymentIntent; TRPC adds getAutoRechargeSettings and updateAutoRechargeSettings guarded by IS_SMS_CREDITS_ENABLED; web BillingCredits UI shows status and a modal to configure settings; email templates include auto-recharge flags and adjusted subjects/text.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR includes several changes that appear out-of-scope or breaking relative to the linked issues, notably Prisma schema renames (CreditPurchaseLog.createdAt → date), removal of an index on DelegationCredential, and public API/signature changes in StripeBillingService (constructor signature, removed interface implementation, altered handleSubscriptionCreation), plus other repository and export signature changes; these are not strictly required to implement auto-recharge and could break consumers or require separate migrations. Split or isolate breaking and non-essential refactors into separate PRs, provide explicit DB migration scripts and a compatibility plan, and update or audit all callers/tests that rely on changed public APIs before merging this feature PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: auto-recharge-credit-balance" is concise, clearly focused on the primary feature introduced by the changeset (auto-recharge for credit balances), and directly matches the main implementation work described in the diff and PR objectives.
Linked Issues Check ✅ Passed The code changes implement the core linked-issue requirements: CreditService gained threshold detection and recharge processing (_handleAutoRecharge and _processAutoRecharge), the schema and repository expose autoRechargeEnabled/threshold/amount and lastAutoRechargeAt, TRPC endpoints allow getting/updating settings, UI components provide configuration, and Stripe integration (stripeCustomerId storage and createAutoRechargePaymentIntent) supports off-session payments; together these satisfy the objective to detect when balance falls below a threshold and top it up to a configured amount.
Description Check ✅ Passed The PR description is directly related to the changeset: it references the closed issues, summarizes the implemented auto-recharge functionality, notes integration points (Stripe, emails, CreditService), and indicates tests/docs were updated, so it meets the lenient relevance requirement.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

❤️ Share

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

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Sep 14, 2025
@github-actions github-actions bot added Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request labels Sep 14, 2025
@graphite-app graphite-app bot requested a review from a team September 14, 2025 14:53
@dosubot dosubot bot added billing area: billing, stripe, payments, paypal, get paid ❗️ migrations contains migration files labels Sep 14, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/features/ee/billing/api/webhook/_checkout.session.completed.ts (1)

40-41: Fix: session is out of scope in saveToCreditBalance (breaks on runtime).

saveToCreditBalance references session.customer, but session only exists in handler. Pass stripeCustomerId from the caller and use it inside the function.

Apply this diff:

-  await saveToCreditBalance({ userId, teamId, nrOfCredits });
+  await saveToCreditBalance({
+    userId,
+    teamId,
+    nrOfCredits,
+    stripeCustomerId:
+      typeof session.customer === "string" ? session.customer : session.customer?.id,
+  });
-async function saveToCreditBalance({
-  userId,
-  teamId,
-  nrOfCredits,
-}: {
-  userId?: number;
-  teamId?: number;
-  nrOfCredits: number;
-}) {
+async function saveToCreditBalance({
+  userId,
+  teamId,
+  nrOfCredits,
+  stripeCustomerId,
+}: {
+  userId?: number;
+  teamId?: number;
+  nrOfCredits: number;
+  stripeCustomerId?: string | null;
+}) {
   const creditBalance = await CreditsRepository.findCreditBalance({ teamId, userId });
-
-  const stripeCustomerId = session.customer as string;
+  const customerId = stripeCustomerId ?? null;
-    await CreditsRepository.updateCreditBalance({
+    await CreditsRepository.updateCreditBalance({
       id: creditBalance.id,
-      data: {
+      data: {
         additionalCredits: { increment: nrOfCredits },
         limitReachedAt: null,
         warningSentAt: null,
-        stripeCustomerId: stripeCustomerId, // Store customer ID for future auto-recharge
+        stripeCustomerId: customerId, // Store customer ID for future auto-recharge
       },
     });
-    const newCreditBalance = await CreditsRepository.createCreditBalance({
+    const newCreditBalance = await CreditsRepository.createCreditBalance({
       teamId: teamId,
       userId: !teamId ? userId : undefined,
       additionalCredits: nrOfCredits,
-      stripeCustomerId: stripeCustomerId, // Store customer ID for future auto-recharge
+      stripeCustomerId: customerId, // Store customer ID for future auto-recharge
     });

Also applies to: 56-57, 63-69, 75-76

packages/features/ee/billing/credit-service.ts (1)

404-423: Missing repository method: createCreditExpenseLog — update call or restore method

CreditsRepository no longer exposes createCreditExpenseLog; createCreditPurchaseLog exists in packages/lib/server/repository/credits.ts (≈line 235). Update the call in packages/features/ee/billing/credit-service.ts (lines 404–423) to use the maintained API (or reintroduce createCreditExpenseLog with equivalent behavior/signature).

packages/lib/server/repository/credits.ts (3)

20-35: Use composite unique key (teamId_userId) and explicit null checks.

Schema has a composite unique on (teamId, userId). findUnique({ where: { teamId } }) / { userId } won’t use that index and may fail after the schema change. Also avoid truthy checks for numeric IDs.

-    if (teamId) {
-      return await prismaClient.creditBalance.findUnique({
-        where: {
-          teamId,
-        },
-        select,
-      });
-    }
-
-    if (userId) {
-      return await prismaClient.creditBalance.findUnique({
-        where: { userId },
-        select,
-      });
-    }
+    if (teamId != null && userId != null) {
+      throw new Error("Provide either teamId or userId, not both.");
+    }
+    const where =
+      teamId != null
+        ? ({ teamId_userId: { teamId, userId: null } } as Prisma.CreditBalanceWhereUniqueInput)
+        : userId != null
+        ? ({ teamId_userId: { teamId: null, userId } } as Prisma.CreditBalanceWhereUniqueInput)
+        : null;
+    if (!where) return null;
+    return await prismaClient.creditBalance.findUnique({ where, select });

95-109: Unify to composite unique lookup; remove separate if branches.

Use (teamId_userId) with explicit nulls. This also avoids truthy checks on IDs.

-    if (teamId) {
-      return await prismaClient.creditBalance.findUnique({
-        where: {
-          teamId,
-        },
-        select,
-      });
-    }
-
-    if (userId) {
-      return await prismaClient.creditBalance.findUnique({
-        where: { userId },
-        select,
-      });
-    }
+    if (teamId == null && userId == null) return null;
+    const where: Prisma.CreditBalanceWhereUniqueInput = {
+      teamId_userId: { teamId: teamId ?? null, userId: userId ?? null },
+    };
+    return await prismaClient.creditBalance.findUnique({ where, select });

126-159: findUnique should use the composite unique teamId_userId.

The current where mixes single fields. Align with the composite key.

-    return await prismaClient.creditBalance.findUnique({
-      where: {
-        teamId,
-        ...(!teamId ? { userId } : {}),
-      },
+    return await prismaClient.creditBalance.findUnique({
+      where: {
+        teamId_userId: { teamId: teamId ?? null, userId: userId ?? null },
+      },
       select: {
         additionalCredits: true,
         expenseLogs: {
           where: {
             date: {
               gte: startDate,
               lte: endDate,
             },
             ...(creditType ? { creditType } : {}),
           },
           orderBy: {
             date: "desc",
           },
           select: {
             date: true,
             credits: true,
             creditType: true,
             bookingUid: true,
             smsSid: true,
             smsSegments: true,
             phoneNumber: true,
             email: true,
             callDuration: true,
             externalRef: true,
           },
         },
       },
     });
🧹 Nitpick comments (9)
packages/features/ee/billing/api/webhook/_checkout.session.completed.ts (1)

60-85: Optional: wrap balance update + purchase log in a DB transaction.

Prevents partial state if logging fails after increment.

packages/prisma/schema.prisma (1)

606-614: Check uniqueness strategy; add helpful indexes.

  • You have unique on userId, unique on teamId, and composite @@unique([teamId, userId]). The composite looks redundant; keep either the two single-column uniques or only the composite, not both, to avoid unnecessary indexes.

Apply (if you prefer composite only):

 model CreditBalance {
   id                String              @id @default(uuid())
-  userId            Int?                @unique
-  teamId            Int?                @unique
+  userId            Int?
+  teamId            Int?
   additionalCredits Int                 @default(0)
   limitReachedAt    DateTime?
   warningSentAt     DateTime?
   team              Team?               @relation(fields: [teamId], references: [id], onDelete: Cascade)
   user              User?               @relation(fields: [userId], references: [id], onDelete: Cascade)
   expenseLogs       CreditExpenseLog[]
   purchaseLogs      CreditPurchaseLog[]
   // Add auto-recharge fields
   autoRechargeEnabled   Boolean   @default(false)
   autoRechargeThreshold Int?
   autoRechargeAmount    Int?
   stripeCustomerId      String?
   lastAutoRechargeAt    DateTime?
 
   @@unique([teamId, userId])
+  @@index([stripeCustomerId])
+  @@index([lastAutoRechargeAt])
 }
packages/emails/templates/credit-balance-limit-reached-email.ts (1)

62-69: Consider localizing plain-text fallback.

getTextBody() returns hard-coded English. If these are user-facing, consider this.user.t(...) variants.

apps/web/modules/settings/billing/components/BillingCredits.tsx (2)

183-190: Localize link text.

Replace hard-coded “Learn more” with t("learn_more") (or existing key).

-              Learn more
+              {t("learn_more")}

140-141: Nit: replace all spaces when generating filename.

Current .replace(" ", "-") changes only 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`;
packages/features/ee/billing/credit-service.ts (1)

755-816: Consider basic throttling for auto‑recharge.

To avoid multiple quick consecutive charges, gate by lastAutoRechargeAt (e.g., min interval).

-    if (remainingCredits < creditBalance.autoRechargeThreshold) {
+    const minIntervalMs = 10 * 60 * 1000; // 10 minutes
+    const recent = creditBalance.lastAutoRechargeAt
+      ? Date.now() - new Date(creditBalance.lastAutoRechargeAt).getTime() < minIntervalMs
+      : false;
+    if (!recent && remainingCredits < creditBalance.autoRechargeThreshold) {
packages/lib/server/repository/credits.ts (3)

63-94: Potential overfetch/PII from team.members.user selection.

You’re selecting email/locale for all team members. If only a subset is needed for notifications, consider trimming to the minimal fields to reduce payload and PII exposure.


116-118: Timezone semantics for default startDate.

dayjs().startOf("month").toDate() is local-timezone based. Confirm this matches reporting expectations (vs. UTC boundaries).


122-122: Avoid falsy checks for IDs.

if (!teamId && !userId) treats 0 as absent. Use explicit null checks.

-    if (!teamId && !userId) return null;
+    if (teamId == null && userId == null) return null;
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3aab13b and c8064f8.

📒 Files selected for processing (9)
  • apps/web/modules/settings/billing/components/BillingCredits.tsx (8 hunks)
  • packages/emails/templates/credit-balance-limit-reached-email.ts (2 hunks)
  • packages/emails/templates/credit-balance-low-warning-email.ts (3 hunks)
  • packages/features/ee/billing/api/webhook/_checkout.session.completed.ts (1 hunks)
  • packages/features/ee/billing/credit-service.ts (2 hunks)
  • packages/features/ee/billing/stripe-billling-service.ts (4 hunks)
  • packages/lib/server/repository/credits.ts (2 hunks)
  • packages/prisma/schema.prisma (1 hunks)
  • packages/trpc/server/routers/viewer/credits.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/ee/billing/api/webhook/_checkout.session.completed.ts
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/emails/templates/credit-balance-low-warning-email.ts
  • packages/lib/server/repository/credits.ts
  • packages/features/ee/billing/credit-service.ts
  • packages/features/ee/billing/stripe-billling-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/api/webhook/_checkout.session.completed.ts
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/trpc/server/routers/viewer/credits.tsx
  • packages/emails/templates/credit-balance-low-warning-email.ts
  • apps/web/modules/settings/billing/components/BillingCredits.tsx
  • packages/lib/server/repository/credits.ts
  • packages/features/ee/billing/credit-service.ts
  • packages/features/ee/billing/stripe-billling-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/api/webhook/_checkout.session.completed.ts
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/trpc/server/routers/viewer/credits.tsx
  • packages/emails/templates/credit-balance-low-warning-email.ts
  • apps/web/modules/settings/billing/components/BillingCredits.tsx
  • packages/lib/server/repository/credits.ts
  • packages/features/ee/billing/credit-service.ts
  • packages/features/ee/billing/stripe-billling-service.ts
**/*.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:

  • packages/trpc/server/routers/viewer/credits.tsx
  • apps/web/modules/settings/billing/components/BillingCredits.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-17T22:00:16.329Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts:117-126
Timestamp: 2025-08-17T22:00:16.329Z
Learning: In calcom/cal.com PR #22995, packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts, the enabled input parameter in the update endpoint is intentionally not forwarded to aiService.updateAgentConfiguration() as the enabled/disabled agent functionality is not required at the moment (per maintainer Udit-takkar). Future reviews should not flag this as missing functionality unless requirements change.

Applied to files:

  • packages/trpc/server/routers/viewer/credits.tsx
📚 Learning: 2025-08-14T10:48:52.586Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/trpc/server/routers/viewer/ai/_router.ts:46-84
Timestamp: 2025-08-14T10:48:52.586Z
Learning: In calcom/cal.com PR #22995, packages/trpc/server/routers/viewer/ai/_router.ts, the voiceId input parameter in the create endpoint is intentionally not forwarded to aiService.createAgent() as voice customization is not required at the moment (per maintainer Udit-takkar). Future reviews should not flag this as missing functionality unless requirements change.

Applied to files:

  • packages/trpc/server/routers/viewer/credits.tsx
🧬 Code graph analysis (5)
packages/features/ee/billing/api/webhook/_checkout.session.completed.ts (1)
packages/lib/server/repository/credits.ts (1)
  • CreditsRepository (6-256)
packages/emails/templates/credit-balance-limit-reached-email.ts (1)
packages/lib/constants.ts (1)
  • EMAIL_FROM_NAME (32-32)
packages/trpc/server/routers/viewer/credits.tsx (2)
packages/trpc/server/trpc.ts (1)
  • router (13-13)
packages/lib/constants.ts (1)
  • IS_SMS_CREDITS_ENABLED (227-228)
apps/web/modules/settings/billing/components/BillingCredits.tsx (4)
packages/trpc/react/trpc.ts (1)
  • trpc (54-138)
packages/ui/components/button/Button.tsx (1)
  • Button (221-349)
packages/platform/atoms/src/components/ui/dialog.tsx (2)
  • DialogTitle (125-125)
  • DialogDescription (126-126)
packages/ui/components/form/index.ts (1)
  • InputError (22-22)
packages/features/ee/billing/credit-service.ts (2)
packages/lib/server/repository/credits.ts (1)
  • CreditsRepository (6-256)
packages/features/ee/billing/stripe-billling-service.ts (1)
  • StripeBillingService (6-247)
🪛 Biome (2.1.2)
apps/web/modules/settings/billing/components/BillingCredits.tsx

[error] 120-120: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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: Detect changes
🔇 Additional comments (7)
packages/emails/templates/credit-balance-limit-reached-email.ts (2)

41-47: Subject logic LGTM.

Conditional subjects read well and use i18n.


49-57: Payload wiring LGTM.

Props include the new flags; matches template usage.

packages/emails/templates/credit-balance-low-warning-email.ts (1)

58-63: Looks good; matches settings flag.

Plain-text branch varies on autoRechargeEnabled appropriately.

packages/lib/server/repository/credits.ts (4)

37-49: LGTM on targeted select.

Narrow select and unique lookup by externalRef look good.


145-155: PII in expense logs — ensure not exposed beyond server boundary.

Returning smsSid, phoneNumber, and email is sensitive. Verify downstream (tRPC/API) never forwards these fields to clients; if needed, map to a DTO/safe projection there.


235-255: LGTM on purchase log creation.

Minimal fields, explicit date, and autoRecharged flag look good.


13-18: Auto‑recharge settings not selected by finder methods.

If any caller expects autoRechargeEnabled/Threshold/Amount/stripeCustomerId from these finders (for threshold checks or UI), add them to the select; otherwise confirm they’re fetched elsewhere.

Also applies to: 64-68

Signed-off-by: Hemant M Mehta <[email protected]>
@hemantmm hemantmm requested a review from a team September 14, 2025 15:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/lib/server/repository/credits.ts (1)

179-184: Bug: gte: startDate / lte: endDate pass undefined to Prisma when omitted.

Build the date filter conditionally; otherwise Prisma may reject the query.

-            date: {
-              gte: startDate,
-              lte: endDate,
-            },
+            ...(startDate || endDate
+              ? {
+                  date: {
+                    ...(startDate ? { gte: startDate } : {}),
+                    ...(endDate ? { lte: endDate } : {}),
+                  },
+                }
+              : {}),

If you prefer clarity, compute a local const dateWhere before the query and spread it.

♻️ Duplicate comments (3)
packages/features/ee/billing/stripe-billling-service.ts (1)

201-251: Charge amount calculation is incorrect; use configured Stripe Price and default PM.

Hardcoding 1 cent per credit will misbill; also select customer’s default payment method and fail fast on SCA.

   async createAutoRechargePaymentIntent({
     customerId,
-    amount,
+    amount, // quantity of credits
     metadata,
   }: {
     customerId: string;
     amount: number;
     metadata: {
       creditBalanceId: string;
       teamId: string;
       userId: string;
       autoRecharge: string;
     };
   }): Promise<{ success: boolean; error?: string }> {
     try {
-      const priceInCents = 1; // Price per credit in cents
-      const totalAmount = amount * priceInCents;
+      if (!Number.isInteger(amount) || amount <= 0) {
+        return { success: false, error: "Invalid recharge quantity" };
+      }
+      const creditsPriceId = process.env.NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID;
+      if (!creditsPriceId) {
+        return { success: false, error: "Credits price not configured" };
+      }
+      const price = await this.stripe.prices.retrieve(creditsPriceId);
+      const unitAmount = (price as Stripe.Price).unit_amount;
+      if (!unitAmount || unitAmount <= 0) {
+        return { success: false, error: "Invalid unit amount on credits price" };
+      }
+      const totalAmount = unitAmount * amount;
 
       // Get customer's payment methods
       const paymentMethods = await this.stripe.paymentMethods.list({
         customer: customerId,
         type: "card",
       });
 
       if (!paymentMethods.data.length) {
         return { success: false, error: "No payment methods found for customer" };
       }
 
-      // Use the default payment method (most recently added)
-      const defaultPaymentMethod = paymentMethods.data[0].id;
+      // Use customer's default PM if set; else first available
+      const customer = (await this.stripe.customers.retrieve(customerId)) as Stripe.Customer;
+      const defaultPm =
+        (customer.invoice_settings?.default_payment_method as string | null) ??
+        paymentMethods.data[0].id;
 
       // Create and confirm a payment intent
-      await this.stripe.paymentIntents.create({
+      await this.stripe.paymentIntents.create(
+        {
           amount: totalAmount,
           currency: "usd",
           customer: customerId,
-          payment_method: defaultPaymentMethod,
+          payment_method: defaultPm,
           off_session: true,
-          confirm: true,
+          confirm: true,
+          error_on_requires_action: true,
           metadata,
-      });
+        },
+        {
+          idempotencyKey: `auto_recharge:${metadata.creditBalanceId}:${metadata.userId}:${Date.now()}`,
+        }
+      );
 
       return { success: true };
     } catch (error) {
       console.error("Error processing auto-recharge payment:", error);
       return {
         success: false,
         error: error instanceof Error ? error.message : "Unknown error",
       };
     }
   }
packages/lib/server/repository/credits.ts (2)

237-256: Enforce exactly one of teamId or userId on create; avoid truthy checks.

This was previously flagged; still missing the guard and uses truthy checks for IDs.

   static async createCreditBalance(
@@
   ) {
+    if ((teamId == null) === (userId == null)) {
+      throw new Error("Exactly one of teamId or userId must be provided.");
+    }
     return tx.creditBalance.create({
       data: {
@@
-        team: teamId
+        team: teamId != null
           ? {
               connect: {
                 id: teamId,
               },
             }
           : undefined,
-        user: userId
+        user: userId != null
           ? {
               connect: {
                 id: userId,
               },
             }
           : undefined,

Also applies to: 264-277


221-229: Avoid truthy checks and clarify error in updateCreditBalance.

Use explicit null checks; keep the guard error message consistent.

-    if (id) {
+    if (id != null) {
       where = { id };
-    } else if (teamId) {
+    } else if (teamId != null) {
       where = { teamId };
-    } else if (userId) {
+    } else if (userId != null) {
       where = { userId };
     } else {
-      throw new Error("Either id, teamId or userId must be provided");
+      throw new Error("updateCreditBalance requires id or one of teamId/userId");
     }
🧹 Nitpick comments (8)
packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx (1)

27-39: Consider adding a “manage auto‑recharge” CTA when enabled.

Helps recipients discover/adjust settings directly.

       if (autoRechargeEnabled) {
         return (
           <>
             <p style={{ fontWeight: 400, lineHeight: "24px" }}>
               {user.t("hi_user_name", { name: user.name })},
             </p>
             <p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
               {team
                 ? user.t("team_credits_low_auto_recharge", { teamName: team.name, balance })
                 : user.t("user_credits_low_auto_recharge", { balance })}
             </p>
+            <div style={{ textAlign: "center", marginTop: "24px" }}>
+              <CallToAction
+                label={user.t("manage_auto_recharge")}
+                href={team ? `${WEBAPP_URL}/settings/teams/${team.id}/billing` : `${WEBAPP_URL}/settings/billing`}
+                endIconName="linkIcon"
+              />
+            </div>
           </>
         );
       }
packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx (1)

106-116: Subject could reflect auto‑recharge enabled state.

Optional: when autoRechargeEnabled is true, subject can reduce urgency.

   const getSubject = () => {
     if (autoRechargeFailed) {
       return user.t("auto_recharge_failed");
     }
-
-    if (team) {
+    if (autoRechargeEnabled) {
+      return team
+        ? user.t("credit_limit_reached_auto_recharge", { teamName: team.name })
+        : user.t("credit_limit_reached_auto_recharge_user");
+    }
+    if (team) {
       return user.t("action_required_out_of_credits", { teamName: team.name });
     }
     return user.t("action_required_user_out_of_credits");
   };
packages/emails/templates/credit-balance-limit-reached-email.ts (1)

9-9: Default export is acceptable here; consider named export later.

Template classes are primary module exports; keep as is unless you plan a broader refactor.

packages/features/ee/billing/stripe-billling-service.ts (3)

8-12: Optional: set retry/timeout for Stripe client.

Improve resilience on transient failures.

-    this.stripe = new Stripe(key, {
-      apiVersion: "2023-10-16",
-    });
+    this.stripe = new Stripe(key, {
+      apiVersion: "2023-10-16",
+      maxNetworkRetries: 2,
+      // timeout: 20000, // enable if supported in your Stripe SDK version
+    });

53-63: Avoid as never; use proper params type for Checkout session.

Explicit types prevent masking API drift.

// import type { Stripe } from "stripe";
const params: Stripe.Checkout.SessionCreateParams = {
  line_items: [{ price: priceId, quantity }],
  mode: "payment",
  success_url: successUrl,
  cancel_url: cancelUrl,
  metadata,
  invoice_creation: { enabled: true },
};
const session = await this.stripe.checkout.sessions.create(params);

1-1: Nit: filename has triple “l” (billling).

Consider renaming to stripe-billing-service.ts for consistency.

packages/lib/server/repository/credits.ts (2)

164-169: Redundant prismaClient shadowing.

You already default tx to prisma; drop the extra indirection.

-    const prismaClient = tx ?? prisma;
-
-    return await prismaClient.creditBalance.findUnique({
+    return await tx.creditBalance.findUnique({

282-301: Optional: allow date injection for backfills/replays.

Consider accepting an optional date param here (defaulting to now) to support deterministic log backfills.

-      autoRecharged = false,
+      autoRecharged = false,
+      date = new Date(),
@@
-        date: new Date(),
+        date,
📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between c8064f8 and 927a2e5.

📒 Files selected for processing (8)
  • packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx (1 hunks)
  • packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx (1 hunks)
  • packages/emails/templates/credit-balance-limit-reached-email.ts (2 hunks)
  • packages/emails/templates/credit-balance-low-warning-email.ts (3 hunks)
  • packages/features/ee/billing/credit-service.ts (2 hunks)
  • packages/features/ee/billing/stripe-billling-service.ts (4 hunks)
  • packages/lib/server/repository/credits.ts (3 hunks)
  • packages/platform/libraries/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/emails/templates/credit-balance-low-warning-email.ts
  • packages/features/ee/billing/credit-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/platform/libraries/index.ts
  • packages/features/ee/billing/stripe-billling-service.ts
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/lib/server/repository/credits.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/platform/libraries/index.ts
  • packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx
  • packages/features/ee/billing/stripe-billling-service.ts
  • packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/lib/server/repository/credits.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/platform/libraries/index.ts
  • packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx
  • packages/features/ee/billing/stripe-billling-service.ts
  • packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx
  • packages/emails/templates/credit-balance-limit-reached-email.ts
  • packages/lib/server/repository/credits.ts
**/*.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:

  • packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx
  • packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx
🧬 Code graph analysis (3)
packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx (1)
packages/lib/constants.ts (1)
  • WEBAPP_URL (12-18)
packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx (1)
packages/lib/constants.ts (1)
  • WEBAPP_URL (12-18)
packages/emails/templates/credit-balance-limit-reached-email.ts (1)
packages/lib/constants.ts (1)
  • EMAIL_FROM_NAME (32-32)
🔇 Additional comments (8)
packages/platform/libraries/index.ts (1)

141-141: Re-export looks fine; confirm server-only usage.

Ensure CreditService isn’t imported by client bundles (web/SSR boundary). If needed, gate via server-only entrypoint.

packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx (2)

24-41: Solid i18n usage and clear branching.

Props and content paths look correct; CTA paths are appropriate for team vs user.


35-37: Verify new translation keys exist.

Ensure these keys are added to locales: team_credits_low_auto_recharge, user_credits_low_auto_recharge, team_credits_low_warning_message, user_credits_low_warning_message, buy_credits, manage_auto_recharge (if added).

Also applies to: 49-50, 66-66

packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx (2)

26-65: Good consolidation via helpers; branches read well.


79-83: CTA destinations are correct (team vs user).

No changes needed.

Also applies to: 96-100

packages/emails/templates/credit-balance-limit-reached-email.ts (2)

41-47: Subject logic matches TSX template.

Looks consistent with UI template.


63-69: ```shell
#!/bin/bash

safer verification script — don't exit on first error

file="packages/emails/templates/credit-balance-limit-reached-email.ts"
echo "PWD: $(pwd)"
echo

if [ -f "$file" ]; then
echo "=== SHOWING $file (with line numbers) ==="
awk '{printf "%4d: %s\n", NR, $0}' "$file" | sed -n '1,240p' || cat "$file"
else
echo "FILE_NOT_FOUND: $file"
echo "Searching for similar filenames..."
rg -n --hidden "credit-balance-limit-reached-email|credit.*balance.*limit" -S || true
fi

echo
echo "=== Search for translation usage (.t, translate, i18n) ==="
rg -n --hidden "\.t\(|translate\(|\bi18n\b" -S || true

echo
echo "=== Search for 'this.user' / 'this.team' / 'user.t' occurrences ==="
rg -n --hidden "this\.user|this\.team|user\.t\(|this\.user\.t\(" -S || true

echo
echo "=== Search for autoRecharge flags and getTextBody definitions ==="
rg -n --hidden "autoRechargeFailed|autoRechargeEnabled|getTextBody\s*\(" -S || true

echo
echo "=== List files under packages/emails for context ==="
ls -la packages/emails || true

echo
echo "=== Done ==="


</blockquote></details>
<details>
<summary>packages/lib/server/repository/credits.ts (1)</summary><blockquote>

`11-26`: **Confirmed: CreditBalance.teamId and CreditBalance.userId are individually @unique (packages/prisma/schema.prisma).**
Both fields have @unique and there is also @@unique([teamId, userId]); using findUnique({ where: { teamId } }) / findUnique({ where: { userId } }) is valid.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Contributor

@Devanshusharma2005 Devanshusharma2005 left a comment

Choose a reason for hiding this comment

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

lets Address the coderabbit suggestions.
Typechecks are failing also and please add unit tests also.
And before and after change in loom would be better.

@kart1ka kart1ka marked this pull request as draft September 15, 2025 08:53
@github-actions
Copy link
Contributor

github-actions bot commented Oct 9, 2025

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label Oct 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

billing area: billing, stripe, payments, paypal, get paid community Created by Linear-GitHub Sync consumer ✨ feature New feature or request Medium priority Created by Linear-GitHub Sync ❗️ migrations contains migration files size/XXL Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto recharge credit balance

2 participants