test: testing workflow triggers (#12823)

* add workflows to bookingScenario

* activate sandbox mode for unit/integreation tests

* add sendgrid specific code to SendgridProvider

* Refactor WIP

* remove duplicate sendgridProvider file

* first implementation for testing workflows

* revert unintended changes

* comment out Workflow trigger tests

* move sendgrid check after test mode

* Update signup.tsx

* fix esLint

* test webhooks on all tests in fresh-booking.test.ts

* fix subjectPattern as title can be different

* add workflow tests to reschedule.test.ts

* code clean up

* code clean up

* fix sendgrid credentials missing message

* code clean up

---------

Co-authored-by: CarinaWolli <[email protected]>
This commit is contained in:
Carina Wollendorfer
2023-12-21 18:57:06 +05:30
committed by GitHub
co-authored by CarinaWolli
parent a03a1ba34e
commit 076868d243
17 changed files with 453 additions and 158 deletions
@@ -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<AppMeta>[];
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;
}
+35 -2
View File
@@ -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(
@@ -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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
@@ -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: "[email protected]",
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: "[email protected]",
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,
@@ -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: {
@@ -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: "[email protected]",
@@ -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: {
@@ -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: [
{
@@ -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({
@@ -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";
@@ -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;
@@ -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<MailData>, 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 (
@@ -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<MailData>,
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,
});
}
@@ -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";
@@ -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);
@@ -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 {
+1 -1
View File
@@ -6,7 +6,7 @@ declare global {
content: string;
};
to: string;
from: string;
from: string | { email: string; name: string };
subject: string;
html: string;
}[];
+1
View File
@@ -335,6 +335,7 @@
"TELEMETRY_DEBUG",
"TWILIO_MESSAGING_SID",
"TWILIO_PHONE_NUMBER",
"TWILIO_WHATSAPP_PHONE_NUMBER",
"TWILIO_SID",
"TWILIO_TOKEN",
"TWILIO_VERIFY_SID",