Skip to content

Commit cba0fa8

Browse files
chore: Clean up billing-related redundant files (calcom#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>
1 parent 530cb94 commit cba0fa8

6 files changed

Lines changed: 61 additions & 64 deletions

File tree

packages/features/ee/billing/billing-service-factory.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/features/ee/billing/credit-service.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,6 @@ vi.mock("../workflows/lib/reminders/reminderScheduler", () => ({
101101
vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({
102102
default: vi.fn().mockResolvedValue(null),
103103
}));
104-
vi.mock("@calcom/features/ee/billing/stripe-billing-service", () => {
105-
return {
106-
StripeBillingService: vi.fn().mockImplementation(() => ({
107-
getPrice: async (priceId: string) => {
108-
const stripe = (await import("@calcom/features/ee/payments/server/stripe")).default;
109-
return stripe.prices.retrieve(priceId);
110-
},
111-
checkoutSessionIsPaid: vi.fn(),
112-
handleSubscriptionCancel: vi.fn(),
113-
handleSubscriptionCreation: vi.fn(),
114-
handleSubscriptionUpdate: vi.fn(),
115-
})),
116-
};
117-
});
118104

119105
const creditService = new CreditService();
120106

packages/features/ee/billing/credit-service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,8 @@ export class CreditService {
466466
const { totalMonthlyCredits } = await this._getAllCreditsForTeam({ teamId, tx });
467467
warningLimit = totalMonthlyCredits * 0.2;
468468
} else if (userId) {
469-
const billing = (await import("@calcom/features/ee/billing")).default;
469+
const { StripeBillingService } = await import("./stripe-billing-service");
470+
const billing = new StripeBillingService();
470471
const teamMonthlyPrice = await billing.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || "");
471472
const pricePerSeat = teamMonthlyPrice?.unit_amount ?? 0;
472473
warningLimit = (pricePerSeat / 2) * 0.2;
@@ -671,7 +672,8 @@ export class CreditService {
671672
return activeMembers * creditsPerSeat;
672673
}
673674

674-
const billing = (await import("@calcom/features/ee/billing")).default;
675+
const { StripeBillingService } = await import("./stripe-billing-service");
676+
const billing = new StripeBillingService();
675677
const priceId = process.env.STRIPE_TEAM_MONTHLY_PRICE_ID;
676678

677679
if (!priceId) {

packages/features/ee/billing/index.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/features/ee/billing/teams/internal-team-billing.test.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
55
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
66
import { WEBAPP_URL } from "@calcom/lib/constants";
77

8-
import * as billingModule from "..";
98
import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory";
9+
import { StripeBillingService } from "../stripe-billing-service";
1010
import { InternalTeamBilling } from "./internal-team-billing";
1111
import { TeamBillingPublishResponseStatus } from "./team-billing";
1212

13+
const {
14+
mockHandleSubscriptionCancel,
15+
mockHandleSubscriptionUpdate,
16+
mockCheckoutSessionIsPaid,
17+
mockGetSubscriptionStatus,
18+
mockHandleEndTrial,
19+
} = vi.hoisted(() => ({
20+
mockHandleSubscriptionCancel: vi.fn(),
21+
mockHandleSubscriptionUpdate: vi.fn(),
22+
mockCheckoutSessionIsPaid: vi.fn(),
23+
mockGetSubscriptionStatus: vi.fn(),
24+
mockHandleEndTrial: vi.fn(),
25+
}));
26+
27+
vi.mock("../stripe-billing-service", () => ({
28+
StripeBillingService: vi.fn(),
29+
}));
30+
1331
vi.mock("@calcom/lib/constants", async () => {
1432
const actual = await vi.importActual("@calcom/lib/constants");
1533
return {
@@ -18,14 +36,6 @@ vi.mock("@calcom/lib/constants", async () => {
1836
};
1937
});
2038

21-
vi.mock("..", () => ({
22-
default: {
23-
handleSubscriptionCancel: vi.fn(),
24-
handleSubscriptionUpdate: vi.fn(),
25-
checkoutSessionIsPaid: vi.fn(),
26-
},
27-
}));
28-
2939
vi.mock("@calcom/features/ee/teams/lib/payments", () => ({
3040
purchaseTeamOrOrgSubscription: vi.fn(),
3141
}));
@@ -43,20 +53,29 @@ const mockTeam = {
4353
};
4454

4555
describe("InternalTeamBilling", () => {
56+
let internalTeamBilling: InternalTeamBilling;
57+
4658
beforeEach(() => {
47-
vi.resetAllMocks();
59+
vi.clearAllMocks();
60+
vi.mocked(StripeBillingService).mockImplementation(() => ({
61+
handleSubscriptionCancel: mockHandleSubscriptionCancel,
62+
handleSubscriptionUpdate: mockHandleSubscriptionUpdate,
63+
checkoutSessionIsPaid: mockCheckoutSessionIsPaid,
64+
getSubscriptionStatus: mockGetSubscriptionStatus,
65+
handleEndTrial: mockHandleEndTrial,
66+
}));
67+
internalTeamBilling = new InternalTeamBilling(mockTeam);
4868
});
4969

5070
afterEach(() => {
5171
vi.restoreAllMocks();
5272
});
5373

5474
describe("cancel", () => {
55-
const internalTeamBilling = new InternalTeamBilling(mockTeam);
5675
it("should cancel the subscription and downgrade the team", async () => {
5776
await internalTeamBilling.cancel();
5877

59-
expect(billingModule.default.handleSubscriptionCancel).toHaveBeenCalledWith("sub_123");
78+
expect(mockHandleSubscriptionCancel).toHaveBeenCalledWith("sub_123");
6079
expect(prismaMock.team.update).toHaveBeenCalledWith({
6180
where: { id: 1 },
6281
data: {
@@ -67,9 +86,8 @@ describe("InternalTeamBilling", () => {
6786
});
6887

6988
describe("publish", () => {
70-
const internalTeamBilling = new InternalTeamBilling(mockTeam);
7189
it("should create a checkout session and update the team", async () => {
72-
vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(false);
90+
mockCheckoutSessionIsPaid.mockResolvedValue(false);
7391
vi.mocked(purchaseTeamOrOrgSubscription).mockResolvedValue({
7492
url: "http://checkout.url",
7593
});
@@ -89,7 +107,6 @@ describe("InternalTeamBilling", () => {
89107
});
90108
});
91109
it("should return upgrade url if upgrade is required", async () => {
92-
const internalTeamBilling = new InternalTeamBilling(mockTeam);
93110
const mockUrl = `${WEBAPP_URL}/api/teams/${mockTeam.id}/upgrade?session_id=cs_789`;
94111
vi.spyOn(internalTeamBilling, "checkIfTeamPaymentRequired").mockResolvedValue({
95112
url: mockUrl,
@@ -123,7 +140,7 @@ describe("InternalTeamBilling", () => {
123140

124141
await internalTeamBilling.updateQuantity();
125142

126-
expect(billingModule.default.handleSubscriptionUpdate).toHaveBeenCalledWith({
143+
expect(mockHandleSubscriptionUpdate).toHaveBeenCalledWith({
127144
subscriptionId: "sub_123",
128145
subscriptionItemId: "si_456",
129146
membershipCount: 10,
@@ -141,22 +158,28 @@ describe("InternalTeamBilling", () => {
141158

142159
await internalTeamBilling.updateQuantity();
143160

144-
expect(billingModule.default.handleSubscriptionUpdate).not.toHaveBeenCalled();
161+
expect(mockHandleSubscriptionUpdate).not.toHaveBeenCalled();
145162
});
146163
});
147164

148165
describe("checkIfTeamPaymentRequired", () => {
149-
const internalTeamBilling = new InternalTeamBilling(mockTeam);
150166
it("should return payment required if no paymentId", async () => {
151-
internalTeamBilling.team.metadata.paymentId = undefined;
167+
const mockTeamNoPayment = {
168+
...mockTeam,
169+
metadata: {
170+
...mockTeam.metadata,
171+
paymentId: undefined,
172+
},
173+
};
174+
const internalTeamBilling = new InternalTeamBilling(mockTeamNoPayment);
152175

153176
const result = await internalTeamBilling.checkIfTeamPaymentRequired();
154177

155178
expect(result).toEqual({ url: null, paymentId: null, paymentRequired: true });
156179
});
157180

158181
it("should return payment required if checkout session is not paid", async () => {
159-
vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(false);
182+
mockCheckoutSessionIsPaid.mockResolvedValue(false);
160183
const internalTeamBilling = new InternalTeamBilling(mockTeam);
161184

162185
const result = await internalTeamBilling.checkIfTeamPaymentRequired();
@@ -165,8 +188,9 @@ describe("InternalTeamBilling", () => {
165188
});
166189

167190
it("should return upgrade URL if checkout session is paid", async () => {
168-
vi.spyOn(billingModule.default, "checkoutSessionIsPaid").mockResolvedValue(true);
191+
mockCheckoutSessionIsPaid.mockResolvedValue(true);
169192
const internalTeamBilling = new InternalTeamBilling(mockTeam);
193+
170194
const result = await internalTeamBilling.checkIfTeamPaymentRequired();
171195

172196
expect(result).toEqual({

packages/features/ee/billing/teams/internal-team-billing.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { prisma } from "@calcom/prisma";
1111
import type { Prisma } from "@calcom/prisma/client";
1212
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
1313

14-
import billing from "..";
1514
import { IBillingRepository, IBillingRepositoryCreateArgs } from "../repository/IBillingRepository";
1615
import { BillingRepositoryFactory } from "../repository/billingRepositoryFactory";
16+
import { StripeBillingService } from "../stripe-billing-service";
1717
import { TeamBillingPublishResponseStatus, type TeamBilling, type TeamBillingInput } from "./team-billing";
1818

1919
const log = logger.getSubLogger({ prefix: ["TeamBilling"] });
@@ -25,9 +25,11 @@ export class InternalTeamBilling implements TeamBilling {
2525
metadata: NonNullable<z.infer<typeof teamPaymentMetadataSchema>>;
2626
};
2727
private billingRepository: IBillingRepository;
28+
private billingService: StripeBillingService;
2829
constructor(team: TeamBillingInput) {
2930
this.team = team;
3031
this.billingRepository = BillingRepositoryFactory.getRepository(team.isOrganization);
32+
this.billingService = new StripeBillingService();
3133
}
3234
set team(team: TeamBillingInput) {
3335
const metadata = teamPaymentMetadataSchema.parse(team.metadata || {});
@@ -54,7 +56,7 @@ export class InternalTeamBilling implements TeamBilling {
5456
const { subscriptionId } = this.team.metadata;
5557
log.info(`Cancelling subscription ${subscriptionId} for team ${this.team.id}`);
5658
if (!subscriptionId) throw Error("missing subscriptionId");
57-
await billing.handleSubscriptionCancel(subscriptionId);
59+
await this.billingService.handleSubscriptionCancel(subscriptionId);
5860
await this.downgrade();
5961
log.info(`Cancelled subscription ${subscriptionId} for team ${this.team.id}`);
6062
} catch (error) {
@@ -154,7 +156,11 @@ export class InternalTeamBilling implements TeamBilling {
154156
}
155157
if (!subscriptionId) throw Error("missing subscriptionId");
156158
if (!subscriptionItemId) throw Error("missing subscriptionItemId");
157-
await billing.handleSubscriptionUpdate({ subscriptionId, subscriptionItemId, membershipCount });
159+
await this.billingService.handleSubscriptionUpdate({
160+
subscriptionId,
161+
subscriptionItemId,
162+
membershipCount,
163+
});
158164
log.info(`Updated subscription ${subscriptionId} for team ${teamId} to ${membershipCount} seats.`);
159165
} catch (error) {
160166
this.logErrorFromUnknown(error);
@@ -166,7 +172,7 @@ export class InternalTeamBilling implements TeamBilling {
166172
/** If there's no paymentId, we need to pay this team */
167173
if (!paymentId) return { url: null, paymentId: null, paymentRequired: true };
168174
/** If there's a pending session but it isn't paid, we need to pay this team */
169-
const checkoutSessionIsPaid = await billing.checkoutSessionIsPaid(paymentId);
175+
const checkoutSessionIsPaid = await this.billingService.checkoutSessionIsPaid(paymentId);
170176
if (!checkoutSessionIsPaid) return { url: null, paymentId, paymentRequired: true };
171177
/** If the session is already paid we return the upgrade URL so team is updated. */
172178
return {
@@ -179,7 +185,7 @@ export class InternalTeamBilling implements TeamBilling {
179185
async getSubscriptionStatus() {
180186
const { subscriptionId } = this.team.metadata;
181187
if (!subscriptionId) return null;
182-
return await billing.getSubscriptionStatus(subscriptionId);
188+
return await this.billingService.getSubscriptionStatus(subscriptionId);
183189
}
184190

185191
/**
@@ -197,7 +203,7 @@ export class InternalTeamBilling implements TeamBilling {
197203
}
198204

199205
// End the trial by converting to regular subscription
200-
await billing.handleEndTrial(subscriptionId);
206+
await this.billingService.handleEndTrial(subscriptionId);
201207
log.info(`Successfully ended trial for team ${this.team.id}`);
202208
return true;
203209
} catch (error) {

0 commit comments

Comments
 (0)