diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index d87c665d14..06271d0f8e 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -16,6 +16,7 @@ import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/date-fns"; import type { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; +import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client"; import type { SchedulingType } from "@calcom/prisma/enums"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { AppMeta } from "@calcom/types/App"; @@ -36,6 +37,16 @@ type InputWebhook = { eventTriggers: WebhookTriggerEvents[]; subscriberUrl: string; }; + +type InputWorkflow = { + userId?: number | null; + teamId?: number | null; + name?: string; + activeEventTypeId?: number; + trigger: WorkflowTriggerEvents; + action: WorkflowActions; + template: WorkflowTemplates; +}; /** * Data to be mocked */ @@ -55,6 +66,7 @@ export type ScenarioData = { apps?: Partial[]; bookings?: InputBooking[]; webhooks?: InputWebhook[]; + workflows?: InputWorkflow[]; }; type InputCredential = typeof TestData.credentials.google & { @@ -371,6 +383,43 @@ async function addWebhooks(webhooks: InputWebhook[]) { await addWebhooksToDb(webhooks); } +async function addWorkflowsToDb(workflows: InputWorkflow[]) { + await prismock.$transaction( + workflows.map((workflow) => { + return prismock.workflow.create({ + data: { + userId: workflow.userId, + teamId: workflow.teamId, + trigger: workflow.trigger, + name: workflow.name ? workflow.name : "Test Workflow", + steps: { + create: { + stepNumber: 1, + action: workflow.action, + template: workflow.template, + numberVerificationPending: false, + includeCalendarEvent: false, + }, + }, + activeOn: { + create: workflow.activeEventTypeId ? { eventTypeId: workflow.activeEventTypeId } : undefined, + }, + }, + include: { + activeOn: true, + steps: true, + }, + }); + }) + ); +} + +async function addWorkflows(workflows: InputWorkflow[]) { + log.silly("TestData: Creating Workflows", safeStringify(workflows)); + + await addWorkflowsToDb(workflows); +} + async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma.ScheduleCreateInput[] })[]) { log.silly("TestData: Creating Users", JSON.stringify(users)); await prismock.user.createMany({ @@ -510,6 +559,8 @@ export async function createBookingScenario(data: ScenarioData) { // mockBusyCalendarTimes([]); await addWebhooks(data.webhooks || []); // addPaymentMock(); + await addWorkflows(data.workflows || []); + return { eventTypes, }; @@ -872,6 +923,7 @@ export function getScenarioData( usersApartFromOrganizer = [], apps = [], webhooks, + workflows, bookings, }: // hosts = [], { @@ -880,6 +932,7 @@ export function getScenarioData( apps?: ScenarioData["apps"]; usersApartFromOrganizer?: ScenarioData["users"]; webhooks?: ScenarioData["webhooks"]; + workflows?: ScenarioData["workflows"]; bookings?: ScenarioData["bookings"]; // hosts?: ScenarioData["hosts"]; }, @@ -928,6 +981,7 @@ export function getScenarioData( apps: [...apps], webhooks, bookings: bookings || [], + workflows, } satisfies ScenarioData; } diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index 263b706775..4486b3eff1 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -304,8 +304,41 @@ export function expectWebhookToHaveBeenCalledWith( } } -export function expectWorkflowToBeTriggered() { - // TODO: Implement this. +export function expectWorkflowToBeTriggered({ + emails, + organizer, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string; timeZone: string }; +}) { + const subjectPattern = /^Reminder: /i; + expect(emails.get()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + subject: expect.stringMatching(subjectPattern), + to: organizer.email, + }), + ]) + ); +} + +export function expectWorkflowToBeNotTriggered({ + emails, + organizer, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string; timeZone: string }; +}) { + const subjectPattern = /^Reminder: /i; + + expect(emails.get()).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + subject: expect.stringMatching(subjectPattern), + to: organizer.email, + }), + ]) + ); } export async function expectBookingToBeInDatabase( diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 199eae4436..a34790ccfe 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -51,6 +51,7 @@ import { expectBookingPaymentIntiatedWebhookToHaveBeenFired, expectBrokenIntegrationEmails, expectSuccessfulCalendarEventCreationInCalendar, + expectWorkflowToBeNotTriggered, expectICalUIDAsString, } from "@calcom/web/test/utils/bookingScenario/expects"; import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; @@ -72,7 +73,7 @@ describe("handleNewBooking", () => { 1. Should create a booking in the database 2. Should send emails to the booker as well as organizer 3. Should create a booking in the event's destination calendar - 3. Should trigger BOOKING_CREATED webhook + 4. Should trigger BOOKING_CREATED webhook `, async ({ emails, org }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; @@ -107,6 +108,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -197,7 +207,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "event-type-1@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -263,6 +273,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -348,7 +367,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", // We won't be sending evt.destinationCalendar in this case. @@ -417,6 +436,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -501,7 +529,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -532,7 +560,7 @@ describe("handleNewBooking", () => { test( `an error in creating a calendar event should not stop the booking creation - Current behaviour is wrong as the booking is created but no-one is notified of it`, - async ({}) => { + async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const booker = getBooker({ email: "booker@example.com", @@ -563,6 +591,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -626,7 +663,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); // FIXME: We should send Broken Integration emails on calendar event creation failure // expectCalendarEventCreationFailureEmails({ booker, organizer, emails }); @@ -675,6 +712,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -767,7 +813,8 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); + expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -830,6 +877,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -914,7 +970,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -1312,6 +1368,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1374,7 +1439,7 @@ describe("handleNewBooking", () => { status: BookingStatus.PENDING, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeNotTriggered({ organizer, emails }); expectBookingRequestedEmails({ booker, @@ -1429,6 +1494,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1490,7 +1564,7 @@ describe("handleNewBooking", () => { }), }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeNotTriggered({ organizer, emails }); expectBookingRequestedEmails({ booker, @@ -1544,6 +1618,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1609,7 +1692,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); @@ -1667,6 +1750,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1733,7 +1825,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeNotTriggered({ organizer, emails }); expectBookingRequestedEmails({ booker, organizer, emails }); @@ -1864,6 +1956,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1902,7 +2003,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ organizer, emails }); const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); @@ -1932,6 +2033,8 @@ describe("handleNewBooking", () => { 2. Should send email to the booker for Payment request 3. Should trigger BOOKING_PAYMENT_INITIATED webhook 4. Once payment is successful, should trigger BOOKING_CREATED webhook + 5. Workflow should not trigger before payment is made + 6. Workflow triggers once payment is successful `, async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; @@ -1959,6 +2062,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -2035,7 +2147,8 @@ describe("handleNewBooking", () => { }), }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeNotTriggered({ organizer, emails }); + expectAwaitingPaymentEmails({ organizer, booker, emails }); expectBookingPaymentIntiatedWebhookToHaveBeenFired({ @@ -2058,6 +2171,8 @@ describe("handleNewBooking", () => { status: BookingStatus.ACCEPTED, }); + expectWorkflowToBeTriggered({ organizer, emails }); + expectBookingCreatedWebhookToHaveBeenFired({ booker, organizer, @@ -2106,6 +2221,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -2176,7 +2300,9 @@ describe("handleNewBooking", () => { eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, }); - expectWorkflowToBeTriggered(); + + expectWorkflowToBeNotTriggered({ organizer, emails }); + expectAwaitingPaymentEmails({ organizer, booker, emails }); expectBookingPaymentIntiatedWebhookToHaveBeenFired({ booker, diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index 04f7e72266..393d86ec2c 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -19,7 +19,7 @@ import { } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { - expectWorkflowToBeTriggered, + // expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, expectBookingToBeInDatabase, expectBookingCreatedWebhookToHaveBeenFired, @@ -202,7 +202,7 @@ describe("handleNewBooking", () => { }); } - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulBookingCreationEmails({ booker, @@ -548,7 +548,7 @@ describe("handleNewBooking", () => { }); } - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulBookingCreationEmails({ booker, @@ -763,7 +763,7 @@ describe("handleNewBooking", () => { }); } - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulBookingCreationEmails({ booking: { diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index a88d9e95c0..b5ce57afbb 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -88,6 +88,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -231,8 +240,7 @@ describe("handleNewBooking", () => { ], }, }); - - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ emails, organizer }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -317,6 +325,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -442,7 +459,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ emails, organizer }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -486,7 +503,7 @@ describe("handleNewBooking", () => { test( `an error in updating a calendar event should not stop the rescheduling - Current behaviour is wrong as the booking is resheduled but no-one is notified of it`, - async ({}) => { + async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const booker = getBooker({ email: "booker@example.com", @@ -520,6 +537,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -630,7 +656,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ emails, organizer }); // FIXME: We should send Broken Integration emails on calendar event updation failure // expectBrokenIntegrationEmails({ booker, organizer, emails }); @@ -694,6 +720,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -815,7 +850,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered({emails, organizer}); expectBookingRequestedEmails({ booker, @@ -892,6 +927,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1041,7 +1085,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ emails, organizer }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -1134,6 +1178,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1259,7 +1312,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + //expectWorkflowToBeTriggered({emails, organizer}); expectBookingRequestedEmails({ booker, @@ -1337,6 +1390,15 @@ describe("handleNewBooking", () => { appId: null, }, ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], eventTypes: [ { id: 1, @@ -1497,7 +1559,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered(); + expectWorkflowToBeTriggered({ emails, organizer }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index 89531c5cee..d4b0407f88 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -26,7 +26,7 @@ import { } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { - expectWorkflowToBeTriggered, + // expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, expectBookingToBeInDatabase, expectBookingCreatedWebhookToHaveBeenFired, @@ -198,7 +198,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { destinationCalendars: [ { @@ -513,7 +513,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { destinationCalendars: [ { @@ -818,7 +818,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { destinationCalendars: [ { @@ -1033,7 +1033,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { destinationCalendars: [ { @@ -1260,7 +1260,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered(); + // expectWorkflowToBeTriggered(); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { destinationCalendars: [ { diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 0ad746735e..8dec83f79e 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -1,12 +1,10 @@ /* Schedule any workflow reminder that falls within 72 hours for email */ import client from "@sendgrid/client"; -import sgMail from "@sendgrid/mail"; import type { NextApiRequest, NextApiResponse } from "next"; import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; -import { SENDER_NAME } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; @@ -21,16 +19,11 @@ import { getAllUnscheduledReminders, } from "../lib/getWorkflowReminders"; import { getiCalEventAsString } from "../lib/getiCalEventAsString"; +import { sendSendgridMail } from "../lib/reminders/providers/sendgridProvider"; import type { VariablesType } from "../lib/reminders/templates/customTemplate"; import customTemplate from "../lib/reminders/templates/customTemplate"; import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; -const sendgridAPIKey = process.env.SENDGRID_API_KEY as string; -const senderEmail = process.env.SENDGRID_EMAIL as string; - -sgMail.setApiKey(sendgridAPIKey); -client.setApiKey(sendgridAPIKey); - async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.headers.authorization || req.query.apiKey; if (process.env.CRON_API_KEY !== apiKey) { @@ -43,8 +36,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false; - // delete batch_ids with already past scheduled date from scheduled_sends const remindersToDelete: { referenceId: string | null }[] = await getAllRemindersToDelete(); @@ -240,34 +231,30 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) { sendEmailPromises.push( - sgMail.send({ - to: sendTo, - from: { - email: senderEmail, - name: reminder.workflowStep.sender || SENDER_NAME, + sendSendgridMail( + { + to: sendTo, + subject: emailContent.emailSubject, + html: emailContent.emailBody, + batchId: batchId, + sendAt: dayjs(reminder.scheduledDate).unix(), + replyTo: reminder.booking.user?.email, + attachments: reminder.workflowStep.includeCalendarEvent + ? [ + { + content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString( + "base64" + ), + filename: "event.ics", + type: "text/calendar; method=REQUEST", + disposition: "attachment", + contentId: uuidv4(), + }, + ] + : undefined, }, - subject: emailContent.emailSubject, - html: emailContent.emailBody, - batchId: batchId, - sendAt: dayjs(reminder.scheduledDate).unix(), - replyTo: reminder.booking.user?.email || senderEmail, - mailSettings: { - sandboxMode: { - enable: sandboxMode, - }, - }, - attachments: reminder.workflowStep.includeCalendarEvent - ? [ - { - content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"), - filename: "event.ics", - type: "text/calendar; method=REQUEST", - disposition: "attachment", - contentId: uuidv4(), - }, - ] - : undefined, - }) + { sender: reminder.workflowStep.sender } + ) ); } @@ -319,24 +306,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const batchId = batchIdResponse[1].batch_id; sendEmailPromises.push( - sgMail.send({ - to: sendTo, - from: { - email: senderEmail, - name: reminder.workflowStep?.sender || SENDER_NAME, + sendSendgridMail( + { + to: sendTo, + subject: emailContent.emailSubject, + html: emailContent.emailBody, + batchId: batchId, + sendAt: dayjs(reminder.scheduledDate).unix(), + replyTo: reminder.booking.user?.email, }, - subject: emailContent.emailSubject, - html: emailContent.emailBody, - batchId: batchId, - sendAt: dayjs(reminder.scheduledDate).unix(), - replyTo: reminder.booking.user?.email || senderEmail, - mailSettings: { - sandboxMode: { - enable: sandboxMode, - }, - }, - attachments: undefined, - }) + { sender: reminder.workflowStep?.sender } + ) ); await prisma.workflowReminder.update({ diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index fe88358c64..ef10656b40 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -10,7 +10,7 @@ import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/pri import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { getSenderId } from "../lib/alphanumericSenderIdSupport"; -import * as twilio from "../lib/reminders/smsProviders/twilioProvider"; +import * as twilio from "../lib/reminders/providers/twilioProvider"; import type { VariablesType } from "../lib/reminders/templates/customTemplate"; import customTemplate from "../lib/reminders/templates/customTemplate"; import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate"; diff --git a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts index 8b05d276cc..c4021c4712 100644 --- a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts +++ b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts @@ -8,7 +8,7 @@ import prisma from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums"; import { getWhatsappTemplateFunction } from "../lib/actionHelperFunctions"; -import * as twilio from "../lib/reminders/smsProviders/twilioProvider"; +import * as twilio from "../lib/reminders/providers/twilioProvider"; async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.headers.authorization || req.query.apiKey; diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 3184c7f2f4..e45cdaa920 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -1,6 +1,4 @@ -import client from "@sendgrid/client"; import type { MailData } from "@sendgrid/helpers/classes/mail"; -import sgMail from "@sendgrid/mail"; import { createEvent } from "ics"; import type { ParticipationStatus } from "ics"; import type { DateArray } from "ics"; @@ -9,7 +7,6 @@ import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils"; -import { SENDER_NAME } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; @@ -21,33 +18,13 @@ import { } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; +import { getBatchId, sendSendgridMail } from "./providers/sendgridProvider"; import type { AttendeeInBookingInfo, BookingInfo, timeUnitLowerCase } from "./smsReminderManager"; import type { VariablesType } from "./templates/customTemplate"; import customTemplate from "./templates/customTemplate"; import emailReminderTemplate from "./templates/emailReminderTemplate"; -let sendgridAPIKey, senderEmail: string; - const log = logger.getSubLogger({ prefix: ["[emailReminderManager]"] }); -if (process.env.SENDGRID_API_KEY) { - sendgridAPIKey = process.env.SENDGRID_API_KEY as string; - senderEmail = process.env.SENDGRID_EMAIL as string; - - sgMail.setApiKey(sendgridAPIKey); - client.setApiKey(sendgridAPIKey); -} - -async function getBatchId() { - if (!process.env.SENDGRID_API_KEY) { - console.info("No sendgrid API key provided, returning DUMMY_BATCH_ID"); - return "DUMMY_BATCH_ID"; - } - const batchIdResponse = await client.request({ - url: "/v3/mail/batch", - method: "POST", - }); - return batchIdResponse[1].batch_id as string; -} function getiCalEventAsString(evt: BookingInfo, status?: ParticipationStatus) { const uid = uuidv4(); @@ -149,12 +126,6 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null; } - if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) { - console.error("Sendgrid credentials are missing from the .env file"); - } - - const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false; - let attendeeEmailToBeUsedInMail: string | null = null; let attendeeToBeUsedInMail: AttendeeInBookingInfo | null = null; let name = ""; @@ -258,11 +229,6 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => const batchId = await getBatchId(); function sendEmail(data: Partial, triggerEvent?: WorkflowTriggerEvents) { - if (!process.env.SENDGRID_API_KEY) { - console.info("No sendgrid API key provided, skipping email"); - return Promise.resolve(); - } - const status: ParticipationStatus = triggerEvent === WorkflowTriggerEvents.AFTER_EVENT ? "COMPLETED" @@ -270,34 +236,28 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => ? "DECLINED" : "ACCEPTED"; - return sgMail.send({ - to: data.to, - from: { - email: senderEmail, - name: sender || SENDER_NAME, + return sendSendgridMail( + { + to: data.to, + subject: emailContent.emailSubject, + html: emailContent.emailBody, + batchId, + replyTo: evt.organizer.email, + attachments: includeCalendarEvent + ? [ + { + content: Buffer.from(getiCalEventAsString(evt, status) || "").toString("base64"), + filename: "event.ics", + type: "text/calendar; method=REQUEST", + disposition: "attachment", + contentId: uuidv4(), + }, + ] + : undefined, + sendAt: data.sendAt, }, - subject: emailContent.emailSubject, - html: emailContent.emailBody, - batchId, - replyTo: evt.organizer.email, - mailSettings: { - sandboxMode: { - enable: sandboxMode, - }, - }, - attachments: includeCalendarEvent - ? [ - { - content: Buffer.from(getiCalEventAsString(evt, status) || "").toString("base64"), - filename: "event.ics", - type: "text/calendar; method=REQUEST", - disposition: "attachment", - contentId: uuidv4(), - }, - ] - : undefined, - sendAt: data.sendAt, - }); + { sender } + ); } if ( diff --git a/packages/features/ee/workflows/lib/reminders/providers/sendgridProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/sendgridProvider.ts new file mode 100644 index 0000000000..f4ba166af8 --- /dev/null +++ b/packages/features/ee/workflows/lib/reminders/providers/sendgridProvider.ts @@ -0,0 +1,79 @@ +import client from "@sendgrid/client"; +import type { MailData } from "@sendgrid/helpers/classes/mail"; +import sgMail from "@sendgrid/mail"; + +import { SENDER_NAME } from "@calcom/lib/constants"; +import { setTestEmail } from "@calcom/lib/testEmails"; + +let sendgridAPIKey: string; +let senderEmail: string; + +function assertSendgrid() { + if (process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL) { + sendgridAPIKey = process.env.SENDGRID_API_KEY as string; + senderEmail = process.env.SENDGRID_EMAIL as string; + sgMail.setApiKey(sendgridAPIKey); + client.setApiKey(sendgridAPIKey); + } else { + console.error("Sendgrid credentials are missing from the .env file"); + } +} + +export async function getBatchId() { + assertSendgrid(); + if (!process.env.SENDGRID_API_KEY) { + console.info("No sendgrid API key provided, returning DUMMY_BATCH_ID"); + return "DUMMY_BATCH_ID"; + } + const batchIdResponse = await client.request({ + url: "/v3/mail/batch", + method: "POST", + }); + return batchIdResponse[1].batch_id as string; +} + +export function sendSendgridMail( + mailData: Partial, + addData: { sender?: string | null; includeCalendarEvent?: boolean } +) { + assertSendgrid(); + + const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE; + if (testMode) { + if (!mailData.sendAt) { + setTestEmail({ + to: mailData.to?.toString() || "", + from: { + email: senderEmail, + name: addData.sender || SENDER_NAME, + }, + subject: mailData.subject || "", + html: mailData.html || "", + }); + } + console.log( + "Skipped Sending Email as process.env.NEXT_PUBLIC_IS_E2E or process.env.INTEGRATION_TEST_MODE is set. Emails are available in globalThis.testEmails" + ); + + return new Promise((r) => r("Skipped sendEmail for Unit Tests")); + } + + if (!sendgridAPIKey) { + console.info("No sendgrid API key provided, skipping email"); + return Promise.resolve(); + } + + return sgMail.send({ + to: mailData.to, + from: { + email: senderEmail, + name: addData.sender || SENDER_NAME, + }, + subject: mailData.subject, + html: mailData.html || "", + batchId: mailData.batchId, + replyTo: mailData.replyTo || senderEmail, + attachments: mailData.attachments, + sendAt: mailData.sendAt, + }); +} diff --git a/packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts similarity index 100% rename from packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts rename to packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 71a24f1345..176c8ca8fc 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -11,7 +11,7 @@ import type { CalEventResponses, RecurringEvent } from "@calcom/types/Calendar"; import { getSenderId } from "../alphanumericSenderIdSupport"; import type { ScheduleReminderArgs } from "./emailReminderManager"; -import * as twilio from "./smsProviders/twilioProvider"; +import * as twilio from "./providers/twilioProvider"; import type { VariablesType } from "./templates/customTemplate"; import customTemplate from "./templates/customTemplate"; import smsReminderTemplate from "./templates/smsReminderTemplate"; diff --git a/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts b/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts index 6041747e5d..03c079335b 100644 --- a/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts +++ b/packages/features/ee/workflows/lib/reminders/verifyPhoneNumber.ts @@ -1,6 +1,6 @@ import prisma from "@calcom/prisma"; -import * as twilio from "./smsProviders/twilioProvider"; +import * as twilio from "./providers/twilioProvider"; export const sendVerificationCode = async (phoneNumber: string) => { return twilio.sendVerificationCode(phoneNumber); diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index 07a774aa9b..6652924898 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -8,7 +8,7 @@ import { WorkflowMethods, } from "@calcom/prisma/enums"; -import * as twilio from "./smsProviders/twilioProvider"; +import * as twilio from "./providers/twilioProvider"; import type { ScheduleTextReminderArgs, timeUnitLowerCase } from "./smsReminderManager"; import { deleteScheduledSMSReminder } from "./smsReminderManager"; import { diff --git a/packages/lib/testEmails.ts b/packages/lib/testEmails.ts index f660ec1ca6..a0d6787177 100644 --- a/packages/lib/testEmails.ts +++ b/packages/lib/testEmails.ts @@ -6,7 +6,7 @@ declare global { content: string; }; to: string; - from: string; + from: string | { email: string; name: string }; subject: string; html: string; }[]; diff --git a/turbo.json b/turbo.json index 0443281b82..2a25f41ad3 100644 --- a/turbo.json +++ b/turbo.json @@ -335,6 +335,7 @@ "TELEMETRY_DEBUG", "TWILIO_MESSAGING_SID", "TWILIO_PHONE_NUMBER", + "TWILIO_WHATSAPP_PHONE_NUMBER", "TWILIO_SID", "TWILIO_TOKEN", "TWILIO_VERIFY_SID",