Skip to content

Commit 7e4d9e2

Browse files
joeauyeungcubic-dev-ai[bot]devin-ai-integration[bot]alishaz-polymath
authored
refactor: implement DI in team billing service and team billing data repository factory (calcom#24803)
* Move TeamBillingRepositories * WIP refactor team internal billing service * Remove duplicate billing repository files * Remove logic check in repository for billing is enabled * Rename repository to `TeamBillingData` * Use repository factory in main service * Fix new import paths * Rename to * Ensure `IS_TEAM_BILLING_ENABLED` is of type boolean * Rename classes to TeamBillingService and TeamBillingServiceFactory * Implement DI in `BookingServiceFactory` * `TeamBillingService` use repository in `getOrgIfNeeded` * DI `isTeamBillingEnabled` to `TeamBillingServiceFactory` * Rename files for consistency * Return stub BillingRepository if billing is not enabled * Move Stripe billing service to service folder * Rename file * `StripeBillingService.getSubscriptionStatus` return `SubscriptionStatus` * Type fices in StripeBillingService * Type fix in `stubTeamBillingService` * DI the `BillingProviderService` into the `TeamBillingService` * Implement DI in `skipTeamTrials.handler` * Implement DI for team billing in `inviteMember.handler` * `skipTeamTrials.handler` use `team.isOrganization` * Implement DI for billing in `hasActiveTeamPlan.handler` * Type fixes * Implement DI in `bulkDeleteUsers.handler` * Implement `BillingProviderServiceFactory` in `updateProfile.handler` * Implment `BillingProviderServiceFactory` in `buyCredits.handler` * Fix import in `stripeCustomer.handler` * Add a constructor to `teamBillingServiceFactory` * Add DI to `PrismaTeamBillingRepository` * Add DI to `StripeBillingService` * Implement singleton in `BillingProviderServiceFactory` * Add DI folder and contents to billing folder * Use `getTeamBillingServiceFactory` in `inviteMember.handler` * Add `saveTeamBilling` method to `ITeamBillingService` * Implement DI in new team route * Implement DI in `teamService` * Implement DI in `OrganizationPaymentService` * Implement DI in `credit-service` * In `StripeBillingService` remove `static` from status methods * Implemnt DI in `_invoice.paid.org` * Refactor `hasActiveTeamPlan` to use `getTeamBillingFactory` * Refactor `skipTeamTrials` to use `getTeamBillingFactory` * Refactor `skipTeamTrials` to use `getTeamBillingServiceFactory` * `stripeCustomer.handler` to use `getBillingProviderService` * Remove old factories * Type fix * Remove unused factory * Refactor `updateProfile.handler` to use `getBillingProviderService` * Change name to `TeamBillingDataRepositoryFactory` * Type Prisma return in `prisma.module` * Type fix * Refactor `buyCredits.handler` to use `getBillingProviderService` * Refactor `credit-service` to use billing DI containers * Type fix * Add `getTeamBillingDataRepository` * Refactor `_invoice.paid.org` to use DI container * Refactor `_customer.subscription.deleted.team-plan` to use DI container * Refactor `calcomHandler` to use DI container * Refactor `getCustomerAndCheckoutSession` to use DI container * Refactor `verify-email` to use DI containers * Refactor `api/create/route` to use DI container * Refactor downgradeUsers to use DI container * Type fix * Clean up console.logs * Add await to `this.billingRepository.create` in `saveTeamBilling` Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Fix type errors * Address comments * fix: update tests to work with new DI pattern - Update teamBillingService.test.ts to properly inject DI dependencies - Remove unused billingModule import and mock - Fix import naming in teamService.integration-test.ts (remove unused rename) - Fix import path for TeamBillingPublishResponseStatus All tests now properly mock IBillingProviderService, ITeamBillingDataRepository, and IBillingRepository instead of using the old BillingRepositoryFactory pattern. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add compatibility layer and env setup for unit tests - Add STRIPE_PRIVATE_KEY dummy value to vitest.config.ts to prevent DI module errors - Fix import paths in credit-service.test.ts (StripeBillingService, TeamBillingService) - Create compatibility barrel at packages/features/ee/billing/teams/index.ts for test mocking Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: update unit tests to mock DI container properly - Update teamService.test.ts to mock getTeamBillingServiceFactory() instead of TeamBilling.findAndInit - Update teamService.alternative.test.ts to mock DI container - Update credit-service.test.ts to mock getBillingProviderService() and use SubscriptionStatus enum values - Update OrganizationPaymentService.test.ts to mock DI container instead of direct StripeBillingService import - Remove all 'as any' type casting to comply with Cal.com coding standards - Fix unused variable warnings by prefixing with underscore All 53 tests now passing (16 + 1 + 30 + 6) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: update remaining unit tests to use DI pattern - Fix StripeBillingService.test.ts to inject mock Stripe client directly - Fix teamBillingFactory.test.ts to mock getTeamBillingServiceFactory() from DI container - Fix skipTeamTrials.test.ts to mock DI container and use SubscriptionStatus enum All 11 previously failing tests now pass (5 + 5 + 1) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Undo changes made to Prisma module * fix: address test-related PR comments - Fix OrganizationPaymentService.test.ts mock path from @calcom/ee to @calcom/features/ee - Refactor teamBillingFactory.test.ts to test real factory logic instead of mocking container - Remove duplicate teamBillingService.test..ts file with incorrect double-dot filename All three test files now pass successfully with proper DI patterns. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Address feedback * fix: update teamService integration test to mock new DI factory pattern Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: remove duplicate imports in credit-service.test.ts Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Remove unused index file * `getBySubscriptionId` to return team or null * Address feedback * Merge fix * Refactor file names * fix: correct mockStripe variable name to stripeMock in StripeBillingService.test.ts Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: update internal-team-billing.test.ts to use new DI structure with TeamBillingService - Replace InternalTeamBilling with TeamBillingService - Use constructor injection with mock dependencies instead of factory pattern - Remove BillingRepositoryFactory mock and import - Update all test cases to use mockBillingProviderService, mockTeamBillingDataRepository, and mockBillingRepository - Simplify saveTeamBilling tests to focus on repository.create calls - All 11 tests now pass with the new DI structure Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: update createWithPaymentIntent.handler.test.ts to mock DI container's getBillingProviderService - OrganizationPaymentService now uses getBillingProviderService() from DI container - Test was mocking @calcom/features/ee/payments/server/stripe directly, which no longer works - Added mock for @calcom/features/ee/billing/di/containers/Billing module - Mock returns fake billing provider that delegates to mockSharedStripe - Preserves all existing test assertions and helpers - Fixed lint error by prefixing unused lastCreatedSessionId with underscore - All 11 tests now pass (1 skipped as expected) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
1 parent a7352cf commit 7e4d9e2

61 files changed

Lines changed: 1416 additions & 753 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/app/api/cron/downgradeUsers/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { NextRequest } from "next/server";
33
import { NextResponse } from "next/server";
44
import { z } from "zod";
55

6-
import { TeamBilling } from "@calcom/ee/billing/teams";
6+
import { getTeamBillingServiceFactory } from "@calcom/features/ee/billing/di/containers/Billing";
77
import prisma from "@calcom/prisma";
88

99
const querySchema = z.object({
@@ -17,7 +17,6 @@ async function postHandler(request: NextRequest) {
1717
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
1818
}
1919

20-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2120
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
2221

2322
let { page: pageNumber } = querySchema.parse(Object.fromEntries(request.nextUrl.searchParams));
@@ -43,7 +42,8 @@ async function postHandler(request: NextRequest) {
4342
break;
4443
}
4544

46-
const teamsBilling = TeamBilling.initMany(teams);
45+
const teamBillingFactory = getTeamBillingServiceFactory();
46+
const teamsBilling = teamBillingFactory.initMany(teams);
4747
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
4848
await Promise.allSettled(teamBillingPromises);
4949

apps/web/app/api/teams/api/create/route.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { NextResponse } from "next/server";
44
import type Stripe from "stripe";
55
import { z } from "zod";
66

7-
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
8-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
9-
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
7+
import { getBillingProviderService } from "@calcom/ee/billing/di/containers/Billing";
8+
import { getTeamBillingServiceFactory } from "@calcom/ee/billing/di/containers/Billing";
9+
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/billing/IBillingRepository";
1010
import stripe from "@calcom/features/ee/payments/server/stripe";
1111
import { HttpError } from "@calcom/lib/http-error";
1212
import { prisma } from "@calcom/prisma";
@@ -58,11 +58,12 @@ async function handler(request: NextRequest) {
5858
});
5959

6060
if (checkoutSessionSubscription) {
61-
const { subscriptionStart } =
62-
StripeBillingService.extractSubscriptionDates(checkoutSessionSubscription);
61+
const billingService = getBillingProviderService();
62+
const { subscriptionStart } = billingService.extractSubscriptionDates(checkoutSessionSubscription);
6363

64-
const internalBillingService = new InternalTeamBilling(finalizedTeam);
65-
await internalBillingService.saveTeamBilling({
64+
const teamBillingServiceFactory = getTeamBillingServiceFactory();
65+
const teamBillingService = teamBillingServiceFactory.init(finalizedTeam);
66+
await teamBillingService.saveTeamBilling({
6667
teamId: finalizedTeam.id,
6768
subscriptionId: checkoutSessionSubscription.id,
6869
subscriptionItemId: checkoutSessionSubscription.items.data[0].id,

apps/web/app/api/teams/create/route.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { NextResponse } from "next/server";
44
import type Stripe from "stripe";
55
import { z } from "zod";
66

7-
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
8-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
9-
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
7+
import {
8+
getBillingProviderService,
9+
getTeamBillingServiceFactory,
10+
} from "@calcom/features/ee/billing/di/containers/Billing";
11+
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/billing/IBillingRepository";
1012
import stripe from "@calcom/features/ee/payments/server/stripe";
1113
import { WEBAPP_URL } from "@calcom/lib/constants";
1214
import { HttpError } from "@calcom/lib/http-error";
@@ -91,9 +93,11 @@ async function getHandler(req: NextRequest) {
9193
});
9294

9395
if (checkoutSession && subscription) {
94-
const { subscriptionStart } = StripeBillingService.extractSubscriptionDates(subscription);
95-
const internalBillingService = new InternalTeamBilling(team);
96-
await internalBillingService.saveTeamBilling({
96+
const billingProviderService = getBillingProviderService();
97+
const { subscriptionStart } = billingProviderService.extractSubscriptionDates(subscription);
98+
const teamBillingServiceFactory = getTeamBillingServiceFactory();
99+
const teamBillingService = teamBillingServiceFactory.init(team);
100+
await teamBillingService.saveTeamBilling({
97101
teamId: team.id,
98102
subscriptionId: subscription.id,
99103
subscriptionItemId: subscription.items.data[0].id,

apps/web/lib/pages/auth/verify-email.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
22
import { z } from "zod";
33

44
import dayjs from "@calcom/dayjs";
5-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
5+
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
66
import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container";
77
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
88
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -45,7 +45,6 @@ export async function moveUserToMatchingOrg({ email }: { email: string }) {
4545

4646
export async function handler(req: NextApiRequest, res: NextApiResponse) {
4747
const { token } = verifySchema.parse(req.query);
48-
const billingService = new StripeBillingService();
4948

5049
const foundToken = await prisma.verificationToken.findFirst({
5150
where: {
@@ -133,6 +132,7 @@ export async function handler(req: NextApiRequest, res: NextApiResponse) {
133132
});
134133

135134
if (IS_STRIPE_ENABLED && userMetadataParsed.stripeCustomerId) {
135+
const billingService = getBillingProviderService();
136136
await billingService.updateCustomer({
137137
customerId: userMetadataParsed.stripeCustomerId,
138138
email: updatedEmail,

packages/app-store/stripepayment/lib/getCustomerAndCheckoutSession.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
1+
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
22

33
export async function getCustomerAndCheckoutSession(checkoutSessionId: string) {
4-
const billingService = new StripeBillingService();
4+
const billingService = getBillingProviderService();
55
const checkoutSession = await billingService.getCheckoutSession(checkoutSessionId);
66
const customerOrCustomerId = checkoutSession.customer;
77
let customerId = null;

packages/features/auth/signup/handlers/calcomHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
77
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
88
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
99
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
10-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
10+
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
1111
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
1212
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
1313
import { hashPassword } from "@calcom/lib/auth/hashPassword";
@@ -44,7 +44,7 @@ const handler: CustomNextApiHandler = async (body, usernameStatus) => {
4444
})
4545
.parse(body);
4646

47-
const billingService = new StripeBillingService();
47+
const billingService = getBillingProviderService();
4848

4949
const shouldLockByDefault = await checkIfEmailIsBlockedInWatchlistController({
5050
email: _email,

packages/features/di/di.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Container, Module } from "@evyweb/ioctopus";
1+
import type { Container, Module, ResolveFunction } from "@evyweb/ioctopus";
22
import { createContainer, createModule } from "@evyweb/ioctopus";
33

44
export type ModuleLoader = { token: string | symbol; loadModule: (container: Container) => void };
@@ -111,4 +111,4 @@ export function bindModuleToClassOnToken<TClass extends new (deps: any) => any>(
111111
}
112112
};
113113
}
114-
export { createContainer, createModule, type Container, type Module };
114+
export { createContainer, createModule, type Container, type Module, type ResolveFunction };

packages/features/ee/billing/api/webhook/_customer.subscription.deleted.team-plan.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { z } from "zod";
22

3-
import { TeamBilling } from "../../teams";
3+
import logger from "@calcom/lib/logger";
4+
5+
import { getTeamBillingDataRepository, getTeamBillingServiceFactory } from "../../di/containers/Billing";
46
import type { SWHMap } from "./__handler";
57

68
const metadataSchema = z.object({
@@ -9,15 +11,25 @@ const metadataSchema = z.object({
911

1012
const handler = async (data: SWHMap["customer.subscription.deleted"]["data"]) => {
1113
const subscription = data.object;
14+
const teamBillingFactory = getTeamBillingServiceFactory();
15+
const log = logger.getSubLogger({
16+
prefix: [`[customer.subscription.deleted.team-plan]: subscriptionId: ${subscription.id}`],
17+
});
18+
1219
try {
1320
const { teamId } = metadataSchema.parse(subscription.metadata);
14-
const teamBilling = await TeamBilling.findAndInit(teamId);
15-
await teamBilling.downgrade();
21+
const teamBillingService = await teamBillingFactory.findAndInit(teamId);
22+
await teamBillingService.downgrade();
1623
return { success: true };
17-
} catch (error) {
24+
} catch {
25+
const teamBillingDataRepository = getTeamBillingDataRepository();
1826
// If stripe metadata is missing teamId, we attempt to find by sub ID.
19-
const team = await TeamBilling.repo.findBySubscriptionId(subscription.id);
20-
const teamBilling = TeamBilling.init(team);
27+
const team = await teamBillingDataRepository.findBySubscriptionId(subscription.id);
28+
if (!team) {
29+
log.warn("No team found with subscriptionId");
30+
return { success: false };
31+
}
32+
const teamBilling = teamBillingFactory.init(team);
2133
await teamBilling.downgrade();
2234
return { success: true };
2335
}

packages/features/ee/billing/api/webhook/_invoice.paid.org.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { z } from "zod";
22

3-
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/IBillingRepository";
4-
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
5-
import { InternalTeamBilling } from "@calcom/features/ee/billing/teams/internal-team-billing";
3+
import { getBillingProviderService } from "@calcom/ee/billing/di/containers/Billing";
4+
import { Plan, SubscriptionStatus } from "@calcom/features/ee/billing/repository/billing/IBillingRepository";
65
import { BillingEnabledOrgOnboardingService } from "@calcom/features/ee/organizations/lib/service/onboarding/BillingEnabledOrgOnboardingService";
76
import stripe from "@calcom/features/ee/payments/server/stripe";
87
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
@@ -11,6 +10,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
1110
import { OrganizationOnboardingRepository } from "@calcom/lib/server/repository/organizationOnboarding";
1211
import { prisma } from "@calcom/prisma";
1312

13+
import { getTeamBillingServiceFactory } from "../../di/containers/Billing";
1414
import type { SWHMap } from "./__handler";
1515

1616
const invoicePaidSchema = z.object({
@@ -123,10 +123,12 @@ const handler = async (data: SWHMap["invoice.paid"]["data"]) => {
123123

124124
// Get the Stripe subscription object
125125
const stripeSubscription = await stripe.subscriptions.retrieve(paymentSubscriptionId);
126-
const { subscriptionStart } = StripeBillingService.extractSubscriptionDates(stripeSubscription);
126+
const billingService = getBillingProviderService();
127+
const { subscriptionStart } = billingService.extractSubscriptionDates(stripeSubscription);
127128

128-
const internalTeamBillingService = new InternalTeamBilling(organization);
129-
await internalTeamBillingService.saveTeamBilling({
129+
const teamBillingServiceFactory = getTeamBillingServiceFactory();
130+
const teamBillingService = teamBillingServiceFactory.init(organization);
131+
await teamBillingService.saveTeamBilling({
130132
teamId: organization.id,
131133
subscriptionId: paymentSubscriptionId,
132134
subscriptionItemId: paymentSubscriptionItemId,

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

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CreditsRepository } from "@calcom/lib/server/repository/credits";
99
import { CreditType } from "@calcom/prisma/enums";
1010

1111
import { CreditService } from "./credit-service";
12-
import { InternalTeamBilling } from "./teams/internal-team-billing";
12+
import { SubscriptionStatus } from "./repository/billing/IBillingRepository";
1313

1414
const MOCK_TX = {
1515
team: {
@@ -102,6 +102,12 @@ vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({
102102
default: vi.fn().mockResolvedValue(null),
103103
}));
104104

105+
vi.mock("@calcom/ee/billing/di/containers/Billing", () => ({
106+
getBillingProviderService: vi.fn(),
107+
getTeamBillingServiceFactory: vi.fn(),
108+
getTeamBillingDataRepository: vi.fn(),
109+
}));
110+
105111
const creditService = new CreditService();
106112

107113
vi.spyOn(creditService, "_getAllCreditsForTeam").mockResolvedValue({
@@ -126,7 +132,7 @@ CreditsRepository.findCreditBalance.mockResolvedValueOnce({
126132
describe("CreditService", () => {
127133
let creditService: CreditService;
128134

129-
beforeEach(() => {
135+
beforeEach(async () => {
130136
vi.restoreAllMocks();
131137

132138
mockStripe.prices.retrieve.mockResolvedValue({ id: "price_123", unit_amount: 1000 });
@@ -135,6 +141,25 @@ describe("CreditService", () => {
135141
creditService = new CreditService();
136142

137143
vi.mocked(CreditsRepository.findCreditExpenseLogByExternalRef).mockResolvedValue(null);
144+
145+
const { getBillingProviderService, getTeamBillingServiceFactory } = await import(
146+
"@calcom/ee/billing/di/containers/Billing"
147+
);
148+
149+
const mockBillingProviderService = {
150+
getPrice: vi.fn().mockResolvedValue({ unit_amount: 1500 }),
151+
};
152+
vi.mocked(getBillingProviderService).mockReturnValue(mockBillingProviderService);
153+
154+
const mockTeamBillingService = {
155+
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
156+
};
157+
const mockTeamBillingServiceFactory = {
158+
init: vi.fn().mockReturnValue(mockTeamBillingService),
159+
findAndInit: vi.fn().mockResolvedValue(mockTeamBillingService),
160+
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBillingService]),
161+
};
162+
vi.mocked(getTeamBillingServiceFactory).mockReturnValue(mockTeamBillingServiceFactory);
138163
});
139164

140165
describe("Team credits", () => {
@@ -450,11 +475,14 @@ describe("CreditService", () => {
450475
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository);
451476

452477
const mockTeamBillingService = {
453-
getSubscriptionStatus: vi.fn().mockResolvedValue("trialing"),
478+
getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.TRIALING),
454479
};
455-
vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation(
456-
mockTeamBillingService.getSubscriptionStatus
457-
);
480+
const { getTeamBillingServiceFactory } = await import("@calcom/ee/billing/di/containers/Billing");
481+
vi.mocked(getTeamBillingServiceFactory).mockReturnValue({
482+
init: vi.fn().mockReturnValue(mockTeamBillingService),
483+
findAndInit: vi.fn().mockResolvedValue(mockTeamBillingService),
484+
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBillingService]),
485+
});
458486

459487
const result = await creditService.getMonthlyCredits(1);
460488
expect(result).toBe(0);
@@ -471,13 +499,20 @@ describe("CreditService", () => {
471499
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository);
472500

473501
const mockTeamBillingService = {
474-
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
502+
getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE),
503+
};
504+
const mockBillingProviderService = {
505+
getPrice: vi.fn().mockResolvedValue({ unit_amount: 1000 }),
475506
};
476-
vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation(
477-
mockTeamBillingService.getSubscriptionStatus
507+
const { getBillingProviderService, getTeamBillingServiceFactory } = await import(
508+
"@calcom/ee/billing/di/containers/Billing"
478509
);
479-
480-
mockStripe.prices.retrieve.mockResolvedValue({ id: "price_123", unit_amount: 1000 });
510+
vi.mocked(getBillingProviderService).mockReturnValue(mockBillingProviderService);
511+
vi.mocked(getTeamBillingServiceFactory).mockReturnValue({
512+
init: vi.fn().mockReturnValue(mockTeamBillingService),
513+
findAndInit: vi.fn().mockResolvedValue(mockTeamBillingService),
514+
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBillingService]),
515+
});
481516

482517
const result = await creditService.getMonthlyCredits(1);
483518
expect(result).toBe(1500); // (3 members * 1000 price) / 2
@@ -495,18 +530,20 @@ describe("CreditService", () => {
495530
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository);
496531

497532
const mockTeamBillingService = {
498-
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
533+
getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE),
499534
};
500-
vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation(
501-
mockTeamBillingService.getSubscriptionStatus
502-
);
535+
const { getTeamBillingServiceFactory } = await import("@calcom/ee/billing/di/containers/Billing");
536+
vi.mocked(getTeamBillingServiceFactory).mockReturnValue({
537+
init: vi.fn().mockReturnValue(mockTeamBillingService),
538+
findAndInit: vi.fn().mockResolvedValue(mockTeamBillingService),
539+
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBillingService]),
540+
});
503541

504542
const result = await creditService.getMonthlyCredits(1);
505543
expect(result).toBe(3000); // 2 members * 1500 credits per seat
506544
});
507545

508546
it("should calculate credits for organizations with default 1000 credits per seat", async () => {
509-
// Clear ORG_MONTHLY_CREDITS to test default behavior
510547
vi.stubEnv("ORG_MONTHLY_CREDITS", undefined);
511548
const mockTeamRepo = {
512549
findTeamWithMembers: vi.fn().mockResolvedValue({
@@ -518,11 +555,14 @@ describe("CreditService", () => {
518555
vi.mocked(TeamRepository).mockImplementation(() => mockTeamRepo as unknown as TeamRepository);
519556

520557
const mockTeamBillingService = {
521-
getSubscriptionStatus: vi.fn().mockResolvedValue("active"),
558+
getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE),
522559
};
523-
vi.spyOn(InternalTeamBilling.prototype, "getSubscriptionStatus").mockImplementation(
524-
mockTeamBillingService.getSubscriptionStatus
525-
);
560+
const { getTeamBillingServiceFactory } = await import("@calcom/ee/billing/di/containers/Billing");
561+
vi.mocked(getTeamBillingServiceFactory).mockReturnValue({
562+
init: vi.fn().mockReturnValue(mockTeamBillingService),
563+
findAndInit: vi.fn().mockResolvedValue(mockTeamBillingService),
564+
findAndInitMany: vi.fn().mockResolvedValue([mockTeamBillingService]),
565+
});
526566

527567
const result = await creditService.getMonthlyCredits(1);
528568
expect(result).toBe(3000); // 3 members * 1000 credits per seat (default)

0 commit comments

Comments
 (0)