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>
This commit is contained in:
Kartik Saini
2025-08-27 13:28:33 -03:00
committed by GitHub
co-authored by Anik Dhabal Babu
parent a4915184cd
commit afea724e4d
10 changed files with 107 additions and 44 deletions
@@ -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
+31 -12
View File
@@ -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<unknown>[] = [];
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<unknown>[] = [];
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));
@@ -107,6 +107,9 @@ export const BaseScheduledEmail = (
}
withSpacer
/>
{props.reassigned?.reason && (
<Info label={t("reason")} description={props.reassigned.reason} withSpacer />
)}
</>
)}
{props.reassigned && props.reassigned.byUser && (
@@ -1,11 +1,19 @@
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
export const OrganizerCancelledEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title="event_request_cancelled"
headerType="xCircle"
subject="event_cancelled_subject"
callToAction={null}
{...props}
/>
);
export const OrganizerCancelledEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
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 (
<OrganizerScheduledEmail
title={title}
subtitle={subtitle}
headerType="xCircle"
subject={subject}
callToAction={null}
reassigned={props.reassigned}
{...props}
/>
);
};
@@ -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<Record<string, unknown>> {
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,
});
}
}
@@ -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) {
@@ -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
@@ -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);
});
});
@@ -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) {
@@ -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,
});
}
}