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:
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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user