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 <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 <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 <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 <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<IBillingProviderService>

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* feat: handle cancel and reschedule usage

* fix: restore IsUserInBillingOrg to billing module providers

Co-Authored-By: morgan@cal.com <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>
This commit is contained in:
Morgan
2026-01-20 15:26:50 +09:00
committed by GitHub
co-authored by unknown Devin AI
parent 38a7fcfedd
commit c60addc528
38 changed files with 931 additions and 100 deletions
+2
View File
@@ -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",
};
};
+2
View File
@@ -35,4 +35,6 @@ export type AppConfig = {
e2e: boolean;
enableSlotsWorkers: boolean;
slotsWorkerPoolSize: number;
vercel: boolean;
enableAsyncTasker: boolean;
};
+2
View File
@@ -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 = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
@@ -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 {}
@@ -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);
}
}
@@ -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<IBillingProviderService, "createSubscriptionUsageRecord">
{
constructor(readonly stripeService: StripeService) {
super(stripeService.getStripe());
}
}
@@ -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,
});
}
}
@@ -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,
});
}
}
@@ -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,
});
}
}
@@ -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,
});
}
}
@@ -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,
@@ -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 {
@@ -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<AppConfig>,
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<BillingData> {
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<string> {
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<string> {
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<string> {
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<ReturnType<typeof BillingRepository.prototype.updateTeamBilling>> {
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<ReturnType<typeof BillingRepository.prototype.updateTeamBilling>> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<number> {
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<void> {
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<boolean | undefined> {
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<boolean | undefined> {
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<void> {
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<void> {
try {
await this.billingQueue.close();
} catch (err) {
@@ -83,6 +83,13 @@ export interface IBillingProviderService {
finalizeInvoice(invoiceId: string): Promise<void>;
// Usage-based billing
createSubscriptionUsageRecord(args: {
subscriptionId: string;
action: "increment" | "set";
quantity: number;
}): Promise<void>;
// Subscription queries
getSubscription(subscriptionId: string): Promise<{
items: Array<{
@@ -272,6 +272,40 @@ export class StripeBillingService implements IBillingProviderService {
await this.stripe.invoices.finalizeInvoice(invoiceId);
}
async createSubscriptionUsageRecord(
args: Parameters<IBillingProviderService["createSubscriptionUsageRecord"]>[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;
@@ -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;
@@ -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;
@@ -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<PlatformOrganizationBillingTaskService>(taskServiceModuleLoader.token);
}
@@ -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;
@@ -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<PlatformOrganizationBillingTasker>(taskerModuleLoader.token);
}
@@ -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;
@@ -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;
@@ -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"),
};
@@ -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<IPlatformOrganizationBillingTasker["incrementUsage"]>[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<IPlatformOrganizationBillingTasker["cancelUsageIncrement"]>[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<IPlatformOrganizationBillingTasker["rescheduleUsageIncrement"]>[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 };
}
}
@@ -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<IBillingProviderService, "createSubscriptionUsageRecord">;
}
export class PlatformOrganizationBillingTaskService implements PlatformOrganizationBillingTasks {
constructor(
public readonly dependencies: {
logger: ITaskerDependencies["logger"];
} & IPlatformOrganizationBillingTaskServiceDependencies
) {}
async incrementUsage(
payload: Parameters<PlatformOrganizationBillingTasks["incrementUsage"]>[0]
): Promise<void> {
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,
});
}
}
@@ -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<IPlatformOrganizationBillingTasker> {
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<TriggerOptions["delay"]> };
}): 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;
}
}
@@ -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<IPlatformOrganizationBillingTasker["incrementUsage"]>[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<IPlatformOrganizationBillingTasker["cancelUsageIncrement"]>[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<IPlatformOrganizationBillingTasker["rescheduleUsageIncrement"]>[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 };
}
}
@@ -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<typeof platformBillingCancelUsageIncrementTaskSchema>) => {
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);
},
});
@@ -0,0 +1,24 @@
import type { Queue, schemaTask } from "@trigger.dev/sdk";
import { queue } from "@trigger.dev/sdk";
type PlatformBillingTask = Pick<Parameters<typeof schemaTask>[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",
},
},
};
@@ -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<typeof INCREMENT_USAGE_JOB_ID, typeof platformBillingTaskSchema> =
schemaTask({
id: INCREMENT_USAGE_JOB_ID,
...platformBillingTaskConfig,
schema: platformBillingTaskSchema,
run: async (payload: z.infer<typeof platformBillingTaskSchema>) => {
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;
}
},
});
@@ -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<typeof platformBillingRescheduleUsageIncrementTaskSchema>) => {
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);
},
});
@@ -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(),
});
@@ -0,0 +1,27 @@
import type { TriggerOptions } from "@trigger.dev/sdk";
import type { z } from "zod";
import type { platformBillingTaskSchema } from "./trigger/schema";
type WithVoidReturns<T> = {
[K in keyof T]: T[K] extends (...args: infer P) => unknown ? (...args: P) => void : T[K];
};
export type PlatformOrganizationBillingTaskPayload = z.infer<typeof platformBillingTaskSchema>;
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<TriggerOptions["delay"]> }
): Promise<{ runId: string }>;
}
export type PlatformOrganizationBillingTasks = WithVoidReturns<
Pick<IPlatformOrganizationBillingTasker, "incrementUsage">
>;
@@ -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,
},
});
}
}
@@ -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,
},
});
}
}
+1
View File
@@ -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
+33 -49
View File
@@ -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";
@@ -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";