From afea724e4d46d3c07be8e387a0fe2ec3e44ef4e8 Mon Sep 17 00:00:00 2001 From: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:58:33 +0530 Subject: [PATCH] fix: improve cancellation email of reschedule RR booking (#17768) * fix: improve cancellation email of reschedule RR booking * fix failing test * fix: uses same lucky user when rescheduling RR booking also has a fixed host * chore * chore * better message in the cancelled email * fix * fix * add booker rescheduled in reassigned email to old host * test * add reassigned subtite * fix: use same lucky user when reschedule RR booking has a fixed host * fix: test * fix: subtitle --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> --- .../web/test/utils/bookingScenario/expects.ts | 15 +++++++ packages/emails/email-manager.ts | 43 +++++++++++++------ .../src/templates/BaseScheduledEmail.tsx | 3 ++ .../src/templates/OrganizerCancelledEmail.tsx | 26 +++++++---- .../templates/organizer-cancelled-email.ts | 9 ++-- .../features/bookings/lib/handleNewBooking.ts | 18 +++++++- .../handleNewBooking/test/reschedule.test.ts | 3 +- .../roundRobinManualReassignment.test.ts | 6 +-- .../roundRobinManualReassignment.ts | 14 +++--- .../ee/round-robin/roundRobinReassignment.ts | 14 +++--- 10 files changed, 107 insertions(+), 44 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index e73fc8a893..c859601abd 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -711,10 +711,12 @@ export function expectSuccessfulRoundRobinReschedulingEmails({ emails, newOrganizer, prevOrganizer, + bookerReschedule, }: { emails: Fixtures["emails"]; newOrganizer: { email: string; name: string }; prevOrganizer: { email: string; name: string }; + bookerReschedule?: boolean; }) { if (newOrganizer !== prevOrganizer) { vi.waitFor(() => { @@ -738,6 +740,19 @@ export function expectSuccessfulRoundRobinReschedulingEmails({ `${prevOrganizer.email}` ); }); + + // if booking is rescheduled by booker, old organizer should recieve reassigned emails + if (bookerReschedule) { + vi.waitFor(() => { + expect(emails).toHaveEmail( + { + heading: "event_request_reassigned", + to: `${prevOrganizer.email}`, + }, + `${prevOrganizer.email}` + ); + }); + } } else { vi.waitFor(() => { // organizer should receive rescheduled emails diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index a376865726..ff1ab0698a 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -237,25 +237,44 @@ export const sendRoundRobinCancelledEmailsAndSMS = async ( calEvent: CalendarEvent, members: Person[], eventTypeMetadata?: EventTypeMetadata, - reassignedTo?: { name: string | null; email: string } + reassignedTo?: { name: string | null; email: string; reason?: string } ) => { if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsAndSMSToSend: Promise[] = []; const successfullyReScheduledSMS = new EventCancelledSMS(calEvent); for (const teamMember of members) { - if (!reassignedTo) { - emailsAndSMSToSend.push( - sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember })) - ); - } else { - emailsAndSMSToSend.push( - sendEmail( - () => - new OrganizerReassignedEmail({ calEvent: calendarEvent, teamMember, reassigned: reassignedTo }) - ) - ); + emailsAndSMSToSend.push( + sendEmail( + () => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember, reassigned: reassignedTo }) + ) + ); + + if (teamMember.phoneNumber) { + emailsAndSMSToSend.push(successfullyReScheduledSMS.sendSMSToAttendee(teamMember)); } + } + + await Promise.all(emailsAndSMSToSend); +}; + +export const sendRoundRobinReassignedEmailsAndSMS = async (args: { + calEvent: CalendarEvent; + members: Person[]; + reassignedTo: { name: string | null; email: string }; + eventTypeMetadata?: EventTypeMetadata; +}) => { + const { calEvent, members, reassignedTo, eventTypeMetadata } = args; + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; + const calendarEvent = formatCalEvent(calEvent); + const emailsAndSMSToSend: Promise[] = []; + const successfullyReScheduledSMS = new EventCancelledSMS(calEvent); + for (const teamMember of members) { + emailsAndSMSToSend.push( + sendEmail( + () => new OrganizerReassignedEmail({ calEvent: calendarEvent, teamMember, reassigned: reassignedTo }) + ) + ); if (teamMember.phoneNumber) { emailsAndSMSToSend.push(successfullyReScheduledSMS.sendSMSToAttendee(teamMember)); diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 37056c6829..e990744290 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -107,6 +107,9 @@ export const BaseScheduledEmail = ( } withSpacer /> + {props.reassigned?.reason && ( + + )} )} {props.reassigned && props.reassigned.byUser && ( diff --git a/packages/emails/src/templates/OrganizerCancelledEmail.tsx b/packages/emails/src/templates/OrganizerCancelledEmail.tsx index b16ef43681..2e3848508f 100644 --- a/packages/emails/src/templates/OrganizerCancelledEmail.tsx +++ b/packages/emails/src/templates/OrganizerCancelledEmail.tsx @@ -1,11 +1,19 @@ import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail"; -export const OrganizerCancelledEmail = (props: React.ComponentProps) => ( - -); +export const OrganizerCancelledEmail = (props: React.ComponentProps) => { + const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate; + const title = props.reassigned ? "event_request_reassigned" : "event_request_cancelled"; + const subtitle = props.reassigned ? t("event_reassigned_subtitle") : ""; + const subject = props.reassigned ? "event_reassigned_subject" : "event_cancelled_subject"; + return ( + + ); +}; diff --git a/packages/emails/templates/organizer-cancelled-email.ts b/packages/emails/templates/organizer-cancelled-email.ts index 25ab1a2097..9a102d6bb8 100644 --- a/packages/emails/templates/organizer-cancelled-email.ts +++ b/packages/emails/templates/organizer-cancelled-email.ts @@ -4,10 +4,12 @@ import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; +import type { Reassigned } from "./organizer-scheduled-email"; export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; + const subject = this.reassigned ? "event_reassigned_subject" : "event_cancelled_subject"; return { icalEvent: generateIcsFile({ @@ -17,19 +19,20 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { }), from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`, to: toAddresses.join(","), - subject: `${this.t("event_cancelled_subject", { + subject: `${this.t(subject, { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: await this.getHtml(this.calEvent, this.calEvent.organizer), + html: await this.getHtml(this.calEvent, this.calEvent.organizer, this.reassigned), text: this.getTextBody("event_request_cancelled"), }; } - async getHtml(calEvent: CalendarEvent, organizer: Person) { + async getHtml(calEvent: CalendarEvent, organizer: Person, reassigned: Reassigned | undefined) { return await renderEmail("OrganizerCancelledEmail", { calEvent, attendee: organizer, + reassigned, }); } } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 96f9646585..eed351c7b9 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -80,8 +80,8 @@ import { CreationSource } from "@calcom/prisma/enums"; import { eventTypeAppMetadataOptionalSchema, eventTypeMetaDataSchemaWithTypedApps, + userMetadata as userMetadataSchema, } from "@calcom/prisma/zod-utils"; -import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util"; import type { AdditionalInformation, @@ -1929,7 +1929,21 @@ async function handler( members: newBookedMembers, eventTypeMetadata: eventType.metadata, }); - sendRoundRobinCancelledEmailsAndSMS(cancelledRRHostEvt, cancelledMembers, eventType.metadata); + const reassignedTo = users.find( + (user) => !user.isFixed && newBookedMembers.some((member) => member.email === user.email) + ); + sendRoundRobinCancelledEmailsAndSMS( + cancelledRRHostEvt, + cancelledMembers, + eventType.metadata, + !!reassignedTo + ? { + name: reassignedTo.name, + email: reassignedTo.email, + ...(reqBody.rescheduledBy === bookerEmail && { reason: "Booker Rescheduled" }), + } + : undefined + ); } } else { if (!isDryRun) { diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index 6103d8b892..eb3afff940 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -2006,7 +2006,7 @@ describe("handleNewBooking", () => { }); describe("Team event-type", () => { test( - "should send correct schedule/cancellation emails to hosts when round robin is rescheduled to different host", + "should send correct schedule/cancellation/reassigned emails to hosts when round robin is rescheduled to different host", async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; const booker = getBooker({ @@ -2158,6 +2158,7 @@ describe("handleNewBooking", () => { prevOrganizer: roundRobinHost1, newOrganizer: roundRobinHost2, emails, + bookerReschedule: true, }); }, timeout diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts index ecdda111db..51b2537f96 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts @@ -463,9 +463,9 @@ describe("roundRobinManualReassignment test", () => { const roundRobinManualReassignment = (await import("./roundRobinManualReassignment")).default; await mockEventManagerReschedule(); - const sendRoundRobinCancelledEmailsAndSMSSpy = vi.spyOn( + const sendRoundRobinReassignedEmailsAndSMSSpy = vi.spyOn( await import("@calcom/emails"), - "sendRoundRobinCancelledEmailsAndSMS" + "sendRoundRobinReassignedEmailsAndSMS" ); const testDestinationCalendar = createTestDestinationCalendar(); @@ -521,7 +521,7 @@ describe("roundRobinManualReassignment test", () => { reassignedById: 1, }); - expect(sendRoundRobinCancelledEmailsAndSMSSpy).toHaveBeenCalledTimes(1); + expect(sendRoundRobinReassignedEmailsAndSMSSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index fa337a6217..1ed40527bc 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -3,7 +3,7 @@ import { cloneDeep } from "lodash"; import dayjs from "@calcom/dayjs"; import { - sendRoundRobinCancelledEmailsAndSMS, + sendRoundRobinReassignedEmailsAndSMS, sendRoundRobinScheduledEmailsAndSMS, sendRoundRobinUpdatedEmailsAndSMS, } from "@calcom/emails"; @@ -373,9 +373,9 @@ export const roundRobinManualReassignment = async ({ }; if (previousRRHost && emailsEnabled) { - await sendRoundRobinCancelledEmailsAndSMS( - cancelledEvt, - [ + await sendRoundRobinReassignedEmailsAndSMS({ + calEvent: cancelledEvt, + members: [ { ...previousRRHost, name: previousRRHost.name || "", @@ -384,9 +384,9 @@ export const roundRobinManualReassignment = async ({ language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" }, }, ], - eventType?.metadata as EventTypeMetadata, - { name: newUser.name, email: newUser.email } - ); + reassignedTo: { name: newUser.name, email: newUser.email }, + eventTypeMetadata: eventType?.metadata as EventTypeMetadata, + }); } if (hasOrganizerChanged) { diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index a7f1606015..476a466fd6 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -4,7 +4,7 @@ import { cloneDeep } from "lodash"; import { OrganizerDefaultConferencingAppType, getLocationValueForDB } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import { - sendRoundRobinCancelledEmailsAndSMS, + sendRoundRobinReassignedEmailsAndSMS, sendRoundRobinScheduledEmailsAndSMS, sendRoundRobinUpdatedEmailsAndSMS, } from "@calcom/emails"; @@ -420,9 +420,9 @@ export const roundRobinReassignment = async ({ } if (emailsEnabled) { - await sendRoundRobinCancelledEmailsAndSMS( - cancelledRRHostEvt, - [ + await sendRoundRobinReassignedEmailsAndSMS({ + calEvent: cancelledRRHostEvt, + members: [ { ...previousRRHost, name: previousRRHost.name || "", @@ -431,9 +431,9 @@ export const roundRobinReassignment = async ({ language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" }, }, ], - eventType?.metadata as EventTypeMetadata, - { name: reassignedRRHost.name, email: reassignedRRHost.email } - ); + reassignedTo: { name: reassignedRRHost.name, email: reassignedRRHost.email }, + eventTypeMetadata: eventType?.metadata as EventTypeMetadata, + }); } }