From 6c936472d24c2e0894037c6a00f4c64ada41979f Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:43:23 -0400 Subject: [PATCH] feat: add cancelledBy and rescheduledBy parameters to workflow links (#22438) * feat: add cancelledBy and rescheduledBy parameters to workflow links - Extended workflow reminder selection to include user and team data - Added getWorkflowAssigneeEmail helper function to resolve assignee email - Updated email and SMS workflow templates to include assignee in cancel/reschedule links - For user workflows, uses user email; for team workflows, uses team owner email - Maintains backward compatibility when assignee data is unavailable Co-Authored-By: joe@cal.com * fix: use workflow recipient email instead of assignee email for link parameters - Updated getWorkflowRecipientEmail to determine recipient based on workflow action type - EMAIL_ADDRESS uses sendTo email, EMAIL_HOST uses organizer, EMAIL_ATTENDEE uses attendee - SMS_ATTENDEE/WHATSAPP_ATTENDEE use attendee email for cancelledBy/rescheduledBy params - Maintains backward compatibility when recipient data unavailable Co-Authored-By: joe@cal.com * fix: use emailReminderManager for email workflows with recipient parameters - Updated emailReminderManager to include recipient email in cancel/reschedule links - Added getEmailWorkflowRecipientEmail function for proper recipient determination - Removed changes from deprecated scheduleEmailReminders.ts - SMS_ATTENDEE correctly uses attendee email like EMAIL_ATTENDEE Co-Authored-By: joe@cal.com * Get attendee's email when sending SMS * Remove unused query * feat: add cancelledBy and rescheduledBy parameters to getSMSMessageWithVariables Co-Authored-By: joe@cal.com * Clean up Devin --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../workflows/api/scheduleEmailReminders.ts | 16 +++++++- .../ee/workflows/api/scheduleSMSReminders.ts | 16 ++++++-- .../ee/workflows/lib/getWorkflowReminders.ts | 39 ++++++++++++++++++- .../lib/reminders/emailReminderManager.ts | 15 ++++++- .../ee/workflows/lib/reminders/utils.ts | 13 ++++++- 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 7914739d46..9b31468416 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -21,6 +21,7 @@ import { getAllRemindersToCancel, getAllRemindersToDelete, getAllUnscheduledReminders, + getWorkflowRecipientEmail, } from "../lib/getWorkflowReminders"; import { sendOrScheduleWorkflowEmails } from "../lib/reminders/providers/emailProvider"; import { @@ -187,6 +188,13 @@ export async function handler(req: NextRequest) { reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null ); + const recipientEmail = getWorkflowRecipientEmail({ + action: reminder.workflowStep.action || WorkflowActions.EMAIL_ADDRESS, + attendeeEmail: reminder.booking.attendees[0].email, + organizerEmail: reminder.booking.user?.email, + sendToEmail: reminder.workflowStep.sendTo, + }); + const variables: VariablesType = { eventName: reminder.booking.eventType?.title || "", organizerName: reminder.booking.user?.name || "", @@ -199,8 +207,12 @@ export async function handler(req: NextRequest) { additionalNotes: reminder.booking.description, responses: responses, meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl, - cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true`, - rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`, + cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true${ + recipientEmail ? `&cancelledBy=${encodeURIComponent(recipientEmail)}` : "" + }`, + rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}${ + recipientEmail ? `?rescheduledBy=${encodeURIComponent(recipientEmail)}` : "" + }`, ratingUrl: `${bookerUrl}/booking/${reminder.booking.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${reminder.booking.uid}?noShow=true`, attendeeTimezone: reminder.booking.attendees[0].timeZone, diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index feb6a69ae4..d3ed23d6a1 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -16,7 +16,7 @@ import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { getSenderId } from "../lib/alphanumericSenderIdSupport"; import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders"; -import { select } from "../lib/getWorkflowReminders"; +import { select, getWorkflowRecipientEmail } from "../lib/getWorkflowReminders"; import type { VariablesType } from "../lib/reminders/templates/customTemplate"; import customTemplate from "../lib/reminders/templates/customTemplate"; import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate"; @@ -108,10 +108,20 @@ export async function handler(req: NextRequest) { reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null ); + const recipientEmail = getWorkflowRecipientEmail({ + action: reminder.workflowStep.action || WorkflowActions.SMS_NUMBER, + attendeeEmail: reminder.booking.attendees[0].email, + organizerEmail: reminder.booking.user?.email, + }); + const urls = { meetingUrl: bookingMetadataSchema.parse(reminder.booking?.metadata || {})?.videoCallUrl || "", - cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true` || "", - rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}` || "", + cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true${ + recipientEmail ? `&cancelledBy=${recipientEmail}` : "" + }`, + rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}${ + recipientEmail ? `?rescheduledBy=${recipientEmail}` : "" + }`, }; const [{ shortLink: meetingUrl }, { shortLink: cancelLink }, { shortLink: rescheduleLink }] = diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts index ca246583a3..4d4e89b3ec 100644 --- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts +++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts @@ -4,7 +4,12 @@ import type { EventType, User, WorkflowReminder, WorkflowStep, Prisma } from "@c import { WorkflowMethods } from "@calcom/prisma/enums"; type PartialWorkflowStep = - | (Partial & { workflow: { userId?: number; teamId?: number } }) + | (Partial & { + workflow: { + userId?: number; + teamId?: number; + }; + }) | null; type Booking = Prisma.BookingGetPayload<{ @@ -219,3 +224,35 @@ export async function getAllUnscheduledReminders(): Promise const bookerUrl = evt.bookerUrl ?? WEBSITE_URL; if (emailBody) { + const recipientEmail = getWorkflowRecipientEmail({ + action, + attendeeEmail: attendeeToBeUsedInMail.email, + organizerEmail: evt.organizer.email, + sendToEmail: sendTo[0], + }); const variables: VariablesType = { eventName: evt.title || "", organizerName: evt.organizer.name, @@ -153,9 +160,13 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => additionalNotes: evt.additionalNotes, responses: evt.responses, meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl, - cancelLink: `${bookerUrl}/booking/${evt.uid}?cancel=true`, + cancelLink: `${bookerUrl}/booking/${evt.uid}?cancel=true${ + recipientEmail ? `&cancelledBy=${encodeURIComponent(recipientEmail)}` : "" + }`, cancelReason: evt.cancellationReason, - rescheduleLink: `${bookerUrl}/reschedule/${evt.uid}`, + rescheduleLink: `${bookerUrl}/reschedule/${evt.uid}${ + recipientEmail ? `?rescheduledBy=${encodeURIComponent(recipientEmail)}` : "" + }`, rescheduleReason: evt.rescheduleReason, ratingUrl: `${bookerUrl}/booking/${evt.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${evt.uid}?noShow=true`, diff --git a/packages/features/ee/workflows/lib/reminders/utils.ts b/packages/features/ee/workflows/lib/reminders/utils.ts index 2255ae5faf..2ff24bcf0b 100644 --- a/packages/features/ee/workflows/lib/reminders/utils.ts +++ b/packages/features/ee/workflows/lib/reminders/utils.ts @@ -4,6 +4,7 @@ import { WEBSITE_URL } from "@calcom/lib/constants"; import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; +import { getWorkflowRecipientEmail } from "../getWorkflowReminders"; import type { AttendeeInBookingInfo, BookingInfo } from "./smsReminderManager"; import type { VariablesType } from "./templates/customTemplate"; import customTemplate from "./templates/customTemplate"; @@ -40,10 +41,18 @@ export const getSMSMessageWithVariables = async ( attendeeToBeUsedInSMS: AttendeeInBookingInfo, action: WorkflowActions ) => { + 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`, - rescheduleLink: `${evt.bookerUrl ?? WEBSITE_URL}/reschedule/${evt.uid}`, + 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 [{ shortLink: meetingUrl }, { shortLink: cancelLink }, { shortLink: rescheduleLink }] =