diff --git a/packages/features/ee/billing/billing-service-factory.ts b/packages/features/ee/billing/billing-service-factory.ts deleted file mode 100644 index aa26ea520c..0000000000 --- a/packages/features/ee/billing/billing-service-factory.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { StripeBillingService } from "./stripe-billing-service"; - -export class BillingFactory { - constructor() { - return new StripeBillingService(); - } -} diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index 6dfd55ca82..b10ff0f493 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -101,20 +101,6 @@ 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 8fa19b40da..c2237e0be4 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -466,7 +466,8 @@ export class CreditService { const { totalMonthlyCredits } = await this._getAllCreditsForTeam({ teamId, tx }); warningLimit = totalMonthlyCredits * 0.2; } else if (userId) { - const billing = (await import("@calcom/features/ee/billing")).default; + const { StripeBillingService } = await import("./stripe-billing-service"); + const billing = new StripeBillingService(); const teamMonthlyPrice = await billing.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || ""); const pricePerSeat = teamMonthlyPrice?.unit_amount ?? 0; warningLimit = (pricePerSeat / 2) * 0.2; @@ -671,7 +672,8 @@ export class CreditService { return activeMembers * creditsPerSeat; } - const billing = (await import("@calcom/features/ee/billing")).default; + const { StripeBillingService } = await import("./stripe-billing-service"); + const billing = new StripeBillingService(); const priceId = process.env.STRIPE_TEAM_MONTHLY_PRICE_ID; if (!priceId) { diff --git a/packages/features/ee/billing/index.ts b/packages/features/ee/billing/index.ts deleted file mode 100644 index 86cf66bfe0..0000000000 --- a/packages/features/ee/billing/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BillingService } from "./billing-service"; -import { BillingFactory } from "./billing-service-factory"; - -const globalForBilling = global as unknown as { - billingService: BillingService; -}; - -const billing = globalForBilling.billingService || new BillingFactory(); - -if (process.env.NODE_ENV !== "production") { - globalForBilling.billingService = billing; -} - -export default billing; diff --git a/packages/features/ee/billing/teams/internal-team-billing.test.ts b/packages/features/ee/billing/teams/internal-team-billing.test.ts index 548ae8ba7e..c37f0ee9f4 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.test.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.test.ts @@ -5,11 +5,29 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import * as billingModule from ".."; import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory"; +import { StripeBillingService } from "../stripe-billing-service"; import { InternalTeamBilling } from "./internal-team-billing"; import { TeamBillingPublishResponseStatus } from "./team-billing"; +const { + mockHandleSubscriptionCancel, + mockHandleSubscriptionUpdate, + mockCheckoutSessionIsPaid, + mockGetSubscriptionStatus, + mockHandleEndTrial, +} = vi.hoisted(() => ({ + mockHandleSubscriptionCancel: vi.fn(), + mockHandleSubscriptionUpdate: vi.fn(), + mockCheckoutSessionIsPaid: vi.fn(), + mockGetSubscriptionStatus: vi.fn(), + mockHandleEndTrial: vi.fn(), +})); + +vi.mock("../stripe-billing-service", () => ({ + StripeBillingService: vi.fn(), +})); + vi.mock("@calcom/lib/constants", async () => { const actual = await vi.importActual("@calcom/lib/constants"); return { @@ -18,14 +36,6 @@ vi.mock("@calcom/lib/constants", async () => { }; }); -vi.mock("..", () => ({ - default: { - handleSubscriptionCancel: vi.fn(), - handleSubscriptionUpdate: vi.fn(), - checkoutSessionIsPaid: vi.fn(), - }, -})); - vi.mock("@calcom/features/ee/teams/lib/payments", () => ({ purchaseTeamOrOrgSubscription: vi.fn(), })); @@ -43,8 +53,18 @@ const mockTeam = { }; describe("InternalTeamBilling", () => { + let internalTeamBilling: InternalTeamBilling; + beforeEach(() => { - vi.resetAllMocks(); + vi.clearAllMocks(); + vi.mocked(StripeBillingService).mockImplementation(() => ({ + handleSubscriptionCancel: mockHandleSubscriptionCancel, + handleSubscriptionUpdate: mockHandleSubscriptionUpdate, + checkoutSessionIsPaid: mockCheckoutSessionIsPaid, + getSubscriptionStatus: mockGetSubscriptionStatus, + handleEndTrial: mockHandleEndTrial, + })); + internalTeamBilling = new InternalTeamBilling(mockTeam); }); afterEach(() => { @@ -52,11 +72,10 @@ describe("InternalTeamBilling", () => { }); describe("cancel", () => { - const internalTeamBilling = new InternalTeamBilling(mockTeam); it("should cancel the subscription and downgrade the team", async () => { await internalTeamBilling.cancel(); - expect(billingModule.default.handleSubscriptionCancel).toHaveBeenCalledWith("sub_123"); + expect(mockHandleSubscriptionCancel).toHaveBeenCalledWith("sub_123"); expect(prismaMock.team.update).toHaveBeenCalledWith({ where: { id: 1 }, data: { @@ -67,9 +86,8 @@ describe("InternalTeamBilling", () => { }); describe("publish", () => { - const internalTeamBilling = new InternalTeamBilling(mockTeam); it("should create a checkout session and update the team", async () => { - vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(false); + mockCheckoutSessionIsPaid.mockResolvedValue(false); vi.mocked(purchaseTeamOrOrgSubscription).mockResolvedValue({ url: "http://checkout.url", }); @@ -89,7 +107,6 @@ describe("InternalTeamBilling", () => { }); }); it("should return upgrade url if upgrade is required", async () => { - const internalTeamBilling = new InternalTeamBilling(mockTeam); const mockUrl = `${WEBAPP_URL}/api/teams/${mockTeam.id}/upgrade?session_id=cs_789`; vi.spyOn(internalTeamBilling, "checkIfTeamPaymentRequired").mockResolvedValue({ url: mockUrl, @@ -123,7 +140,7 @@ describe("InternalTeamBilling", () => { await internalTeamBilling.updateQuantity(); - expect(billingModule.default.handleSubscriptionUpdate).toHaveBeenCalledWith({ + expect(mockHandleSubscriptionUpdate).toHaveBeenCalledWith({ subscriptionId: "sub_123", subscriptionItemId: "si_456", membershipCount: 10, @@ -141,14 +158,20 @@ describe("InternalTeamBilling", () => { await internalTeamBilling.updateQuantity(); - expect(billingModule.default.handleSubscriptionUpdate).not.toHaveBeenCalled(); + expect(mockHandleSubscriptionUpdate).not.toHaveBeenCalled(); }); }); describe("checkIfTeamPaymentRequired", () => { - const internalTeamBilling = new InternalTeamBilling(mockTeam); it("should return payment required if no paymentId", async () => { - internalTeamBilling.team.metadata.paymentId = undefined; + const mockTeamNoPayment = { + ...mockTeam, + metadata: { + ...mockTeam.metadata, + paymentId: undefined, + }, + }; + const internalTeamBilling = new InternalTeamBilling(mockTeamNoPayment); const result = await internalTeamBilling.checkIfTeamPaymentRequired(); @@ -156,7 +179,7 @@ describe("InternalTeamBilling", () => { }); it("should return payment required if checkout session is not paid", async () => { - vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(false); + mockCheckoutSessionIsPaid.mockResolvedValue(false); const internalTeamBilling = new InternalTeamBilling(mockTeam); const result = await internalTeamBilling.checkIfTeamPaymentRequired(); @@ -165,8 +188,9 @@ describe("InternalTeamBilling", () => { }); it("should return upgrade URL if checkout session is paid", async () => { - vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(true); + mockCheckoutSessionIsPaid.mockResolvedValue(true); const internalTeamBilling = new InternalTeamBilling(mockTeam); + const result = await internalTeamBilling.checkIfTeamPaymentRequired(); expect(result).toEqual({ diff --git a/packages/features/ee/billing/teams/internal-team-billing.ts b/packages/features/ee/billing/teams/internal-team-billing.ts index 2b76ca11a0..02adde6ae3 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.ts @@ -11,9 +11,9 @@ import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; -import billing from ".."; import { IBillingRepository, IBillingRepositoryCreateArgs } from "../repository/IBillingRepository"; import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory"; +import { StripeBillingService } from "../stripe-billing-service"; import { TeamBillingPublishResponseStatus, type TeamBilling, type TeamBillingInput } from "./team-billing"; const log = logger.getSubLogger({ prefix: ["TeamBilling"] }); @@ -25,9 +25,11 @@ export class InternalTeamBilling implements TeamBilling { metadata: NonNullable>; }; private billingRepository: IBillingRepository; + private billingService: StripeBillingService; constructor(team: TeamBillingInput) { this.team = team; this.billingRepository = BillingRepositoryFactory.getRepository(team.isOrganization); + this.billingService = new StripeBillingService(); } set team(team: TeamBillingInput) { const metadata = teamPaymentMetadataSchema.parse(team.metadata || {}); @@ -54,7 +56,7 @@ export class InternalTeamBilling implements TeamBilling { const { subscriptionId } = this.team.metadata; log.info(`Cancelling subscription ${subscriptionId} for team ${this.team.id}`); if (!subscriptionId) throw Error("missing subscriptionId"); - await billing.handleSubscriptionCancel(subscriptionId); + await this.billingService.handleSubscriptionCancel(subscriptionId); await this.downgrade(); log.info(`Cancelled subscription ${subscriptionId} for team ${this.team.id}`); } catch (error) { @@ -154,7 +156,11 @@ export class InternalTeamBilling implements TeamBilling { } if (!subscriptionId) throw Error("missing subscriptionId"); if (!subscriptionItemId) throw Error("missing subscriptionItemId"); - await billing.handleSubscriptionUpdate({ subscriptionId, subscriptionItemId, membershipCount }); + await this.billingService.handleSubscriptionUpdate({ + subscriptionId, + subscriptionItemId, + membershipCount, + }); log.info(`Updated subscription ${subscriptionId} for team ${teamId} to ${membershipCount} seats.`); } catch (error) { this.logErrorFromUnknown(error); @@ -166,7 +172,7 @@ export class InternalTeamBilling implements TeamBilling { /** If there's no paymentId, we need to pay this team */ if (!paymentId) return { url: null, paymentId: null, paymentRequired: true }; /** If there's a pending session but it isn't paid, we need to pay this team */ - const checkoutSessionIsPaid = await billing.checkoutSessionIsPaid(paymentId); + const checkoutSessionIsPaid = await this.billingService.checkoutSessionIsPaid(paymentId); if (!checkoutSessionIsPaid) return { url: null, paymentId, paymentRequired: true }; /** If the session is already paid we return the upgrade URL so team is updated. */ return { @@ -179,7 +185,7 @@ export class InternalTeamBilling implements TeamBilling { async getSubscriptionStatus() { const { subscriptionId } = this.team.metadata; if (!subscriptionId) return null; - return await billing.getSubscriptionStatus(subscriptionId); + return await this.billingService.getSubscriptionStatus(subscriptionId); } /** @@ -197,7 +203,7 @@ export class InternalTeamBilling implements TeamBilling { } // End the trial by converting to regular subscription - await billing.handleEndTrial(subscriptionId); + await this.billingService.handleEndTrial(subscriptionId); log.info(`Successfully ended trial for team ${this.team.id}`); return true; } catch (error) {