Files
calendar/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
+1
Carina WollendorferGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>[email protected] <[email protected]>Amit SharmaCarinaWollicoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Udit TakkarBenny Joocal.comMorgan
44a3a9eabb feat: form submitted workflow triggers #1 (#23704)
* feat: add 5 new workflow triggers for booking events

- Add BOOKING_REJECTED, BOOKING_REQUESTED, BOOKING_PAYMENT_INITIATED, BOOKING_PAID, BOOKING_NO_SHOW_UPDATED to WorkflowTriggerEvents enum
- Update workflow constants to include new trigger options
- Implement workflow trigger logic for booking rejected and requested events
- Add translations for new workflow triggers following {enum}_trigger format
- Generate updated Prisma types for new schema changes

Co-Authored-By: [email protected] <[email protected]>

* fix: type check, remove as any

* feat: add workflow trigger for BOOKING_REQUESTED in handleNewBooking.ts

- Add WorkflowTriggerEvents import to handleNewBooking.ts
- Implement workflow trigger logic for BOOKING_REQUESTED in else block
- Filter workflows by BOOKING_REQUESTED trigger and call scheduleWorkflowReminders
- Use proper calendar event object construction without type casting
- Add error handling for workflow reminder scheduling

Co-Authored-By: [email protected] <[email protected]>

* fix: resolve type errors in workflow trigger implementations

- Add proper database includes for user information in handleConfirmation.ts
- Fix ExtendedCalendarEvent type structure with correct hosts mapping
- Add missing properties to calendar event objects in handleMarkNoShow.ts
- Ensure all workflow triggers follow proper type patterns

Co-Authored-By: [email protected] <[email protected]>

* feat: add workflow test configurations for new booking triggers

- Add workflow configurations for BOOKING_REQUESTED and BOOKING_PAYMENT_INITIATED in fresh-booking.test.ts
- Add workflow configuration for BOOKING_REJECTED in confirm.handler.test.ts
- Enable previously skipped confirm.handler.test.ts
- Remove workflow test assertions temporarily until triggers are fully functional
- Maintain webhook test coverage while adding workflow test infrastructure

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing mockSuccessfulVideoMeetingCreation import to confirm.handler.test.ts

- Import mockSuccessfulVideoMeetingCreation from bookingScenario utils
- Add mock call to BOOKING_REJECTED workflow test case
- Resolves ReferenceError that was causing unit test CI failure

Co-Authored-By: [email protected] <[email protected]>

* add new triggers

* refactor: improve _scheduleWorkflowReminders readability and add missing booking trigger events

- Extract complex conditional logic into helper functions (isImmediateTrigger, isTimeBased, shouldProcessWorkflow)
- Add missing workflow trigger events with immediate execution logic
- Update test workflows to use different actions (EMAIL_ATTENDEE, SMS_ATTENDEE) for better differentiation
- Fix translation function mock in confirm.handler.test.ts using mockNoTranslations utility
- Maintain existing functionality while improving code maintainability

Co-Authored-By: [email protected] <[email protected]>

* only show customt emplate for form triggers

* filter outside scheduleWorkflowReminder

* fix type check

* chore: add more tests

* test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage

- Create handleMarkNoShow.test.ts following confirm.handler.test.ts pattern
- Add expectBookingNoShowUpdatedWebhookToHaveBeenFired utility function
- Test both webhook and workflow triggers for BOOKING_NO_SHOW_UPDATED
- Cover attendee/host no-show scenarios, multiple attendees, and error cases
- All 6 unit tests pass with proper mocking of external dependencies

Co-Authored-By: [email protected] <[email protected]>

* Revert "test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage"

This reverts commit 7642992202.

* fix: add new workflow triggers to api/v2

* update swagger docs

* fix: e2e

* fix type check

* fix tests, add test for before after events

* fix unit tests

* revert confirm.handler.test

* fix: unit tests

* dummy form variables

* add routing forms to active on dropdown

* add migration file

* Ui fixes for variables dropdown

* remove other translation keys

* review fixes

* allow routing forms for activeOn

* use repository function to get routing forms

* remove unnecessary code

* adjust logic in update handler

* add triggers to api v2

* remvoe unused file

* rename to getAcitveOnOptions handler

* remove routingFormOptions handler

* clean up getActiveOnOptions

* refactor WorkflowService

* remove logs

* remove unused

* fix: type check

* fix: missed before after events for recurring

* fix: calendarEvent handleMarkNoShow

* fix error message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* don't query disabled routing forms

* create tasker function

* add tasker code

* move isFormTrigger function

* small adjustments + todo comments

* remove email to host action for form triggers

* throw trpc error if email to host is added as step

* fix dialog on how to use form responses as variables

* remove add variable dropdown for form triggers

* remove form workfows in event workflows tab

* improvements for workflow logic on form submission

* review fixes

* base setup for seperate schedule functions (evt and form)

* add missing BOOKING_PAID workflow trigger

* fix pathname

