chore: Clean up billing-related redundant files (#25060)

* remove billing service factory

* remove index file

* update internal team billing

* update test

* fix unit test

* fix: update internal-team-billing tests to use direct StripeBillingService mocking

- Changed mock path from @calcom/features/ee/billing/stripe-billing-service to ../stripe-billing-service to match actual import
- Used vi.hoisted() to ensure mock functions are available in hoisted mock factory context
- Changed vi.resetAllMocks() to vi.clearAllMocks() and re-apply mock implementation in beforeEach to preserve constructor mock
- Removed obsolete vi.mock("..") for deleted billing singleton module
- Fixed test pollution by creating new instances instead of mutating shared state
- Removed describe-level instance creation that was executed before mocks were set up
- All 12 tests now pass successfully

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Benny JooandDevin AI authored and GitHub committed 2025-11-13 09:17:36 +00:00
1 parent 530cb94f0b
commit cba0fa8551
6 files changed
+61 -64

No files matched your search

@@ -1,7 +0,0 @@
import { StripeBillingService } from "./stripe-billing-service";
export class BillingFactory {
constructor() {
return new StripeBillingService();
}
}
@@ -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();
@@ -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) {
-14
View File
@@ -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;
@@ -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({
@@ -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<z.infer<typeof teamPaymentMetadataSchema>>;
};
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) {