diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts index 88bc6d542e..ea8e76a68d 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts @@ -247,4 +247,78 @@ describe("roundRobinManualReassignment test", () => { expect(attendees.some((attendee) => attendee.email === currentRRHost.email)).toBe(false); expect(attendees.some((attendee) => attendee.email === newHost.email)).toBe(true); }); + + test("sends cancellation email to previous RR host when reassigning", async ({ emails }) => { + const roundRobinManualReassignment = (await import("./roundRobinManualReassignment")).default; + const EventManager = (await import("@calcom/core/EventManager")).default; + + const eventManagerSpy = vi.spyOn(EventManager.prototype as any, "reschedule"); + eventManagerSpy.mockResolvedValue({ referencesToCreate: [] }); + + const sendRoundRobinCancelledEmailsAndSMSSpy = vi.spyOn( + await import("@calcom/emails"), + "sendRoundRobinCancelledEmailsAndSMS" + ); + + const users = testUsers; + const originalHost = users[0]; + const previousRRHost = users[1]; + const newHost = users[2]; + + const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 }); + + const bookingToReassignUid = "booking-to-reassign-with-previous-rr-host"; + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slug: "round-robin-event", + schedulingType: SchedulingType.ROUND_ROBIN, + length: 45, + users: users.map((user) => ({ id: user.id })), + hosts: users.map((user) => ({ userId: user.id, isFixed: false })), + }, + ], + bookings: [ + { + id: 124, + eventTypeId: 1, + userId: originalHost.id, + uid: bookingToReassignUid, + status: BookingStatus.ACCEPTED, + startTime: `${dateStringPlusOne}T05:00:00.000Z`, + endTime: `${dateStringPlusOne}T05:15:00.000Z`, + attendees: [ + getMockBookingAttendee({ + id: 2, + name: "attendee", + email: "attendee@test.com", + locale: "en", + timeZone: "Asia/Kolkata", + }), + getMockBookingAttendee({ + id: previousRRHost.id, + name: previousRRHost.name, + email: previousRRHost.email, + locale: "en", + timeZone: previousRRHost.timeZone, + }), + ], + }, + ], + organizer: originalHost, + usersApartFromOrganizer: users.slice(1), + }) + ); + + await roundRobinManualReassignment({ + bookingId: 124, + newUserId: newHost.id, + orgId: null, + }); + + expect(sendRoundRobinCancelledEmailsAndSMSSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 94dd8484aa..7117dbe693 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -104,6 +104,24 @@ export const roundRobinManualReassignment = async ({ const newUserT = await getTranslation(newUser.locale || "en", "common"); const originalOrganizerT = await getTranslation(originalOrganizer.locale || "en", "common"); + const roundRobinHosts = eventType.hosts.filter((host) => !host.isFixed); + + const attendeeEmailsSet = new Set(booking.attendees.map((attendee) => attendee.email)); + + // Find the current round robin host assigned + const previousRRHost = (() => { + for (const host of roundRobinHosts) { + if (host.user.id === booking.userId) { + return host.user; + } + if (attendeeEmailsSet.has(host.user.email)) { + return host.user; + } + } + })(); + + const previousRRHostT = await getTranslation(previousRRHost?.locale || "en", "common"); + if (hasOrganizerChanged) { const bookingResponses = booking.responses; const responseSchema = getBookingResponsesSchema({ @@ -166,24 +184,38 @@ export const roundRobinManualReassignment = async ({ hasOrganizerChanged, }); + const organizer = hasOrganizerChanged ? newUser : booking.user ?? newUser; + + const organizerT = await getTranslation(organizer?.locale || "en", "common"); + const teamMembers = await getTeamMembers({ eventTypeHosts, attendees: booking.attendees, - organizer: newUser, - previousHost: originalOrganizer, + organizer, + previousHost: previousRRHost || null, reassignedHost: newUser, }); - const attendeePromises = booking.attendees.map(async (attendee) => ({ - email: attendee.email, - name: attendee.name, - timeZone: attendee.timeZone, - language: { - translate: await getTranslation(attendee.locale ?? "en", "common"), - locale: attendee.locale ?? "en", - }, - phoneNumber: attendee.phoneNumber || undefined, - })); + const attendeePromises = []; + for (const attendee of booking.attendees) { + if ( + attendee.email === newUser.email || + attendee.email === previousRRHost?.email || + teamMembers.some((member) => member.email === attendee.email) + ) { + continue; + } + + attendeePromises.push( + getTranslation(attendee.locale ?? "en", "common").then((tAttendee) => ({ + email: attendee.email, + name: attendee.name, + timeZone: attendee.timeZone, + language: { translate: tAttendee, locale: attendee.locale ?? "en" }, + phoneNumber: attendee.phoneNumber || undefined, + })) + ); + } const attendeeList = await Promise.all(attendeePromises); @@ -194,10 +226,10 @@ export const roundRobinManualReassignment = async ({ startTime: dayjs(booking.startTime).utc().format(), endTime: dayjs(booking.endTime).utc().format(), organizer: { - email: newUser.email, - name: newUser.name || "", - timeZone: newUser.timeZone, - language: { translate: newUserT, locale: newUser.locale || "en" }, + email: organizer.email, + name: organizer.name || "", + timeZone: organizer.timeZone, + language: { translate: organizerT, locale: organizer.locale || "en" }, }, attendees: attendeeList, uid: booking.uid, @@ -242,8 +274,10 @@ export const roundRobinManualReassignment = async ({ newReferencesToCreate, }); + const { cancellationReason, ...evtWithoutCancellationReason } = evt; + // Send emails - await sendRoundRobinScheduledEmailsAndSMS(evt, [ + await sendRoundRobinScheduledEmailsAndSMS(evtWithoutCancellationReason, [ { ...newUser, name: newUser.name || "", @@ -262,19 +296,21 @@ export const roundRobinManualReassignment = async ({ language: { translate: originalOrganizerT, locale: originalOrganizer.locale || "en" }, }; - await sendRoundRobinCancelledEmailsAndSMS( - cancelledEvt, - [ - { - ...originalOrganizer, - name: originalOrganizer.name || "", - username: originalOrganizer.username || "", - timeFormat: getTimeFormatStringFromUserTimeFormat(originalOrganizer.timeFormat), - language: { translate: originalOrganizerT, locale: originalOrganizer.locale || "en" }, - }, - ], - eventType?.metadata as EventTypeMetadata - ); + if (previousRRHost) { + await sendRoundRobinCancelledEmailsAndSMS( + cancelledEvt, + [ + { + ...previousRRHost, + name: previousRRHost.name || "", + username: previousRRHost.username || "", + timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat), + language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" }, + }, + ], + eventType?.metadata as EventTypeMetadata + ); + } if (hasOrganizerChanged) { // Handle changing workflows with organizer diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index 80a6409093..b4fdc71eb4 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -151,17 +151,6 @@ export const roundRobinReassignment = async ({ reassignedHost: reassignedRRHost, }); - // Assume the RR host was labelled as a team member - if (reassignedRRHost.email !== organizer.email) { - teamMembers.push({ - id: reassignedRRHost.id, - email: reassignedRRHost.email, - name: reassignedRRHost.name || "", - timeZone: reassignedRRHost.timeZone, - language: { translate: reassignedRRHostT, locale: reassignedRRHost.locale ?? "en" }, - }); - } - const attendeePromises = []; for (const attendee of booking.attendees) { if (