Skip to content

Commit a050ccb

Browse files
ThyMinimalDevhbjORbjdevin-ai-integration[bot]
authored
feat: Booking EmailAndSms Notifications Tasker (calcom#24944)
* wip * wip * feature: Booking Tasker without DI yet * feature: Booking Tasker with DI * fix type check 1 * fix type check 2 * fix * comment booking tasker for now * fix: DI regularBookingService api v2 * fix: convert trigger.dev SDK imports to dynamic imports to fix unit tests The unit tests were failing because BookingEmailAndSmsTriggerTasker.ts had static imports of trigger files that depend on @trigger.dev/sdk. This caused Vitest to try to resolve the SDK at module load time, even though it should be optional. Changed all imports in BookingEmailAndSmsTriggerTasker.ts from static to dynamic (using await import()) so the trigger files are only loaded when the tasker methods are actually called, not at module load time during tests. This fixes the 'Failed to load url @trigger.dev/sdk' errors that were causing 28+ test failures. Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix unit tests * keep inline smsAndEmailHandler.send calls * chore: add team feature flag * add satisfies ModuleLoader * fix type check app flags * move trigger in feature * fix: add trigger.dev prisma generator * fix: email app statuses * fix: CalEvtBuilder unit test * chore: improvements, schema, config, retry * fixup! chore: improvements, schema, config, retry * chore: cleanup code * chore: cleanup code * chore: clean code and give full payload * remove log * add booking notifications queue * add attendee phone number for sms * bump trigger to 4.1.0 * add missing booking seat data in attendee * update config * fix logger regular booking service * fix: prisma as external deps of trigger * fix yarn.lock * revert change to example app booking page * fix: resolve circular dependencies and improve cold start performance in trigger tasks - Convert BookingRepository import to type-only in CalendarEventBuilder.ts to eliminate circular dependency risk - Convert EventNameObjectType, CalendarEvent, and JsonObject imports to type-only in BookingEmailAndSmsTaskService.ts - Use dynamic imports in all trigger notification tasks (confirm, request, reschedule, rr-reschedule) to reduce cold start time - Move heavy imports (BookingEmailSmsHandler, BookingRepository, prisma, TriggerDevLogger, BookingEmailAndSmsTaskService) inside run functions - Eliminates module-level prisma import which violates repo guidelines and adds cold start overhead - Reduces initial module dependency graph by deferring heavy imports (email templates, workflows, large repositories) until task execution Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: improve cold start performance in reminderScheduler with dynamic imports - Remove module-level prisma import (violates 'No prisma outside repositories' guideline) - Use dynamic imports for UserRepository (1,168 lines) - only loaded when needed in EMAIL_ATTENDEE action - Use dynamic imports for twilio provider (386 lines) - only loaded in cancelScheduledMessagesAndScheduleEmails - Use dynamic imports for all manager functions by action type: - scheduleSMSReminder (387 lines) - loaded only for SMS actions - scheduleEmailReminder (459 lines) - loaded only for Email actions - scheduleWhatsappReminder (266 lines) - loaded only for WhatsApp actions - scheduleAIPhoneCall (478 lines) - loaded only for AI phone call actions - Use dynamic imports for sendOrScheduleWorkflowEmails in cancelScheduledMessagesAndScheduleEmails - Significantly reduces cold start time by deferring heavy module loading until execution paths need them - Eliminates module-level prisma import that violated repository pattern guidelines Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: improve cold start performance in BookingEmailSmsHandler with dynamic imports - Remove module-level imports of all email-manager functions (653 LOC + 30+ email templates) - Add dynamic imports in each method (_handleRescheduled, _handleRoundRobinRescheduled, _handleConfirmed, _handleRequested, handleAddGuests) - Defer heavy email-manager loading until method execution - Verified no circular dependencies between email-manager and bookings - Significantly reduces cold start time for RegularBookingService and BookingEmailAndSmsTaskService Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: use dynamic imports * update yarn lock * code review * trigger config project ref in env * update yarn lock * add .env.example trigger variables * add .env.example trigger variables * fix: cleanup error handling and loggin * fix: trigger config from env * fix: small typo fix * fix: ai review comments * fix: ai review comments * ai review * prettier --------- Co-authored-by: hbjORbj <sldisek783@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 2522638 commit a050ccb

56 files changed

Lines changed: 2991 additions & 151 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,9 @@ TZ=UTC
496496
# test oauth client for `packages/platform/examples/base` (optional)
497497
SEED_PLATFORM_OAUTH_CLIENT_ID=
498498
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
499+
500+
# Trigger.dev
501+
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
502+
TRIGGER_SECRET_KEY=
503+
TRIGGER_API_URL=https://api.trigger.dev
504+
TRIGGER_DEV_PROJECT_REF=

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ packages/**/.yarn/ci-cache/
105105

106106
# AI
107107
.claude
108+
109+
# trigger.dev
110+
.trigger

apps/api/v2/.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ LOGGER_BRIDGE_LOG_LEVEL="1"
4141

4242
# 1: Rewrite /api/v2 to /v2
4343
# 0: Don't rewrite
44-
REWRITE_API_V2_PREFIX="1"
44+
REWRITE_API_V2_PREFIX="1"
45+
46+
47+
# Trigger.dev
48+
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
49+
TRIGGER_SECRET_KEY=
50+
TRIGGER_API_URL=https://api.trigger.dev
51+
TRIGGER_DEV_PROJECT_REF=

apps/api/v2/src/lib/logger.bridge.ts

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Injectable, Logger as NestLogger, Scope } from "@nestjs/common";
2+
import type { Logger as TsLogger } from "tslog";
23

