Files
calendar/packages/features/ee/workflows/lib/reminders/utils.ts
T
Amit SharmaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Keith Williams
353f71bdc6 feat: Sink url shortner for sms workflow reminders (#26608)
* feat: Sink url shortner for sms workflow reminders

* fix: remove hardcoded dub values

* update .env.example

* fix: unit tests

* chore: add tests for scheduleSmsReminder and utils

* review refactor

* fix: type check

* review refactor

* fix: update test to account for smsReminderNumber fallback from main

Co-Authored-By: unknown <>

* feat: add feature flag for sink and more tests to verify

* fix: type check

* use proper feature flags for sink

* Apply suggestion from @keithwillcode

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-03-16 20:51:28 +00:00

116 lines
4.4 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import { UrlShortenerFactory } from "@calcom/features/url-shortener/UrlShortenerFactory";
import { DUB_SMS_DOMAIN, DUB_SMS_FOLDER_ID, WEBSITE_URL } from "@calcom/lib/constants";
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { IMMEDIATE_WORKFLOW_TRIGGER_EVENTS } from "../constants";
import { getWorkflowRecipientEmail } from "../getWorkflowReminders";
import type { AttendeeInBookingInfo, BookingInfo } from "../types";
import type { VariablesType } from "./templates/customTemplate";
import customTemplate, { transformBookingResponsesToVariableFormat } from "./templates/customTemplate";
export const getSMSMessageWithVariables = async (
smsMessage: string,
evt: BookingInfo,
attendeeToBeUsedInSMS: AttendeeInBookingInfo,
action: WorkflowActions,
userId?: number | null,
teamId?: number | null
) => {
const recipientEmail = getWorkflowRecipientEmail({
action,
attendeeEmail: attendeeToBeUsedInSMS.email,
});
const urls = {
meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl || "",
cancelLink: `${evt.bookerUrl ?? WEBSITE_URL}/booking/${evt.uid}?cancel=true${
recipientEmail ? `&cancelledBy=${recipientEmail}` : ""
}`,
rescheduleLink: `${evt.bookerUrl ?? WEBSITE_URL}/reschedule/${evt.uid}${
recipientEmail ? `?rescheduledBy=${recipientEmail}` : ""
}`,
};
const shortener = await UrlShortenerFactory.create({ userId, teamId });
const [{ shortLink: meetingUrl }, { shortLink: cancelLink }, { shortLink: rescheduleLink }] =
await shortener.shortenMany([urls.meetingUrl, urls.cancelLink, urls.rescheduleLink], {
domain: DUB_SMS_DOMAIN,
folderId: DUB_SMS_FOLDER_ID,
});
const timeZone =
action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE
? attendeeToBeUsedInSMS.timeZone
: evt.organizer.timeZone;
const variables: VariablesType = {
eventName: evt.title,
organizerName: evt.organizer.name,
attendeeName: attendeeToBeUsedInSMS.name,
attendeeFirstName: attendeeToBeUsedInSMS.firstName,
attendeeLastName: attendeeToBeUsedInSMS.lastName,
attendeeEmail: attendeeToBeUsedInSMS.email,
eventDate: dayjs(evt.startTime).tz(timeZone),
eventEndTime: dayjs(evt.endTime).tz(timeZone),
timeZone: timeZone,
location: evt.location,
additionalNotes: evt.additionalNotes,
responses: transformBookingResponsesToVariableFormat(evt.responses),
meetingUrl,
cancelLink,
rescheduleLink,
cancelReason: evt.cancellationReason,
rescheduleReason: evt.rescheduleReason,
attendeeTimezone: evt.attendees[0].timeZone,
eventTimeInAttendeeTimezone: dayjs(evt.startTime).tz(evt.attendees[0].timeZone),
eventEndTimeInAttendeeTimezone: dayjs(evt.endTime).tz(evt.attendees[0].timeZone),
};
const locale =
action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE
? attendeeToBeUsedInSMS.language?.locale
: evt.organizer.language.locale;
const customMessage = customTemplate(smsMessage, variables, locale, evt.organizer.timeFormat);
smsMessage = customMessage.text;
return smsMessage;
};
export const getAttendeeToBeUsedInSMS = (
action: WorkflowActions,
evt: BookingInfo,
reminderPhone: string | null
) => {
let attendeeToBeUsedInSMS: AttendeeInBookingInfo | null = null;
if (action === WorkflowActions.SMS_ATTENDEE) {
const attendeeWithReminderPhoneAsSMSReminderNumber =
reminderPhone && evt.attendees.find((attendee) => attendee.email === evt.responses?.email?.value);
attendeeToBeUsedInSMS = attendeeWithReminderPhoneAsSMSReminderNumber
? attendeeWithReminderPhoneAsSMSReminderNumber
: evt.attendees[0];
} else {
attendeeToBeUsedInSMS = evt.attendees[0];
}
return attendeeToBeUsedInSMS;
};
export const shouldUseTwilio = (trigger: WorkflowTriggerEvents, scheduledDate: dayjs.Dayjs | null) => {
if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(trigger)) {
return true;
}
if (trigger === WorkflowTriggerEvents.BEFORE_EVENT || trigger === WorkflowTriggerEvents.AFTER_EVENT) {
const currentDate = dayjs();
if (
scheduledDate &&
currentDate.isBefore(scheduledDate.subtract(15, "minute")) &&
!scheduledDate.isAfter(currentDate.add(2, "hour"))
) {
return true;
}
}
return false;
};