* fix: test for BOOKING_REQUESTED

* fix activeOn ids

* pass hideBranding and smsReminderNumber

* adjustments to reminderScheduler

* create empty scheduelForForm functions

* pass locale and timezone with form user

* pass formData instead of responses

* pass timeFormat and locale

* reusable function for email sending and reminder creation

* implement scheduleEmailReminderForForm

* remove added editor field from merge conflict

* don't support cal.ai action with form triggers

* throw bad request if form trigger and cal.ai is combined

* add tests for scheduleFormWorkflows

* add form submission tests

* remove form response varibe info

* clean up workflow actions

* fixes for getting template options

* pass triggerType to getAllWorkflows

* move reusable logic to scheduleSMSReminder

* add formdata to param type

* type fixes for text reminder managers

* implement scheduleSMSReminderForForm

* fix import

* fix isAuthorizedToAddActiveOnIds

* disble whatsapp action

* implement triggerFormSubmittedNoEventWorkflow

* code clean up

* Merge branch 'devin/1755107037-add-workflow-triggers' into feat/routing-form-workflow-triggers

* fix type errors

* remove async from getSubmitterEmail

* fix type errors

* revert cal.ai changes

* fix type error

* add sublogger

* code clean up

* fix type errors

* remove label for attendee whatsapp action

* code clean up

* fixes saving teams on org workflows

* fix type error

* code improvements for activeOn ids

* Revert "code improvements for activeOn ids"

This reverts commit 0a3590a4e2.

* improve variable name

* fix unit tests

* small fixes

* type fixes

* remove unused translation keys

* fix merge conflict issues

* code clean up

* remove SMS action support

* remove more SMS code

* add missing imports

* set custom template for form action

* type fixes

* fix tasker endpoint

* fix duplicate check

* fix workfows.test.ts

* use repository funciton to getHideBranding

* code clean up

* fix hasDuplicateSubmission

* code clean up

* select only needed properties

* remove repository functions

* Revert "remove repository functions"

This reverts commit 7aa47b1c59.

* add scheduleWorkflows function

* Revert "add scheduleWorkflows function"

This reverts commit fe5db4fe3b.

* move type to /types

* Revert "move type to /types"

This reverts commit 91e0152154.

* revert changes causing type errors

* remove import

* remove unused import

* Revert "remove unused import"

This reverts commit 1916768c87.

* revert changed from attempt to fix type errors

* pass object to gt all workflows

* fix isAuthorized check

* trigger filtering

* remove form submitted no event booked code

* remove form submitted no event from schema

* remove more code

* remove test

* fixes

* add getSubmitterEmail function

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* code clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* fix bug with key

* add missing trigger in update-workflow.input.ts

* ForEvt and ForForm function for aiPhoneCallManager

* chore: add support for form workflows on api v2

* fixup! chore: add support for form workflows on api v2

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* chore: handle workflows api v2

* chore: handle workflows api v2, split in 2 endpoints

* fix workflow step creation

* remove connect agent and fixes types

* add type to workflow

* chore: use workflow type in apiv2 WorkflowsOutputService

* update worklfow type on update

* chore: use workflow type in apiv2 WorkflowsOutputService

* fix template body for torm trigger

* some UI fixes for email subject/body

* resetting email body when changing form triggers

* use type field to query workflows

* clean up all old active on values

* remove responseId from all funciton calls

* remove undefined from updateTemplate

* refactor: split routing form and event-type workflows code

* refactor: split routing form and event-type workflows code

* fix template  text when adding action

* chore: don't rename WorkflowActivationDto to avoid ci blocking

* refine update schedule to use only allowed actions

* fix type error

* don't allow whatsapp action with form trigger

* fix type error

* return early if activeOn array is empty

* fix: from step type in BaseFormWorkflowStepDto

* fixup! fix: from step type in BaseFormWorkflowStepDto

* move all prisma calls to repository (service/workflows.ts)

* use FORM_TRIGGER_WORKFLOW_EVENTS for form queries

* use userRepository

* use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger

* code clean up

* code clean up

* add back trpc  import

* fix agent repository functions

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Amit Sharma <[email protected]>
Co-authored-by: CarinaWolli <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <[email protected]>
Co-authored-by: Benny Joo <[email protected]>
Co-authored-by: cal.com <[email protected]>
Co-authored-by: Morgan <[email protected]>
2025-10-13 12:20:47 +02:00

