feat: Booking EmailAndSms Notifications Tasker (#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>
This commit is contained in:
Morgan
2025-11-28 14:00:04 +00:00
committed by GitHub
co-authored by morgan@cal.com <morgan@cal.com> hbjORbj Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2522638414
commit a050ccb4ee
56 changed files with 2993 additions and 153 deletions
+6
View File
@@ -496,3 +496,9 @@ TZ=UTC
# test oauth client for `packages/platform/examples/base` (optional)
SEED_PLATFORM_OAUTH_CLIENT_ID=
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
# Trigger.dev
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
+3
View File
@@ -105,3 +105,6 @@ packages/**/.yarn/ci-cache/
# AI
.claude
# trigger.dev
.trigger
+8 -1
View File
@@ -41,4 +41,11 @@ LOGGER_BRIDGE_LOG_LEVEL="1"
# 1: Rewrite /api/v2 to /v2
# 0: Don't rewrite
REWRITE_API_V2_PREFIX="1"
REWRITE_API_V2_PREFIX="1"
# Trigger.dev
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
TRIGGER_DEV_PROJECT_REF=
+43 -21
View File
@@ -1,11 +1,12 @@
import { Injectable, Logger as NestLogger, Scope } from "@nestjs/common";
import type { Logger as TsLogger } from "tslog";
// 1. Define an interface for the settings
interface IMyLoggerSettings {
minLevel: number; // Example: 0=debug, 1=info, 2=warn, 3=error
displayTimestamp: boolean;
logFormat: "pretty" | "json" | "simple";
// Add any other settings you need, mimicking tslog or your own requirements
// Add unknown other settings you need, mimicking tslog or your own requirements
// e.g., name?: string; displayFunctionName?: boolean; etc.
}
@@ -23,7 +24,10 @@ const LogLevel = {
* (e.g., sending to Axiom) and adds an optional prefix for context.
*/
@Injectable({ scope: Scope.TRANSIENT }) // TRANSIENT ensures getSubLogger provides truly independent instances if needed elsewhere
export class Logger {
export class Logger
implements
Pick<TsLogger<unknown>, "log" | "silly" | "trace" | "debug" | "info" | "warn" | "error" | "getSubLogger">
{
// Use NestLogger for the actual logging output
private readonly nestLogger = new NestLogger("LoggerBridge");
// Prefix to add to messages for this instance
@@ -57,50 +61,68 @@ export class Logger {
* @param options.prefix - An array of strings to join as the log prefix.
* @returns A new LoggerBridge instance with the specified prefix.
*/
getSubLogger(options: { prefix?: string[] }): Logger {
getSubLogger(options: { prefix?: string[] }): TsLogger<unknown> {
const newLogger = new Logger();
// Set the prefix for the *new* instance
newLogger.prefix = options?.prefix?.join(" ") ?? "";
return newLogger;
return newLogger as unknown as TsLogger<unknown>;
}
// --- Public logging methods ---
info(...args: any[]) {
this.settings.minLevel <= 1 && this.logInternal("log", ...args);
log(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.INFO) this.logInternal("log", ...args);
}
warn(...args: any[]) {
this.settings.minLevel <= 2 && this.logInternal("warn", ...args);
info(...args: unknown[]): undefined {
if (this.settings.minLevel <= 1) {
this.logInternal("log", ...args);
}
}
error(...args: any[]) {
this.settings.minLevel <= 3 && this.logInternal("error", ...args);
warn(...args: unknown[]): undefined {
if (this.settings.minLevel <= 2) {
this.logInternal("warn", ...args);
}
}
debug(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("debug", ...args);
error(...args: unknown[]): undefined {
if (this.settings.minLevel <= 3) {
this.logInternal("error", ...args);
}
}
trace(...args: any[]) {
this.settings.minLevel === 0 && this.logInternal("verbose", ...args);
debug(...args: unknown[]): undefined {
if (this.settings.minLevel === 0) {
this.logInternal("debug", ...args);
}
}
fatal(...args: any[]) {
trace(...args: unknown[]): undefined {
if (this.settings.minLevel === 0) {
this.logInternal("verbose", ...args);
}
}
fatal(...args: unknown[]): undefined {
// Prepend FATAL: to the message and log as error
const fatalMessage = `fatal: ${this.formatArgsAsString(args)}`;
this.settings.minLevel <= 3 && this.logInternal("error", fatalMessage);
if (this.settings.minLevel <= 3) {
this.logInternal("error", fatalMessage);
}
}
silly(...args: any[]) {
silly(...args: unknown[]): undefined {
const sillyMessage = `silly: ${this.formatArgsAsString(args)}`;
this.settings.minLevel === 0 && this.logInternal("verbose", sillyMessage);
if (this.settings.minLevel === 0) {
this.logInternal("verbose", sillyMessage);
}
}
// --- Internal logging implementation ---
/** Formats arguments into a single string */
private formatArgsAsString(args: any[]): string {
private formatArgsAsString(args: unknown[]): string {
return args
.map((arg) => {
if (typeof arg === "string") {
@@ -109,7 +131,7 @@ export class Logger {
// Attempt to stringify non-string arguments
try {
return JSON.stringify(arg);
} catch (e) {
} catch {
return "[Unserializable Object]";
}
})
@@ -117,7 +139,7 @@ export class Logger {
}
/** Central method to forward logs to NestLogger */
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: any[]) {
private logInternal(level: "log" | "warn" | "error" | "debug" | "verbose", ...args: unknown[]) {
try {
// Format message from potentially multiple arguments
const formattedMessage = this.formatArgsAsString(args);
@@ -1,18 +1,23 @@
import { Logger } from "@/lib/logger.bridge";
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { Logger } from "@/lib/logger.bridge";
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { RegularBookingService } from "@/lib/services/regular-booking.service";
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
import { Module, Scope } from "@nestjs/common";
@Module({
imports: [PrismaModule],
@@ -28,14 +33,20 @@ import { Module } from "@nestjs/common";
useFactory: () => {
return new Logger();
},
scope: Scope.TRANSIENT,
},
BookingEventHandlerService,
CheckBookingAndDurationLimitsService,
CheckBookingLimitsService,
HashedLinkService,
LuckyUserService,
BookingEmailSmsService,
BookingEmailAndSmsTaskService,
BookingEmailAndSmsSyncTaskerService,
BookingEmailAndSmsTriggerTaskerService,
BookingEmailAndSmsTasker,
RegularBookingService,
],
exports: [RegularBookingService],
})
export class RegularBookingModule { }
export class RegularBookingModule {}
@@ -0,0 +1,13 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { BookingEmailSmsHandler } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailSmsService extends BookingEmailSmsHandler {
constructor(logger: Logger) {
super({
logger,
});
}
}
@@ -1,9 +1,11 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { HashedLinkService } from "@/lib/services/hashed-link.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@@ -19,6 +21,8 @@ export class RegularBookingService extends BaseRegularBookingService {
hashedLinkService: HashedLinkService,
luckyUserService: LuckyUserService,
userRepository: PrismaUserRepository,
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
featuresRepository: PrismaFeaturesRepository,
bookingEventHandler: BookingEventHandlerService
) {
super({
@@ -28,6 +32,8 @@ export class RegularBookingService extends BaseRegularBookingService {
hashedLinkService,
luckyUserService,
userRepository,
bookingEmailAndSmsTasker,
featuresRepository,
bookingEventHandler,
});
}
@@ -0,0 +1,15 @@
import { Logger } from "@/lib/logger.bridge";
import { BookingEmailAndSmsTaskService } from "@/lib/services/tasker/booking-emails-sms-task.service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsSyncTasker as BaseBookingEmailAndSmsSyncTaskerService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsSyncTaskerService extends BaseBookingEmailAndSmsSyncTaskerService {
constructor(bookingTaskService: BookingEmailAndSmsTaskService, logger: Logger) {
super({
logger,
bookingTaskService,
});
}
}
@@ -0,0 +1,21 @@
import { Logger } from "@/lib/logger.bridge";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { BookingEmailSmsService } from "@/lib/services/booking-emails-sms-service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTaskService as BaseBookingEmailAndSmsTaskService } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTaskService extends BaseBookingEmailAndSmsTaskService {
constructor(
bookingEmailSmsService: BookingEmailSmsService,
bookingRepository: PrismaBookingRepository,
logger: Logger
) {
super({
logger,
emailsAndSmsHandler: bookingEmailSmsService,
bookingRepository,
});
}
}
@@ -0,0 +1,21 @@
import { Logger } from "@/lib/logger.bridge";
import { BookingEmailAndSmsSyncTaskerService } from "@/lib/services/tasker/booking-emails-sms-sync-tasker.service";
import { BookingEmailAndSmsTriggerTaskerService } from "@/lib/services/tasker/booking-emails-sms-trigger-tasker.service";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTasker as BaseBookingEmailAndSmsTasker } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTasker extends BaseBookingEmailAndSmsTasker {
constructor(
syncTasker: BookingEmailAndSmsSyncTaskerService,
asyncTasker: BookingEmailAndSmsTriggerTaskerService,
logger: Logger
) {
super({
logger,
asyncTasker: asyncTasker,
syncTasker: syncTasker,
});
}
}
@@ -0,0 +1,13 @@
import { Logger } from "@/lib/logger.bridge";
import { Injectable } from "@nestjs/common";
import { BookingEmailAndSmsTriggerDevTasker as BaseBookingEmailAndSmsTriggerDevTasker } from "@calcom/platform-libraries/bookings";
@Injectable()
export class BookingEmailAndSmsTriggerTaskerService extends BaseBookingEmailAndSmsTriggerDevTasker {
constructor(logger: Logger) {
super({
logger,
});
}
}
+2
View File
@@ -41,6 +41,8 @@
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
"dev:trigger": "turbo run dev:trigger --filter=\"@calcom/features\"",
"deploy:trigger": "turbo run deploy:trigger --filter=\"@calcom/features\"",
"dev": "turbo run dev --filter=\"@calcom/web\"",
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
"dx": "turbo run dx",
+57 -16
View File
@@ -2,7 +2,8 @@ import type { TFunction } from "i18next";
import { describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { BookingForCalEventBuilder, CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { Person } from "@calcom/types/Calendar";
vi.mock("@calcom/features/ee/organizations/lib/getBookerUrlServer", () => ({
@@ -805,6 +806,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from a basic booking", async () => {
const mockBooking = {
uid: "booking-123",
metadata: null,
title: "Test Booking",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -842,6 +844,7 @@ describe("CalendarEventBuilder", () => {
eventType: {
id: 100,
slug: "test-event",
title: "60 minutes",
description: "Test event description",
hideCalendarNotes: false,
hideCalendarEventDetails: false,
@@ -865,7 +868,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -892,6 +895,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from booking with video call data", async () => {
const mockBooking = {
uid: "booking-456",
metadata: null,
title: "Video Meeting",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -925,6 +929,7 @@ describe("CalendarEventBuilder", () => {
destinationCalendar: null,
eventType: {
id: 200,
title: "60 minutes",
slug: "video-call",
description: "Video call event",
hideCalendarNotes: false,
@@ -950,13 +955,14 @@ describe("CalendarEventBuilder", () => {
references: [
{
type: "zoom_video",
uid: "123423432sdqnwhdh",
meetingId: "zoom-123",
meetingPassword: "password123",
meetingUrl: "https://zoom.us/j/123",
},
],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -976,6 +982,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from booking with team", async () => {
const mockBooking = {
uid: "booking-789",
metadata: null,
title: "Team Meeting",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1020,6 +1027,7 @@ describe("CalendarEventBuilder", () => {
},
destinationCalendar: null,
eventType: {
title: "60 minutes",
id: 300,
slug: "team-event",
description: "Team event",
@@ -1103,7 +1111,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -1123,6 +1131,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from booking with recurring event", async () => {
const mockBooking = {
uid: "booking-recurring",
metadata: null,
title: "Recurring Meeting",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1155,6 +1164,7 @@ describe("CalendarEventBuilder", () => {
},
destinationCalendar: null,
eventType: {
title: "60 minutes",
id: 400,
slug: "recurring-event",
description: "Recurring event",
@@ -1180,7 +1190,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -1197,6 +1207,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from booking with seats", async () => {
const mockBooking = {
uid: "booking-seats",
metadata: null,
title: "Webinar",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1233,6 +1244,7 @@ describe("CalendarEventBuilder", () => {
destinationCalendar: null,
eventType: {
id: 500,
title: "60 minutes",
slug: "webinar",
description: "Webinar event",
hideCalendarNotes: false,
@@ -1267,7 +1279,7 @@ describe("CalendarEventBuilder", () => {
},
},
],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -1284,6 +1296,7 @@ describe("CalendarEventBuilder", () => {
it("should create a calendar event from booking with custom inputs and responses", async () => {
const mockBooking = {
uid: "booking-custom",
metadata: null,
title: "Custom Event",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1321,6 +1334,7 @@ describe("CalendarEventBuilder", () => {
destinationCalendar: null,
eventType: {
id: 600,
title: "60 minutes",
slug: "custom-event",
description: "Custom event type",
hideCalendarNotes: true,
@@ -1352,7 +1366,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtEvent = eventFromBooking.build();
@@ -1375,6 +1389,7 @@ describe("CalendarEventBuilder", () => {
it("should match calendar event built from booking with manually built event", async () => {
const mockBooking = {
uid: "booking-match",
metadata: null,
title: "Match Test",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1411,6 +1426,7 @@ describe("CalendarEventBuilder", () => {
destinationCalendar: null,
eventType: {
id: 700,
title: "60 minutes",
slug: "match-event",
description: "Match event type",
hideCalendarNotes: false,
@@ -1435,7 +1451,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtFromBooking = eventFromBooking.build();
@@ -1448,7 +1464,7 @@ describe("CalendarEventBuilder", () => {
username: "matchhost",
timeZone: "America/New_York",
language: { translate: mockTranslate, locale: "en" },
timeFormat: "h:mma" as const,
timeFormat: TimeFormat["TWENTY_FOUR_HOUR"],
};
const attendeePerson = {
name: "Match User",
@@ -1510,6 +1526,7 @@ describe("CalendarEventBuilder", () => {
it("should throw error when booking is missing user", async () => {
const mockBooking = {
uid: "booking-no-user",
metadata: null,
title: "No User",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1524,6 +1541,7 @@ describe("CalendarEventBuilder", () => {
user: null,
destinationCalendar: null,
eventType: {
title: "60 minutes",
id: 800,
slug: "test",
description: null,
@@ -1549,7 +1567,7 @@ describe("CalendarEventBuilder", () => {
},
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
await expect(CalendarEventBuilder.fromBooking(mockBooking)).rejects.toThrow(
"Booking booking-no-user is missing an organizer"
@@ -1559,6 +1577,7 @@ describe("CalendarEventBuilder", () => {
it("should throw error when booking is missing eventType", async () => {
const mockBooking = {
uid: "booking-no-eventtype",
metadata: null,
title: "No EventType",
startTime: new Date(mockStartTime),
endTime: new Date(mockEndTime),
@@ -1585,7 +1604,7 @@ describe("CalendarEventBuilder", () => {
eventType: null,
references: [],
seatsReferences: [],
};
} satisfies BookingForCalEventBuilder;
await expect(CalendarEventBuilder.fromBooking(mockBooking)).rejects.toThrow(
"Booking booking-no-eventtype is missing eventType"
@@ -1598,6 +1617,7 @@ describe("CalendarEventBuilder", () => {
const mockBooking = {
uid: "complete-booking-uid",
metadata: null,
title: "Complete Team Event",
startTime,
endTime,
@@ -1655,6 +1675,7 @@ describe("CalendarEventBuilder", () => {
eventType: {
id: 1000,
slug: "complete-event",
title: "60 minutes",
description: "Complete event type description",
hideCalendarNotes: true,
hideCalendarEventDetails: true,
@@ -1742,8 +1763,16 @@ describe("CalendarEventBuilder", () => {
workflows: [],
},
references: [
{
type: "google_calendar",
uid: "",
meetingId: "google-meeting-123",
meetingPassword: "google-pass-123",
meetingUrl: "https://google.com/j/123456789",
},
{
type: "zoom_video",
uid: "_e1cj2jap9hll6e319l3jeuii9hn6khi5acpk0gr1dgn66rrd",
meetingId: "zoom-meeting-123",
meetingPassword: "zoom-pass-123",
meetingUrl: "https://zoom.us/j/123456789",
@@ -1760,7 +1789,7 @@ describe("CalendarEventBuilder", () => {
},
},
],
};
} satisfies BookingForCalEventBuilder;
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtFromBooking = eventFromBooking.build();
@@ -1833,10 +1862,22 @@ describe("CalendarEventBuilder", () => {
expect(builtFromBooking.videoCallData?.url).toBe("https://zoom.us/j/123456789");
expect(builtFromBooking.appsStatus).toBeDefined();
expect(builtFromBooking.appsStatus).toHaveLength(1);
expect(builtFromBooking.appsStatus?.[0].type).toBe("zoom_video");
expect(builtFromBooking.appsStatus?.[0].success).toBe(1);
expect(builtFromBooking.appsStatus?.[0].failures).toBe(0);
expect(builtFromBooking.appsStatus).toHaveLength(2);
const zoomVideo = builtFromBooking.appsStatus?.find((app) => app.type === "zoom_video");
expect(zoomVideo).toBeDefined();
if (zoomVideo) {
expect(zoomVideo.type).toBe("zoom_video");
expect(zoomVideo.success).toBe(1);
expect(zoomVideo.failures).toBe(0);
}
const googleCalendar = builtFromBooking.appsStatus?.find((app) => app.type === "google_calendar");
expect(googleCalendar).toBeDefined();
if (googleCalendar) {
expect(googleCalendar.type).toBe("google_calendar");
expect(googleCalendar.success).toBe(0);
expect(googleCalendar.failures).toBe(1);
}
expect(builtFromBooking.responses).toBeDefined();
expect(builtFromBooking.userFieldsResponses).toBeDefined();
+40 -20
View File
@@ -1,21 +1,21 @@
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import type { TFunction } from "i18next";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat, type TimeFormat } from "@calcom/lib/timeFormat";
import type { Attendee, DestinationCalendar, Prisma, User } from "@calcom/prisma/client";
import type { Attendee, BookingSeat, DestinationCalendar, Prisma, User } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import { bookingResponses as bookingResponsesSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent, Person, CalEventResponses, AppsStatus } from "@calcom/types/Calendar";
import type { VideoCallData } from "@calcom/types/VideoApiAdapter";
type BookingForCalEventBuilder = NonNullable<
export type BookingForCalEventBuilder = NonNullable<
Awaited<ReturnType<BookingRepository["getBookingForCalEventBuilder"]>>
>;
type BookingMetaOptions = {
export type BookingMetaOptions = {
conferenceCredentialId?: number;
platformClientId?: string;
platformRescheduleUrl?: string;
@@ -38,7 +38,14 @@ async function _buildPersonFromUser(
} satisfies Person;
}
async function _buildPersonFromAttendee(attendee: Pick<Attendee, "locale" | "name" | "timeZone" | "email">) {
async function _buildPersonFromAttendee(
attendee: Pick<Attendee, "locale" | "name" | "timeZone" | "email" | "phoneNumber"> & {
bookingSeat: Pick<
BookingSeat,
"id" | "referenceUid" | "bookingId" | "metadata" | "data" | "attendeeId"
> | null;
}
) {
const translate = await getTranslation(attendee.locale ?? "en", "common");
return {
@@ -46,6 +53,8 @@ async function _buildPersonFromAttendee(attendee: Pick<Attendee, "locale" | "nam
email: attendee.email,
timeZone: attendee.timeZone,
language: { translate, locale: attendee.locale ?? "en" },
phoneNumber: attendee.phoneNumber,
bookingSeat: attendee.bookingSeat,
} satisfies Person;
}
@@ -110,6 +119,7 @@ export class CalendarEventBuilder {
url: videoRef.meetingUrl,
}
: undefined;
const appsStatus: AppsStatus[] = [];
const organizationId = user.profiles?.[0]?.organizationId ?? null;
const bookerUrl = await getBookerBaseUrl(eventType.team?.parentId ?? organizationId);
@@ -188,19 +198,29 @@ export class CalendarEventBuilder {
}
// Video
if (videoCallData && videoCallData.id && videoCallData.password && videoCallData.url) {
builder
.withVideoCallData({
...videoCallData,
id: videoCallData.id,
password: videoCallData.password,
url: videoCallData.url,
})
.withAppsStatus([
{ appName: videoCallData.type, type: videoCallData.type, success: 1, failures: 0, errors: [] },
]);
} else {
builder.withAppsStatus([]);
if (videoCallData && videoCallData.id && videoCallData.url) {
builder.withVideoCallData({
...videoCallData,
id: videoCallData.id,
password: videoCallData.password ?? "",
url: videoCallData.url,
});
}
references
.filter((r) => r && r.type)
.forEach((ref) => {
appsStatus.push({
appName: ref.type.replace("_", "-"),
type: ref.type,
success: ref.uid ? 1 : 0,
failures: ref.uid ? 0 : 1,
errors: [],
});
});
if (appsStatus.length) {
builder.withAppsStatus(appsStatus);
}
// Team & calendars
@@ -0,0 +1,11 @@
import type { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
import { createContainer } from "@calcom/features/di/di";
import { moduleLoader as BookingEmailSmsHandlerModule } from "./BookingEmailSmsHandler.module";
const container = createContainer();
export function getInstantBookingCreateService(): BookingEmailSmsHandler {
BookingEmailSmsHandlerModule.loadModule(container);
return container.get<BookingEmailSmsHandler>(BookingEmailSmsHandlerModule.token);
}
@@ -0,0 +1,23 @@
import { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { BOOKING_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = BOOKING_DI_TOKENS.BOOKING_EMAIL_SMS_HANDLER;
const moduleToken = BOOKING_DI_TOKENS.BOOKING_EMAIL_SMS_HANDLER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingEmailSmsHandler,
depsMap: {
logger: loggerServiceModule,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -1,14 +1,17 @@
import { moduleLoader as bookingAuditServiceModuleLoader } from "@calcom/features/booking-audit/di/BookingAuditService.module";
import { moduleLoader as bookingEventHandlerModuleLoader } from "@calcom/features/bookings/di/BookingEventHandlerService.module";
import { RegularBookingService } from "@calcom/features/bookings/lib/service/RegularBookingService";
import { bindModuleToClassOnToken, createModule } from "@calcom/features/di/di";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as bookingRepositoryModuleLoader } from "@calcom/features/di/modules/Booking";
import { moduleLoader as checkBookingAndDurationLimitsModuleLoader } from "@calcom/features/di/modules/CheckBookingAndDurationLimits";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/Features";
import { moduleLoader as luckyUserServiceModuleLoader } from "@calcom/features/di/modules/LuckyUser";
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";
import { DI_TOKENS } from "@calcom/features/di/tokens";
import { moduleLoader as hashedLinkServiceModuleLoader } from "@calcom/features/hashedLink/di/HashedLinkService.module";
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
import { moduleLoader as bookingEventHandlerModuleLoader } from "@calcom/features/bookings/di/BookingEventHandlerService.module";
import { moduleLoader as bookingEmailAndSmsTaskerModuleLoader } from "./tasker/BookingEmailAndSmsTasker.module";
const thisModule = createModule();
const token = DI_TOKENS.REGULAR_BOOKING_SERVICE;
@@ -26,6 +29,8 @@ const loadModule = bindModuleToClassOnToken({
luckyUserService: luckyUserServiceModuleLoader,
userRepository: userRepositoryModuleLoader,
hashedLinkService: hashedLinkServiceModuleLoader,
bookingEmailAndSmsTasker: bookingEmailAndSmsTaskerModuleLoader,
featuresRepository: featuresRepositoryModuleLoader,
bookingEventHandler: bookingEventHandlerModuleLoader,
},
});
@@ -33,6 +38,6 @@ const loadModule = bindModuleToClassOnToken({
export const moduleLoader = {
token,
loadModule,
};
} satisfies ModuleLoader;
export type { RegularBookingService };
@@ -0,0 +1,11 @@
import { BookingEmailAndSmsSyncTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsSyncTasker";
import { createContainer } from "@calcom/features/di/di";
import { moduleLoader as BookingEmailAndSmsSyncTaskerModule } from "./BookingEmailAndSmsSyncTasker.module";
const container = createContainer();
export function getBookingEmailAndSmsSyncTasker(): BookingEmailAndSmsSyncTasker {
BookingEmailAndSmsSyncTaskerModule.loadModule(container);
return container.get<BookingEmailAndSmsSyncTasker>(BookingEmailAndSmsSyncTaskerModule.token);
}
@@ -0,0 +1,25 @@
import { BookingEmailAndSmsSyncTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsSyncTasker";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { moduleLoader as BookingEmailAndSmsTaskServiceModuleLoader } from "./BookingEmailAndSmsTaskService.module";
import { BOOKING_EMAIL_SMS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_SYNC_TASKER;
const moduleToken = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_SYNC_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingEmailAndSmsSyncTasker,
depsMap: {
logger: loggerServiceModule,
bookingTaskService: BookingEmailAndSmsTaskServiceModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,11 @@
import { BookingEmailAndSmsTaskService } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTaskService";
import { createContainer } from "@calcom/features/di/di";
import { moduleLoader as BookingEmailAndSmsTaskServiceModule } from "./BookingEmailAndSmsTaskService.module";
const container = createContainer();
export function getBookingEmailAndSmsTaskService(): BookingEmailAndSmsTaskService {
BookingEmailAndSmsTaskServiceModule.loadModule(container);
return container.get<BookingEmailAndSmsTaskService>(BookingEmailAndSmsTaskServiceModule.token);
}
@@ -0,0 +1,27 @@
import { BookingEmailAndSmsTaskService } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTaskService";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as bookingRepositoryModuleLoader } from "@calcom/features/di/modules/Booking";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { moduleLoader as BookingEmailSmsHandlerModuleLoader } from "../BookingEmailSmsHandler.module";
import { BOOKING_EMAIL_SMS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TASK_SERVICE;
const moduleToken = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TASK_SERVICE_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingEmailAndSmsTaskService,
depsMap: {
logger: loggerServiceModule,
bookingRepository: bookingRepositoryModuleLoader,
emailsAndSmsHandler: BookingEmailSmsHandlerModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,11 @@
import { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker";
import { createContainer } from "@calcom/features/di/di";
import { moduleLoader as BookingEmailAndSmsTaskServiceModule } from "./BookingEmailAndSmsTaskService.module";
const container = createContainer();
export function getBookingEmailAndSmsTaskService(): BookingEmailAndSmsTasker {
BookingEmailAndSmsTaskServiceModule.loadModule(container);
return container.get<BookingEmailAndSmsTasker>(BookingEmailAndSmsTaskServiceModule.token);
}
@@ -0,0 +1,27 @@
import { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { moduleLoader as BookingEmailAndSmsSyncTasker } from "./BookingEmailAndSmsSyncTasker.module";
import { moduleLoader as BookingEmailAndSmsTriggerTasker } from "./BookingEmailAndSmsTriggerDevTasker.module";
import { BOOKING_EMAIL_SMS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TASKER;
const moduleToken = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingEmailAndSmsTasker,
depsMap: {
logger: loggerServiceModule,
asyncTasker: BookingEmailAndSmsTriggerTasker,
syncTasker: BookingEmailAndSmsSyncTasker,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,11 @@
import { BookingEmailAndSmsTriggerDevTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTriggerTasker";
import { createContainer } from "@calcom/features/di/di";
import { moduleLoader as BookingEmailAndSmsTriggerDevTaskerModule } from "./BookingEmailAndSmsTriggerDevTasker.module";
const container = createContainer();
export function getBookingEmailAndSmsTriggerDevTasker(): BookingEmailAndSmsTriggerDevTasker {
BookingEmailAndSmsTriggerDevTaskerModule.loadModule(container);
return container.get<BookingEmailAndSmsTriggerDevTasker>(BookingEmailAndSmsTriggerDevTaskerModule.token);
}
@@ -0,0 +1,23 @@
import { BookingEmailAndSmsTriggerDevTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTriggerTasker";
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
import { moduleLoader as loggerServiceModule } from "@calcom/features/di/shared/services/logger.service";
import { BOOKING_EMAIL_SMS_TASKER_DI_TOKENS } from "./tokens";
const thisModule = createModule();
const token = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TRIGGER_TASKER;
const moduleToken = BOOKING_EMAIL_SMS_TASKER_DI_TOKENS.BOOKING_EMAIL_SMS_TRIGGER_TASKER_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingEmailAndSmsTriggerDevTasker,
depsMap: {
logger: loggerServiceModule,
},
});
export const moduleLoader = {
token,
loadModule,
} satisfies ModuleLoader;
@@ -0,0 +1,10 @@
export const BOOKING_EMAIL_SMS_TASKER_DI_TOKENS = {
BOOKING_EMAIL_SMS_SYNC_TASKER: Symbol("BookingEmailAndSmsSyncTasker"),
BOOKING_EMAIL_SMS_SYNC_TASKER_MODULE: Symbol("BookingEmailAndSmsSyncTaskerModule"),
BOOKING_EMAIL_SMS_TASKER: Symbol("BookingEmailAndSmsTasker"),
BOOKING_EMAIL_SMS_TASKER_MODULE: Symbol("BookingEmailAndSmsTaskerModule"),
BOOKING_EMAIL_SMS_TRIGGER_TASKER: Symbol("BookingEmailAndSmsTriggerDevTasker"),
BOOKING_EMAIL_SMS_TRIGGER_TASKER_MODULE: Symbol("BookingEmailAndSmsTriggerDevTaskerModule"),
BOOKING_EMAIL_SMS_TASK_SERVICE: Symbol("BookingEmailAndSmsTaskService"),
BOOKING_EMAIL_SMS_TASK_SERVICE_MODULE: Symbol("BookingEmailAndSmsTaskServiceModule"),
};
+2
View File
@@ -7,6 +7,8 @@ export const BOOKING_DI_TOKENS = {
INSTANT_BOOKING_CREATE_SERVICE_MODULE: Symbol("InstantBookingCreateServiceModule"),
BOOKING_CANCEL_SERVICE: Symbol("BookingCancelService"),
BOOKING_CANCEL_SERVICE_MODULE: Symbol("BookingCancelServiceModule"),
BOOKING_EMAIL_SMS_HANDLER: Symbol("BookingEmailSmsHandler"),
BOOKING_EMAIL_SMS_HANDLER_MODULE: Symbol("BookingEmailSmsHandlerModule"),
BOOKING_EVENT_HANDLER_SERVICE: Symbol("BookingEventHandlerService"),
BOOKING_EVENT_HANDLER_SERVICE_MODULE: Symbol("BookingEventHandlerServiceModule"),
};
@@ -7,23 +7,13 @@ import {
allowDisablingAttendeeConfirmationEmails,
} from "@calcom/ee/workflows/lib/allowDisablingStandardEmails";
import type { Workflow as WorkflowType } from "@calcom/ee/workflows/lib/types";
import {
sendRoundRobinRescheduledEmailsAndSMS,
sendRoundRobinScheduledEmailsAndSMS,
sendRoundRobinCancelledEmailsAndSMS,
sendRescheduledEmailsAndSMS,
sendScheduledEmailsAndSMS,
sendOrganizerRequestEmail,
sendAttendeeRequestEmailAndSMS,
sendAddGuestsEmailsAndSMS,
} from "@calcom/emails/email-manager";
import type { BookingType } from "@calcom/features/bookings/lib/handleNewBooking/originalRescheduledBookingUtils";
import type { EventNameObjectType } from "@calcom/features/eventtypes/lib/eventNaming";
import { getPiiFreeCalendarEvent } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { DestinationCalendar, Prisma, User } from "@calcom/prisma/client";
import type { Prisma, User } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -34,6 +24,11 @@ export const BookingActionMap = {
requested: "BOOKING_REQUESTED",
} as const;
export type BookingActionType =
| (typeof BookingActionMap)["confirmed"]
| (typeof BookingActionMap)["rescheduled"]
| (typeof BookingActionMap)["requested"];
type EmailAndSmsPayload = {
evt: CalendarEvent;
eventType: {
@@ -47,8 +42,7 @@ type RescheduleEmailAndSmsPayload = EmailAndSmsPayload & {
additionalInformation: AdditionalInformation;
additionalNotes: string | null | undefined;
iCalUID: string;
users: (Pick<User, "id" | "name" | "timeZone" | "locale" | "email"> & {
destinationCalendar: DestinationCalendar | null;
users: (Pick<User, "name" | "email"> & {
isFixed?: boolean;
})[];
changedOrganizer?: boolean;
@@ -94,11 +88,11 @@ export type EmailsAndSmsSideEffectsPayload =
| ConfirmedSideEffectsPayload;
export interface IBookingEmailSmsHandler {
logger: Logger<unknown>;
logger: Pick<Logger<unknown>, "warn" | "debug" | "error" | "getSubLogger">;
}
export class BookingEmailSmsHandler {
private readonly log: Logger<unknown>;
private readonly log: Pick<Logger<unknown>, "warn" | "debug" | "error">;
constructor(dependencies: IBookingEmailSmsHandler) {
this.log = dependencies.logger.getSubLogger({ prefix: ["BookingEmailSmsHandler"] });
@@ -130,6 +124,7 @@ export class BookingEmailSmsHandler {
additionalInformation,
} = data;
const { sendRescheduledEmailsAndSMS } = await import("@calcom/emails/email-manager");
await sendRescheduledEmailsAndSMS(
{
...evt,
@@ -229,6 +224,12 @@ export class BookingEmailSmsHandler {
(user) => !user.isFixed && newBookedMembers.some((member) => member.email === user.email)
);
const {
sendRoundRobinRescheduledEmailsAndSMS,
sendRoundRobinScheduledEmailsAndSMS,
sendRoundRobinCancelledEmailsAndSMS,
} = await import("@calcom/emails/email-manager");
try {
await Promise.all([
sendRoundRobinRescheduledEmailsAndSMS(
@@ -284,6 +285,8 @@ export class BookingEmailSmsHandler {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows);
}
const { sendScheduledEmailsAndSMS } = await import("@calcom/emails/email-manager");
try {
await sendScheduledEmailsAndSMS(
{ ...evt, additionalInformation, additionalNotes, customInputs },
@@ -318,6 +321,10 @@ export class BookingEmailSmsHandler {
const eventWithNotes = { ...evt, additionalNotes };
const { sendOrganizerRequestEmail, sendAttendeeRequestEmailAndSMS } = await import(
"@calcom/emails/email-manager"
);
try {
await Promise.all([
sendOrganizerRequestEmail(eventWithNotes, metadata),
@@ -343,6 +350,8 @@ export class BookingEmailSmsHandler {
safeStringify({ calEvent: getPiiFreeCalendarEvent(evt) })
);
const { sendAddGuestsEmailsAndSMS } = await import("@calcom/emails/email-manager");
try {
await sendAddGuestsEmailsAndSMS({
calEvent: evt,
@@ -34,6 +34,7 @@ import { handlePayment } from "@calcom/features/bookings/lib/handlePayment";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService";
import { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker";
import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
@@ -42,6 +43,7 @@ import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/Workf
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
import { getEventName, updateHostInEventName } from "@calcom/features/eventtypes/lib/eventNaming";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { getFullName } from "@calcom/features/form-builder/utils";
import type { HashedLinkService } from "@calcom/features/hashedLink/lib/service/HashedLinkService";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
@@ -59,7 +61,7 @@ import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
import { DEFAULT_GROUP_ID, ENABLE_ASYNC_TASKER } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
@@ -95,7 +97,7 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { BookingRepository } from "../../repositories/BookingRepository";
import { BookingActionMap, BookingEmailSmsHandler } from "../BookingEmailSmsHandler";
import { BookingActionMap, BookingEmailSmsHandler, type BookingActionType } from "../BookingEmailSmsHandler";
import { getAllCredentialsIncludeServiceAccountKey } from "../getAllCredentialsForUsersOnEvent/getAllCredentials";
import { refreshCredentials } from "../getAllCredentialsForUsersOnEvent/refreshCredentials";
import getBookingDataSchema from "../getBookingDataSchema";
@@ -417,6 +419,8 @@ export interface IBookingServiceDependencies {
luckyUserService: LuckyUserService;
userRepository: UserRepository;
hashedLinkService: HashedLinkService;
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker;
featuresRepository: FeaturesRepository;
bookingEventHandler: BookingEventHandlerService;
}
@@ -480,6 +484,7 @@ async function handler(
skipCalendarSyncTaskCreation = false,
traceContext: passedTraceContext,
} = input;
let bookingEmailsAndSmsTaskerAction: BookingActionType = BookingActionMap.requested;
const traceContext = passedTraceContext
? passedTraceContext
@@ -2074,6 +2079,7 @@ async function handler(
changedOrganizer,
},
});
bookingEmailsAndSmsTaskerAction = BookingActionMap.rescheduled;
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
@@ -2199,6 +2205,7 @@ async function handler(
customInputs,
},
});
bookingEmailsAndSmsTaskerAction = BookingActionMap.confirmed;
}
}
}
@@ -2232,6 +2239,7 @@ async function handler(
action: BookingActionMap.requested,
data: { evt, attendees: attendeesList, eventType, additionalNotes },
});
bookingEmailsAndSmsTaskerAction = BookingActionMap.requested;
}
}
@@ -2607,6 +2615,37 @@ async function handler(
},
isTeamEventType,
});
// Unused until we deploy to trigger.dev production
// for now we only enable for cal.com org and we keep our current email system
// cal.com org members will see emails in double while we test
if (ENABLE_ASYNC_TASKER && !noEmail) {
try {
if (orgId) {
const hasTeamFeature = await deps.featuresRepository.checkIfTeamHasFeature(
orgId,
"booking-email-sms-tasker"
);
if (hasTeamFeature) {
await deps.bookingEmailAndSmsTasker.send({
action: bookingEmailsAndSmsTaskerAction,
schedulingType: evtWithMetadata.eventType.schedulingType,
payload: {
bookingId: booking.id,
conferenceCredentialId,
platformClientId,
platformRescheduleUrl,
platformCancelUrl,
platformBookingUrl,
isRescheduledByBooker: reqBody.rescheduledBy === bookerEmail,
},
});
}
}
} catch (err) {
tracingLogger.error("bookingEmailAndSmsTasker error:", err);
}
}
}
// TODO: Refactor better so this booking object is not passed
@@ -0,0 +1,38 @@
import { nanoid } from "nanoid";
import { BookingEmailAndSmsTaskService } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTaskService";
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import { IBookingEmailAndSmsTasker } from "./types";
export interface IBookingSyncTaskerDependencies {
bookingTaskService: BookingEmailAndSmsTaskService;
}
export class BookingEmailAndSmsSyncTasker implements IBookingEmailAndSmsTasker {
constructor(public readonly dependencies: ITaskerDependencies & IBookingSyncTaskerDependencies) {}
async request(payload: Parameters<IBookingEmailAndSmsTasker["request"]>[0]) {
const runId = `sync_${nanoid(10)}`;
await this.dependencies.bookingTaskService.request(payload);
return { runId };
}
async confirm(payload: Parameters<IBookingEmailAndSmsTasker["confirm"]>[0]) {
const runId = `sync_${nanoid(10)}`;
await this.dependencies.bookingTaskService.confirm(payload);
return { runId };
}
async reschedule(payload: Parameters<IBookingEmailAndSmsTasker["reschedule"]>[0]) {
const runId = `sync_${nanoid(10)}`;
await this.dependencies.bookingTaskService.reschedule(payload);
return { runId };
}
async rrReschedule(payload: Parameters<IBookingEmailAndSmsTasker["rrReschedule"]>[0]) {
const runId = `sync_${nanoid(10)}`;
await this.dependencies.bookingTaskService.rrReschedule(payload);
return { runId };
}
}
@@ -0,0 +1,153 @@
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import type { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
import { getOriginalRescheduledBooking } from "@calcom/features/bookings/lib/handleNewBooking/originalRescheduledBookingUtils";
import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import type { EventNameObjectType } from "@calcom/features/eventtypes/lib/eventNaming";
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import { SchedulingType } from "@calcom/prisma/enums";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { JsonObject } from "@calcom/types/Json";
import { BookingEmailAndSmsAsyncTasksPayload, BookingTasks } from "./types";
export interface IBookingTaskServiceDependencies {
emailsAndSmsHandler: BookingEmailSmsHandler;
bookingRepository: BookingRepository;
}
export class BookingEmailAndSmsTaskService implements BookingTasks {
constructor(
public readonly dependencies: { logger: ITaskerDependencies["logger"] } & IBookingTaskServiceDependencies
) {}
private async _getVerifiedBookingData(payload: BookingEmailAndSmsAsyncTasksPayload) {
const { bookingId, ...bookingMeta } = payload;
const { bookingRepository } = this.dependencies;
const booking = await bookingRepository.getBookingForCalEventBuilder(bookingId);
if (!booking) {
throw new Error(`Booking with id '${bookingId}' was not found.`);
}
if (!booking.eventType) {
throw new Error(`EventType of Booking with id '${bookingId}' was not found.`);
}
const calendarEvent = (await CalendarEventBuilder.fromBooking(booking, bookingMeta)).build();
if (!calendarEvent) {
throw new Error(`CalendarEvent could not be built from Booking with id '${bookingId}'.`);
}
return {
booking: booking,
eventType: booking.eventType,
calendarEvent: calendarEvent as NonNullable<CalendarEvent>,
};
}
async request(payload: Parameters<BookingTasks["request"]>[0]) {
const { eventType, calendarEvent } = await this._getVerifiedBookingData(payload);
await this.dependencies.emailsAndSmsHandler.send({
action: "BOOKING_REQUESTED",
data: {
evt: calendarEvent,
attendees: calendarEvent.attendees,
eventType: {
schedulingType: eventType.schedulingType ?? null,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
additionalNotes: calendarEvent.additionalNotes,
},
});
}
async confirm(payload: Parameters<BookingTasks["confirm"]>[0]) {
const { booking, eventType, calendarEvent } = await this._getVerifiedBookingData(payload);
const attendeesEmail = booking.attendees.map((attendee) => attendee.email);
const bookedTeamMembers = eventType.team
? eventType.team.members.filter((teamUser) => attendeesEmail.includes(teamUser.user.email))
: [];
const eventNameObject = {
attendeeName: calendarEvent.organizer.name || "Nameless",
eventType: eventType.title,
eventName: eventType.eventName,
// we send on behalf of team if >1 round robin attendee | collective
teamName:
eventType.schedulingType === "COLLECTIVE" || bookedTeamMembers.length > 1
? eventType.team?.name
: null,
host: calendarEvent.organizer.name || "Nameless",
location: booking.location,
eventDuration: dayjs(booking.endTime).diff(booking.startTime, "minutes"),
bookingFields: eventType.bookingFields as JsonObject,
t: calendarEvent.organizer.language.translate,
} satisfies EventNameObjectType;
const workflows = await getAllWorkflowsFromEventType(
{
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
calendarEvent.organizer.id
);
await this.dependencies.emailsAndSmsHandler.send({
action: "BOOKING_CONFIRMED",
data: {
eventType: {
schedulingType: booking.eventType?.schedulingType ?? null,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
eventNameObject,
workflows: workflows,
evt: calendarEvent,
additionalInformation: calendarEvent.additionalInformation ?? {},
additionalNotes: calendarEvent.additionalNotes,
customInputs: calendarEvent.customInputs,
},
});
}
async reschedule(payload: Parameters<BookingTasks["reschedule"]>[0]) {
const { booking, eventType, calendarEvent } = await this._getVerifiedBookingData(payload);
const originalBookingData = await getOriginalRescheduledBooking(
booking.uid,
!!booking.eventType?.seatsPerTimeSlot
);
const changedOrganizer =
!!originalBookingData &&
(eventType.schedulingType === SchedulingType.ROUND_ROBIN ||
eventType.schedulingType === SchedulingType.COLLECTIVE) &&
originalBookingData.userId !== calendarEvent.organizer.id;
await this.dependencies.emailsAndSmsHandler.send({
action: "BOOKING_RESCHEDULED",
data: {
evt: calendarEvent,
eventType: {
schedulingType: eventType.schedulingType ?? null,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
additionalInformation: calendarEvent.additionalInformation ?? {},
additionalNotes: calendarEvent.additionalNotes,
iCalUID: calendarEvent.iCalUID ?? "",
originalRescheduledBooking: originalBookingData,
rescheduleReason: (calendarEvent?.responses?.["rescheduleReason"]?.value as string) ?? undefined,
users: eventType.hosts.map((h) => h.user) ?? [],
changedOrganizer: changedOrganizer,
isRescheduledByBooker: payload.isRescheduledByBooker ?? false,
},
});
}
async rrReschedule(payload: Parameters<BookingTasks["rrReschedule"]>[0]) {
return this.reschedule(payload);
}
}
@@ -0,0 +1,58 @@
import { BookingActionMap, BookingActionType } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
import { Tasker } from "@calcom/lib/tasker/Tasker";
import type { ILogger } from "@calcom/lib/tasker/types";
import { SchedulingType } from "@calcom/prisma/client";
import { BookingEmailAndSmsSyncTasker } from "./BookingEmailAndSmsSyncTasker";
import { BookingEmailAndSmsTriggerDevTasker } from "./BookingEmailAndSmsTriggerTasker";
import { BookingEmailAndSmsTaskPayload, IBookingEmailAndSmsTasker } from "./types";
export interface IBookingEmailAndSmsTaskerDependencies {
asyncTasker: BookingEmailAndSmsTriggerDevTasker;
syncTasker: BookingEmailAndSmsSyncTasker;
logger: ILogger;
}
export class BookingEmailAndSmsTasker extends Tasker<IBookingEmailAndSmsTasker> {
constructor(public readonly dependencies: IBookingEmailAndSmsTaskerDependencies) {
super(dependencies);
}
public async send(data: {
action: BookingActionType;
schedulingType: SchedulingType | null;
payload: BookingEmailAndSmsTaskPayload;
}): Promise<{ runId: string }> {
const { action, schedulingType, payload } = data;
let taskResponse: {
runId: string;
} = { runId: "task-not-found" };
try {
if (action === BookingActionMap.rescheduled) {
if (schedulingType === "ROUND_ROBIN") {
taskResponse = await this.dispatch("rrReschedule", payload);
} else {
taskResponse = await this.dispatch("reschedule", payload);
}
}
if (action === BookingActionMap.confirmed) {
taskResponse = await this.dispatch("confirm", payload);
}
if (action === BookingActionMap.requested) {
taskResponse = await this.dispatch("request", payload);
}
this.logger.info(`BookingEmailAndSmsTasker send ${action} success:`, taskResponse, {
bookingId: payload.bookingId,
});
} catch {
taskResponse = { runId: "task-failed" };
this.logger.error(`BookingEmailAndSmsTasker send ${action} failed`, taskResponse, {
bookingId: payload.bookingId,
});
}
return taskResponse;
}
}
@@ -0,0 +1,31 @@
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
import { IBookingEmailAndSmsTasker } from "./types";
export class BookingEmailAndSmsTriggerDevTasker implements IBookingEmailAndSmsTasker {
constructor(public readonly dependencies: ITaskerDependencies) {}
async request(payload: Parameters<IBookingEmailAndSmsTasker["request"]>[0]) {
const { request } = await import("./trigger/notifications/request");
const handle = await request.trigger(payload);
return { runId: handle.id };
}
async confirm(payload: Parameters<IBookingEmailAndSmsTasker["confirm"]>[0]) {
const { confirm } = await import("./trigger/notifications/confirm");
const handle = await confirm.trigger(payload);
return { runId: handle.id };
}
async reschedule(payload: Parameters<IBookingEmailAndSmsTasker["reschedule"]>[0]) {
const { reschedule } = await import("./trigger/notifications/reschedule");
const handle = await reschedule.trigger(payload);
return { runId: handle.id };
}
async rrReschedule(payload: Parameters<IBookingEmailAndSmsTasker["rrReschedule"]>[0]) {
const { rrReschedule } = await import("./trigger/notifications/rr-reschedule");
const handle = await rrReschedule.trigger(payload);
return { runId: handle.id };
}
}
@@ -0,0 +1,24 @@
import { type schemaTask } from "@trigger.dev/sdk";
import { queue } from "@trigger.dev/sdk";
type BookingNotificationTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
export const bookingNotificationsQueue = queue({
name: "booking-notifications",
concurrencyLimit: 20,
});
export const bookingNotificationsTaskConfig: BookingNotificationTask = {
queue: bookingNotificationsQueue,
machine: "small-2x",
retry: {
maxAttempts: 3,
factor: 1.8,
minTimeoutInMs: 500,
maxTimeoutInMs: 30_000,
randomize: true,
outOfMemory: {
machine: "medium-1x",
},
},
};
@@ -0,0 +1,27 @@
import { schemaTask } from "@trigger.dev/sdk";
import { bookingNotificationsTaskConfig } from "./config";
import { bookingNotificationTaskSchema } from "./schema";
export const confirm = schemaTask({
id: "booking.send.confirm.notifications",
...bookingNotificationsTaskConfig,
schema: bookingNotificationTaskSchema,
run: async (payload) => {
const { TriggerDevLogger } = await import("@calcom/lib/triggerDevLogger");
const { BookingEmailSmsHandler } = await import("@calcom/features/bookings/lib/BookingEmailSmsHandler");
const { BookingRepository } = await import("@calcom/features/bookings/repositories/BookingRepository");
const { prisma } = await import("@calcom/prisma");
const { BookingEmailAndSmsTaskService } = await import("../../BookingEmailAndSmsTaskService");
const triggerDevLogger = new TriggerDevLogger();
const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: triggerDevLogger });
const bookingRepo = new BookingRepository(prisma);
const bookingTaskService = new BookingEmailAndSmsTaskService({
logger: triggerDevLogger,
bookingRepository: bookingRepo,
emailsAndSmsHandler: emailsAndSmsHandler,
});
await bookingTaskService.confirm(payload);
},
});
@@ -0,0 +1,27 @@
import { schemaTask } from "@trigger.dev/sdk";
import { bookingNotificationsTaskConfig } from "./config";
import { bookingNotificationTaskSchema } from "./schema";
export const request = schemaTask({
id: "booking.send.request.notifications",
schema: bookingNotificationTaskSchema,
...bookingNotificationsTaskConfig,
run: async (payload) => {
const { TriggerDevLogger } = await import("@calcom/lib/triggerDevLogger");
const { BookingEmailSmsHandler } = await import("@calcom/features/bookings/lib/BookingEmailSmsHandler");
const { BookingRepository } = await import("@calcom/features/bookings/repositories/BookingRepository");
const { prisma } = await import("@calcom/prisma");
const { BookingEmailAndSmsTaskService } = await import("../../BookingEmailAndSmsTaskService");
const triggerDevLogger = new TriggerDevLogger();
const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: triggerDevLogger });
const bookingRepo = new BookingRepository(prisma);
const bookingTaskService = new BookingEmailAndSmsTaskService({
logger: triggerDevLogger,
bookingRepository: bookingRepo,
emailsAndSmsHandler: emailsAndSmsHandler,
});
await bookingTaskService.request(payload);
},
});
@@ -0,0 +1,27 @@
import { schemaTask } from "@trigger.dev/sdk";
import { bookingNotificationsTaskConfig } from "./config";
import { bookingNotificationTaskSchema } from "./schema";
export const reschedule = schemaTask({
id: "booking.send.reschedule.notifications",
...bookingNotificationsTaskConfig,
schema: bookingNotificationTaskSchema,
run: async (payload) => {
const { TriggerDevLogger } = await import("@calcom/lib/triggerDevLogger");
const { BookingEmailSmsHandler } = await import("@calcom/features/bookings/lib/BookingEmailSmsHandler");
const { BookingRepository } = await import("@calcom/features/bookings/repositories/BookingRepository");
const { prisma } = await import("@calcom/prisma");
const { BookingEmailAndSmsTaskService } = await import("../../BookingEmailAndSmsTaskService");
const triggerDevLogger = new TriggerDevLogger();
const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: triggerDevLogger });
const bookingRepo = new BookingRepository(prisma);
const bookingTaskService = new BookingEmailAndSmsTaskService({
logger: triggerDevLogger,
bookingRepository: bookingRepo,
emailsAndSmsHandler: emailsAndSmsHandler,
});
await bookingTaskService.reschedule(payload);
},
});
@@ -0,0 +1,27 @@
import { schemaTask } from "@trigger.dev/sdk";
import { bookingNotificationsTaskConfig } from "./config";
import { bookingNotificationTaskSchema } from "./schema";
export const rrReschedule = schemaTask({
id: "booking.send.rr.reschedule.notifications",
...bookingNotificationsTaskConfig,
schema: bookingNotificationTaskSchema,
run: async (payload) => {
const { TriggerDevLogger } = await import("@calcom/lib/triggerDevLogger");
const { BookingEmailSmsHandler } = await import("@calcom/features/bookings/lib/BookingEmailSmsHandler");
const { BookingRepository } = await import("@calcom/features/bookings/repositories/BookingRepository");
const { prisma } = await import("@calcom/prisma");
const { BookingEmailAndSmsTaskService } = await import("../../BookingEmailAndSmsTaskService");
const triggerDevLogger = new TriggerDevLogger();
const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: triggerDevLogger });
const bookingRepo = new BookingRepository(prisma);
const bookingTaskService = new BookingEmailAndSmsTaskService({
logger: triggerDevLogger,
bookingRepository: bookingRepo,
emailsAndSmsHandler: emailsAndSmsHandler,
});
await bookingTaskService.rrReschedule(payload);
},
});
@@ -0,0 +1,10 @@
import { z } from "zod";
export const bookingNotificationTaskSchema = z.object({
bookingId: z.number(),
conferenceCredentialId: z.number().optional(),
platformClientId: z.string().optional(),
platformRescheduleUrl: z.string().optional(),
platformCancelUrl: z.string().optional(),
platformBookingUrl: z.string().optional(),
});
@@ -0,0 +1,33 @@
import { EmailsAndSmsSideEffectsPayload } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
export type BookingEmailAndSmsTaskPayload = {
bookingId: number;
conferenceCredentialId?: number;
platformClientId?: string;
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
isRescheduledByBooker?: boolean;
};
export type BookingEmailAndSmsSyncSendPayload = EmailsAndSmsSideEffectsPayload;
export type BookingEmailAndSmsAsyncTasksPayload = BookingEmailAndSmsTaskPayload;
export interface IBookingEmailAndSmsTasker {
request(payload: BookingEmailAndSmsAsyncTasksPayload): Promise<{ runId: string }>;
confirm(payload: BookingEmailAndSmsAsyncTasksPayload): Promise<{ runId: string }>;
reschedule(payload: BookingEmailAndSmsAsyncTasksPayload): Promise<{ runId: string }>;
rrReschedule(payload: BookingEmailAndSmsAsyncTasksPayload): Promise<{ runId: string }>;
}
type WithVoidReturns<T> = {
// Loop over every key 'K' in the type 'T'
[K in keyof T]: T[K] extends (...args: infer P) => unknown // Check if the property is a function
? // If yes, create a new function with original parameters 'P' and 'void' return
(...args: P) => void
: // If not a function, keep the original type
T[K];
};
export type BookingTasks = WithVoidReturns<IBookingEmailAndSmsTasker>;
@@ -1201,6 +1201,16 @@ export class BookingRepository {
timeZone: true,
locale: true,
phoneNumber: true,
bookingSeat: {
select: {
id: true,
referenceUid: true,
bookingId: true,
attendeeId: true,
data: true,
metadata: true,
},
},
},
},
user: {
@@ -1222,6 +1232,7 @@ export class BookingRepository {
eventType: {
select: {
id: true,
title: true,
slug: true,
description: true,
hideCalendarNotes: true,
@@ -1291,15 +1302,6 @@ export class BookingRepository {
},
},
},
workflows: {
select: {
workflow: {
select: {
id: true,
},
},
},
},
},
},
references: {
@@ -1308,11 +1310,7 @@ export class BookingRepository {
meetingId: true,
meetingPassword: true,
meetingUrl: true,
},
where: {
type: {
endsWith: "_video",
},
uid: true,
},
},
seatsReferences: {
@@ -1,8 +1,11 @@
import { bindModuleToClassOnToken, createModule } from "@calcom/features/di/di";
import { createModule } from "@evyweb/ioctopus";
import { Logger } from "tslog";
import { SHARED_TOKENS } from "../shared.tokens";
import { bindModuleToClassOnToken, type ModuleLoader } from "@calcom/features/di/di";
import { loggerConfig } from "@calcom/lib/logger";
import { SHARED_TOKENS } from "../shared.tokens";
/**
* Minimal logger interface that supports common logging methods.
* This allows both tslog Logger and bridge Logger implementations to be used interchangeably.
@@ -32,7 +35,6 @@ const loadModule = bindModuleToClassOnToken({
});
export const moduleLoader = {
token,
token: SHARED_TOKENS.LOGGER,
loadModule,
};
} satisfies ModuleLoader;
@@ -7,8 +7,6 @@ import {
isWhatsappAction,
isCalAIAction,
} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { sendOrScheduleWorkflowEmails } from "@calcom/features/ee/workflows/lib/reminders/providers/emailProvider";
import * as twilio from "@calcom/features/ee/workflows/lib/reminders/providers/twilioProvider";
import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types";
import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
@@ -22,11 +20,8 @@ import { SchedulingType } from "@calcom/prisma/enums";
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { scheduleAIPhoneCall } from "./aiPhoneCallManager";
import { scheduleEmailReminder } from "./emailReminderManager";
import type { BookingInfo } from "./smsReminderManager";
import { scheduleSMSReminder, type ScheduleTextReminderAction } from "./smsReminderManager";
import { scheduleWhatsappReminder } from "./whatsappReminderManager";
import type { ScheduleTextReminderAction } from "./smsReminderManager";
export type FormSubmissionData = {
responses: FORM_SUBMITTED_WEBHOOK_RESPONSES;
@@ -121,6 +116,7 @@ const processWorkflowStep = async (
};
if (isSMSAction(step.action)) {
const { scheduleSMSReminder } = await import("./smsReminderManager");
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleSMSReminder({
@@ -137,6 +133,7 @@ const processWorkflowStep = async (
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
) {
const { scheduleEmailReminder } = await import("./emailReminderManager");
let sendTo: string[] = [];
switch (step.action) {
@@ -208,6 +205,7 @@ const processWorkflowStep = async (
return;
}
const { scheduleWhatsappReminder } = await import("./whatsappReminderManager");
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder({
@@ -219,6 +217,7 @@ const processWorkflowStep = async (
evt,
});
} else if (isCalAIAction(step.action)) {
const { scheduleAIPhoneCall } = await import("./aiPhoneCallManager");
await scheduleAIPhoneCall({
triggerEvent: workflow.trigger,
timeSpan: {
@@ -318,6 +317,11 @@ const _cancelScheduledMessagesAndScheduleEmails = async ({
userIdsWithNoCredits,
});
const [twilio, { sendOrScheduleWorkflowEmails }] = await Promise.all([
import("./providers/twilioProvider"),
import("./providers/emailProvider"),
]);
await Promise.allSettled(scheduledMessages.map((msg) => twilio.cancelSMS(msg.referenceId ?? "")));
await Promise.allSettled(
+1
View File
@@ -31,6 +31,7 @@ export type AppFlags = {
"onboarding-v3": boolean;
"booker-botid": boolean;
"booking-calendar-view": boolean;
"booking-email-sms-tasker": boolean;
"bookings-v3": boolean;
};
+1
View File
@@ -30,6 +30,7 @@ const initialData: AppFlags = {
"onboarding-v3": false,
"booker-botid": false,
"booking-calendar-view": false,
"booking-email-sms-tasker": false,
"bookings-v3": false,
};
+6 -2
View File
@@ -7,7 +7,9 @@
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"lint": "eslint ."
"lint": "eslint .",
"dev:trigger": "npx trigger.dev@latest dev --analyze",
"deploy:trigger": "npx trigger.dev@latest deploy"
},
"dependencies": {
"@calcom/atoms": "workspace:*",
@@ -18,6 +20,7 @@
"@lexical/react": "^0.9.0",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.10.9",
"@trigger.dev/sdk": "4.1.2",
"@vercel/functions": "^1.4.0",
"akismet-api": "^6.0.0",
"class-variance-authority": "^0.7.1",
@@ -32,6 +35,7 @@
},
"devDependencies": {
"@testing-library/react-hooks": "^8.0.1",
"@types/web-push": "^3.6.3"
"@types/web-push": "^3.6.3",
"trigger.dev": "^4.0.0"
}
}
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig } from "@trigger.dev/sdk";
import dotEnv from "dotenv";
dotEnv.config({ path: "../../.env" });
export default defineConfig({
// Your project ref from the Trigger.dev dashboard
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
// Retry configuration
retries: {
enabledInDev: false, // Enable retries in development
default: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
factor: 2,
randomize: true,
},
},
// Build configuration (optional)
build: {
external: ["@prisma/client", "nodemailer", "jsdom", "playwright-core", "playwright", "chromium-bidi"],
},
// Max duration of a task in seconds
maxDuration: 600,
});
+3
View File
@@ -260,3 +260,6 @@ export const ORG_TRIAL_DAYS = process.env.STRIPE_ORG_TRIAL_DAYS
: null;
export const IS_API_V2_E2E = process.env.IS_E2E === "true";
export const ENABLE_ASYNC_TASKER =
process.env.ENABLE_ASYNC_TASKER === "true" && !process.env.NEXT_PUBLIC_IS_E2E && !IS_API_V2_E2E;
+79
View File
@@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { configure } from "@trigger.dev/sdk";
import { ENABLE_ASYNC_TASKER } from "../constants";
import type { ILogger } from "./types";
const isAsyncTaskerEnabled =
ENABLE_ASYNC_TASKER && process.env.TRIGGER_SECRET_KEY && process.env.TRIGGER_API_URL;
export abstract class Tasker<T> {
protected readonly asyncTasker: T;
protected readonly syncTasker: T;
protected readonly logger: ILogger;
constructor(dependencies: { asyncTasker: T; syncTasker: T; logger: ILogger }) {
this.logger = dependencies.logger;
if (!isAsyncTaskerEnabled) {
if (ENABLE_ASYNC_TASKER && (!process.env.TRIGGER_SECRET_KEY || !process.env.TRIGGER_API_URL)) {
this.logger.info(
"Missing env variables TRIGGER_SECRET_KEY or TRIGGER_API_URL, falling back to Sync tasker."
);
}
}
if (isAsyncTaskerEnabled) {
configure({
accessToken: process.env.TRIGGER_SECRET_KEY,
baseURL: process.env.TRIGGER_API_URL,
});
}
this.asyncTasker = isAsyncTaskerEnabled ? dependencies.asyncTasker : dependencies.syncTasker;
this.syncTasker = dependencies.syncTasker;
}
public async dispatch<K extends keyof T>(
taskName: K,
...args: T[K] extends (...args: any[]) => any ? Parameters<T[K]> : never
): Promise<T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never> {
this.logger.info(`Safely Dispatching task '${String(taskName)}'`, { args });
return this._safeDispatch(taskName, ...args);
}
private async _safeDispatch<K extends keyof T>(
taskName: K,
...args: T[K] extends (...args: any[]) => any ? Parameters<T[K]> : never
): Promise<T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never> {
try {
this.logger.info(
`${isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker"} '${String(taskName)}' dispatched.`
);
const method = this.asyncTasker[taskName] as (...args: any[]) => any;
return await method.apply(this.asyncTasker, args);
} catch (err) {
this.logger.error(
`${isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker"} failed for '${String(taskName)}'.`,
(err as Error)?.message ?? "ERROR MESSAGE UNAVAILABLE"
);
if (this.asyncTasker === this.syncTasker) {
throw err;
}
this.logger.warn(`Trying again with SyncTasker for '${String(taskName)}'.`);
try {
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"
);
throw err;
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { Logger as TsLogger } from "tslog";
// 1. The interface for the logger's configuration settings.
// This is extracted directly from your class.
export interface IMyLoggerSettings {
minLevel: number;
displayTimestamp: boolean;
logFormat: "pretty" | "json" | "simple";
}
// 2. The main interface for the Logger instance.
// This describes the public "shape" of your Logger class.
export type ILogger = Pick<
TsLogger<unknown>,
"log" | "silly" | "trace" | "debug" | "info" | "warn" | "error" | "getSubLogger"
>;
export interface ITaskerDependencies {
logger: ILogger;
}
+95
View File
@@ -0,0 +1,95 @@
import { logger } from "@trigger.dev/sdk";
import type { ISettingsParam, Logger as TsLogger } from "tslog";
interface ITriggerDevLoggerSettings {
minLevel: number; // 0=debug, 1=info, 2=warn, 3=error
}
const LogLevel = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
export class TriggerDevLogger
implements
Pick<TsLogger<unknown>, "log" | "silly" | "trace" | "debug" | "info" | "warn" | "error" | "getSubLogger">
{
private prefix = "";
public settings: ITriggerDevLoggerSettings;
constructor(userSettings?: Partial<ITriggerDevLoggerSettings>) {
this.settings = {
minLevel: LogLevel.INFO,
...userSettings,
};
}
getSubLogger(settings?: ISettingsParam<unknown>): TsLogger<unknown> {
const subLogger = new TriggerDevLogger(this.settings);
subLogger.prefix = settings?.name ?? this.prefix;
return subLogger as unknown as TsLogger<unknown>;
}
log(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.INFO) this.logInternal("info", ...args);
}
info(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.INFO) this.logInternal("info", ...args);
}
debug(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.DEBUG) this.logInternal("debug", ...args);
}
trace(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.DEBUG) this.logInternal("debug", ...args);
}
warn(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.WARN) this.logInternal("warn", ...args);
}
error(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.ERROR) this.logInternal("error", ...args);
}
silly(...args: unknown[]): undefined {
if (this.settings.minLevel <= LogLevel.DEBUG) this.logInternal("debug", ...args);
}
private formatArgsAsString(args: unknown[]): string {
return args
.map((arg) => {
if (typeof arg === "string") return arg;
try {
return JSON.stringify(arg);
} catch {
return "[Unserializable Object]";
}
})
.join(" ");
}
private logInternal(level: "info" | "warn" | "error" | "debug", ...args: unknown[]): void {
const message = this.prefix
? `${this.prefix} ${this.formatArgsAsString(args)}`
: this.formatArgsAsString(args);
switch (level) {
case "info":
logger.info(message);
break;
case "warn":
logger.warn(message);
break;
case "error":
logger.error(message);
break;
case "debug":
logger.debug(message);
break;
}
}
}
+6 -2
View File
@@ -58,7 +58,6 @@ export default defineConfig(({ mode }) => {
entry: [resolve(__dirname, "index.ts")],
name: "CalAtoms",
fileName: "cal-atoms",
},
rollupOptions: {
external: [
@@ -71,7 +70,12 @@ export default defineConfig(({ mode }) => {
"react-dom/client",
"@prisma/client",
"react/jsx-dev-runtime",
, "react-awesome-query-builder", "react-awesome-query-builder", "react-awesome-query-builder"],
,
"react-awesome-query-builder",
"react-awesome-query-builder",
"react-awesome-query-builder",
"@trigger.dev/sdk",
],
output: {
globals: {
react: "React",
+5
View File
@@ -12,3 +12,8 @@ export type {
} from "@calcom/features/bookings/lib/dto/types";
export { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository";
export { addGuestsHandler } from "@calcom/trpc/server/routers/viewer/bookings/addGuests.handler";
export { BookingEmailAndSmsTaskService } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTaskService";
export { BookingEmailAndSmsSyncTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsSyncTasker";
export { BookingEmailAndSmsTriggerDevTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTriggerTasker";
export { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker";
export { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
+1 -1
View File
@@ -49,7 +49,7 @@ export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarMan
export type { BookingCreateBody, BookingResponse } from "@calcom/features/bookings/types";
export { HttpError } from "@calcom/lib/http-error";
export { MINUTES_TO_BOOK } from "@calcom/lib/constants";
export { MINUTES_TO_BOOK, ENABLE_ASYNC_TASKER } from "@calcom/lib/constants";
export { cityTimezonesHandler } from "@calcom/features/cityTimezones/cityTimezonesHandler";
export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler";
+16 -1
View File
@@ -284,6 +284,10 @@
"ENTERPRISE_SLUGS",
"PLATFORM_ENTERPRISE_SLUGS",
"USE_POOL",
"TRIGGER_SECRET_KEY",
"TRIGGER_API_URL",
"TRIGGER_DEV_PROJECT_REF",
"ENABLE_ASYNC_TASKER",
"STRIPE_ORG_PRODUCT_ID",
"GOOGLE_ADS_ENABLED",
"LINKEDIN_ADS_ENABLED",
@@ -381,7 +385,8 @@
"DATABASE_WRITE_URL",
"LOG_LEVEL",
"NEXTAUTH_SECRET",
"USE_POOL"
"USE_POOL",
"LOGGER_BRIDGE_LOG_LEVEL"
]
},
"@calcom/ai#build": {
@@ -453,6 +458,16 @@
"dependsOn": ["//#env-check:common", "//#env-check:app-store"],
"cache": false
},
"dev:trigger": {
"inputs": ["./.env"],
"env": ["TRIGGER_DEV_PROJECT_REF"],
"cache": false
},
"deploy:trigger": {
"inputs": ["./.env"],
"env": ["TRIGGER_DEV_PROJECT_REF"],
"cache": false
},
"dx": {
"dependsOn": ["//#env-check:common", "//#env-check:app-store"],
"cache": false
+1717 -40
View File
File diff suppressed because it is too large Load Diff