34
// 1. Define an interface for the settings
45
interface IMyLoggerSettings {
56
minLevel: number; // Example: 0=debug, 1=info, 2=warn, 3=error
67
displayTimestamp: boolean;
78
logFormat: "pretty" | "json" | "simple";
8-
// Add any other settings you need, mimicking tslog or your own requirements
9+
// Add unknown other settings you need, mimicking tslog or your own requirements
910
// e.g., name?: string; displayFunctionName?: boolean; etc.
1011
}
1112

@@ -23,7 +24,10 @@ const LogLevel = {
2324
* (e.g., sending to Axiom) and adds an optional prefix for context.
2425
*/
2526
@Injectable({ scope: Scope.TRANSIENT }) // TRANSIENT ensures getSubLogger provides truly independent instances if needed elsewhere
26-
export class Logger {
27+
export class Logger
28+
implements
29+
Pick<TsLogger<unknown>, "log" | "silly" | "trace" | "debug" | "info" | "warn" | "error" | "getSubLogger">
30+
{
2731
// Use NestLogger for the actual logging output
2832
private readonly nestLogger = new NestLogger("LoggerBridge");
2933
// Prefix to add to messages for this instance
@@ -57,50 +61,68 @@ export class Logger {
5761
* @param options.prefix - An array of strings to join as the log prefix.
5862
* @returns A new LoggerBridge instance with the specified prefix.
5963
*/
60-
getSubLogger(options: { prefix?: string[] }): Logger {
64+
getSubLogger(options: { prefix?: string[] }): TsLogger<unknown> {
6165
const newLogger = new Logger();
6266
// Set the prefix for the *new* instance
6367
newLogger.prefix = options?.prefix?.join(" ") ?? "";
64-
return newLogger;
68+
return newLogger as unknown as TsLogger<unknown>;
6569
}
6670

6771
// --- Public logging methods ---
6872

69-
info(...args: any[]) {
70-
this.settings.minLevel <= 1 && this.logInternal("log", ...args);
73+
log(...args: unknown[]): undefined {
74+
if (this.settings.minLevel <= LogLevel.INFO) this.logInternal("log", ...args);
7175
}
7276

73-
warn(...args: any[]) {
74-
this.settings.minLevel <= 2 && this.logInternal("warn", ...args);
77+
info(...args: unknown[]): undefined {
78+
if (this.settings.minLevel <= 1) {
79+
this.logInternal("log", ...args);
80+
}
7581
}
7682

77-
error(...args: any[]) {
78-
this.settings.minLevel <= 3 && this.logInternal("error", ...args);
83+
warn(...args: unknown[]): undefined {
84+
if (this.settings.minLevel <= 2) {
85+
this.logInternal("warn", ...args);
86+
}
7987
}
8088

81-
debug(...args: any[]) {
82-
this.settings.minLevel === 0 && this.logInternal("debug", ...args);
89+
error(...args: unknown[]): undefined {
90+
if (this.settings.minLevel <= 3) {
91+
this.logInternal("error", ...args);
92+
}
8393
}
8494

85-
trace(...args: any[]) {
86-
this.settings.minLevel === 0 && this.logInternal("verbose", ...args);
95+
debug(...args: unknown[]): undefined {
96+
if (this.settings.minLevel === 0) {
97+
this.logInternal("debug", ...args);
98+
}
8799
}
88100

89-
fatal(...args: any[]) {
101+
trace(...args: unknown[]): undefined {
102+
if (this.settings.minLevel === 0) {
103+
this.logInternal("verbose", ...args);
104+
}
105+
}
106+
107+
fatal(...args: unknown[]): undefined {
90108
// Prepend FATAL: to the message and log as error
91109
const fatalMessage = `fatal: ${this.formatArgsAsString(args)}`;
92-
this.settings.minLevel <= 3 && this.logInternal("error", fatalMessage);
110+
if (this.settings.minLevel <= 3) {
111+
this.logInternal("error", fatalMessage);
112+
}
93113
}
94114

95-
silly(...args: any[]) {
115+
silly(...args: unknown[]): undefined {
96116
const sillyMessage = `silly: ${this.formatArgsAsString(args)}`;
97-
this.settings.minLevel === 0 && this.logInternal("verbose", sillyMessage);
117+
if (this.settings.minLevel === 0) {
118+
this.logInternal("verbose", sillyMessage);
119+
}
98120
}
99121

100122
// --- Internal logging implementation ---
101123

102124
/** Formats arguments into a single string */
103-
private formatArgsAsString(args: any[]): string {
125+
private formatArgsAsString(args: unknown[]): string {
104126
return args
105127
.map((arg) => {
106128
if (typeof arg === "string") {
@@ -109,15 +131,15 @@ export class Logger {
109131
// Attempt to stringify non-string arguments
110132
try {
111133
return JSON.stringify(arg);
112-
} catch (e) {
134+
} catch {
113135
return "[Unserializable Object]";
114136
}
115137
})
116138
.join(" "); // Use space as separator, adjust if needed
117139
}
118140

119141
/** Central method to forward logs to NestLogger */
120-
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: any[]) {
142+
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: unknown[]) {
121143
try {
122144
// Format message from potentially multiple arguments
123145
const formattedMessage = this.formatArgsAsString(args);

apps/api/v2/src/lib/modules/regular-booking.module.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import { Logger } from "@/lib/logger.bridge";
12
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
23
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
34
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
45
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
56
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
67
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
7-
import { Logger } from "@/lib/logger.bridge";
8+
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
89
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
910
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
1011
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
1112
import { HashedLinkService } from "@/lib/services/hashed-link.service";
1213
import { LuckyUserService } from "@/lib/services/lucky-user.service";
1314
import { RegularBookingService } from "@/lib/services/regular-booking.service";
15+
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
16+
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
17+
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
18+
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
1419
import { PrismaModule } from "@/modules/prisma/prisma.module";
15-
import { Module } from "@nestjs/common";
20+
import { Module, Scope } from "@nestjs/common";
1621

1722
@Module({
1823
imports: [PrismaModule],
@@ -28,14 +33,20 @@ import { Module } from "@nestjs/common";
2833
useFactory: () => {
2934
return new Logger();
3035
},
36+
scope: Scope.TRANSIENT,
3137
},
3238
BookingEventHandlerService,
3339
CheckBookingAndDurationLimitsService,
3440
CheckBookingLimitsService,
3541
HashedLinkService,
3642
LuckyUserService,
43+
BookingEmailSmsService,
44+
BookingEmailAndSmsTaskService,
45+
BookingEmailAndSmsSyncTaskerService,
46+
BookingEmailAndSmsTriggerTaskerService,
47+
BookingEmailAndSmsTasker,
3748
RegularBookingService,
3849
],
3950
exports: [RegularBookingService],
4051
})
41-
export class RegularBookingModule { }
52+
export class RegularBookingModule {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Logger } from "@/lib/logger.bridge";
2+
import { Injectable } from "@nestjs/common";
3+
4+
import { BookingEmailSmsHandler } from "@calcom/platform-libraries/bookings";
5+
6+
@Injectable()
7+
export class BookingEmailSmsService extends BookingEmailSmsHandler {
8+
constructor(logger: Logger) {
9+
super({
10+
logger,
11+
});
12+
}
13+
}

apps/api/v2/src/lib/services/regular-booking.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
2+
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
23
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
34
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
45
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
56
import { HashedLinkService } from "@/lib/services/hashed-link.service";
67
import { LuckyUserService } from "@/lib/services/lucky-user.service";
8+
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
79
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
810
import { Injectable } from "@nestjs/common";
911

@@ -19,6 +21,8 @@ export class RegularBookingService extends BaseRegularBookingService {
1921
hashedLinkService: HashedLinkService,
2022
luckyUserService: LuckyUserService,
2123
userRepository: PrismaUserRepository,
24+
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
25+
featuresRepository: PrismaFeaturesRepository,
2226
bookingEventHandler: BookingEventHandlerService
2327
) {
2428
super({
@@ -28,6 +32,8 @@ export class RegularBookingService extends BaseRegularBookingService {
2832
hashedLinkService,
2933
luckyUserService,
3034
userRepository,
35+
bookingEmailAndSmsTasker,
36+
featuresRepository,
3137
bookingEventHandler,
3238
});
3339
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Logger } from "@/lib/logger.bridge";
2+
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
3+
import { Injectable } from "@nestjs/common";
4+
5+
import { BookingEmailAndSmsSyncTasker as BaseBookingEmailAndSmsSyncTaskerService } from "@calcom/platform-libraries/bookings";
6+
7+
@Injectable()
8+
export class BookingEmailAndSmsSyncTaskerService extends BaseBookingEmailAndSmsSyncTaskerService {
9+
constructor(bookingTaskService: BookingEmailAndSmsTaskService, logger: Logger) {
10+
super({
11+
logger,
12+
bookingTaskService,
13+
});
14+
}
15+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Logger } from "@/lib/logger.bridge";
2+
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
3+
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
4+
import { Injectable } from "@nestjs/common";
5+
6+
import { BookingEmailAndSmsTaskService as BaseBookingEmailAndSmsTaskService } from "@calcom/platform-libraries/bookings";
7+
8+
@Injectable()
9+
export class BookingEmailAndSmsTaskService extends BaseBookingEmailAndSmsTaskService {
10+
constructor(
11+
bookingEmailSmsService: BookingEmailSmsService,
12+
bookingRepository: PrismaBookingRepository,
13+
logger: Logger
14+
) {
15+
super({
16+
logger,
17+
emailsAndSmsHandler: bookingEmailSmsService,
18+
bookingRepository,
19+
});
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Logger } from "@/lib/logger.bridge";
2+
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
3+
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
4+
import { Injectable } from "@nestjs/common";
5+
6+
import { BookingEmailAndSmsTasker as BaseBookingEmailAndSmsTasker } from "@calcom/platform-libraries/bookings";
7+
8+
@Injectable()
9+
export class BookingEmailAndSmsTasker extends BaseBookingEmailAndSmsTasker {
10+
constructor(
11+
syncTasker: BookingEmailAndSmsSyncTaskerService,
12+
asyncTasker: BookingEmailAndSmsTriggerTaskerService,
13+
logger: Logger
14+
) {
15+
super({
16+
logger,
17+
asyncTasker: asyncTasker,
18+
syncTasker: syncTasker,
19+
});
20+
}
21+
}

0 commit comments

Comments
 (0)