267 lines
8.3 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import {
WorkflowTriggerEvents,
WorkflowTemplates,
WorkflowActions,
WorkflowMethods,
} from "@calcom/prisma/enums";
import { isAttendeeAction } from "../actionHelperFunctions";
import { IMMEDIATE_WORKFLOW_TRIGGER_EVENTS } from "../constants";
import {
getContentSidForTemplate,
getContentVariablesForTemplate,
} from "../reminders/templates/whatsapp/ContentSidMapping";
import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher";
import type { BookingInfo, ScheduleTextReminderArgs, timeUnitLowerCase } from "./smsReminderManager";
import {
whatsappEventCancelledTemplate,
whatsappEventCompletedTemplate,
whatsappEventRescheduledTemplate,
whatsappReminderTemplate,
} from "./templates/whatsapp";
const log = logger.getSubLogger({ prefix: ["[whatsappReminderManager]"] });
export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs & { evt: BookingInfo }) => {
const {
evt,
reminderPhone,
triggerEvent,
action,
timeSpan,
message = "",
workflowStepId,
template,
userId,
teamId,
isVerificationPending = false,
seatReferenceUid,
verifiedAt,
} = args;
if (!verifiedAt) {
log.warn(`Workflow step ${workflowStepId} not verified`);
return;
}
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;
//WHATSAPP_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)
async function getIsNumberVerified() {
if (action === WorkflowActions.WHATSAPP_ATTENDEE) return true;
const verifiedNumber = await prisma.verifiedNumber.findFirst({
where: {
OR: [{ userId }, { teamId }],
phoneNumber: reminderPhone || "",
},
});
if (!!verifiedNumber) return true;
return isVerificationPending;
}
const isNumberVerified = await getIsNumberVerified();
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;
}
const name = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].name : evt.organizer.name;
const attendeeName =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name;
const timeZone =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone;
const locale = evt.organizer.language.locale;
const timeFormat = evt.organizer.timeFormat;
const contentSid = getContentSidForTemplate(template);
const contentVariables = getContentVariablesForTemplate({
name,
attendeeName,
eventName: evt.title,
eventDate: dayjs(startTime).tz(timeZone).locale(locale).format("YYYY MMM D"),
startTime: dayjs(startTime)
.tz(timeZone)
.locale(locale)
.format(timeFormat || "h:mma"),
timeZone,
});
let textMessage = message;
switch (template) {
case WorkflowTemplates.REMINDER:
textMessage =
whatsappReminderTemplate(
false,
locale,
action,
timeFormat,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
case WorkflowTemplates.CANCELLED:
textMessage =
whatsappEventCancelledTemplate(
false,
locale,
action,
timeFormat,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
case WorkflowTemplates.RESCHEDULED:
textMessage =
whatsappEventRescheduledTemplate(
false,
locale,
action,
timeFormat,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
case WorkflowTemplates.COMPLETED:
textMessage =
whatsappEventCompletedTemplate(
false,
locale,
action,
timeFormat,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
default:
textMessage =
whatsappReminderTemplate(
false,
locale,
action,
timeFormat,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
}
// Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages
log.debug(`Sending Whatsapp for trigger ${triggerEvent}`, textMessage);
if (textMessage.length > 0 && reminderPhone && isNumberVerified) {
//send WHATSAPP when event is booked/cancelled/rescheduled
if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(triggerEvent)) {
try {
await sendSmsOrFallbackEmail({
twilioData: {
phoneNumber: reminderPhone,
body: textMessage,
sender: "",
bookingUid: evt.uid,
userId,
teamId,
isWhatsapp: true,
contentSid,
contentVariables,
},
fallbackData: isAttendeeAction(action)
? {
email: evt.attendees[0].email,
t: await getTranslation(evt.attendees[0].language.locale ?? "en", "common"),
replyTo: evt.organizer.email,
}
: undefined,
});
} catch (error) {
console.log(`Error sending WHATSAPP with error ${error}`);
}
} else if (
(triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) &&
scheduledDate
) {
// schedule at least 15 minutes in advance and at most 2 hours in advance
if (
currentDate.isBefore(scheduledDate.subtract(15, "minute")) &&
!scheduledDate.isAfter(currentDate.add(2, "hour"))
) {
try {
const scheduledNotification = await scheduleSmsOrFallbackEmail({
twilioData: {
phoneNumber: reminderPhone,
body: textMessage,
scheduledDate: scheduledDate.toDate(),
sender: "",
bookingUid: evt.uid ?? "",
userId,
teamId,
isWhatsapp: true,
contentSid,
contentVariables,
},
fallbackData: isAttendeeAction(action)
? {
email: evt.attendees[0].email,
t: await getTranslation(evt.attendees[0].language.locale ?? "en", "common"),
replyTo: evt.organizer.email,
workflowStepId,
}
: undefined,
});
if (scheduledNotification?.sid) {
await prisma.workflowReminder.create({
data: {
bookingUid: uid,
workflowStepId: workflowStepId,
method: WorkflowMethods.WHATSAPP,
scheduledDate: scheduledDate.toDate(),
scheduled: true,
referenceId: scheduledNotification.sid,
seatReferenceId: seatReferenceUid,
},
});
}
} catch (error) {
console.log(`Error scheduling WHATSAPP with error ${error}`);
}
} else if (scheduledDate.isAfter(currentDate.add(2, "hour"))) {
// 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.WHATSAPP,
scheduledDate: scheduledDate.toDate(),
scheduled: false,
seatReferenceId: seatReferenceUid,
},
});
}
}
}
};