feat: mandatory email reminder for attendees with @gmail.com (#12747)

Co-authored-by: Chiranjeev Vishnoi <somu209e@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina Wollendorfer 2023-12-12 21:23:48 -05:00 committed by GitHub
parent 6b26dbc6da
commit 3164cd4ae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 651 additions and 431 deletions

View File

@ -2,6 +2,7 @@ import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@pri
import type { EventManagerUser } from "@calcom/core/EventManager";
import EventManager from "@calcom/core/EventManager";
import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder";
import { sendScheduledEmails } from "@calcom/emails";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
@ -256,27 +257,27 @@ export async function handleConfirmation(args: {
//Workflows - set reminders for confirmed events
try {
for (let index = 0; index < updatedBookings.length; index++) {
if (updatedBookings[index].eventType?.workflows) {
const evtOfBooking = evt;
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
evtOfBooking.uid = updatedBookings[index].uid;
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
const isFirstBooking = index === 0;
await scheduleWorkflowReminders({
workflows: updatedBookings[index]?.eventType?.workflows || [],
smsReminderNumber: updatedBookings[index].smsReminderNumber,
calendarEvent: {
...evtOfBooking,
...{ metadata: { videoCallUrl }, eventType: { slug: eventTypeSlug } },
},
isFirstRecurringEvent: isFirstBooking,
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
eventTypeRequiresConfirmation: true,
});
}
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
const evtOfBooking = { ...evt, metadata: { videoCallUrl }, eventType: { slug: eventTypeSlug } };
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
evtOfBooking.uid = updatedBookings[index].uid;
const isFirstBooking = index === 0;
await scheduleMandatoryReminder(
evtOfBooking,
updatedBookings[index]?.eventType?.workflows || [],
false,
!!updatedBookings[index].eventType?.owner?.hideBranding,
evt.attendeeSeatId
);
await scheduleWorkflowReminders({
workflows: updatedBookings[index]?.eventType?.workflows || [],
smsReminderNumber: updatedBookings[index].smsReminderNumber,
calendarEvent: evtOfBooking,
isFirstRecurringEvent: isFirstBooking,
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
eventTypeRequiresConfirmation: true,
});
}
} catch (error) {
// Silently fail

View File

@ -25,6 +25,7 @@ import { getEventName } from "@calcom/core/event";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder";
import {
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
@ -2718,15 +2719,21 @@ async function handler(
}
const metadataFromEvent = videoCallUrl ? { videoCallUrl } : undefined;
const evtWithMetadata = { ...evt, metadata: metadataFromEvent, eventType: { slug: eventType.slug } };
await scheduleMandatoryReminder(
evtWithMetadata,
eventType.workflows || [],
!isConfirmedByDefault,
!!eventType.owner?.hideBranding,
evt.attendeeSeatId
);
try {
await scheduleWorkflowReminders({
workflows: eventType.workflows,
smsReminderNumber: smsReminderNumber || null,
calendarEvent: {
...evt,
...{ metadata: metadataFromEvent, eventType: { slug: eventType.slug } },
},
calendarEvent: evtWithMetadata,
isNotConfirmed: !isConfirmedByDefault,
isRescheduleEvent: !!rescheduleUid,
isFirstRecurringEvent: true,

View File

@ -6,6 +6,7 @@ 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";
@ -121,100 +122,185 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
for (const reminder of unscheduledReminders) {
if (!reminder.workflowStep || !reminder.booking) {
if (!reminder.booking) {
continue;
}
try {
let sendTo;
if (!reminder.isMandatoryReminder && reminder.workflowStep) {
try {
let sendTo;
switch (reminder.workflowStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = reminder.booking.user?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = reminder.booking.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = reminder.workflowStep.sendTo;
}
switch (reminder.workflowStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = reminder.booking.user?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = reminder.booking.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = reminder.workflowStep.sendTo;
}
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].name
: reminder.booking.user?.name;
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].name
: reminder.booking.user?.name;
const attendeeName =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.user?.name
: reminder.booking.attendees[0].name;
const attendeeName =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.user?.name
: reminder.booking.attendees[0].name;
const timeZone =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].timeZone
: reminder.booking.user?.timeZone;
const timeZone =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].timeZone
: reminder.booking.user?.timeZone;
const locale =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
? reminder.booking.attendees[0].locale
: reminder.booking.user?.locale;
const locale =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
? reminder.booking.attendees[0].locale
: reminder.booking.user?.locale;
let emailContent = {
emailSubject: reminder.workflowStep.emailSubject || "",
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
};
let emailBodyEmpty = false;
if (reminder.workflowStep.reminderBody) {
const { responses } = getCalEventResponses({
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
booking: reminder.booking,
});
const variables: VariablesType = {
eventName: reminder.booking.eventType?.title || "",
organizerName: reminder.booking.user?.name || "",
attendeeName: reminder.booking.attendees[0].name,
attendeeEmail: reminder.booking.attendees[0].email,
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
timeZone: timeZone,
location: reminder.booking.location || "",
additionalNotes: reminder.booking.description,
responses: responses,
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
let emailContent = {
emailSubject: reminder.workflowStep.emailSubject || "",
emailBody: `<body style="white-space: pre-wrap;">${
reminder.workflowStep.reminderBody || ""
}</body>`,
};
const emailLocale = locale || "en";
const emailSubject = customTemplate(
reminder.workflowStep.emailSubject || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).text;
emailContent.emailSubject = emailSubject;
emailContent.emailBody = customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).html;
emailBodyEmpty =
customTemplate(
let emailBodyEmpty = false;
if (reminder.workflowStep.reminderBody) {
const { responses } = getCalEventResponses({
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
booking: reminder.booking,
});
const variables: VariablesType = {
eventName: reminder.booking.eventType?.title || "",
organizerName: reminder.booking.user?.name || "",
attendeeName: reminder.booking.attendees[0].name,
attendeeEmail: reminder.booking.attendees[0].email,
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
timeZone: timeZone,
location: reminder.booking.location || "",
additionalNotes: reminder.booking.description,
responses: responses,
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
};
const emailLocale = locale || "en";
const emailSubject = customTemplate(
reminder.workflowStep.emailSubject || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).text;
emailContent.emailSubject = emailSubject;
emailContent.emailBody = customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
).text.length === 0;
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).html;
emailBodyEmpty =
customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
).text.length === 0;
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
emailContent = emailReminderTemplate(
false,
reminder.workflowStep.action,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
reminder.booking.startTime.toISOString() || "",
reminder.booking.endTime.toISOString() || "",
reminder.booking.eventType?.title || "",
timeZone || "",
attendeeName || "",
name || "",
!!reminder.booking.user?.hideBranding
);
}
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
method: "POST",
});
const batchId = batchIdResponse[1].batch_id;
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
sendEmailPromises.push(
sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: reminder.workflowStep.sender || SENDER_NAME,
},
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,
})
);
}
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: batchId,
},
});
}
} catch (error) {
logger.error(`Error scheduling Email with error ${error}`);
}
} else if (reminder.isMandatoryReminder) {
try {
const sendTo = reminder.booking.attendees[0].email;
const name = reminder.booking.attendees[0].name;
const attendeeName = reminder.booking.user?.name;
const timeZone = reminder.booking.attendees[0].timeZone;
let emailContent = {
emailSubject: "",
emailBody: "",
};
const emailBodyEmpty = false;
emailContent = emailReminderTemplate(
false,
reminder.workflowStep.action,
WorkflowActions.EMAIL_ATTENDEE,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
reminder.booking.startTime.toISOString() || "",
reminder.booking.endTime.toISOString() || "",
@ -224,23 +310,20 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
name || "",
!!reminder.booking.user?.hideBranding
);
}
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
method: "POST",
});
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
method: "POST",
});
const batchId = batchIdResponse[1].batch_id;
const batchId = batchIdResponse[1].batch_id;
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
sendEmailPromises.push(
sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: reminder.workflowStep.sender || "Cal.com",
name: reminder.workflowStep?.sender || SENDER_NAME,
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
@ -252,33 +335,23 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
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,
attachments: undefined,
})
);
}
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: batchId,
},
});
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: batchId,
},
});
}
} catch (error) {
logger.error(`Error scheduling Email with error ${error}`);
}
} catch (error) {
logger.error(`Error scheduling Email with error ${error}`);
}
}

