diff --git a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts index 89744b284f9512..54cfe2b78ba3b4 100644 --- a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts @@ -404,7 +404,7 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => { invalidWorkflow.steps = [ { stepNumber: 1, - action: "sms_number", + action: "cal_ai_phone_call", recipient: PHONE_NUMBER, template: REMINDER, verifiedPhoneId: verifiedPhoneId, @@ -439,6 +439,18 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => { html: "

Reminder for your event {EVENT_NAME}.

", }, }, + { + stepNumber: 2, + action: "sms_attendee", + recipient: EMAIL, + template: REMINDER, + phoneRequired: false, + sender: "updatedSender", + message: { + subject: "Update Upcoming: {EVENT_NAME}", + text: "Update Reminder for your event {EVENT_NAME}.

", + }, + }, ]; return request(app.getHttpServer()) .post(`${basePath}/routing-form`) @@ -459,12 +471,15 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => { } expect(responseBody.data.trigger.type).toEqual(sampleCreateWorkflowRoutingFormDto.trigger.type); - expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length); + expect(responseBody.data.steps).toHaveLength(validWorkflow.steps.length); expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( "CalcomE2EStep1" ); + expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined(); + expect(responseBody.data.steps.find((step) => step.action === "sms_attendee")).toBeDefined(); + const trigger = sampleCreateWorkflowRoutingFormDto.trigger as OnFormSubmittedTriggerDto; expect(responseBody.data.trigger?.type).toEqual(trigger.type); diff --git a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts b/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts index 7ec8ee909cbf88..75876f620f7eef 100644 --- a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts +++ b/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts @@ -7,8 +7,12 @@ import { EMAIL_ADDRESS, EMAIL_ATTENDEE, FORM_ALLOWED_STEP_ACTIONS, + SMS_NUMBER, WorkflowEmailAddressStepDto, WorkflowEmailAttendeeStepDto, + WorkflowPhoneNumberStepDto, + WorkflowPhoneAttendeeStepDto, + SMS_ATTENDEE, } from "./workflow-step.input"; import { RoutingFormWorkflowTriggerDto, @@ -85,6 +89,8 @@ export class CreateFormWorkflowDto { oneOf: [ { $ref: getSchemaPath(WorkflowEmailAddressStepDto) }, { $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) }, + { $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) }, + { $ref: getSchemaPath(WorkflowPhoneNumberStepDto) }, ], type: "array", }) @@ -99,8 +105,15 @@ export class CreateFormWorkflowDto { subTypes: [ { value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS }, { value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE }, + { value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE }, + { value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER }, ], }, }) - steps!: (WorkflowEmailAddressStepDto | WorkflowEmailAttendeeStepDto)[]; + steps!: ( + | WorkflowEmailAddressStepDto + | WorkflowEmailAttendeeStepDto + | WorkflowPhoneAttendeeStepDto + | WorkflowPhoneNumberStepDto + )[]; } diff --git a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts b/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts index d25295917e8e19..30c4b4d7154e12 100644 --- a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts +++ b/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts @@ -8,6 +8,8 @@ import { EMAIL_ADDRESS, EMAIL_ATTENDEE, FORM_ALLOWED_STEP_ACTIONS, + SMS_ATTENDEE, + SMS_NUMBER, UpdateEmailAddressWorkflowStepDto, UpdateEmailAttendeeWorkflowStepDto, UpdateEmailHostWorkflowStepDto, @@ -75,6 +77,8 @@ export class UpdateFormWorkflowDto { oneOf: [ { $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) }, { $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) }, + { $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) }, + { $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) }, ], type: "array", }) @@ -90,8 +94,15 @@ export class UpdateFormWorkflowDto { subTypes: [ { value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS }, { value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE }, + { value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE }, + { value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER }, ], }, }) - steps?: (UpdateEmailAddressWorkflowStepDto | UpdateEmailAttendeeWorkflowStepDto)[]; + steps?: ( + | UpdateEmailAddressWorkflowStepDto + | UpdateEmailAttendeeWorkflowStepDto + | UpdatePhoneNumberWorkflowStepDto + | UpdatePhoneAttendeeWorkflowStepDto + )[]; } diff --git a/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts b/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts index ac9a51971576b1..d22702d92390ef 100644 --- a/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts +++ b/apps/api/v2/src/modules/workflows/inputs/workflow-step.input.ts @@ -24,7 +24,7 @@ export const STEP_ACTIONS = [ CAL_AI_PHONE_CALL, ] as const; -export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS] as const; +export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS, SMS_ATTENDEE, SMS_NUMBER] as const; export const STEP_ACTIONS_TO_ENUM = { [EMAIL_HOST]: WorkflowActions.EMAIL_HOST, diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 85639b8533fbe6..108c15e2f65101 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1460,6 +1460,7 @@ "email_host_action": "send email to host", "email_attendee_action": "send email to attendees", "email_attendee_action_form": "Send email to submitted email address", + "sms_attendee_action_form": "Send SMS to submitted phone number", "sms_attendee_action": "Send SMS to attendee", "sms_number_action": "send SMS to a specific number", "send_reminder_sms": "Easily send meeting reminders via SMS to your attendees", diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index 96e4c5b223060f..6959bd5f37d265 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -171,11 +171,7 @@ export async function handler(req: NextRequest) { } if (message?.length && message?.length > 0 && sendTo) { - const smsMessageWithoutOptOut = message; - - if (process.env.TWILIO_OPT_OUT_ENABLED === "true") { - message = await WorkflowOptOutService.addOptOutMessage(message, locale || "en"); - } + const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage(message, locale || "en"); const scheduledNotification = await scheduleSmsOrFallbackEmail({ twilioData: { diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index f2662b9cee46ba..afb3902ecf5bf5 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -79,6 +79,8 @@ export default function WorkflowDetailsPage(props: Props) { if (isFormTrigger(form.getValues("trigger"))) { if (option.value === WorkflowActions.EMAIL_ATTENDEE) { label = t("email_attendee_action_form"); + } else if (option.value === WorkflowActions.SMS_ATTENDEE) { + label = t("sms_attendee_action_form"); } } @@ -167,11 +169,11 @@ export default function WorkflowDetailsPage(props: Props) { <>
-
+
+
{t("trigger")}
@@ -208,11 +210,11 @@ export default function WorkflowDetailsPage(props: Props) {
-
+
+
{t("action")}
diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 04620593c9fb12..cc2e0a4826c217 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -380,11 +380,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { }); const hasAiAction = hasCalAIAction(steps); - const hasSMSAction = steps.some((s) => isSMSAction(s.action)); - const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action)); const hasEmailToHostAction = steps.some((s) => s.action === WorkflowActions.EMAIL_HOST); + const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action)); - const disallowFormTriggers = hasAiAction || hasSMSAction || hasEmailToHostAction || hasWhatsappAction; + const disallowFormTriggers = hasAiAction || hasEmailToHostAction || hasWhatsappAction; const filteredTriggerOptions = triggerOptions.filter( (option) => !(isFormTrigger(option.value) && disallowFormTriggers) diff --git a/packages/features/ee/workflows/lib/constants.ts b/packages/features/ee/workflows/lib/constants.ts index a9703887b0b3b7..fb937664a84b1a 100644 --- a/packages/features/ee/workflows/lib/constants.ts +++ b/packages/features/ee/workflows/lib/constants.ts @@ -108,4 +108,6 @@ export const FORM_TRIGGER_WORKFLOW_EVENTS: WorkflowTriggerEvents[] = [ export const ALLOWED_FORM_WORKFLOW_ACTIONS = [ WorkflowActions.EMAIL_ATTENDEE, WorkflowActions.EMAIL_ADDRESS, + WorkflowActions.SMS_ATTENDEE, + WorkflowActions.SMS_NUMBER, ] as const; diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index cb1e4c9eae1637..70f22f1c43794a 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -340,6 +340,7 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e }); }; +// sends all immediately, no scheduling needed const scheduleEmailReminderForForm = async ( args: scheduleEmailReminderArgs & { formData: FormSubmissionData; diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index d1f2609127e705..bc2ecd88eb9e47 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -114,10 +114,6 @@ const processWorkflowStep = async ( }; if (isSMSAction(step.action)) { - if (!evt) { - // SMS action not not yet supported for form triggers - return; - } const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo; await scheduleSMSReminder({ @@ -127,7 +123,7 @@ const processWorkflowStep = async ( message: step.reminderBody || "", sender: step.sender, isVerificationPending: step.numberVerificationPending, - evt, + ...contextData, }); } else if ( step.action === WorkflowActions.EMAIL_ATTENDEE || diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 6a037864f4477f..643cb60f2caa5b 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -4,6 +4,7 @@ import { getSMSMessageWithVariables, shouldUseTwilio, } from "@calcom/ee/workflows/lib/reminders/utils"; +import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation"; import { SENDER_ID } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -24,6 +25,7 @@ import { WorkflowOptOutService } from "../service/workflowOptOutService"; import type { ScheduleReminderArgs } from "./emailReminderManager"; import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher"; import * as twilio from "./providers/twilioProvider"; +import type { FormSubmissionData } from "./reminderScheduler"; import smsReminderTemplate from "./templates/smsReminderTemplate"; export enum timeUnitLowerCase { @@ -76,7 +78,6 @@ export type ScheduleTextReminderAction = Extract< WorkflowActions, "SMS_ATTENDEE" | "SMS_NUMBER" | "WHATSAPP_ATTENDEE" | "WHATSAPP_NUMBER" >; - export type ScheduleTextReminderArgs = ScheduleReminderArgs & { reminderPhone: string | null; message: string; @@ -87,43 +88,38 @@ export type ScheduleTextReminderArgs = ScheduleReminderArgs & { prisma?: PrismaClient; verifiedAt: Date | null; }; -export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs & { evt: BookingInfo }) => { - const { - evt, - reminderPhone, - triggerEvent, - action, - timeSpan, - message = "", - workflowStepId, - template, - sender, - userId, - teamId, - isVerificationPending = false, - seatReferenceUid, - verifiedAt, - } = args; +export type ScheduleTextReminderArgsWithRequiredFields = Omit< + ScheduleTextReminderArgs, + "reminderPhone" | "sender" +> & { + reminderPhone: string; // Required, not nullable + sender: string; // Required, not nullable +}; + +export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => { + const { reminderPhone, sender, verifiedAt, workflowStepId, action, userId, teamId, isVerificationPending } = + args; if (!verifiedAt) { log.warn(`Workflow step ${workflowStepId} not yet verified`); return; } - if (reminderPhone && (await WorkflowOptOutContactRepository.isOptedOut(reminderPhone))) { - log.warn( - `Phone number opted out of SMS workflows`, - safeStringify({ workflowStep: workflowStepId, eventUid: evt.uid }) - ); + if (!reminderPhone) { + log.warn(`No phone number provided for WhatsApp reminder in workflow step ${workflowStepId}`); return; } - const { startTime, endTime } = evt; - const uid = evt.uid as string; - const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase; - let scheduledDate = null; + if (await WorkflowOptOutContactRepository.isOptedOut(reminderPhone)) { + log.warn(`Phone number opted out of SMS workflows`, safeStringify({ workflowStep: workflowStepId })); + return; + } const senderID = getSenderId(reminderPhone, sender || SENDER_ID); + const params: ScheduleTextReminderArgs = { + ...args, + sender: senderID, + }; //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) @@ -138,141 +134,224 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs & { evt if (!!verifiedNumber) return true; return isVerificationPending; } + const isNumberVerified = await getIsNumberVerified(); + if (!isNumberVerified) { + log.warn(`Phone number not verified`, safeStringify({ reminderPhone, isNumberVerified })); + return; + } + + if (params.evt) { + await scheduleSMSReminderForEvt( + params as ScheduleTextReminderArgsWithRequiredFields & { evt: BookingInfo } + ); + } else { + await scheduleSMSReminderForForm( + params as ScheduleTextReminderArgsWithRequiredFields & { + formData: FormSubmissionData; + } + ); + } +}; + +const scheduleSMSReminderForEvt = async ( + args: ScheduleTextReminderArgsWithRequiredFields & { evt: BookingInfo } +) => { + const { + evt, + reminderPhone, + triggerEvent, + action, + timeSpan, + message = "", + workflowStepId, + template, + sender, + userId, + teamId, + seatReferenceUid, + } = args; + + const { startTime, endTime } = evt; + const uid = evt.uid as string; + const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase; + let scheduledDate = null; + if (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT) { scheduledDate = timeSpan.time && timeUnit ? dayjs(startTime).subtract(timeSpan.time, timeUnit) : null; } else if (triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) { scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null; } - if (reminderPhone && isNumberVerified) { - const useTwilio = shouldUseTwilio(triggerEvent, scheduledDate); - if (useTwilio) { - const attendeeToBeUsedInSMS = getAttendeeToBeUsedInSMS(action, evt, reminderPhone); - - const name = action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.name : ""; - const attendeeName = - action === WorkflowActions.SMS_ATTENDEE ? evt.organizer.name : attendeeToBeUsedInSMS.name; - const timeZone = - action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.timeZone : evt.organizer.timeZone; - - let smsMessage = message; - - if (smsMessage) { - smsMessage = await getSMSMessageWithVariables(smsMessage, evt, attendeeToBeUsedInSMS, action); - } else if (template === WorkflowTemplates.REMINDER) { - smsMessage = - smsReminderTemplate( - false, - evt.organizer.language.locale, - action, - evt.organizer.timeFormat, - evt.startTime, - evt.title, - timeZone, - attendeeName, - name - ) || message; - } + const useTwilio = shouldUseTwilio(triggerEvent, scheduledDate); + if (useTwilio) { + const attendeeToBeUsedInSMS = getAttendeeToBeUsedInSMS(action, evt, reminderPhone); - if (smsMessage.length > 0) { - const smsMessageWithoutOptOut = smsMessage; + const name = action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.name : ""; + const attendeeName = + action === WorkflowActions.SMS_ATTENDEE ? evt.organizer.name : attendeeToBeUsedInSMS.name; + const timeZone = + action === WorkflowActions.SMS_ATTENDEE ? attendeeToBeUsedInSMS.timeZone : evt.organizer.timeZone; - if (process.env.TWILIO_OPT_OUT_ENABLED === "true") { - smsMessage = await WorkflowOptOutService.addOptOutMessage( - smsMessage, - evt.organizer.language.locale - ); - } - // Allows debugging generated email content without waiting for sendgrid to send emails - log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage); - - if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(triggerEvent)) { - try { - await sendSmsOrFallbackEmail({ - twilioData: { - phoneNumber: reminderPhone, - body: smsMessage, - sender: senderID, - bodyWithoutOptOut: smsMessageWithoutOptOut, - bookingUid: evt.uid, - userId, - teamId, - }, - fallbackData: isAttendeeAction(action) - ? { - email: evt.attendees[0].email, - t: await getTranslation(evt.attendees[0].language.locale, "common"), - replyTo: evt.organizer.email, - } - : undefined, - }); - } catch (error) { - log.error(`Error sending SMS with error ${error}`); - } + let smsMessage = message; + + if (smsMessage) { + smsMessage = await getSMSMessageWithVariables(smsMessage, evt, attendeeToBeUsedInSMS, action); + } else if (template === WorkflowTemplates.REMINDER) { + smsMessage = + smsReminderTemplate( + false, + evt.organizer.language.locale, + action, + evt.organizer.timeFormat, + evt.startTime, + evt.title, + timeZone, + attendeeName, + name + ) || message; + } + + if (smsMessage.trim().length > 0) { + const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage( + smsMessage, + evt.organizer.language.locale + ); + + // Allows debugging generated email content without waiting for sendgrid to send emails + log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage); + + if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(triggerEvent)) { + try { + await sendSmsOrFallbackEmail({ + twilioData: { + phoneNumber: reminderPhone, + body: smsMessage, + sender, + bodyWithoutOptOut: smsMessageWithoutOptOut, + bookingUid: evt.uid, + userId, + teamId, + }, + fallbackData: isAttendeeAction(action) + ? { + email: evt.attendees[0].email, + t: await getTranslation(evt.attendees[0].language.locale, "common"), + replyTo: evt.organizer.email, + } + : undefined, + }); + } catch (error) { + log.error(`Error sending SMS with error ${error}`); } + } + + if ( + (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT || + triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) && + scheduledDate + ) { + try { + // schedule at least 15 minutes in advance and at most 2 hours in advance + const scheduledNotification = await scheduleSmsOrFallbackEmail({ + twilioData: { + phoneNumber: reminderPhone, + body: smsMessage, + scheduledDate: scheduledDate.toDate(), + sender, + bookingUid: evt.uid, + userId, + teamId, + }, + fallbackData: isAttendeeAction(action) + ? { + email: evt.attendees[0].email, + t: await getTranslation(evt.attendees[0].language.locale, "common"), + replyTo: evt.organizer.email, + workflowStepId, + } + : undefined, + }); - if ( - (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT || - triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) && - scheduledDate - ) { - try { - // schedule at least 15 minutes in advance and at most 2 hours in advance - const scheduledNotification = await scheduleSmsOrFallbackEmail({ - twilioData: { - phoneNumber: reminderPhone, - body: smsMessage, + if (scheduledNotification?.sid) { + await prisma.workflowReminder.create({ + data: { + bookingUid: uid, + workflowStepId: workflowStepId, + method: WorkflowMethods.SMS, scheduledDate: scheduledDate.toDate(), - sender: senderID, - bookingUid: evt.uid, - userId, - teamId, + scheduled: true, + referenceId: scheduledNotification.sid, + seatReferenceId: seatReferenceUid, }, - fallbackData: isAttendeeAction(action) - ? { - email: evt.attendees[0].email, - t: await getTranslation(evt.attendees[0].language.locale, "common"), - replyTo: evt.organizer.email, - workflowStepId, - } - : undefined, }); - - if (scheduledNotification?.sid) { - await prisma.workflowReminder.create({ - data: { - bookingUid: uid, - workflowStepId: workflowStepId, - method: WorkflowMethods.SMS, - scheduledDate: scheduledDate.toDate(), - scheduled: true, - referenceId: scheduledNotification.sid, - seatReferenceId: seatReferenceUid, - }, - }); - } - } catch (error) { - log.error(`Error scheduling SMS with error ${error}`); } + } catch (error) { + log.error(`Error scheduling SMS with error ${error}`); } } - return; } + return; + } + + if (!useTwilio && scheduledDate) { + // Write to DB and send to CRON if scheduled reminder date is past 2 hours from now + await prisma.workflowReminder.create({ + data: { + bookingUid: uid, + workflowStepId: workflowStepId, + method: WorkflowMethods.SMS, + scheduledDate: scheduledDate.toDate(), + scheduled: false, + seatReferenceId: seatReferenceUid, + }, + }); + } +}; + +// sends all immediately, no scheduling needed +const scheduleSMSReminderForForm = async ( + args: ScheduleTextReminderArgsWithRequiredFields & { + formData: FormSubmissionData; + } +) => { + const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData } = args; + + const smsMessage = message; + + if (smsMessage.trim().length > 0) { + const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage( + smsMessage, + formData.user.locale + ); + + // Allows debugging generated email content without waiting for sendgrid to send emails + log.debug(`Sending sms for trigger ${triggerEvent}`, smsMessage); + + try { + const submitterEmail = getSubmitterEmail(formData.responses); - if (!useTwilio && scheduledDate) { - // Write to DB and send to CRON if scheduled reminder date is past 2 hours from now - await prisma.workflowReminder.create({ - data: { - bookingUid: uid, - workflowStepId: workflowStepId, - method: WorkflowMethods.SMS, - scheduledDate: scheduledDate.toDate(), - scheduled: false, - seatReferenceId: seatReferenceUid, + await sendSmsOrFallbackEmail({ + twilioData: { + phoneNumber: reminderPhone, + body: smsMessage, + sender, + bodyWithoutOptOut: smsMessageWithoutOptOut, + userId, + teamId, }, + fallbackData: + isAttendeeAction(action) && submitterEmail + ? { + email: submitterEmail, + t: await getTranslation(formData.user.locale, "common"), + replyTo: formData.user.email, + } + : undefined, }); + } catch (error) { + log.error(`Error sending SMS with error ${error}`); } } }; diff --git a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts index 728588c096f093..59f2bdb6efc107 100644 --- a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts +++ b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts @@ -26,7 +26,11 @@ export class WorkflowOptOutService { } static async addOptOutMessage(message: string, locale: string) { - const t = await getTranslation(locale, "common"); - return `${message}\n\n${t("sms_opt_out_message")}`; + if (process.env.TWILIO_OPT_OUT_ENABLED === "true") { + const t = await getTranslation(locale, "common"); + return `${message}\n\n${t("sms_opt_out_message")}`; + } + + return message; } }