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 <j.auyeung419@gmail.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 <j.auyeung419@gmail.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 <j.auyeung419@gmail.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 <j.auyeung419@gmail.com>

* Clean up Devin

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-07-28 16:43:23 +00:00
1 parent 3b823670ea
commit 6c936472d2
5 files changed
+89 -10

No files matched your search

@@ -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,
@@ -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 }] =
@@ -4,7 +4,12 @@ import type { EventType, User, WorkflowReminder, WorkflowStep, Prisma } from "@c
import { WorkflowMethods } from "@calcom/prisma/enums";
type PartialWorkflowStep =
| (Partial<WorkflowStep> & { workflow: { userId?: number; teamId?: number } })
| (Partial<WorkflowStep> & {
workflow: {
userId?: number;
teamId?: number;
};
})
| null;
type Booking = Prisma.BookingGetPayload<{
@@ -219,3 +224,35 @@ export async function getAllUnscheduledReminders(): Promise<PartialWorkflowRemin
return unscheduledReminders;
}
export function getWorkflowRecipientEmail({
action,
organizerEmail,
attendeeEmail,
sendToEmail,
}: {
action: string;
organizerEmail?: string;
attendeeEmail?: string;
sendToEmail?: string | null;
}): string | null {
// const action = reminder.workflowStep.action;
switch (action) {
case "EMAIL_ADDRESS":
return sendToEmail || null;
case "EMAIL_HOST":
return organizerEmail || null;
case "EMAIL_ATTENDEE":
return attendeeEmail || null;
case "SMS_ATTENDEE":
return attendeeEmail || null;
case "WHATSAPP_ATTENDEE":
return attendeeEmail || null;
case "SMS_NUMBER":
case "WHATSAPP_NUMBER":
return null;
default:
return null;
}
}
@@ -19,6 +19,7 @@ import {
} from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { getWorkflowRecipientEmail } from "../getWorkflowReminders";
import { sendOrScheduleWorkflowEmails } from "./providers/emailProvider";
import { getBatchId, sendSendgridMail } from "./providers/sendgridProvider";
import type { AttendeeInBookingInfo, BookingInfo, timeUnitLowerCase } from "./smsReminderManager";
@@ -139,6 +140,12 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
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`,
@@ -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 }] =