View File

@ -26,7 +26,10 @@ type PartialBooking =
> & { eventType: Partial<EventType> | null } & { user: Partial<User> | null })
| null;
export type PartialWorkflowReminder = Pick<WorkflowReminder, "id" | "scheduledDate"> & {
export type PartialWorkflowReminder = Pick<
WorkflowReminder,
"id" | "isMandatoryReminder" | "scheduledDate"
> & {
booking: PartialBooking | null;
} & { workflowStep: PartialWorkflowStep };
@ -113,6 +116,7 @@ export async function getAllUnscheduledReminders(): Promise<PartialWorkflowRemin
const select: Prisma.WorkflowReminderSelect = {
id: true,
scheduledDate: true,
isMandatoryReminder: true,
workflowStep: {
select: {
action: true,

View File

@ -9,6 +9,7 @@ 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";
@ -94,24 +95,46 @@ type ScheduleEmailReminderAction = Extract<
"EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS"
>;
export const scheduleEmailReminder = async (
evt: BookingInfo,
triggerEvent: WorkflowTriggerEvents,
action: ScheduleEmailReminderAction,
export interface ScheduleReminderArgs {
evt: BookingInfo;
triggerEvent: WorkflowTriggerEvents;
timeSpan: {
time: number | null;
timeUnit: TimeUnit | null;
},
sendTo: MailData["to"],
emailSubject: string,
emailBody: string,
workflowStepId: number,
template: WorkflowTemplates,
sender: string,
hideBranding?: boolean,
seatReferenceUid?: string,
includeCalendarEvent?: boolean
) => {
};
template: WorkflowTemplates;
sender?: string | null;
workflowStepId?: number;
seatReferenceUid?: string;
}
interface scheduleEmailReminderArgs extends ScheduleReminderArgs {
sendTo: MailData["to"];
action: ScheduleEmailReminderAction;
emailSubject?: string;
emailBody?: string;
hideBranding?: boolean;
includeCalendarEvent?: boolean;
isMandatoryReminder?: boolean;
}
export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => {
const {
evt,
triggerEvent,
timeSpan,
template,
sender,
workflowStepId,
seatReferenceUid,
sendTo,
emailSubject = "",
emailBody = "",
hideBranding,
includeCalendarEvent,
isMandatoryReminder,
action,
} = args;
if (action === WorkflowActions.EMAIL_ADDRESS) return;
const { startTime, endTime } = evt;
const uid = evt.uid as string;
@ -251,7 +274,7 @@ export const scheduleEmailReminder = async (
to: data.to,
from: {
email: senderEmail,
name: sender,
name: sender || SENDER_NAME,
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
@ -289,7 +312,7 @@ export const scheduleEmailReminder = async (
// TODO: Maybe don't await for this?
await Promise.all(promises);
} catch (error) {
console.log("Error sending Email");
log.error("Error sending Email");
}
} else if (
(triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
@ -311,32 +334,59 @@ export const scheduleEmailReminder = async (
},
triggerEvent
);
if (!isMandatoryReminder) {
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.EMAIL,
scheduledDate: scheduledDate.toDate(),
scheduled: true,
referenceId: batchId,
seatReferenceId: seatReferenceUid,
},
});
} else {
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
method: WorkflowMethods.EMAIL,
scheduledDate: scheduledDate.toDate(),
scheduled: true,
referenceId: batchId,
seatReferenceId: seatReferenceUid,
isMandatoryReminder: true,
},
});
}
} catch (error) {
log.error(`Error scheduling email with error ${error}`);
}
} else if (scheduledDate.isAfter(currentDate.add(72, "hour"))) {
// Write to DB and send to CRON if scheduled reminder date is past 72 hours
if (!isMandatoryReminder) {
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.EMAIL,
scheduledDate: scheduledDate.toDate(),
scheduled: true,
referenceId: batchId,
scheduled: false,
seatReferenceId: seatReferenceUid,
},
});
} catch (error) {
console.log(`Error scheduling email with error ${error}`);
} else {
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
method: WorkflowMethods.EMAIL,
scheduledDate: scheduledDate.toDate(),
scheduled: false,
seatReferenceId: seatReferenceUid,
isMandatoryReminder: true,
},
});
}
} else if (scheduledDate.isAfter(currentDate.add(72, "hour"))) {
// Write to DB and send to CRON if scheduled reminder date is past 72 hours
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.EMAIL,
scheduledDate: scheduledDate.toDate(),
scheduled: false,
seatReferenceId: seatReferenceUid,
},
});
}
}
};
@ -362,6 +412,6 @@ export const deleteScheduledEmailReminder = async (reminderId: number, reference
},
});
} catch (error) {
console.log(`Error canceling reminder with error ${error}`);
log.error(`Error canceling reminder with error ${error}`);
}
};

