feat: move platform active billing logic to services (#27704)
* wip * feat: migrate from v2 * format * remove plan
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { getActiveUserBillingService } from "@calcom/platform-libraries/organizations";
|
||||
import { getIncrementUsageIdempotencyKey, getIncrementUsageJobTag } from "@calcom/platform-libraries/tasker";
|
||||
import { InjectQueue } from "@nestjs/bull";
|
||||
import {
|
||||
@@ -342,36 +343,13 @@ export class BillingService implements IBillingService, OnModuleDestroy {
|
||||
invoiceStart: Date,
|
||||
invoiceEnd: Date
|
||||
): Promise<number> {
|
||||
const managedUsersEmails =
|
||||
await this.usersRepository.getOrgsManagedUserEmailsBySubscriptionId(subscriptionId);
|
||||
|
||||
if (!managedUsersEmails) return 0;
|
||||
|
||||
if (!invoiceStart || !invoiceEnd) {
|
||||
this.logger.log("Invoice period start or end date is null");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activeManagedUserEmailsAsHost = await this.usersRepository.getActiveManagedUsersAsHost(
|
||||
subscriptionId,
|
||||
invoiceStart,
|
||||
invoiceEnd
|
||||
);
|
||||
|
||||
const activeHostEmails = activeManagedUserEmailsAsHost.map((email) => email.email);
|
||||
const notActiveHostEmails = managedUsersEmails
|
||||
.filter((email) => !activeHostEmails.includes(email.email))
|
||||
.map((email) => email.email);
|
||||
|
||||
if (notActiveHostEmails.length === 0) return activeManagedUserEmailsAsHost.length;
|
||||
|
||||
const activeManagedUserEmailsAsAttendee = await this.usersRepository.getActiveManagedUsersAsAttendee(
|
||||
notActiveHostEmails,
|
||||
invoiceStart,
|
||||
invoiceEnd
|
||||
);
|
||||
|
||||
return activeManagedUserEmailsAsAttendee.length + activeManagedUserEmailsAsHost.length;
|
||||
const activeUserBillingService = getActiveUserBillingService();
|
||||
return activeUserBillingService.getActiveUserCountForPlatformOrg(subscriptionId, invoiceStart, invoiceEnd);
|
||||
}
|
||||
|
||||
async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BOOKING_DI_TOKENS } from "@calcom/features/bookings/di/tokens";
|
||||
import { BOOKING_AUDIT_DI_TOKENS } from "@calcom/features/booking-audit/di/tokens";
|
||||
import { ACTIVE_USER_BILLING_DI_TOKENS } from "@calcom/features/ee/billing/active-user/di/tokens";
|
||||
import { FEATURE_OPT_IN_DI_TOKENS } from "@calcom/features/feature-opt-in/di/tokens";
|
||||
import { FLAGS_DI_TOKENS } from "@calcom/features/flags/di/tokens";
|
||||
import { HASHED_LINK_DI_TOKENS } from "@calcom/features/hashedLink/di/tokens";
|
||||
@@ -83,6 +84,7 @@ export const DI_TOKENS = {
|
||||
...HASHED_LINK_DI_TOKENS,
|
||||
...OAUTH_DI_TOKENS,
|
||||
...WATCHLIST_DI_TOKENS,
|
||||
...ACTIVE_USER_BILLING_DI_TOKENS,
|
||||
...ORGANIZATION_DI_TOKENS,
|
||||
...WEBHOOK_TOKENS,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
|
||||
import { ActiveUserBillingRepository } from "@calcom/features/ee/billing/active-user/repositories/ActiveUserBillingRepository";
|
||||
|
||||
import { ACTIVE_USER_BILLING_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = ACTIVE_USER_BILLING_DI_TOKENS.ACTIVE_USER_BILLING_REPOSITORY;
|
||||
const moduleToken = ACTIVE_USER_BILLING_DI_TOKENS.ACTIVE_USER_BILLING_REPOSITORY_MODULE;
|
||||
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: ActiveUserBillingRepository,
|
||||
dep: prismaModuleLoader,
|
||||
});
|
||||
|
||||
export const moduleLoader: ModuleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createContainer } from "@calcom/features/di/di";
|
||||
import type { ActiveUserBillingService } from "@calcom/features/ee/billing/active-user/services/ActiveUserBillingService";
|
||||
|
||||
import { moduleLoader as activeUserBillingServiceModuleLoader } from "./ActiveUserBillingService.module";
|
||||
|
||||
const container = createContainer();
|
||||
|
||||
export function getActiveUserBillingService(): ActiveUserBillingService {
|
||||
activeUserBillingServiceModuleLoader.loadModule(container);
|
||||
return container.get<ActiveUserBillingService>(activeUserBillingServiceModuleLoader.token);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { ActiveUserBillingService } from "@calcom/features/ee/billing/active-user/services/ActiveUserBillingService";
|
||||
|
||||
import { moduleLoader as activeUserBillingRepositoryModuleLoader } from "./ActiveUserBillingRepository.module";
|
||||
import { ACTIVE_USER_BILLING_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = ACTIVE_USER_BILLING_DI_TOKENS.ACTIVE_USER_BILLING_SERVICE;
|
||||
const moduleToken = ACTIVE_USER_BILLING_DI_TOKENS.ACTIVE_USER_BILLING_SERVICE_MODULE;
|
||||
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: ActiveUserBillingService,
|
||||
depsMap: {
|
||||
activeUserBillingRepository: activeUserBillingRepositoryModuleLoader,
|
||||
},
|
||||
});
|
||||
|
||||
export const moduleLoader: ModuleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const ACTIVE_USER_BILLING_DI_TOKENS = {
|
||||
ACTIVE_USER_BILLING_REPOSITORY: Symbol("ActiveUserBillingRepository"),
|
||||
ACTIVE_USER_BILLING_REPOSITORY_MODULE: Symbol("ActiveUserBillingRepositoryModule"),
|
||||
ACTIVE_USER_BILLING_SERVICE: Symbol("ActiveUserBillingService"),
|
||||
ACTIVE_USER_BILLING_SERVICE_MODULE: Symbol("ActiveUserBillingServiceModule"),
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
export class ActiveUserBillingRepository {
|
||||
constructor(private readonly prismaClient: PrismaClient) {}
|
||||
|
||||
/**
|
||||
* Get all managed user emails for a platform org identified by its PlatformBilling subscription ID.
|
||||
*/
|
||||
async getManagedUserEmailsBySubscriptionId(subscriptionId: string): Promise<{ email: string }[]> {
|
||||
return this.prismaClient.user.findMany({
|
||||
distinct: ["email"],
|
||||
where: {
|
||||
isPlatformManaged: true,
|
||||
profiles: {
|
||||
some: {
|
||||
organization: {
|
||||
platformBilling: {
|
||||
subscriptionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all member emails for a regular organization identified by its org (team) ID.
|
||||
* Uses the Membership table to find accepted org members.
|
||||
*/
|
||||
async getOrgMemberEmailsByOrgId(orgId: number): Promise<{ email: string }[]> {
|
||||
return this.prismaClient.user.findMany({
|
||||
distinct: ["email"],
|
||||
where: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId: orgId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find platform-managed users (by email) who hosted at least one booking in the given period.
|
||||
* Filters on isPlatformManaged to match only platform org users.
|
||||
*/
|
||||
async getActivePlatformUsersAsHost(
|
||||
subscriptionId: string,
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<{ email: string }[]> {
|
||||
return this.prismaClient.user.findMany({
|
||||
distinct: ["email"],
|
||||
where: {
|
||||
isPlatformManaged: true,
|
||||
profiles: {
|
||||
some: {
|
||||
organization: {
|
||||
platformBilling: {
|
||||
subscriptionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bookings: {
|
||||
some: {
|
||||
userId: { not: null },
|
||||
startTime: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users (by email) who hosted at least one booking in the given period.
|
||||
*/
|
||||
async getActiveUsersAsHost(
|
||||
userEmails: string[],
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<{ email: string }[]> {
|
||||
return this.prismaClient.user.findMany({
|
||||
distinct: ["email"],
|
||||
where: {
|
||||
email: { in: userEmails },
|
||||
bookings: {
|
||||
some: {
|
||||
userId: { not: null },
|
||||
startTime: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users (by email) who attended at least one booking in the given period.
|
||||
*/
|
||||
async getActiveUsersAsAttendee(
|
||||
userEmails: string[],
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<{ email: string }[]> {
|
||||
return this.prismaClient.attendee.findMany({
|
||||
distinct: ["email"],
|
||||
where: {
|
||||
email: { in: userEmails },
|
||||
booking: {
|
||||
startTime: {
|
||||
gte: periodStart,
|
||||
lte: periodEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ActiveUserBillingRepository } from "../repositories/ActiveUserBillingRepository";
|
||||
import { ActiveUserBillingService } from "./ActiveUserBillingService";
|
||||
|
||||
function createMockRepository(): {
|
||||
[K in keyof ActiveUserBillingRepository]: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
return {
|
||||
getManagedUserEmailsBySubscriptionId: vi.fn().mockResolvedValue([]),
|
||||
getOrgMemberEmailsByOrgId: vi.fn().mockResolvedValue([]),
|
||||
getActivePlatformUsersAsHost: vi.fn().mockResolvedValue([]),
|
||||
getActiveUsersAsHost: vi.fn().mockResolvedValue([]),
|
||||
getActiveUsersAsAttendee: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
describe("ActiveUserBillingService", () => {
|
||||
let service: ActiveUserBillingService;
|
||||
let mockRepo: ReturnType<typeof createMockRepository>;
|
||||
|
||||
const periodStart = new Date("2026-01-01T00:00:00Z");
|
||||
const periodEnd = new Date("2026-02-01T00:00:00Z");
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = createMockRepository();
|
||||
service = new ActiveUserBillingService({
|
||||
activeUserBillingRepository: mockRepo as unknown as ActiveUserBillingRepository,
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActiveUserCountForPlatformOrg", () => {
|
||||
const subscriptionId = "sub_abc123";
|
||||
|
||||
it("returns 0 when there are no managed users", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(0);
|
||||
expect(mockRepo.getManagedUserEmailsBySubscriptionId).toHaveBeenCalledWith(subscriptionId);
|
||||
expect(mockRepo.getActivePlatformUsersAsHost).not.toHaveBeenCalled();
|
||||
expect(mockRepo.getActiveUsersAsAttendee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("counts users who were active as hosts only", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
{ email: "charlie@test.com" },
|
||||
]);
|
||||
mockRepo.getActivePlatformUsersAsHost.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(mockRepo.getActivePlatformUsersAsHost).toHaveBeenCalledWith(
|
||||
subscriptionId,
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
expect(mockRepo.getActiveUsersAsAttendee).toHaveBeenCalledWith(
|
||||
["charlie@test.com"],
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
});
|
||||
|
||||
it("counts users who were active as attendees only", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
]);
|
||||
mockRepo.getActivePlatformUsersAsHost.mockResolvedValue([]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([{ email: "alice@test.com" }]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it("counts combined hosts and attendees without double-counting", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
{ email: "charlie@test.com" },
|
||||
]);
|
||||
mockRepo.getActivePlatformUsersAsHost.mockResolvedValue([{ email: "alice@test.com" }]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([{ email: "bob@test.com" }]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(2);
|
||||
// Should only check non-host emails for attendee activity
|
||||
expect(mockRepo.getActiveUsersAsAttendee).toHaveBeenCalledWith(
|
||||
["bob@test.com", "charlie@test.com"],
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
});
|
||||
|
||||
it("returns count of all users when everyone is a host", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
]);
|
||||
mockRepo.getActivePlatformUsersAsHost.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(2);
|
||||
// Should skip attendee check when all users are already hosts
|
||||
expect(mockRepo.getActiveUsersAsAttendee).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 0 when no users are active", async () => {
|
||||
mockRepo.getManagedUserEmailsBySubscriptionId.mockResolvedValue([
|
||||
{ email: "alice@test.com" },
|
||||
{ email: "bob@test.com" },
|
||||
]);
|
||||
mockRepo.getActivePlatformUsersAsHost.mockResolvedValue([]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([]);
|
||||
|
||||
const count = await service.getActiveUserCountForPlatformOrg(subscriptionId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getActiveUserCountForOrg", () => {
|
||||
const orgId = 42;
|
||||
|
||||
it("returns 0 when org has no members", async () => {
|
||||
mockRepo.getOrgMemberEmailsByOrgId.mockResolvedValue([]);
|
||||
|
||||
const count = await service.getActiveUserCountForOrg(orgId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(0);
|
||||
expect(mockRepo.getOrgMemberEmailsByOrgId).toHaveBeenCalledWith(orgId);
|
||||
expect(mockRepo.getActiveUsersAsHost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("counts org members who were active as hosts", async () => {
|
||||
mockRepo.getOrgMemberEmailsByOrgId.mockResolvedValue([
|
||||
{ email: "member1@org.com" },
|
||||
{ email: "member2@org.com" },
|
||||
]);
|
||||
mockRepo.getActiveUsersAsHost.mockResolvedValue([{ email: "member1@org.com" }]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([]);
|
||||
|
||||
const count = await service.getActiveUserCountForOrg(orgId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(mockRepo.getActiveUsersAsHost).toHaveBeenCalledWith(
|
||||
["member1@org.com", "member2@org.com"],
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
});
|
||||
|
||||
it("counts org members who were active as attendees", async () => {
|
||||
mockRepo.getOrgMemberEmailsByOrgId.mockResolvedValue([
|
||||
{ email: "member1@org.com" },
|
||||
{ email: "member2@org.com" },
|
||||
{ email: "member3@org.com" },
|
||||
]);
|
||||
mockRepo.getActiveUsersAsHost.mockResolvedValue([{ email: "member1@org.com" }]);
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue([{ email: "member2@org.com" }]);
|
||||
|
||||
const count = await service.getActiveUserCountForOrg(orgId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it("handles large org with many members efficiently", async () => {
|
||||
const emails = Array.from({ length: 100 }, (_, i) => ({ email: `user${i}@org.com` }));
|
||||
mockRepo.getOrgMemberEmailsByOrgId.mockResolvedValue(emails);
|
||||
|
||||
const hostEmails = emails.slice(0, 30);
|
||||
mockRepo.getActiveUsersAsHost.mockResolvedValue(hostEmails);
|
||||
|
||||
const attendeeEmails = emails.slice(30, 50).map((e) => ({ email: e.email }));
|
||||
mockRepo.getActiveUsersAsAttendee.mockResolvedValue(attendeeEmails);
|
||||
|
||||
const count = await service.getActiveUserCountForOrg(orgId, periodStart, periodEnd);
|
||||
|
||||
expect(count).toBe(50);
|
||||
// Attendee check should only receive the 70 non-host emails
|
||||
expect(mockRepo.getActiveUsersAsAttendee).toHaveBeenCalledWith(
|
||||
emails.slice(30).map((e) => e.email),
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ActiveUserBillingRepository } from "../repositories/ActiveUserBillingRepository";
|
||||
|
||||
export interface IActiveUserBillingServiceDeps {
|
||||
activeUserBillingRepository: ActiveUserBillingRepository;
|
||||
}
|
||||
|
||||
export class ActiveUserBillingService {
|
||||
constructor(private readonly deps: IActiveUserBillingServiceDeps) {}
|
||||
|
||||
/**
|
||||
* Count active users for a platform organization (uses PlatformBilling subscription ID).
|
||||
* A user is "active" if they hosted or attended at least one booking in the period OR managed.
|
||||
*
|
||||
* Uses the platform-specific host query that filters on isPlatformManaged + subscriptionId
|
||||
* to ensure only bookings belonging to this org's managed users are counted.
|
||||
*/
|
||||
async getActiveUserCountForPlatformOrg(
|
||||
subscriptionId: string,
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<number> {
|
||||
const managedUserEmails =
|
||||
await this.deps.activeUserBillingRepository.getManagedUserEmailsBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
|
||||
if (managedUserEmails.length === 0) return 0;
|
||||
|
||||
const activeHosts =
|
||||
await this.deps.activeUserBillingRepository.getActivePlatformUsersAsHost(
|
||||
subscriptionId,
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
|
||||
const activeHostEmails = new Set(activeHosts.map((h) => h.email));
|
||||
|
||||
const nonHostEmails = managedUserEmails
|
||||
.map((u) => u.email)
|
||||
.filter((email) => !activeHostEmails.has(email));
|
||||
|
||||
if (nonHostEmails.length === 0) return activeHostEmails.size;
|
||||
|
||||
const activeAttendees =
|
||||
await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee(
|
||||
nonHostEmails,
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
|
||||
return activeHostEmails.size + activeAttendees.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count active users for a regular organization (uses org/team ID).
|
||||
* A user is "active" if they hosted or attended at least one booking in the period.
|
||||
*/
|
||||
async getActiveUserCountForOrg(
|
||||
orgId: number,
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<number> {
|
||||
const memberEmails =
|
||||
await this.deps.activeUserBillingRepository.getOrgMemberEmailsByOrgId(
|
||||
orgId
|
||||
);
|
||||
|
||||
return this.countActiveUsers(
|
||||
memberEmails.map((u) => u.email),
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
}
|
||||
|
||||
private async countActiveUsers(
|
||||
candidateEmails: string[],
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): Promise<number> {
|
||||
if (candidateEmails.length === 0) return 0;
|
||||
|
||||
const activeHosts =
|
||||
await this.deps.activeUserBillingRepository.getActiveUsersAsHost(
|
||||
candidateEmails,
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
|
||||
const activeHostEmails = new Set(activeHosts.map((h) => h.email));
|
||||
|
||||
const nonHostEmails = candidateEmails.filter(
|
||||
(email) => !activeHostEmails.has(email)
|
||||
);
|
||||
|
||||
if (nonHostEmails.length === 0) return activeHostEmails.size;
|
||||
|
||||
const activeAttendees =
|
||||
await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee(
|
||||
nonHostEmails,
|
||||
periodStart,
|
||||
periodEnd
|
||||
);
|
||||
|
||||
return activeHostEmails.size + activeAttendees.length;
|
||||
}
|
||||
}
|
||||
@@ -12,3 +12,6 @@ export { PlatformOrganizationBillingSyncTasker } from "@calcom/features/ee/organ
|
||||
export { PlatformOrganizationBillingTriggerTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker";
|
||||
export { PlatformOrganizationBillingTaskService } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService";
|
||||
export type { IBillingProviderService } from "@calcom/features/ee/billing/service/billingProvider/IBillingProviderService";
|
||||
|
||||
export { getActiveUserBillingService } from "@calcom/features/ee/billing/active-user/di/ActiveUserBillingService.container";
|
||||
export { ActiveUserBillingService } from "@calcom/features/ee/billing/active-user/services/ActiveUserBillingService";
|
||||
|
||||
Reference in New Issue
Block a user