feat: monthly-proration-tasker (#26870)
* feat: monthly-proration-taskerh * remove cronjob from tasker implementation * feat: add DI for MonthlyProrationService and TriggerDevLoggerServiceModule - Create TriggerDevLoggerServiceModule for DI injection of TriggerDevLogger - Add tokens for TriggerDevLogger in shared.tokens.ts - Create MonthlyProrationService DI module and container - Update processMonthlyProrationBatch.ts to use DI container - Use redactError in Tasker.ts to avoid logging sensitive information Co-Authored-By: sean@cal.com <Sean@brydon.io> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
sean@cal.com
Devin AI
parent
b7c497f79e
commit
442779d08d
@@ -0,0 +1,29 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import { bindModuleToClassOnToken, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { TriggerDevLogger } from "@calcom/lib/triggerDevLogger";
|
||||
|
||||
import { SHARED_TOKENS } from "../shared.tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = SHARED_TOKENS.TRIGGER_DEV_LOGGER;
|
||||
const moduleToken = SHARED_TOKENS.TRIGGER_DEV_LOGGER_MODULE;
|
||||
|
||||
class TriggerDevLoggerService extends TriggerDevLogger {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: TriggerDevLoggerService,
|
||||
depsMap: {},
|
||||
});
|
||||
|
||||
export const moduleLoader = {
|
||||
token: SHARED_TOKENS.TRIGGER_DEV_LOGGER,
|
||||
loadModule,
|
||||
} satisfies ModuleLoader;
|
||||
@@ -3,4 +3,6 @@ export const SHARED_TOKENS = {
|
||||
TASKER: Symbol("ITasker"),
|
||||
LOGGER: Symbol("ILogger"),
|
||||
LOGGER_MODULE: Symbol("ILoggerModule"),
|
||||
TRIGGER_DEV_LOGGER: Symbol("ITriggerDevLogger"),
|
||||
TRIGGER_DEV_LOGGER_MODULE: Symbol("ITriggerDevLoggerModule"),
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createContainer } from "@calcom/features/di/di";
|
||||
import { MonthlyProrationService } from "@calcom/features/ee/billing/service/proration/MonthlyProrationService";
|
||||
|
||||
import { monthlyProrationServiceModuleLoader } from "../modules/MonthlyProrationService";
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
|
||||
const container = createContainer();
|
||||
|
||||
export function getMonthlyProrationService(): MonthlyProrationService {
|
||||
monthlyProrationServiceModuleLoader.loadModule(container);
|
||||
return container.get<MonthlyProrationService>(DI_TOKENS.MONTHLY_PRORATION_SERVICE);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { moduleLoader as triggerDevLoggerServiceModule } from "@calcom/features/di/shared/services/triggerDevLogger.service";
|
||||
import { MonthlyProrationService } from "@calcom/features/ee/billing/service/proration/MonthlyProrationService";
|
||||
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = DI_TOKENS.MONTHLY_PRORATION_SERVICE;
|
||||
const moduleToken = DI_TOKENS.MONTHLY_PRORATION_SERVICE_MODULE;
|
||||
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MonthlyProrationService,
|
||||
depsMap: {
|
||||
logger: triggerDevLoggerServiceModule,
|
||||
},
|
||||
});
|
||||
|
||||
export const monthlyProrationServiceModuleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
} satisfies ModuleLoader;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
|
||||
import { MonthlyProrationSyncTasker } from "@calcom/features/ee/billing/service/proration/tasker/MonthlyProrationSyncTasker";
|
||||
|
||||
import { MONTHLY_PRORATION_TASKER_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_SYNC_TASKER;
|
||||
const moduleToken = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_SYNC_TASKER_MODULE;
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MonthlyProrationSyncTasker,
|
||||
dep: loggerServiceModule,
|
||||
});
|
||||
|
||||
export const moduleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
} satisfies ModuleLoader;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createContainer } from "@calcom/features/di/di";
|
||||
|
||||
import type { MonthlyProrationTasker } from "@calcom/features/ee/billing/service/proration/tasker/MonthlyProrationTasker";
|
||||
|
||||
import { moduleLoader as monthlyProrationTaskerModule } from "./MonthlyProrationTasker.module";
|
||||
import { MONTHLY_PRORATION_TASKER_DI_TOKENS } from "./tokens";
|
||||
|
||||
const container = createContainer();
|
||||
|
||||
export function getMonthlyProrationTasker(): MonthlyProrationTasker {
|
||||
monthlyProrationTaskerModule.loadModule(container);
|
||||
return container.get<MonthlyProrationTasker>(MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_TASKER);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
|
||||
import { MonthlyProrationTasker } from "@calcom/features/ee/billing/service/proration/tasker/MonthlyProrationTasker";
|
||||
|
||||
import { moduleLoader as monthlyProrationSyncTaskerModule } from "./MonthlyProrationSyncTasker.module";
|
||||
import { moduleLoader as monthlyProrationTriggerTaskerModule } from "./MonthlyProrationTriggerDevTasker.module";
|
||||
import { MONTHLY_PRORATION_TASKER_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_TASKER;
|
||||
const moduleToken = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_TASKER_MODULE;
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MonthlyProrationTasker,
|
||||
depsMap: {
|
||||
logger: loggerServiceModule,
|
||||
asyncTasker: monthlyProrationTriggerTaskerModule,
|
||||
syncTasker: monthlyProrationSyncTaskerModule,
|
||||
},
|
||||
});
|
||||
|
||||
export const moduleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
} satisfies ModuleLoader;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
|
||||
import { MonthlyProrationTriggerDevTasker } from "@calcom/features/ee/billing/service/proration/tasker/MonthlyProrationTriggerDevTasker";
|
||||
|
||||
import { MONTHLY_PRORATION_TASKER_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_TRIGGER_TASKER;
|
||||
const moduleToken = MONTHLY_PRORATION_TASKER_DI_TOKENS.MONTHLY_PRORATION_TRIGGER_TASKER_MODULE;
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MonthlyProrationTriggerDevTasker,
|
||||
depsMap: {
|
||||
logger: loggerServiceModule,
|
||||
},
|
||||
});
|
||||
|
||||
export const moduleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
} satisfies ModuleLoader;
|
||||
@@ -0,0 +1,8 @@
|
||||
export const MONTHLY_PRORATION_TASKER_DI_TOKENS = {
|
||||
MONTHLY_PRORATION_TASKER: Symbol("MonthlyProrationTasker"),
|
||||
MONTHLY_PRORATION_TASKER_MODULE: Symbol("MonthlyProrationTaskerModule"),
|
||||
MONTHLY_PRORATION_SYNC_TASKER: Symbol("MonthlyProrationSyncTasker"),
|
||||
MONTHLY_PRORATION_SYNC_TASKER_MODULE: Symbol("MonthlyProrationSyncTaskerModule"),
|
||||
MONTHLY_PRORATION_TRIGGER_TASKER: Symbol("MonthlyProrationTriggerTasker"),
|
||||
MONTHLY_PRORATION_TRIGGER_TASKER_MODULE: Symbol("MonthlyProrationTriggerTaskerModule"),
|
||||
};
|
||||
@@ -9,4 +9,6 @@ export const DI_TOKENS = {
|
||||
TEAM_BILLING_DATA_REPOSITORY_MODULE: Symbol("TeamBillingDataRepositoryModule"),
|
||||
TEAM_BILLING_SERVICE_FACTORY: Symbol("TeamBillingServiceFactory"),
|
||||
TEAM_BILLING_SERVICE_FACTORY_MODULE: Symbol("TeamBillingServiceFactoryModule"),
|
||||
MONTHLY_PRORATION_SERVICE: Symbol("MonthlyProrationService"),
|
||||
MONTHLY_PRORATION_SERVICE_MODULE: Symbol("MonthlyProrationServiceModule"),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function formatMonthKey(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { Logger } from "tslog";
|
||||
import { MonthlyProrationRepository } from "../../repository/proration/MonthlyProrationRepository";
|
||||
@@ -25,14 +26,27 @@ interface ProcessMonthlyProrationsParams {
|
||||
teamIds?: number[];
|
||||
}
|
||||
|
||||
export interface MonthlyProrationServiceDeps {
|
||||
logger: ISimpleLogger;
|
||||
}
|
||||
|
||||
export class MonthlyProrationService {
|
||||
private logger: Logger<unknown>;
|
||||
private logger: ISimpleLogger;
|
||||
private teamRepository: MonthlyProrationTeamRepository;
|
||||
private prorationRepository: MonthlyProrationRepository;
|
||||
private billingService: IBillingProviderService;
|
||||
|
||||
constructor(customLogger?: Logger<unknown>, billingService?: IBillingProviderService) {
|
||||
this.logger = customLogger || log;
|
||||
constructor(deps: MonthlyProrationServiceDeps);
|
||||
constructor(customLogger?: Logger<unknown>, billingService?: IBillingProviderService);
|
||||
constructor(
|
||||
depsOrLogger?: MonthlyProrationServiceDeps | Logger<unknown>,
|
||||
billingService?: IBillingProviderService
|
||||
) {
|
||||
if (depsOrLogger && typeof depsOrLogger === "object" && "logger" in depsOrLogger) {
|
||||
this.logger = depsOrLogger.logger;
|
||||
} else {
|
||||
this.logger = (depsOrLogger as Logger<unknown>) || log;
|
||||
}
|
||||
this.teamRepository = new MonthlyProrationTeamRepository();
|
||||
this.prorationRepository = new MonthlyProrationRepository();
|
||||
this.billingService = billingService || new StripeBillingService(stripe);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import { MonthlyProrationService } from "../MonthlyProrationService";
|
||||
import type { IMonthlyProrationTasker } from "./types";
|
||||
|
||||
export class MonthlyProrationSyncTasker implements IMonthlyProrationTasker {
|
||||
constructor(private readonly logger: Logger<unknown>) {}
|
||||
|
||||
async processBatch(payload: Parameters<IMonthlyProrationTasker["processBatch"]>[0]) {
|
||||
const runId = `sync_${nanoid(10)}`;
|
||||
const prorationService = new MonthlyProrationService(this.logger);
|
||||
await prorationService.processMonthlyProrations(payload);
|
||||
return { runId };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Tasker } from "@calcom/lib/tasker/Tasker";
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import type { MonthlyProrationSyncTasker } from "./MonthlyProrationSyncTasker";
|
||||
import type { MonthlyProrationTriggerDevTasker } from "./MonthlyProrationTriggerDevTasker";
|
||||
import type { IMonthlyProrationTasker, MonthlyProrationBatchPayload } from "./types";
|
||||
|
||||
export interface MonthlyProrationTaskerDependencies {
|
||||
asyncTasker: MonthlyProrationTriggerDevTasker;
|
||||
syncTasker: MonthlyProrationSyncTasker;
|
||||
logger: Logger<unknown>;
|
||||
}
|
||||
|
||||
export class MonthlyProrationTasker extends Tasker<IMonthlyProrationTasker> {
|
||||
constructor(dependencies: MonthlyProrationTaskerDependencies) {
|
||||
super(dependencies);
|
||||
}
|
||||
|
||||
async processBatch(payload: MonthlyProrationBatchPayload): Promise<{ runId: string }> {
|
||||
return await this.dispatch("processBatch", payload);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
|
||||
|
||||
import type { IMonthlyProrationTasker } from "./types";
|
||||
|
||||
export class MonthlyProrationTriggerDevTasker implements IMonthlyProrationTasker {
|
||||
constructor(public readonly dependencies: ITaskerDependencies) {}
|
||||
|
||||
async processBatch(payload: Parameters<IMonthlyProrationTasker["processBatch"]>[0]) {
|
||||
const { processMonthlyProrationBatch } = await import("./trigger/processMonthlyProrationBatch");
|
||||
const handle = await processMonthlyProrationBatch.trigger(payload);
|
||||
return { runId: handle.id };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const MONTHLY_PRORATION_BATCH_SIZE = 10;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { queue, type schemaTask } from "@trigger.dev/sdk";
|
||||
|
||||
type MonthlyProrationTaskConfig = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
|
||||
|
||||
export const monthlyProrationQueue = queue({
|
||||
name: "monthly-proration",
|
||||
concurrencyLimit: 5,
|
||||
});
|
||||
|
||||
export const monthlyProrationTaskConfig: MonthlyProrationTaskConfig = {
|
||||
queue: monthlyProrationQueue,
|
||||
machine: "small-2x",
|
||||
retry: {
|
||||
maxAttempts: 3,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: true,
|
||||
},
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
|
||||
import { monthlyProrationTaskConfig } from "./config";
|
||||
import { monthlyProrationBatchSchema } from "./schema";
|
||||
|
||||
export const processMonthlyProrationBatch = schemaTask({
|
||||
id: "billing.monthly-proration.batch",
|
||||
...monthlyProrationTaskConfig,
|
||||
schema: monthlyProrationBatchSchema,
|
||||
run: async (payload) => {
|
||||
const { getMonthlyProrationService } = await import(
|
||||
"@calcom/features/ee/billing/di/containers/MonthlyProrationService"
|
||||
);
|
||||
|
||||
const prorationService = getMonthlyProrationService();
|
||||
|
||||
await prorationService.processMonthlyProrations({
|
||||
monthKey: payload.monthKey,
|
||||
teamIds: payload.teamIds,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { MONTHLY_PRORATION_BATCH_SIZE } from "../constants";
|
||||
|
||||
const monthKeyRegex = /^\d{4}-(0[1-9]|1[0-2])$/;
|
||||
|
||||
export const monthlyProrationBatchSchema = z.object({
|
||||
monthKey: z.string().regex(monthKeyRegex),
|
||||
teamIds: z.array(z.number()).min(1).max(MONTHLY_PRORATION_BATCH_SIZE),
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export type MonthlyProrationBatchPayload = {
|
||||
monthKey: string;
|
||||
teamIds: number[];
|
||||
};
|
||||
|
||||
export interface IMonthlyProrationTasker {
|
||||
processBatch(payload: MonthlyProrationBatchPayload): Promise<{ runId: string }>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import process from "node:process";
|
||||
import { syncVercelEnvVars } from "@trigger.dev/build/extensions/core";
|
||||
import { defineConfig } from "@trigger.dev/sdk";
|
||||
import dotEnv from "dotenv";
|
||||
@@ -15,7 +16,7 @@ export default defineConfig({
|
||||
project: process.env.TRIGGER_DEV_PROJECT_REF ?? "", // e.g., "proj_abc123"
|
||||
|
||||
// Directories containing your tasks
|
||||
dirs: ["./bookings/lib/tasker/trigger/notifications"], // Customize based on your project structure
|
||||
dirs: ["./bookings/lib/tasker/trigger/notifications", "./ee/billing/service/proration/tasker/trigger"], // Customize based on your project structure
|
||||
|
||||
// Retry configuration
|
||||
retries: {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { configure } from "@trigger.dev/sdk";
|
||||
|
||||
import process from "node:process";
|
||||
import { configure } from "@trigger.dev/sdk";
|
||||
import { ENABLE_ASYNC_TASKER } from "../constants";
|
||||
import { redactError } from "../redactError";
|
||||
import type { ILogger } from "./types";
|
||||
|
||||
const isAsyncTaskerEnabled =
|
||||
@@ -53,9 +55,13 @@ export abstract class Tasker<T> {
|
||||
const method = this.asyncTasker[taskName] as (...args: any[]) => any;
|
||||
return await method.apply(this.asyncTasker, args);
|
||||
} catch (err) {
|
||||
const taskerLabel = isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker";
|
||||
const baseUrlInfo = isAsyncTaskerEnabled
|
||||
? ` (baseURL: ${process.env.TRIGGER_API_URL ?? "unknown"})`
|
||||
: "";
|
||||
this.logger.error(
|
||||
`${isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker"} failed for '${String(taskName)}'.`,
|
||||
(err as Error)?.message ?? "ERROR MESSAGE UNAVAILABLE"
|
||||
`${taskerLabel} failed for '${String(taskName)}'.${baseUrlInfo}`,
|
||||
this.getErrorDetails(err)
|
||||
);
|
||||
|
||||
if (this.asyncTasker === this.syncTasker) {
|
||||
@@ -68,12 +74,22 @@ export abstract class Tasker<T> {
|
||||
const fallbackMethod = this.syncTasker[taskName] as (...args: any[]) => any;
|
||||
return await fallbackMethod.apply(this.syncTasker, args);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`SyncTasker failed for '${String(taskName)}'.`,
|
||||
(err as Error)?.message ?? "ERROR MESSAGE UNAVAILABLE"
|
||||
);
|
||||
this.logger.error(`SyncTasker failed for '${String(taskName)}'.`, this.getErrorDetails(err));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getErrorDetails(err: unknown) {
|
||||
const redactedError = redactError(err);
|
||||
if (redactedError instanceof Error) {
|
||||
return {
|
||||
name: redactedError.name,
|
||||
message: redactedError.message,
|
||||
stack: redactedError.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return { message: String(redactedError) };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user