* refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|