From ad4b5957fc238cb944621c0ee78ee2b0a20475c8 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:40:51 +0200 Subject: [PATCH] refactor: optimize credit-service imports with lazy loading (#25091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: optimize credit-service imports with lazy loading - Remove top-level imports of heavy modules (reminderScheduler, email services, i18n, billing services) - Implement dynamic imports for modules only when needed: - reminderScheduler: loaded only when SMS credit limit reached - email services: loaded only when sending credit notifications - getTranslation: loaded only when handling low credit balance - InternalTeamBilling: loaded only in getMonthlyCredits method - billing singleton: loaded only when calculating warning limits - Break circular dependency: credit-service → reminderScheduler → ... → credit-service - Update tests to mock StripeBillingService for dynamic imports - All 30 tests passing, no type errors, lint clean This reduces baseline import cost by deferring: - Stripe SDK initialization (loaded twice before) - 557KB+ i18n English translation file - Email template classes - Workflow reminder scheduler Verified with madge: circular dependency successfully resolved Co-Authored-By: morgan@cal.com * fix: add null checks for billing.getPrice() return value Co-Authored-By: morgan@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ee/billing/credit-service.test.ts | 14 ++++++++ .../features/ee/billing/credit-service.ts | 33 +++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index b10ff0f493..6dfd55ca82 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -101,6 +101,20 @@ vi.mock("../workflows/lib/reminders/reminderScheduler", () => ({ vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({ default: vi.fn().mockResolvedValue(null), })); +vi.mock("@calcom/features/ee/billing/stripe-billing-service", () => { + return { + StripeBillingService: vi.fn().mockImplementation(() => ({ + getPrice: async (priceId: string) => { + const stripe = (await import("@calcom/features/ee/payments/server/stripe")).default; + return stripe.prices.retrieve(priceId); + }, + checkoutSessionIsPaid: vi.fn(), + handleSubscriptionCancel: vi.fn(), + handleSubscriptionCreation: vi.fn(), + handleSubscriptionUpdate: vi.fn(), + })), + }; +}); const creditService = new CreditService(); diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts index b362009233..8fa19b40da 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -1,19 +1,11 @@ import type { TFunction } from "i18next"; import dayjs from "@calcom/dayjs"; -import { - sendCreditBalanceLimitReachedEmails, - sendCreditBalanceLowWarningEmails, -} from "@calcom/emails/billing-email-service"; -import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service"; -import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing"; import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; -import { cancelScheduledMessagesAndScheduleEmails } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository"; import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import logger from "@calcom/lib/logger"; -import { getTranslation } from "@calcom/lib/server/i18n"; import { CreditsRepository } from "@calcom/lib/server/repository/credits"; import { prisma, type PrismaTransaction } from "@calcom/prisma"; import { CreditUsageType, CreditType } from "@calcom/prisma/enums"; @@ -474,9 +466,9 @@ export class CreditService { const { totalMonthlyCredits } = await this._getAllCreditsForTeam({ teamId, tx }); warningLimit = totalMonthlyCredits * 0.2; } else if (userId) { - const billingService = new StripeBillingService(); - const teamMonthlyPrice = await billingService.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || ""); - const pricePerSeat = teamMonthlyPrice.unit_amount ?? 0; + const billing = (await import("@calcom/features/ee/billing")).default; + const teamMonthlyPrice = await billing.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || ""); + const pricePerSeat = teamMonthlyPrice?.unit_amount ?? 0; warningLimit = (pricePerSeat / 2) * 0.2; } @@ -495,6 +487,8 @@ export class CreditService { return null; // user has limit already reached or team has already reached limit this month } + const { getTranslation } = await import("@calcom/lib/server/i18n"); + const teamWithAdmins = creditBalance?.team ? { ...creditBalance.team, @@ -591,6 +585,10 @@ export class CreditService { try { if (result.type === "LIMIT_REACHED") { + const { sendCreditBalanceLimitReachedEmails } = await import( + "@calcom/emails/billing-email-service" + ); + const promises: Promise[] = [ sendCreditBalanceLimitReachedEmails({ team: result.team, @@ -602,6 +600,9 @@ export class CreditService { ]; if (!result.creditFor || result.creditFor === CreditUsageType.SMS) { + const { cancelScheduledMessagesAndScheduleEmails } = await import( + "@calcom/features/ee/workflows/lib/reminders/reminderScheduler" + ); promises.push( cancelScheduledMessagesAndScheduleEmails({ teamId: result.teamId, userId: result.userId }).catch( (error) => { @@ -613,6 +614,7 @@ export class CreditService { await Promise.all(promises); } else if (result.type === "WARNING") { + const { sendCreditBalanceLowWarningEmails } = await import("@calcom/emails/billing-email-service"); await sendCreditBalanceLowWarningEmails({ balance: result.balance, team: result.team, @@ -653,6 +655,7 @@ export class CreditService { if (!team) return 0; + const { InternalTeamBilling } = await import("@calcom/features/ee/billing/teams/internal-team-billing"); const teamBillingService = new InternalTeamBilling(team); const subscriptionStatus = await teamBillingService.getSubscriptionStatus(); @@ -668,7 +671,7 @@ export class CreditService { return activeMembers * creditsPerSeat; } - const billingService = new StripeBillingService(); + const billing = (await import("@calcom/features/ee/billing")).default; const priceId = process.env.STRIPE_TEAM_MONTHLY_PRICE_ID; if (!priceId) { @@ -676,7 +679,11 @@ export class CreditService { return 0; } - const monthlyPrice = await billingService.getPrice(priceId); + const monthlyPrice = await billing.getPrice(priceId); + if (!monthlyPrice) { + log.warn("Failed to retrieve monthly price", { teamId, priceId }); + return 0; + } const pricePerSeat = monthlyPrice.unit_amount ?? 0; const creditsPerSeat = pricePerSeat * 0.5;