From c60addc528c436b56257d3924ab6017eb3cfc861 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Tue, 20 Jan 2026 08:26:50 +0200 Subject: [PATCH] feat: add PlatformOrganizationBillingTasker with sync and trigger.dev versions (#26803) * feat: add PlatformOrganizationBillingTasker with sync and trigger.dev versions - Create PlatformOrganizationBillingTasker extending Tasker base class - Add sync tasker (PlatformOrganizationBillingSyncTasker) for synchronous execution - Add trigger.dev tasker (PlatformOrganizationBillingTriggerTasker) for async execution - Create trigger.dev task for incrementing subscription usage - Add PlatformOrganizationBillingTaskService with business logic - Add PlatformOrganizationBillingRepository for data access - Add createSubscriptionUsageRecord method to StripeBillingService - Follow BookingEmailAndSmsTasker pattern for consistency Co-Authored-By: morgan@cal.com * refactor: move repository methods per PR feedback - Move findPlatformOrgByUserId to OrganizationRepository - Create new PlatformBillingRepository for billing queries - Remove PlatformOrganizationBillingRepository (consolidated) - Update task service and trigger.dev task to use new repositories Co-Authored-By: morgan@cal.com * feat: add DI using @evyweb/ioctopus for platform billing tasker - Create di/tasker/ directory following BookingEmailAndSmsTasker pattern - Add tokens.ts with DI tokens for all billing tasker classes - Add module files for PlatformBillingRepository, TaskService, SyncTasker, TriggerTasker, Tasker - Add container files with getter functions - Update trigger.dev task to use DI container instead of manual instantiation Co-Authored-By: morgan@cal.com * refactor: add select statement to PlatformBillingRepository.findByTeamId Only select subscriptionId field since that's the only field used by the task service Co-Authored-By: morgan@cal.com * feat: add NestJS DI modules for platform billing tasker in API v2 - Add platform billing tasker exports to platform-libraries/organizations.ts - Export IBillingProviderService type from platform-libraries - Create StripeBillingProviderService wrapper implementing IBillingProviderService - Create PrismaPlatformBillingRepository extending PlatformBillingRepository - Create NestJS service wrappers for all platform billing tasker classes - Create PlatformBillingTaskerModule with all providers and exports - Update PlatformOrganizationBillingTaskService to use Pick Co-Authored-By: morgan@cal.com * feat: handle cancel and reschedule usage * fix: restore IsUserInBillingOrg to billing module providers Co-Authored-By: morgan@cal.com * chore: add idempotency key * fix: just log error] * fix: logger and typo * fix: address Cubic AI review feedback (high confidence issues) - Fix grammar error: 'Delayed task are' -> 'Delayed tasks are' in PlatformOrganizationBillingSyncTasker.ts (3 occurrences) - Re-throw error after logging in increment-usage.ts to enable trigger.dev retry mechanism Co-Authored-By: unknown <> * fix: remove duplicate code and use DI instead * fix: remove orThrow on find first in findPlatformOrgByUserId * refactor: prevent usage increment task from re-throwing errors --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/api/v2/src/config/app.ts | 2 + apps/api/v2/src/config/type.ts | 2 + apps/api/v2/src/env.ts | 2 + .../modules/platform-billing-tasker.module.ts | 32 +++++ .../prisma-platform-billing.repository.ts | 12 ++ .../stripe-billing-provider.service.ts | 14 ++ .../platform-billing-sync-tasker.service.ts | 15 ++ .../tasker/platform-billing-task.service.ts | 24 ++++ .../tasker/platform-billing-tasker.service.ts | 20 +++ ...platform-billing-trigger-tasker.service.ts | 13 ++ .../v2/src/modules/billing/billing.module.ts | 8 +- .../interfaces/billing-service.interface.ts | 2 +- .../billing/services/billing.service.ts | 129 ++++++++++++------ .../IBillingProviderService.ts | 7 + .../billingProvider/StripeBillingService.ts | 34 +++++ .../PlatformBillingRepository.module.ts | 22 +++ ...ormOrganizationBillingSyncTasker.module.ts | 26 ++++ ...rganizationBillingTaskService.container.ts | 11 ++ ...rmOrganizationBillingTaskService.module.ts | 30 ++++ ...formOrganizationBillingTasker.container.ts | 11 ++ ...latformOrganizationBillingTasker.module.ts | 28 ++++ ...OrganizationBillingTriggerTasker.module.ts | 24 ++++ .../ee/organizations/di/tasker/tokens.ts | 12 ++ .../PlatformOrganizationBillingSyncTasker.ts | 51 +++++++ .../PlatformOrganizationBillingTaskService.ts | 58 ++++++++ .../PlatformOrganizationBillingTasker.ts | 89 ++++++++++++ ...latformOrganizationBillingTriggerTasker.ts | 34 +++++ .../tasker/trigger/cancel-usage-increment.ts | 31 +++++ .../lib/billing/tasker/trigger/config.ts | 24 ++++ .../billing/tasker/trigger/increment-usage.ts | 28 ++++ .../trigger/reschedule-usage-increment.ts | 30 ++++ .../lib/billing/tasker/trigger/schema.ts | 49 +++++++ .../organizations/lib/billing/tasker/types.ts | 27 ++++ .../repositories/OrganizationRepository.ts | 24 +++- .../repositories/PlatformBillingRepository.ts | 16 +++ packages/features/trigger.config.ts | 1 + packages/platform/libraries/index.ts | 82 +++++------ packages/platform/libraries/organizations.ts | 7 + 38 files changed, 931 insertions(+), 100 deletions(-) create mode 100644 apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts create mode 100644 apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts create mode 100644 apps/api/v2/src/lib/services/stripe-billing-provider.service.ts create mode 100644 apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts create mode 100644 apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts create mode 100644 apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts create mode 100644 apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformBillingRepository.module.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingSyncTasker.module.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.container.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.module.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.container.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.module.ts create mode 100644 packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTriggerTasker.module.ts create mode 100644 packages/features/ee/organizations/di/tasker/tokens.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/trigger/cancel-usage-increment.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/trigger/reschedule-usage-increment.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/trigger/schema.ts create mode 100644 packages/features/ee/organizations/lib/billing/tasker/types.ts create mode 100644 packages/features/ee/organizations/repositories/PlatformBillingRepository.ts diff --git a/apps/api/v2/src/config/app.ts b/apps/api/v2/src/config/app.ts index 4a951ee4fe..adc6cfcf7a 100644 --- a/apps/api/v2/src/config/app.ts +++ b/apps/api/v2/src/config/app.ts @@ -47,6 +47,8 @@ const loadConfig = (): AppConfig => { e2e: getEnv("IS_E2E", "false") === "true", enableSlotsWorkers: getEnv("ENABLE_SLOTS_WORKERS", "true") === "true", slotsWorkerPoolSize: Number(getEnv("SLOTS_WORKER_POOL_SIZE", "4")), + vercel: getEnv("VERCEL", "false") === "true", + enableAsyncTasker: getEnv("ENABLE_ASYNC_TASKER", "false") === "true", }; }; diff --git a/apps/api/v2/src/config/type.ts b/apps/api/v2/src/config/type.ts index f3ee7ebe6f..389fa26f65 100644 --- a/apps/api/v2/src/config/type.ts +++ b/apps/api/v2/src/config/type.ts @@ -35,4 +35,6 @@ export type AppConfig = { e2e: boolean; enableSlotsWorkers: boolean; slotsWorkerPoolSize: number; + vercel: boolean; + enableAsyncTasker: boolean; }; diff --git a/apps/api/v2/src/env.ts b/apps/api/v2/src/env.ts index 5d6b8f27c2..6ea987dd17 100644 --- a/apps/api/v2/src/env.ts +++ b/apps/api/v2/src/env.ts @@ -41,6 +41,8 @@ export type Environment = { ENABLE_SLOTS_WORKERS: string; SLOTS_WORKER_POOL_SIZE: string; USE_POOL: string; + VERCEL: string; + ENABLE_ASYNC_TASKER: string; }; export const getEnv = (key: K, fallback?: Environment[K]): Environment[K] => { diff --git a/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts b/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts new file mode 100644 index 0000000000..eedbfec61a --- /dev/null +++ b/apps/api/v2/src/lib/modules/platform-billing-tasker.module.ts @@ -0,0 +1,32 @@ +import { Module, Scope } from "@nestjs/common"; +import { Logger } from "@/lib/logger.bridge"; +import { PrismaPlatformBillingRepository } from "@/lib/repositories/prisma-platform-billing.repository"; +import { StripeBillingProviderService } from "@/lib/services/stripe-billing-provider.service"; +import { PlatformBillingSyncTaskerService } from "@/lib/services/tasker/platform-billing-sync-tasker.service"; +import { PlatformBillingTaskService } from "@/lib/services/tasker/platform-billing-task.service"; +import { PlatformBillingTasker } from "@/lib/services/tasker/platform-billing-tasker.service"; +import { PlatformBillingTriggerTaskerService } from "@/lib/services/tasker/platform-billing-trigger-tasker.service"; +import { OrganizationsModule } from "@/modules/organizations/organizations.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { StripeModule } from "@/modules/stripe/stripe.module"; + +@Module({ + imports: [PrismaModule, StripeModule, OrganizationsModule], + providers: [ + PrismaPlatformBillingRepository, + StripeBillingProviderService, + { + provide: Logger, + useFactory: () => { + return new Logger(); + }, + scope: Scope.TRANSIENT, + }, + PlatformBillingTaskService, + PlatformBillingSyncTaskerService, + PlatformBillingTriggerTaskerService, + PlatformBillingTasker, + ], + exports: [PlatformBillingTasker], +}) +export class PlatformBillingTaskerModule {} diff --git a/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts b/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts new file mode 100644 index 0000000000..637fe27c94 --- /dev/null +++ b/apps/api/v2/src/lib/repositories/prisma-platform-billing.repository.ts @@ -0,0 +1,12 @@ +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +import { PlatformBillingRepository } from "@calcom/platform-libraries/organizations"; +import type { PrismaClient } from "@calcom/prisma"; + +@Injectable() +export class PrismaPlatformBillingRepository extends PlatformBillingRepository { + constructor(dbWrite: PrismaWriteService) { + super(dbWrite.prisma as unknown as PrismaClient); + } +} diff --git a/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts b/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts new file mode 100644 index 0000000000..44ad35f6fa --- /dev/null +++ b/apps/api/v2/src/lib/services/stripe-billing-provider.service.ts @@ -0,0 +1,14 @@ +import { StripeBillingService } from "@calcom/platform-libraries"; +import type { IBillingProviderService } from "@calcom/platform-libraries/organizations"; +import { Injectable } from "@nestjs/common"; +import { StripeService } from "@/modules/stripe/stripe.service"; + +@Injectable() +export class StripeBillingProviderService + extends StripeBillingService + implements Pick +{ + constructor(readonly stripeService: StripeService) { + super(stripeService.getStripe()); + } +} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts new file mode 100644 index 0000000000..cab4bb5201 --- /dev/null +++ b/apps/api/v2/src/lib/services/tasker/platform-billing-sync-tasker.service.ts @@ -0,0 +1,15 @@ +import { Logger } from "@/lib/logger.bridge"; +import { PlatformBillingTaskService } from "@/lib/services/tasker/platform-billing-task.service"; +import { Injectable } from "@nestjs/common"; + +import { PlatformOrganizationBillingSyncTasker as BasePlatformOrganizationBillingSyncTasker } from "@calcom/platform-libraries/organizations"; + +@Injectable() +export class PlatformBillingSyncTaskerService extends BasePlatformOrganizationBillingSyncTasker { + constructor(billingTaskService: PlatformBillingTaskService, logger: Logger) { + super({ + logger, + billingTaskService, + }); + } +} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts new file mode 100644 index 0000000000..1229b751f1 --- /dev/null +++ b/apps/api/v2/src/lib/services/tasker/platform-billing-task.service.ts @@ -0,0 +1,24 @@ +import { Logger } from "@/lib/logger.bridge"; +import { PrismaPlatformBillingRepository } from "@/lib/repositories/prisma-platform-billing.repository"; +import { StripeBillingProviderService } from "@/lib/services/stripe-billing-provider.service"; +import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { Injectable } from "@nestjs/common"; + +import { PlatformOrganizationBillingTaskService as BasePlatformOrganizationBillingTaskService } from "@calcom/platform-libraries/organizations"; + +@Injectable() +export class PlatformBillingTaskService extends BasePlatformOrganizationBillingTaskService { + constructor( + organizationRepository: OrganizationsRepository, + platformBillingRepository: PrismaPlatformBillingRepository, + billingProviderService: StripeBillingProviderService, + logger: Logger + ) { + super({ + logger, + organizationRepository, + platformBillingRepository, + billingProviderService, + }); + } +} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts new file mode 100644 index 0000000000..73a21ff65f --- /dev/null +++ b/apps/api/v2/src/lib/services/tasker/platform-billing-tasker.service.ts @@ -0,0 +1,20 @@ +import { PlatformOrganizationBillingTasker as BasePlatformOrganizationBillingTasker } from "@calcom/platform-libraries/organizations"; +import { Injectable } from "@nestjs/common"; +import { Logger } from "@/lib/logger.bridge"; +import { PlatformBillingSyncTaskerService } from "@/lib/services/tasker/platform-billing-sync-tasker.service"; +import { PlatformBillingTriggerTaskerService } from "@/lib/services/tasker/platform-billing-trigger-tasker.service"; + +@Injectable() +export class PlatformBillingTasker extends BasePlatformOrganizationBillingTasker { + constructor( + syncTasker: PlatformBillingSyncTaskerService, + asyncTasker: PlatformBillingTriggerTaskerService, + logger: Logger + ) { + super({ + logger, + asyncTasker: asyncTasker, + syncTasker: syncTasker, + }); + } +} diff --git a/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts b/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts new file mode 100644 index 0000000000..ca938e8b76 --- /dev/null +++ b/apps/api/v2/src/lib/services/tasker/platform-billing-trigger-tasker.service.ts @@ -0,0 +1,13 @@ +import { Logger } from "@/lib/logger.bridge"; +import { Injectable } from "@nestjs/common"; + +import { PlatformOrganizationBillingTriggerTasker as BasePlatformOrganizationBillingTriggerTasker } from "@calcom/platform-libraries/organizations"; + +@Injectable() +export class PlatformBillingTriggerTaskerService extends BasePlatformOrganizationBillingTriggerTasker { + constructor(logger: Logger) { + super({ + logger, + }); + } +} diff --git a/apps/api/v2/src/modules/billing/billing.module.ts b/apps/api/v2/src/modules/billing/billing.module.ts index 5c75bb59e7..968d6debc1 100644 --- a/apps/api/v2/src/modules/billing/billing.module.ts +++ b/apps/api/v2/src/modules/billing/billing.module.ts @@ -1,11 +1,14 @@ +import { BullModule } from "@nestjs/bull"; +import { Module } from "@nestjs/common"; import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; +import { PlatformBillingTaskerModule } from "@/lib/modules/platform-billing-tasker.module"; import { BillingProcessor } from "@/modules/billing/billing.processor"; import { BillingRepository } from "@/modules/billing/billing.repository"; import { BillingController } from "@/modules/billing/controllers/billing.controller"; import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org"; -import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy"; import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; import { BillingService } from "@/modules/billing/services/billing.service"; +import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy"; import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; @@ -14,8 +17,6 @@ import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; import { UsersModule } from "@/modules/users/users.module"; -import { BullModule } from "@nestjs/bull"; -import { Module } from "@nestjs/common"; @Module({ imports: [ @@ -32,6 +33,7 @@ import { Module } from "@nestjs/common"; }, }), UsersModule, + PlatformBillingTaskerModule, ], providers: [ BillingConfigService, diff --git a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts b/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts index 9c2e1f2115..7935e5e0b4 100644 --- a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts +++ b/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts @@ -7,7 +7,7 @@ import { PlatformBilling, Team } from "@calcom/prisma/client"; export type BillingData = { team: (Team & { platformBilling: PlatformBilling | null }) | null; status: "valid" | "no_subscription" | "no_billing"; - plan: string; + plan: PlatformPlan | "none"; }; export interface IBillingService { diff --git a/apps/api/v2/src/modules/billing/services/billing.service.ts b/apps/api/v2/src/modules/billing/services/billing.service.ts index 8a318dc357..f29ccd1dfc 100644 --- a/apps/api/v2/src/modules/billing/services/billing.service.ts +++ b/apps/api/v2/src/modules/billing/services/billing.service.ts @@ -1,14 +1,3 @@ -import { AppConfig } from "@/config/type"; -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; -import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor"; -import { BillingRepository } from "@/modules/billing/billing.repository"; -import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; -import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; -import { PlatformPlan } from "@/modules/billing/types"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; -import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; -import { StripeService } from "@/modules/stripe/stripe.service"; -import { UsersRepository } from "@/modules/users/users.repository"; import { InjectQueue } from "@nestjs/bull"; import { BadRequestException, @@ -22,6 +11,16 @@ import { ConfigService } from "@nestjs/config"; import { Queue } from "bull"; import { DateTime } from "luxon"; import Stripe from "stripe"; +import { AppConfig } from "@/config/type"; +import { PlatformBillingTasker } from "@/lib/services/tasker/platform-billing-tasker.service"; +import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor"; +import { BillingRepository } from "@/modules/billing/billing.repository"; +import { BillingData, IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; +import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; +import { PlatformPlan } from "@/modules/billing/types"; +import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { StripeService } from "@/modules/stripe/stripe.service"; +import { UsersRepository } from "@/modules/users/users.repository"; @Injectable() export class BillingService implements IBillingService, OnModuleDestroy { @@ -35,14 +34,13 @@ export class BillingService implements IBillingService, OnModuleDestroy { private readonly configService: ConfigService, private readonly billingConfigService: BillingConfigService, private readonly usersRepository: UsersRepository, - private readonly oAuthClientRepository: OAuthClientRepository, - private readonly bookingsRepository: BookingsRepository_2024_08_13, + private readonly platformBillingTasker: PlatformBillingTasker, @InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue ) { this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com"; } - async getBillingData(teamId: number) { + async getBillingData(teamId: number): Promise { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); if (teamWithBilling?.platformBilling) { @@ -52,7 +50,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { return { team: teamWithBilling, status: "valid" as const, - plan: teamWithBilling.platformBilling.plan, + plan: teamWithBilling.platformBilling.plan as PlatformPlan, }; } } else { @@ -62,7 +60,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { async createTeamBilling(teamId: number): Promise { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - let customerId = teamWithBilling?.platformBilling?.customerId; + let customerId = teamWithBilling?.platformBilling?.customerId ?? ""; if (!teamWithBilling?.platformBilling) { customerId = await this.teamsRepository.createNewBillingRelation(teamId); @@ -73,10 +71,14 @@ export class BillingService implements IBillingService, OnModuleDestroy { }); } - return customerId!; + return customerId; } - async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) { + async redirectToSubscribeCheckout( + teamId: number, + plan: PlatformPlan, + customerId?: string + ): Promise { const { url } = await this.stripeService.getStripe().checkout.sessions.create({ customer: customerId, line_items: [ @@ -109,7 +111,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { return url; } - async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan) { + async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); const customerId = teamWithBilling?.platformBilling?.customerId; @@ -134,7 +136,11 @@ export class BillingService implements IBillingService, OnModuleDestroy { return url; } - async setPerBookingSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) { + async setPerBookingSubscriptionForTeam( + teamId: number, + subscriptionId: string, + plan: PlatformPlan + ): Promise> { const billingCycleStart = DateTime.now().get("day"); const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day"); @@ -152,7 +158,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { subscriptionId: string, plan: PlatformPlan, priceId: string - ) { + ): Promise> { const billingCycleStart = DateTime.now().get("day"); const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day"); @@ -166,12 +172,12 @@ export class BillingService implements IBillingService, OnModuleDestroy { ); } - async handleStripeSubscriptionDeleted(event: Stripe.Event) { + async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise { const subscription = event.data.object as Stripe.Subscription; const teamId = subscription?.metadata?.teamId; const plan = PlatformPlan[subscription?.metadata?.plan?.toUpperCase() as keyof typeof PlatformPlan]; if (teamId && plan) { - const currentBilling = await this.billingRepository.getBillingForTeam(Number.parseInt(teamId)); + const currentBilling = await this.billingRepository.getBillingForTeam(Number.parseInt(teamId, 10)); if (currentBilling?.subscriptionId === subscription.id) { await this.billingRepository.deleteBilling(currentBilling.id); this.logger.log(`Stripe Subscription deleted`, { @@ -207,7 +213,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async handleStripePaymentSuccess(event: Stripe.Event) { + async handleStripePaymentSuccess(event: Stripe.Event): Promise { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); const customerId = this.getCustomerIdFromInvoice(invoice); @@ -216,7 +222,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async handleStripePaymentFailed(event: Stripe.Event) { + async handleStripePaymentFailed(event: Stripe.Event): Promise { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); const customerId = this.getCustomerIdFromInvoice(invoice); @@ -225,7 +231,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async handleStripePaymentPastDue(event: Stripe.Event) { + async handleStripePaymentPastDue(event: Stripe.Event): Promise { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); const customerId = this.getCustomerIdFromInvoice(invoice); @@ -249,14 +255,14 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async handleStripeCheckoutEvents(event: Stripe.Event) { + async handleStripeCheckoutEvents(event: Stripe.Event): Promise { const checkoutSession = event.data.object as Stripe.Checkout.Session; if (!checkoutSession.metadata?.teamId) { return; } - const teamId = Number.parseInt(checkoutSession.metadata.teamId); + const teamId = Number.parseInt(checkoutSession.metadata.teamId, 10); const plan = checkoutSession.metadata.plan; if (!plan || !teamId) { this.logger.log("Webhook received but not pertaining to Platform, discarding."); @@ -288,7 +294,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { return; } - async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event) { + async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise { const invoice = event.data.object as Stripe.Invoice; const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); @@ -299,12 +305,16 @@ export class BillingService implements IBillingService, OnModuleDestroy { const teamWithBilling = await this.billingRepository.getBillingForTeamBySubscriptionId(subscriptionId); if (teamWithBilling?.plan === "PER_ACTIVE_USER") { - const activeManagedUsersCount = await this.getActiveManagedUsersCount( + let activeManagedUsersCount = await this.getActiveManagedUsersCount( subscriptionId, new Date(invoice.period_start * 1000), new Date(invoice.period_end * 1000) ); + if (activeManagedUsersCount < 0) { + activeManagedUsersCount = 1; + } + const existingSubscription = await this.stripeService .getStripe() .subscriptions.retrieve(subscriptionId); @@ -321,17 +331,18 @@ export class BillingService implements IBillingService, OnModuleDestroy { } await this.stripeService.getStripe().subscriptions.update(subscriptionId, { - items: [ - { id: subscriptionItem.id, quantity: activeManagedUsersCount > 0 ? activeManagedUsersCount : 1 }, - ], + items: [{ id: subscriptionItem.id, quantity: activeManagedUsersCount }], }); } } - async getActiveManagedUsersCount(subscriptionId: string, invoiceStart: Date, invoiceEnd: Date) { - const managedUsersEmails = await this.usersRepository.getOrgsManagedUserEmailsBySubscriptionId( - subscriptionId - ); + async getActiveManagedUsersCount( + subscriptionId: string, + invoiceStart: Date, + invoiceEnd: Date + ): Promise { + const managedUsersEmails = + await this.usersRepository.getOrgsManagedUserEmailsBySubscriptionId(subscriptionId); if (!managedUsersEmails) return 0; @@ -362,7 +373,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { return activeManagedUserEmailsAsAttendee.length + activeManagedUserEmailsAsHost.length; } - async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) { + async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) { @@ -424,13 +435,37 @@ export class BillingService implements IBillingService, OnModuleDestroy { startTime: Date; fromReschedule?: string | null; } - ) { + ): Promise { if (this.configService.get("e2e")) { return true; } const { uid, startTime, fromReschedule } = booking; - const delay = startTime.getTime() - Date.now(); + if (this.configService.get("enableAsyncTasker")) { + if (fromReschedule) { + this.platformBillingTasker.rescheduleUsageIncrement({ + payload: { bookingUid: uid }, + options: { delay: startTime }, + }); + return true; + } + this.platformBillingTasker.incrementUsage({ + payload: { userId }, + options: { + delay: startTime, + tags: [`platform.billing.usage.${uid}`], + idempotencyKey: `platform.billing.usage.${uid}-${userId}`, + }, + }); + return true; + } + + let delay = startTime.getTime() - Date.now(); + + if (delay < 0) { + delay = 0; + } + if (fromReschedule) { // cancel the usage increment job for the booking that is being rescheduled await this.cancelUsageByBookingUid(fromReschedule); @@ -441,7 +476,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { { userId, } satisfies IncrementJobDataType, - { delay: delay > 0 ? delay : 0, jobId: `increment-${uid}`, removeOnComplete: true } + { delay: delay, jobId: `increment-${uid}`, removeOnComplete: true } ); this.logger.log(`Added stripe usage increment job for booking ${uid} and user ${userId}`); } @@ -451,10 +486,16 @@ export class BillingService implements IBillingService, OnModuleDestroy { * Cancels the usage increment job for a booking when it is cancelled. * Removing an attendee from a booking does not cancel the usage increment job. */ - async cancelUsageByBookingUid(bookingUid: string) { + async cancelUsageByBookingUid(bookingUid: string): Promise { if (this.configService.get("e2e")) { return true; } + + if (this.configService.get("enableAsyncTasker")) { + await this.platformBillingTasker.cancelUsageIncrement({ payload: { bookingUid } }); + return true; + } + const job = await this.billingQueue.getJob(`increment-${bookingUid}`); if (job) { await job.remove(); @@ -462,7 +503,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async cancelTeamSubscription(teamId: number) { + async cancelTeamSubscription(teamId: number): Promise { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); const customerId = teamWithBilling?.platformBilling?.customerId; @@ -484,7 +525,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { } } - async onModuleDestroy() { + async onModuleDestroy(): Promise { try { await this.billingQueue.close(); } catch (err) { diff --git a/packages/features/ee/billing/service/billingProvider/IBillingProviderService.ts b/packages/features/ee/billing/service/billingProvider/IBillingProviderService.ts index b4b4ca43b2..71a3114046 100644 --- a/packages/features/ee/billing/service/billingProvider/IBillingProviderService.ts +++ b/packages/features/ee/billing/service/billingProvider/IBillingProviderService.ts @@ -83,6 +83,13 @@ export interface IBillingProviderService { finalizeInvoice(invoiceId: string): Promise; + // Usage-based billing + createSubscriptionUsageRecord(args: { + subscriptionId: string; + action: "increment" | "set"; + quantity: number; + }): Promise; + // Subscription queries getSubscription(subscriptionId: string): Promise<{ items: Array<{ diff --git a/packages/features/ee/billing/service/billingProvider/StripeBillingService.ts b/packages/features/ee/billing/service/billingProvider/StripeBillingService.ts index ec4d2ec09c..acc151cd72 100644 --- a/packages/features/ee/billing/service/billingProvider/StripeBillingService.ts +++ b/packages/features/ee/billing/service/billingProvider/StripeBillingService.ts @@ -272,6 +272,40 @@ export class StripeBillingService implements IBillingProviderService { await this.stripe.invoices.finalizeInvoice(invoiceId); } + async createSubscriptionUsageRecord( + args: Parameters[0] + ) { + const { subscriptionId, action, quantity } = args; + const log = logger.getSubLogger({ prefix: ["createSubscriptionUsageRecord"] }); + + const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); + if (!subscription?.id) { + log.error(`Failed to retrieve stripe subscription (${subscriptionId})`); + throw new Error(`Failed to retrieve stripe subscription (${subscriptionId})`); + } + + const meteredItem = subscription.items.data.find( + (item) => item.price?.recurring?.usage_type === "metered" + ); + if (!meteredItem) { + log.error(`Stripe subscription (${subscriptionId}) is not usage based`); + throw new Error(`Stripe subscription (${subscriptionId}) is not usage based`); + } + + await this.stripe.subscriptionItems.createUsageRecord(meteredItem.id, { + action, + quantity, + timestamp: "now", + }); + + log.info(`Created usage record for subscription ${subscriptionId}`, { + subscriptionId, + itemId: meteredItem.id, + action, + quantity, + }); + } + async getSubscription(subscriptionId: string) { const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); if (!subscription) return null; diff --git a/packages/features/ee/organizations/di/tasker/PlatformBillingRepository.module.ts b/packages/features/ee/organizations/di/tasker/PlatformBillingRepository.module.ts new file mode 100644 index 0000000000..1afa69dd02 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformBillingRepository.module.ts @@ -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 { PlatformBillingRepository } from "@calcom/features/ee/organizations/repositories/PlatformBillingRepository"; + +import { PLATFORM_BILLING_TASKER_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_BILLING_REPOSITORY; +const moduleToken = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_BILLING_REPOSITORY_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: PlatformBillingRepository, + dep: prismaModuleLoader, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingSyncTasker.module.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingSyncTasker.module.ts new file mode 100644 index 0000000000..242889e3d3 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingSyncTasker.module.ts @@ -0,0 +1,26 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service"; +import { PlatformOrganizationBillingSyncTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker"; + +import { moduleLoader as taskServiceModuleLoader } from "./PlatformOrganizationBillingTaskService.module"; +import { PLATFORM_BILLING_TASKER_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER; +const moduleToken = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: PlatformOrganizationBillingSyncTasker, + depsMap: { + logger: loggerServiceModule, + billingTaskService: taskServiceModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.container.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.container.ts new file mode 100644 index 0000000000..d4b2b7627f --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.container.ts @@ -0,0 +1,11 @@ +import { createContainer } from "@calcom/features/di/di"; +import { PlatformOrganizationBillingTaskService } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService"; + +import { moduleLoader as taskServiceModuleLoader } from "./PlatformOrganizationBillingTaskService.module"; + +const container = createContainer(); + +export function getPlatformOrganizationBillingTaskService(): PlatformOrganizationBillingTaskService { + taskServiceModuleLoader.loadModule(container); + return container.get(taskServiceModuleLoader.token); +} diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.module.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.module.ts new file mode 100644 index 0000000000..7235df9dc8 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.module.ts @@ -0,0 +1,30 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service"; +import { billingProviderServiceModuleLoader } from "@calcom/features/ee/billing/di/modules/BillingProviderService"; +import { PlatformOrganizationBillingTaskService } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService"; + +import { moduleLoader as organizationRepositoryModuleLoader } from "../OrganizationRepository.module"; +import { moduleLoader as platformBillingRepositoryModuleLoader } from "./PlatformBillingRepository.module"; +import { PLATFORM_BILLING_TASKER_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TASK_SERVICE; +const moduleToken = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TASK_SERVICE_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: PlatformOrganizationBillingTaskService, + depsMap: { + logger: loggerServiceModule, + organizationRepository: organizationRepositoryModuleLoader, + platformBillingRepository: platformBillingRepositoryModuleLoader, + billingProviderService: billingProviderServiceModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.container.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.container.ts new file mode 100644 index 0000000000..df840257c0 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.container.ts @@ -0,0 +1,11 @@ +import { createContainer } from "@calcom/features/di/di"; +import { PlatformOrganizationBillingTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker"; + +import { moduleLoader as taskerModuleLoader } from "./PlatformOrganizationBillingTasker.module"; + +const container = createContainer(); + +export function getPlatformOrganizationBillingTasker(): PlatformOrganizationBillingTasker { + taskerModuleLoader.loadModule(container); + return container.get(taskerModuleLoader.token); +} diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.module.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.module.ts new file mode 100644 index 0000000000..6fb2a13c4b --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTasker.module.ts @@ -0,0 +1,28 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service"; +import { PlatformOrganizationBillingTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker"; + +import { moduleLoader as syncTaskerModuleLoader } from "./PlatformOrganizationBillingSyncTasker.module"; +import { moduleLoader as triggerTaskerModuleLoader } from "./PlatformOrganizationBillingTriggerTasker.module"; +import { PLATFORM_BILLING_TASKER_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TASKER; +const moduleToken = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TASKER_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: PlatformOrganizationBillingTasker, + depsMap: { + logger: loggerServiceModule, + asyncTasker: triggerTaskerModuleLoader, + syncTasker: syncTaskerModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; diff --git a/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTriggerTasker.module.ts b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTriggerTasker.module.ts new file mode 100644 index 0000000000..495a2ce200 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/PlatformOrganizationBillingTriggerTasker.module.ts @@ -0,0 +1,24 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service"; +import { PlatformOrganizationBillingTriggerTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker"; + +import { PLATFORM_BILLING_TASKER_DI_TOKENS } from "./tokens"; + +const thisModule = createModule(); +const token = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER; +const moduleToken = PLATFORM_BILLING_TASKER_DI_TOKENS.PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: PlatformOrganizationBillingTriggerTasker, + depsMap: { + logger: loggerServiceModule, + }, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; diff --git a/packages/features/ee/organizations/di/tasker/tokens.ts b/packages/features/ee/organizations/di/tasker/tokens.ts new file mode 100644 index 0000000000..0e25ff0dd7 --- /dev/null +++ b/packages/features/ee/organizations/di/tasker/tokens.ts @@ -0,0 +1,12 @@ +export const PLATFORM_BILLING_TASKER_DI_TOKENS = { + PLATFORM_BILLING_REPOSITORY: Symbol("PlatformBillingRepository"), + PLATFORM_BILLING_REPOSITORY_MODULE: Symbol("PlatformBillingRepositoryModule"), + PLATFORM_ORGANIZATION_BILLING_TASK_SERVICE: Symbol("PlatformOrganizationBillingTaskService"), + PLATFORM_ORGANIZATION_BILLING_TASK_SERVICE_MODULE: Symbol("PlatformOrganizationBillingTaskServiceModule"), + PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER: Symbol("PlatformOrganizationBillingSyncTasker"), + PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER_MODULE: Symbol("PlatformOrganizationBillingSyncTaskerModule"), + PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER: Symbol("PlatformOrganizationBillingTriggerTasker"), + PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER_MODULE: Symbol("PlatformOrganizationBillingTriggerTaskerModule"), + PLATFORM_ORGANIZATION_BILLING_TASKER: Symbol("PlatformOrganizationBillingTasker"), + PLATFORM_ORGANIZATION_BILLING_TASKER_MODULE: Symbol("PlatformOrganizationBillingTaskerModule"), +}; diff --git a/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts new file mode 100644 index 0000000000..3263898162 --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts @@ -0,0 +1,51 @@ +import type { ITaskerDependencies } from "@calcom/lib/tasker/types"; +import { nanoid } from "nanoid"; + +import type { PlatformOrganizationBillingTaskService } from "./PlatformOrganizationBillingTaskService"; +import type { IPlatformOrganizationBillingTasker } from "./types"; + +export interface IPlatformOrganizationBillingSyncTaskerDependencies { + billingTaskService: PlatformOrganizationBillingTaskService; +} + +export class PlatformOrganizationBillingSyncTasker implements IPlatformOrganizationBillingTasker { + constructor( + public readonly dependencies: ITaskerDependencies & IPlatformOrganizationBillingSyncTaskerDependencies + ) {} + + async incrementUsage( + payload: Parameters[0] + ): Promise<{ runId: string }> { + const runId = `sync_${nanoid(10)}`; + this.dependencies.logger.info( + "Delayed tasks are not supported in sync mode, did not increment usage", + runId, + payload + ); + return { runId }; + } + + async cancelUsageIncrement( + payload: Parameters[0] + ): Promise<{ runId: string }> { + const runId = `sync_${nanoid(10)}`; + this.dependencies.logger.info( + "Delayed tasks are not supported in sync mode, did not cancel usage increment", + runId, + payload + ); + return { runId }; + } + + async rescheduleUsageIncrement( + payload: Parameters[0] + ): Promise<{ runId: string }> { + const runId = `sync_${nanoid(10)}`; + this.dependencies.logger.info( + "Delayed tasks are not supported in sync mode, did not reschedule usage increment", + runId, + payload + ); + return { runId }; + } +} diff --git a/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts new file mode 100644 index 0000000000..c2b9e6cc75 --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts @@ -0,0 +1,58 @@ +import type { IBillingProviderService } from "@calcom/features/ee/billing/service/billingProvider/IBillingProviderService"; +import type { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; +import type { PlatformBillingRepository } from "@calcom/features/ee/organizations/repositories/PlatformBillingRepository"; +import type { ITaskerDependencies } from "@calcom/lib/tasker/types"; + +import type { PlatformOrganizationBillingTasks } from "./types"; + +export interface IPlatformOrganizationBillingTaskServiceDependencies { + organizationRepository: OrganizationRepository; + platformBillingRepository: PlatformBillingRepository; + billingProviderService: Pick; +} + +export class PlatformOrganizationBillingTaskService implements PlatformOrganizationBillingTasks { + constructor( + public readonly dependencies: { + logger: ITaskerDependencies["logger"]; + } & IPlatformOrganizationBillingTaskServiceDependencies + ) {} + + async incrementUsage( + payload: Parameters[0] + ): Promise { + const { userId } = payload; + const { organizationRepository, platformBillingRepository, billingProviderService, logger } = + this.dependencies; + + const team = await organizationRepository.findPlatformOrgByUserId(userId); + const teamId = team?.id; + if (!teamId) { + logger.error(`User (${userId}) is not part of the platform organization (${teamId})`, { + teamId, + userId, + }); + return; + } + + const billingSubscription = await platformBillingRepository.findByTeamId(teamId); + if (!billingSubscription || !billingSubscription?.subscriptionId) { + logger.error(`Team ${teamId} did not have stripe subscription associated to it`, { + teamId, + }); + return; + } + + await billingProviderService.createSubscriptionUsageRecord({ + subscriptionId: billingSubscription.subscriptionId, + action: "increment", + quantity: 1, + }); + + logger.info("Increased organization usage for subscription", { + subscriptionId: billingSubscription.subscriptionId, + teamId, + userId, + }); + } +} diff --git a/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts new file mode 100644 index 0000000000..d0401e0931 --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts @@ -0,0 +1,89 @@ +import { Tasker } from "@calcom/lib/tasker/Tasker"; +import type { ILogger } from "@calcom/lib/tasker/types"; +import type { TriggerOptions } from "@trigger.dev/sdk"; +import type { PlatformOrganizationBillingSyncTasker } from "./PlatformOrganizationBillingSyncTasker"; +import type { PlatformOrganizationBillingTriggerTasker } from "./PlatformOrganizationBillingTriggerTasker"; +import type { IPlatformOrganizationBillingTasker, PlatformOrganizationBillingTaskPayload } from "./types"; + +export interface IPlatformOrganizationBillingTaskerDependencies { + asyncTasker: PlatformOrganizationBillingTriggerTasker; + syncTasker: PlatformOrganizationBillingSyncTasker; + logger: ILogger; +} + +export class PlatformOrganizationBillingTasker extends Tasker { + constructor(public readonly dependencies: IPlatformOrganizationBillingTaskerDependencies) { + super(dependencies); + } + + public async incrementUsage(data: { + payload: PlatformOrganizationBillingTaskPayload; + options?: TriggerOptions; + }): Promise<{ runId: string }> { + const { payload } = data; + let taskResponse: { + runId: string; + } = { runId: "task-not-found" }; + + try { + taskResponse = await this.dispatch("incrementUsage", payload, data.options); + + this.logger.info(`PlatformOrganizationBillingTasker incrementUsage success:`, taskResponse, { + userId: payload.userId, + }); + } catch { + taskResponse = { runId: "task-failed" }; + this.logger.error(`PlatformOrganizationBillingTasker incrementUsage failed`, taskResponse, { + userId: payload.userId, + }); + } + + return taskResponse; + } + + public async cancelUsageIncrement(data: { payload: { bookingUid: string } }): Promise<{ runId: string }> { + const { payload } = data; + let taskResponse: { + runId: string; + } = { runId: "task-not-found" }; + + try { + taskResponse = await this.dispatch("cancelUsageIncrement", payload); + + this.logger.info(`PlatformOrganizationBillingTasker cancelUsageIncrement success:`, taskResponse, { + bookingUid: payload.bookingUid, + }); + } catch { + taskResponse = { runId: "task-failed" }; + this.logger.error(`PlatformOrganizationBillingTasker cancelUsageIncrement failed`, taskResponse, { + bookingUid: payload.bookingUid, + }); + } + + return taskResponse; + } + + public async rescheduleUsageIncrement(data: { + payload: { bookingUid: string }; + options: { delay: NonNullable }; + }): Promise<{ runId: string }> { + const { payload } = data; + let taskResponse: { + runId: string; + } = { runId: "task-not-found" }; + + try { + taskResponse = await this.dispatch("rescheduleUsageIncrement", payload, data.options); + this.logger.info(`PlatformOrganizationBillingTasker rescheduleUsageIncrement success:`, taskResponse, { + bookingUid: payload.bookingUid, + }); + } catch { + taskResponse = { runId: "task-failed" }; + this.logger.error(`PlatformOrganizationBillingTasker rescheduleUsageIncrement failed`, taskResponse, { + bookingUid: payload.bookingUid, + }); + } + + return taskResponse; + } +} diff --git a/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker.ts b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker.ts new file mode 100644 index 0000000000..6653e83360 --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTriggerTasker.ts @@ -0,0 +1,34 @@ +import type { ITaskerDependencies } from "@calcom/lib/tasker/types"; +import type { TriggerOptions } from "@trigger.dev/sdk"; +import type { IPlatformOrganizationBillingTasker } from "./types"; + +export class PlatformOrganizationBillingTriggerTasker implements IPlatformOrganizationBillingTasker { + constructor(public readonly dependencies: ITaskerDependencies) {} + + async incrementUsage( + payload: Parameters[0], + options?: TriggerOptions + ): Promise<{ runId: string }> { + const { incrementUsage } = await import("./trigger/increment-usage"); + const handle = await incrementUsage.trigger(payload, options); + return { runId: handle.id }; + } + + async cancelUsageIncrement( + payload: Parameters[0], + options?: TriggerOptions + ): Promise<{ runId: string }> { + const { cancelUsageIncrement } = await import("./trigger/cancel-usage-increment"); + const handle = await cancelUsageIncrement.trigger(payload, options); + return { runId: handle.id }; + } + + async rescheduleUsageIncrement( + payload: Parameters[0], + options?: TriggerOptions + ): Promise<{ runId: string }> { + const { rescheduleUsageIncrement } = await import("./trigger/reschedule-usage-increment"); + const handle = await rescheduleUsageIncrement.trigger(payload, options); + return { runId: handle.id }; + } +} diff --git a/packages/features/ee/organizations/lib/billing/tasker/trigger/cancel-usage-increment.ts b/packages/features/ee/organizations/lib/billing/tasker/trigger/cancel-usage-increment.ts new file mode 100644 index 0000000000..508ccdaea4 --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/trigger/cancel-usage-increment.ts @@ -0,0 +1,31 @@ +import { logger, runs, schemaTask, type TaskWithSchema } from "@trigger.dev/sdk"; +import type { z } from "zod"; + +import { platformBillingTaskConfig } from "./config"; +import { platformBillingCancelUsageIncrementTaskSchema } from "./schema"; + +export const CANCEL_USAGE_INCREMENT_JOB_ID = "platform.billing.cancel-usage-increment"; + +export const cancelUsageIncrement: TaskWithSchema< + typeof CANCEL_USAGE_INCREMENT_JOB_ID, + typeof platformBillingCancelUsageIncrementTaskSchema +> = schemaTask({ + id: CANCEL_USAGE_INCREMENT_JOB_ID, + ...platformBillingTaskConfig, + schema: platformBillingCancelUsageIncrementTaskSchema, + run: async (payload: z.infer) => { + const runId: string = ( + await runs.list({ + tag: `increment-${payload.bookingUid}`, + limit: 1, + }) + )?.data?.[0]?.id; + + if (!runId) { + logger.info("No run found for booking uid", { bookingUid: payload.bookingUid }); + return; + } + + await runs.cancel(runId); + }, +}); diff --git a/packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts b/packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts new file mode 100644 index 0000000000..ea102b201f --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts @@ -0,0 +1,24 @@ +import type { Queue, schemaTask } from "@trigger.dev/sdk"; +import { queue } from "@trigger.dev/sdk"; + +type PlatformBillingTask = Pick[0], "machine" | "retry" | "queue">; + +export const platformBillingQueue: Queue = queue({ + name: "platform-billing", + concurrencyLimit: 5, +}); + +export const platformBillingTaskConfig: PlatformBillingTask = { + queue: platformBillingQueue, + machine: "small-2x", + retry: { + maxAttempts: 3, + factor: 1.8, + minTimeoutInMs: 500, + maxTimeoutInMs: 30_000, + randomize: true, + outOfMemory: { + machine: "medium-1x", + }, + }, +}; diff --git a/packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts b/packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts new file mode 100644 index 0000000000..c90c78ddfb --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts @@ -0,0 +1,28 @@ +import { ErrorWithCode } from "@calcom/lib/errors"; +import { logger, schemaTask, type TaskWithSchema } from "@trigger.dev/sdk"; +import type { z } from "zod"; +import { platformBillingTaskConfig } from "./config"; +import { platformBillingTaskSchema } from "./schema"; + +export const INCREMENT_USAGE_JOB_ID = "platform.billing.increment-usage"; + +export const incrementUsage: TaskWithSchema = + schemaTask({ + id: INCREMENT_USAGE_JOB_ID, + ...platformBillingTaskConfig, + schema: platformBillingTaskSchema, + run: async (payload: z.infer) => { + const { getPlatformOrganizationBillingTaskService } = await import( + "@calcom/features/ee/organizations/di/tasker/PlatformOrganizationBillingTaskService.container" + ); + + const billingTaskService = getPlatformOrganizationBillingTaskService(); + try { + await billingTaskService.incrementUsage(payload); + } catch (error) { + if (error instanceof Error || error instanceof ErrorWithCode) logger.error(error.message); + else logger.error("Unknown error in incrementUsage", { error }); + throw error; + } + }, + }); diff --git a/packages/features/ee/organizations/lib/billing/tasker/trigger/reschedule-usage-increment.ts b/packages/features/ee/organizations/lib/billing/tasker/trigger/reschedule-usage-increment.ts new file mode 100644 index 0000000000..d6c41ee11f --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/trigger/reschedule-usage-increment.ts @@ -0,0 +1,30 @@ +import { logger, runs, schemaTask, type TaskWithSchema } from "@trigger.dev/sdk"; +import type z from "zod"; +import { platformBillingTaskConfig } from "./config"; +import { platformBillingRescheduleUsageIncrementTaskSchema } from "./schema"; + +export const RESCHEDULE_USAGE_INCREMENT_JOB_ID = "platform.billing.reschedule-usage-increment"; + +export const rescheduleUsageIncrement: TaskWithSchema< + typeof RESCHEDULE_USAGE_INCREMENT_JOB_ID, + typeof platformBillingRescheduleUsageIncrementTaskSchema +> = schemaTask({ + id: RESCHEDULE_USAGE_INCREMENT_JOB_ID, + ...platformBillingTaskConfig, + schema: platformBillingRescheduleUsageIncrementTaskSchema, + run: async (payload: z.infer) => { + const runId: string = ( + await runs.list({ + tag: `platform.billing.usage.${payload.bookingUid}`, + limit: 1, + }) + )?.data?.[0]?.id; + + if (!runId) { + logger.info("No run found for booking uid", { bookingUid: payload.bookingUid }); + return; + } + + await runs.cancel(runId); + }, +}); diff --git a/packages/features/ee/organizations/lib/billing/tasker/trigger/schema.ts b/packages/features/ee/organizations/lib/billing/tasker/trigger/schema.ts new file mode 100644 index 0000000000..6a5af1eafd --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/trigger/schema.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +export const platformBillingTaskSchema: z.ZodObject< + { + userId: z.ZodNumber; + }, + "strip", + z.ZodTypeAny, + { + userId: number; + }, + { + userId: number; + } +> = z.object({ + userId: z.number(), +}); + +export const platformBillingCancelUsageIncrementTaskSchema: z.ZodObject< + { + bookingUid: z.ZodString; + }, + "strip", + z.ZodTypeAny, + { + bookingUid: string; + }, + { + bookingUid: string; + } +> = z.object({ + bookingUid: z.string(), +}); + +export const platformBillingRescheduleUsageIncrementTaskSchema: z.ZodObject< + { + bookingUid: z.ZodString; + }, + "strip", + z.ZodTypeAny, + { + bookingUid: string; + }, + { + bookingUid: string; + } +> = z.object({ + bookingUid: z.string(), +}); diff --git a/packages/features/ee/organizations/lib/billing/tasker/types.ts b/packages/features/ee/organizations/lib/billing/tasker/types.ts new file mode 100644 index 0000000000..49f6a5bded --- /dev/null +++ b/packages/features/ee/organizations/lib/billing/tasker/types.ts @@ -0,0 +1,27 @@ +import type { TriggerOptions } from "@trigger.dev/sdk"; +import type { z } from "zod"; +import type { platformBillingTaskSchema } from "./trigger/schema"; + +type WithVoidReturns = { + [K in keyof T]: T[K] extends (...args: infer P) => unknown ? (...args: P) => void : T[K]; +}; + +export type PlatformOrganizationBillingTaskPayload = z.infer; + +export interface IPlatformOrganizationBillingTasker { + incrementUsage( + payload: PlatformOrganizationBillingTaskPayload, + options?: TriggerOptions + ): Promise<{ runId: string }>; + + cancelUsageIncrement(payload: { bookingUid: string }): Promise<{ runId: string }>; + + rescheduleUsageIncrement( + payload: { bookingUid: string }, + options: { delay: NonNullable } + ): Promise<{ runId: string }>; +} + +export type PlatformOrganizationBillingTasks = WithVoidReturns< + Pick +>; diff --git a/packages/features/ee/organizations/repositories/OrganizationRepository.ts b/packages/features/ee/organizations/repositories/OrganizationRepository.ts index e547d9e7a1..a1adabb752 100644 --- a/packages/features/ee/organizations/repositories/OrganizationRepository.ts +++ b/packages/features/ee/organizations/repositories/OrganizationRepository.ts @@ -1,5 +1,3 @@ -import type { z } from "zod"; - import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { getParsedTeam } from "@calcom/features/ee/teams/lib/getParsedTeam"; import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser"; @@ -7,10 +5,11 @@ import { UserRepository } from "@calcom/features/users/repositories/UserReposito import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import type { PrismaClient } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; import type { CreationSource } from "@calcom/prisma/enums"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import type { z } from "zod"; const orgSelect = { id: true, @@ -531,4 +530,23 @@ export class OrganizationRepository { return org?.organizationSettings ?? null; } + + async findPlatformOrgByUserId(userId: number) { + return this.prismaClient.team.findFirst({ + where: { + orgProfiles: { + some: { + userId: userId, + }, + }, + isPlatform: true, + isOrganization: true, + }, + select: { + id: true, + isPlatform: true, + isOrganization: true, + }, + }); + } } diff --git a/packages/features/ee/organizations/repositories/PlatformBillingRepository.ts b/packages/features/ee/organizations/repositories/PlatformBillingRepository.ts new file mode 100644 index 0000000000..17f0f8033a --- /dev/null +++ b/packages/features/ee/organizations/repositories/PlatformBillingRepository.ts @@ -0,0 +1,16 @@ +import type { PrismaClient } from "@calcom/prisma/client"; + +export class PlatformBillingRepository { + constructor(private readonly prismaClient: PrismaClient) {} + + async findByTeamId(teamId: number) { + return this.prismaClient.platformBilling.findUnique({ + where: { + id: teamId, + }, + select: { + subscriptionId: true, + }, + }); + } +} diff --git a/packages/features/trigger.config.ts b/packages/features/trigger.config.ts index 7e7e47b2bc..260719fd9f 100644 --- a/packages/features/trigger.config.ts +++ b/packages/features/trigger.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ "./bookings/lib/tasker/trigger/notifications", "./calendars/lib/tasker/trigger", "./ee/billing/service/proration/tasker/trigger", + "./ee/organizations/lib/billing/tasker/trigger", ], // Customize based on your project structure // Retry configuration diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index e7678bd851..f967479efb 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -6,13 +6,13 @@ import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooki import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment"; import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"; import { - verifyPhoneNumber, sendVerificationCode, + verifyPhoneNumber, } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber"; import { handleCreatePhoneCall } from "@calcom/features/handleCreatePhoneCall"; import handleMarkNoShow from "@calcom/features/handleMarkNoShow"; import { getRoutedUrl } from "@calcom/features/routing-forms/lib/getRoutedUrl"; -import { symmetricEncrypt, symmetricDecrypt } from "@calcom/lib/crypto"; +import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { Prisma } from "@calcom/prisma/client"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; @@ -23,44 +23,39 @@ export { slugify } from "@calcom/lib/slugify"; export { slugifyLenient } from "@calcom/lib/slugify-lenient"; export { getBookingForReschedule }; -export type { EventBusyDate } from "@calcom/types/Calendar"; - +export { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +export { + DEFAULT_WEBHOOK_VERSION, + WebhookVersion, +} from "@calcom/features/webhooks/lib/interface/IWebhookRepository"; export { - CreationSource, - SchedulingType, - PeriodType, AttributeType, + CreationSource, MembershipRole, + PeriodType, + SchedulingType, TimeUnit, WebhookTriggerEvents, - WorkflowTriggerEvents, WorkflowActions, WorkflowTemplates, + WorkflowTriggerEvents, } from "@calcom/prisma/enums"; - -export { - WebhookVersion, - DEFAULT_WEBHOOK_VERSION, -} from "@calcom/features/webhooks/lib/interface/IWebhookRepository"; - -export { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +export type { EventBusyDate } from "@calcom/types/Calendar"; export { handleMarkNoShow }; export { handleCreatePhoneCall }; -export { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars"; - -export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; - export type { BookingCreateBody, BookingResponse, } from "@calcom/features/bookings/types"; -export { MINUTES_TO_BOOK, ENABLE_ASYNC_TASKER } from "@calcom/lib/constants"; +export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; +export { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars"; +export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler"; export { cityTimezonesHandler } from "@calcom/features/cityTimezones/cityTimezonesHandler"; -export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler"; +export { ENABLE_ASYNC_TASKER, MINUTES_TO_BOOK } from "@calcom/lib/constants"; export { TRPCError } from "@trpc/server"; export { createNewUsersConnectToOrgIfExists }; @@ -69,23 +64,19 @@ export { getAllUserBookings }; export { getBookingInfo }; export { handleCancelBooking }; -export { - userMetadata, - bookingMetadataSchema, - teamMetadataSchema, -} from "@calcom/prisma/zod-utils"; +export { dynamicEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; export { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; export { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; -export { dynamicEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +export { bookingMetadataSchema, teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils"; export { symmetricEncrypt, symmetricDecrypt }; export { getTranslation }; -export { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment"; export { roundRobinManualReassignment } from "@calcom/features/ee/round-robin/roundRobinManualReassignment"; +export { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment"; export { validateCustomEventName } from "@calcom/features/eventtypes/lib/eventNaming"; @@ -109,45 +100,38 @@ export { credentialForCalendarServiceSelect }; export { paymentDataSelect }; export { getClientSecretFromPayment }; -export { confirmHandler as confirmBookingHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler"; -export { groupMembershipAttributes } from "@calcom/trpc/server/routers/viewer/attributes/getByUserId.handler"; export type { GroupedAttribute } from "@calcom/trpc/server/routers/viewer/attributes/getByUserId.handler"; +export { groupMembershipAttributes } from "@calcom/trpc/server/routers/viewer/attributes/getByUserId.handler"; +export { confirmHandler as confirmBookingHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler"; export { getBookingFieldsWithSystemFields }; export { getRoutedUrl }; export { getTeamMemberEmailForResponseOrContactUsingUrlQuery }; +export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; +export { getCalendarLinks } from "@calcom/features/bookings/lib/getCalendarLinks"; +export { findTeamMembersMatchingAttributeLogic } from "@calcom/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic"; export { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; export { encryptServiceAccountKey } from "@calcom/lib/server/serviceAccountKey"; export { createHandler as createApiKeyHandler } from "@calcom/trpc/server/routers/viewer/apiKeys/create.handler"; -export { getCalendarLinks } from "@calcom/features/bookings/lib/getCalendarLinks"; - -export { findTeamMembersMatchingAttributeLogic } from "@calcom/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic"; export type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/trpc/server/routers/viewer/attributes/findTeamMembersMatchingAttributeLogic.schema"; -export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; export { verifyPhoneNumber, sendVerificationCode }; export { verifyCodeUnAuthenticated } from "@calcom/features/auth/lib/verifyCodeUnAuthenticated"; - -export { verifyCode as verifyCodeAuthenticated } from "@calcom/trpc/server/routers/viewer/organizations/verifyCode.handler"; - export { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail"; - -export { checkEmailVerificationRequired } from "@calcom/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler"; +export { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; +export { StripeBillingService } from "@calcom/features/ee/billing/service/billingProvider/StripeBillingService"; export { TeamService } from "@calcom/features/ee/teams/services/teamService"; - -export { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; -export { getTasker } from "@calcom/features/tasker/tasker-factory"; -export type { Tasker } from "@calcom/features/tasker/tasker"; - -export { validateUrlForSSRFSync } from "@calcom/lib/ssrfProtection"; - -export { verifyCodeChallenge } from "@calcom/lib/pkce"; -export { generateSecret } from "@calcom/features/oauth/utils/generateSecret"; export { OAuthService } from "@calcom/features/oauth/services/OAuthService"; - +export { generateSecret } from "@calcom/features/oauth/utils/generateSecret"; export { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; +export type { Tasker } from "@calcom/features/tasker/tasker"; +export { getTasker } from "@calcom/features/tasker/tasker-factory"; +export { verifyCodeChallenge } from "@calcom/lib/pkce"; +export { validateUrlForSSRFSync } from "@calcom/lib/ssrfProtection"; +export { checkEmailVerificationRequired } from "@calcom/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler"; +export { verifyCode as verifyCodeAuthenticated } from "@calcom/trpc/server/routers/viewer/organizations/verifyCode.handler"; export type { OrgMembershipLookup } from "@calcom/trpc/server/routers/viewer/slots/util"; diff --git a/packages/platform/libraries/organizations.ts b/packages/platform/libraries/organizations.ts index 0d46ef8949..4408fff179 100644 --- a/packages/platform/libraries/organizations.ts +++ b/packages/platform/libraries/organizations.ts @@ -5,3 +5,10 @@ export type { IOrganizationRepository } from "@calcom/features/ee/organizations/ export { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; export { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync"; + +export { PlatformBillingRepository } from "@calcom/features/ee/organizations/repositories/PlatformBillingRepository"; +export { PlatformOrganizationBillingTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker"; +export { PlatformOrganizationBillingSyncTasker } from "@calcom/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker"; +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";