View File

@ -1,14 +1,16 @@
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import {
isSMSAction,
isTextMessageToAttendeeAction,
isWhatsappAction,
} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { SENDER_NAME } from "@calcom/lib/constants";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { deleteScheduledEmailReminder, scheduleEmailReminder } from "./emailReminderManager";
import type { ScheduleTextReminderAction } from "./smsReminderManager";
import { deleteScheduledSMSReminder, scheduleSMSReminder } from "./smsReminderManager";
import { deleteScheduledWhatsappReminder, scheduleWhatsappReminder } from "./whatsappReminderManager";
@ -51,26 +53,26 @@ const processWorkflowStep = async (
) => {
if (isTextMessageToAttendeeAction(step.action) && !eventTypeRequiresConfirmation) return;
if (step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER) {
if (isSMSAction(step.action)) {
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleSMSReminder(
await scheduleSMSReminder({
evt,
sendTo,
workflow.trigger,
step.action,
{
reminderPhone: sendTo,
triggerEvent: workflow.trigger,
action: step.action as ScheduleTextReminderAction,
timeSpan: {
time: workflow.time,
timeUnit: workflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
step.sender || SENDER_ID,
workflow.userId,
workflow.teamId,
step.numberVerificationPending,
seatReferenceUid
);
message: step.reminderBody || "",
workflowStepId: step.id,
template: step.template,
sender: step.sender,
userId: workflow.userId,
teamId: workflow.teamId,
isVerificationPending: step.numberVerificationPending,
seatReferenceUid,
});
} else if (step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.EMAIL_HOST) {
let sendTo: string[] = [];
@ -88,43 +90,43 @@ const processWorkflowStep = async (
break;
}
await scheduleEmailReminder(
await scheduleEmailReminder({
evt,
workflow.trigger,
step.action,
{
triggerEvent: workflow.trigger,
action: step.action,
timeSpan: {
time: workflow.time,
timeUnit: workflow.timeUnit,
},
sendTo,
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template,
step.sender || SENDER_NAME,
emailSubject: step.emailSubject || "",
emailBody: step.reminderBody || "",
template: step.template,
sender: step.sender || SENDER_NAME,
workflowStepId: step.id,
hideBranding,
seatReferenceUid,
step.includeCalendarEvent
);
includeCalendarEvent: step.includeCalendarEvent,
});
} else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder(
await scheduleWhatsappReminder({
evt,
sendTo,
workflow.trigger,
step.action,
{
reminderPhone: sendTo,
triggerEvent: workflow.trigger,
action: step.action as ScheduleTextReminderAction,
timeSpan: {
time: workflow.time,
timeUnit: workflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
workflow.userId,
workflow.teamId,
step.numberVerificationPending,
seatReferenceUid
);
message: step.reminderBody || "",
workflowStepId: step.id,
template: step.template,
userId: workflow.userId,
teamId: workflow.teamId,
isVerificationPending: step.numberVerificationPending,
seatReferenceUid,
});
}
};
@ -164,7 +166,6 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
) {
continue;
}
for (const step of workflow.steps) {
await processWorkflowStep(workflow, step, {
calendarEvent: evt,

View File

@ -0,0 +1,72 @@
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import type { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking";
import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import type { BookingInfo } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import type { getDefaultEvent } from "@calcom/lib/defaultEvents";
import logger from "@calcom/lib/logger";
import { WorkflowTriggerEvents, TimeUnit, WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums";
const log = logger.getSubLogger({ prefix: ["[scheduleMandatoryReminder]"] });
export type NewBookingEventType =
| Awaited<ReturnType<typeof getDefaultEvent>>
| Awaited<ReturnType<typeof getEventTypesFromDB>>;
export async function scheduleMandatoryReminder(
evt: BookingInfo,
workflows: (WorkflowsOnEventTypes & {
workflow: Workflow & {
steps: WorkflowStep[];
};
})[],
requiresConfirmation: boolean,
hideBranding: boolean,
seatReferenceUid: string | undefined
) {
try {
const hasExistingWorkflow = workflows.some((workflow) => {
return (
workflow.workflow?.trigger === WorkflowTriggerEvents.BEFORE_EVENT &&
((workflow.workflow.time !== null &&
workflow.workflow.time <= 12 &&
workflow.workflow?.timeUnit === TimeUnit.HOUR) ||
(workflow.workflow.time !== null &&
workflow.workflow.time <= 720 &&
workflow.workflow?.timeUnit === TimeUnit.MINUTE)) &&
workflow.workflow?.steps.some((step) => step?.action === WorkflowActions.EMAIL_ATTENDEE)
);
});
if (
!hasExistingWorkflow &&
evt.attendees.some((attendee) => attendee.email.includes("@gmail.com")) &&
!requiresConfirmation
) {
try {
const filteredAttendees =
evt.attendees?.filter((attendee) => attendee.email.includes("@gmail.com")) || [];
await scheduleEmailReminder({
evt,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
action: WorkflowActions.EMAIL_ATTENDEE,
timeSpan: {
time: 1,
timeUnit: TimeUnit.HOUR,
},
sendTo: filteredAttendees,
template: WorkflowTemplates.REMINDER,
hideBranding,
seatReferenceUid,
includeCalendarEvent: false,
isMandatoryReminder: true,
});
} catch (error) {
log.error("Error while scheduling mandatory reminders", JSON.stringify({ error }));
}
}
} catch (error) {
log.error("Error while scheduling mandatory reminders", JSON.stringify({ error }));
}
}

View File

@ -1,15 +1,16 @@
import dayjs from "@calcom/dayjs";
import { SENDER_ID } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import type { TimeUnit } from "@calcom/prisma/enums";
import { WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums";
import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { CalEventResponses, RecurringEvent } from "@calcom/types/Calendar";
import { getSenderId } from "../alphanumericSenderIdSupport";
import type { ScheduleReminderArgs } from "./emailReminderManager";
import * as twilio from "./smsProviders/twilioProvider";
import type { VariablesType } from "./templates/customTemplate";
import customTemplate from "./templates/customTemplate";
@ -55,33 +56,42 @@ export type BookingInfo = {
metadata?: Prisma.JsonValue;
};
type ScheduleSMSReminderAction = Extract<WorkflowActions, "SMS_ATTENDEE" | "SMS_NUMBER">;
export type ScheduleTextReminderAction = Extract<
WorkflowActions,
"SMS_ATTENDEE" | "SMS_NUMBER" | "WHATSAPP_ATTENDEE" | "WHATSAPP_NUMBER"
>;
export interface ScheduleTextReminderArgs extends ScheduleReminderArgs {
reminderPhone: string | null;
message: string;
action: ScheduleTextReminderAction;
userId?: number | null;
teamId?: number | null;
isVerificationPending?: boolean;
}
export const scheduleSMSReminder = async (
evt: BookingInfo,
reminderPhone: string | null,
triggerEvent: WorkflowTriggerEvents,
action: ScheduleSMSReminderAction,
timeSpan: {
time: number | null;
timeUnit: TimeUnit | null;
},
message: string,
workflowStepId: number,
template: WorkflowTemplates,
sender: string,
userId?: number | null,
teamId?: number | null,
isVerificationPending = false,
seatReferenceUid?: string
) => {
export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => {
const {
evt,
reminderPhone,
triggerEvent,
action,
timeSpan,
message = "",
workflowStepId,
template,
sender,
userId,
teamId,
isVerificationPending = false,
seatReferenceUid,
} = args;
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase;
let scheduledDate = null;
const senderID = getSenderId(reminderPhone, sender);
const senderID = getSenderId(reminderPhone, sender || SENDER_ID);
//SMS_ATTENDEE action does not need to be verified
//isVerificationPending is from all already existing workflows (once they edit their workflow, they will also have to verify the number)
@ -126,7 +136,9 @@ export const scheduleSMSReminder = async (
? attendeeToBeUsedInSMS.language?.locale
: evt.organizer.language.locale;
if (message) {
let smsMessage = message;
if (smsMessage) {
const variables: VariablesType = {
eventName: evt.title,
organizerName: evt.organizer.name,
@ -144,10 +156,10 @@ export const scheduleSMSReminder = async (
cancelLink: `/booking/${evt.uid}?cancel=true`,
rescheduleLink: `/${evt.organizer.username}/${evt.eventType.slug}?rescheduleUid=${evt.uid}`,
};
const customMessage = customTemplate(message, variables, locale, evt.organizer.timeFormat);
message = customMessage.text;
const customMessage = customTemplate(smsMessage, variables, locale, evt.organizer.timeFormat);
smsMessage = customMessage.text;
} else if (template === WorkflowTemplates.REMINDER) {
message =
smsMessage =
smsReminderTemplate(
false,
action,
@ -161,9 +173,9 @@ export const scheduleSMSReminder = async (
}
// Allows debugging generated email content without waiting for sendgrid to send emails
log.debug(`Sending sms for trigger ${triggerEvent}`, message);
log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage);
if (message.length > 0 && reminderPhone && isNumberVerified) {
if (smsMessage.length > 0 && reminderPhone && isNumberVerified) {
//send SMS when event is booked/cancelled/rescheduled
if (
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
@ -171,9 +183,9 @@ export const scheduleSMSReminder = async (
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
await twilio.sendSMS(reminderPhone, message, senderID);
await twilio.sendSMS(reminderPhone, smsMessage, senderID);
} catch (error) {
console.log(`Error sending SMS with error ${error}`);
log.error(`Error sending SMS with error ${error}`);
}
} else if (
(triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
@ -188,7 +200,7 @@ export const scheduleSMSReminder = async (
try {
const scheduledSMS = await twilio.scheduleSMS(
reminderPhone,
message,
smsMessage,
scheduledDate.toDate(),
senderID
);
@ -205,7 +217,7 @@ export const scheduleSMSReminder = async (
},
});
} catch (error) {
console.log(`Error scheduling SMS with error ${error}`);
log.error(`Error scheduling SMS with error ${error}`);
}
} else if (scheduledDate.isAfter(currentDate.add(7, "day"))) {
// Write to DB and send to CRON if scheduled reminder date is past 7 days
@ -236,6 +248,6 @@ export const deleteScheduledSMSReminder = async (reminderId: number, referenceId
},
});
} catch (error) {
console.log(`Error canceling reminder with error ${error}`);
log.error(`Error canceling reminder with error ${error}`);
}
};

View File

@ -1,5 +1,3 @@
import type { TimeUnit } from "@prisma/client";
import dayjs from "@calcom/dayjs";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
@ -11,7 +9,7 @@ import {
} from "@calcom/prisma/enums";
import * as twilio from "./smsProviders/twilioProvider";
import type { BookingInfo, timeUnitLowerCase } from "./smsReminderManager";
import type { ScheduleTextReminderArgs, timeUnitLowerCase } from "./smsReminderManager";
import { deleteScheduledSMSReminder } from "./smsReminderManager";
import {
whatsappEventCancelledTemplate,
@ -22,23 +20,21 @@ import {
const log = logger.getSubLogger({ prefix: ["[whatsappReminderManager]"] });
export const scheduleWhatsappReminder = async (
evt: BookingInfo,
reminderPhone: string | null,
triggerEvent: WorkflowTriggerEvents,
action: WorkflowActions,
timeSpan: {
time: number | null;
timeUnit: TimeUnit | null;
},
message: string,
workflowStepId: number,
template: WorkflowTemplates,
userId?: number | null,
teamId?: number | null,
isVerificationPending = false,
seatReferenceUid?: string
) => {
export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) => {
const {
evt,
reminderPhone,
triggerEvent,
action,
timeSpan,
message = "",
workflowStepId,
template,
userId,
teamId,
isVerificationPending = false,
seatReferenceUid,
} = args;
const { startTime, endTime } = evt;
const uid = evt.uid as string;
const currentDate = dayjs();
@ -72,9 +68,11 @@ export const scheduleWhatsappReminder = async (
const timeZone =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone;
let textMessage = message;
switch (template) {
case WorkflowTemplates.REMINDER:
message =
textMessage =
whatsappReminderTemplate(
false,
action,
@ -87,7 +85,7 @@ export const scheduleWhatsappReminder = async (
) || message;
break;
case WorkflowTemplates.CANCELLED:
message =
textMessage =
whatsappEventCancelledTemplate(
false,
action,
@ -100,7 +98,7 @@ export const scheduleWhatsappReminder = async (
) || message;
break;
case WorkflowTemplates.RESCHEDULED:
message =
textMessage =
whatsappEventRescheduledTemplate(
false,
action,
@ -113,7 +111,7 @@ export const scheduleWhatsappReminder = async (
) || message;
break;
case WorkflowTemplates.COMPLETED:
message =
textMessage =
whatsappEventCompletedTemplate(
false,
action,
@ -126,7 +124,7 @@ export const scheduleWhatsappReminder = async (
) || message;
break;
default:
message =
textMessage =
whatsappReminderTemplate(
false,
action,
@ -140,8 +138,8 @@ export const scheduleWhatsappReminder = async (
}
// Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages
log.debug(`Sending Whatsapp for trigger ${triggerEvent}`, message);
if (message.length > 0 && reminderPhone && isNumberVerified) {
log.debug(`Sending Whatsapp for trigger ${triggerEvent}`, textMessage);
if (textMessage.length > 0 && reminderPhone && isNumberVerified) {
//send WHATSAPP when event is booked/cancelled/rescheduled
if (
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
@ -149,7 +147,7 @@ export const scheduleWhatsappReminder = async (
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
await twilio.sendSMS(reminderPhone, message, "", true);
await twilio.sendSMS(reminderPhone, textMessage, "", true);
} catch (error) {
console.log(`Error sending WHATSAPP with error ${error}`);
}
@ -166,7 +164,7 @@ export const scheduleWhatsappReminder = async (
try {
const scheduledWHATSAPP = await twilio.scheduleSMS(
reminderPhone,
message,
textMessage,
scheduledDate.toDate(),
"",
true

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "WorkflowReminder" ADD COLUMN "isMandatoryReminder" BOOLEAN DEFAULT false;

View File

@ -823,17 +823,18 @@ enum TimeUnit {
}
model WorkflowReminder {
id Int @id @default(autoincrement())
bookingUid String?
booking Booking? @relation(fields: [bookingUid], references: [uid])
method WorkflowMethods
scheduledDate DateTime
referenceId String? @unique
scheduled Boolean
workflowStepId Int?
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
cancelled Boolean?
seatReferenceId String?
id Int @id @default(autoincrement())
bookingUid String?
booking Booking? @relation(fields: [bookingUid], references: [uid])
method WorkflowMethods
scheduledDate DateTime
referenceId String? @unique
scheduled Boolean
workflowStepId Int?
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
cancelled Boolean?
seatReferenceId String?
isMandatoryReminder Boolean? @default(false)
@@index([bookingUid])
@@index([workflowStepId])

View File

@ -10,7 +10,6 @@ import {
deleteScheduledWhatsappReminder,
scheduleWhatsappReminder,
} from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/client";
@ -197,54 +196,54 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
break;
}
await scheduleEmailReminder(
bookingInfo,
eventTypeWorkflow.trigger,
step.action,
{
await scheduleEmailReminder({
evt: bookingInfo,
triggerEvent: eventTypeWorkflow.trigger,
action: step.action,
timeSpan: {
time: eventTypeWorkflow.time,
timeUnit: eventTypeWorkflow.timeUnit,
},
sendTo,
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template,
step.sender || SENDER_NAME
);
emailSubject: step.emailSubject || "",
emailBody: step.reminderBody || "",
template: step.template,
sender: step.sender,
workflowStepId: step.id,
});
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
await scheduleSMSReminder(
bookingInfo,
step.sendTo,
eventTypeWorkflow.trigger,
step.action,
{
await scheduleSMSReminder({
evt: bookingInfo,
reminderPhone: step.sendTo,
triggerEvent: eventTypeWorkflow.trigger,
action: step.action,
timeSpan: {
time: eventTypeWorkflow.time,
timeUnit: eventTypeWorkflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
step.sender || SENDER_ID,
booking.userId,
eventTypeWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: step.id,
template: step.template,
sender: step.sender,
userId: booking.userId,
teamId: eventTypeWorkflow.teamId,
});
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo,
eventTypeWorkflow.trigger,
step.action,
{
await scheduleWhatsappReminder({
evt: bookingInfo,
reminderPhone: step.sendTo,
triggerEvent: eventTypeWorkflow.trigger,
action: step.action,
timeSpan: {
time: eventTypeWorkflow.time,
timeUnit: eventTypeWorkflow.timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
booking.userId,
eventTypeWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: step.id,
template: step.template,
userId: booking.userId,
teamId: eventTypeWorkflow.teamId,
});
}
}
}

View File

@ -17,7 +17,7 @@ import {
deleteScheduledWhatsappReminder,
scheduleWhatsappReminder,
} from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager";
import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { PrismaClient } from "@calcom/prisma";
@ -315,54 +315,54 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
sendTo = step.sendTo || "";*/
}
await scheduleEmailReminder(
bookingInfo,
trigger,
step.action,
{
await scheduleEmailReminder({
evt: bookingInfo,
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
sendTo,
step.emailSubject || "",
step.reminderBody || "",
step.id,
step.template,
step.senderName || SENDER_NAME
);
emailSubject: step.emailSubject || "",
emailBody: step.reminderBody || "",
template: step.template,
sender: step.senderName,
workflowStepId: step.id,
});
} else if (step.action === WorkflowActions.SMS_NUMBER) {
await scheduleSMSReminder(
bookingInfo,
step.sendTo || "",
trigger,
step.action,
{
await scheduleSMSReminder({
evt: bookingInfo,
reminderPhone: step.sendTo || "",
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
step.reminderBody || "",
step.id,
step.template,
step.sender || SENDER_ID,
user.id,
userWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: step.id,
template: step.template,
sender: step.sender,
userId: user.id,
teamId: userWorkflow.teamId,
});
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo || "",
trigger,
step.action,
{
await scheduleWhatsappReminder({
evt: bookingInfo,
reminderPhone: step.sendTo || "",
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
step.reminderBody || "",
step.id || 0,
step.template,
user.id,
userWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: step.id || 0,
template: step.template,
userId: user.id,
teamId: userWorkflow.teamId,
});
}
});
await Promise.all(promiseScheduleReminders);
@ -552,54 +552,54 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
sendTo = newStep.sendTo || "";*/
}
await scheduleEmailReminder(
bookingInfo,
trigger,
newStep.action,
{
await scheduleEmailReminder({
evt: bookingInfo,
triggerEvent: trigger,
action: newStep.action,
timeSpan: {
time,
timeUnit,
},
sendTo,
newStep.emailSubject || "",
newStep.reminderBody || "",
newStep.id,
newStep.template,
newStep.senderName || SENDER_NAME
);
emailSubject: newStep.emailSubject || "",
emailBody: newStep.reminderBody || "",
template: newStep.template,
sender: newStep.senderName,
workflowStepId: newStep.id,
});
} else if (newStep.action === WorkflowActions.SMS_NUMBER) {
await scheduleSMSReminder(
bookingInfo,
newStep.sendTo || "",
trigger,
newStep.action,
{
await scheduleSMSReminder({
evt: bookingInfo,
reminderPhone: newStep.sendTo || "",
triggerEvent: trigger,
action: newStep.action,
timeSpan: {
time,
timeUnit,
},
newStep.reminderBody || "",
newStep.id || 0,
newStep.template,
newStep.sender || SENDER_ID,
user.id,
userWorkflow.teamId
);
message: newStep.reminderBody || "",
workflowStepId: newStep.id || 0,
template: newStep.template,
sender: newStep.sender,
userId: user.id,
teamId: userWorkflow.teamId,
});
} else if (newStep.action === WorkflowActions.WHATSAPP_NUMBER) {
await scheduleWhatsappReminder(
bookingInfo,
newStep.sendTo || "",
trigger,
newStep.action,
{
await scheduleWhatsappReminder({
evt: bookingInfo,
reminderPhone: newStep.sendTo || "",
triggerEvent: trigger,
action: newStep.action,
timeSpan: {
time,
timeUnit,
},
newStep.reminderBody || "",
newStep.id || 0,
newStep.template,
user.id,
userWorkflow.teamId
);
message: newStep.reminderBody || "",
workflowStepId: newStep.id || 0,
template: newStep.template,
userId: user.id,
teamId: userWorkflow.teamId,
});
}
});
await Promise.all(promiseScheduleReminders);
@ -703,54 +703,54 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
sendTo = step.sendTo || "";*/
}
await scheduleEmailReminder(
bookingInfo,
trigger,
step.action,
{
await scheduleEmailReminder({
evt: bookingInfo,
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
sendTo,
step.emailSubject || "",
step.reminderBody || "",
createdStep.id,
step.template,
step.senderName || SENDER_NAME
);
emailSubject: step.emailSubject || "",
emailBody: step.reminderBody || "",
template: step.template,
sender: step.senderName,
workflowStepId: createdStep.id,
});
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
await scheduleSMSReminder(
bookingInfo,
step.sendTo,
trigger,
step.action,
{
await scheduleSMSReminder({
evt: bookingInfo,
reminderPhone: step.sendTo,
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
step.reminderBody || "",
createdStep.id,
step.template,
step.sender || SENDER_ID,
user.id,
userWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: createdStep.id,
template: step.template,
sender: step.sender,
userId: user.id,
teamId: userWorkflow.teamId,
});
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
await scheduleWhatsappReminder(
bookingInfo,
step.sendTo,
trigger,
step.action,
{
await scheduleWhatsappReminder({
evt: bookingInfo,
reminderPhone: step.sendTo,
triggerEvent: trigger,
action: step.action,
timeSpan: {
time,
timeUnit,
},
step.reminderBody || "",
createdStep.id,
step.template,
user.id,
userWorkflow.teamId
);
message: step.reminderBody || "",
workflowStepId: createdStep.id,
template: step.template,
userId: user.id,
teamId: userWorkflow.teamId,
});
}
}
}