Skip to content

Commit 18a0e2d

Browse files
fix: event not removed from calendar when reassigned (calcom#23099)
* fix: event not removed from calendar when reassign * fix test * tweak * add test * test: add comprehensive test for calendar event deletion during round robin reassignment - Remove existing tests and replace with focused test for organizer change scenario - Verify deleteEventsAndMeetings is called when organizer changes - Mock reschedule method to simulate real behavior while tracking deletion calls - Test verifies original host's calendar events are properly deleted - Ensures booking reassignment and email notifications work correctly Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * update * revert roundRobinReassignment.test.ts * revert --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 87c89a9 commit 18a0e2d

3 files changed

Lines changed: 160 additions & 3 deletions

File tree

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,8 @@ async function handler(
14301430
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
14311431
originalRescheduledBooking.userId !== evt.organizer.id;
14321432

1433+
const skipDeleteEventsAndMeetings = changedOrganizer;
1434+
14331435
const isBookingRequestedReschedule =
14341436
!!originalRescheduledBooking &&
14351437
!!originalRescheduledBooking.rescheduled &&
@@ -1702,7 +1704,8 @@ async function handler(
17021704
undefined,
17031705
changedOrganizer,
17041706
previousHostDestinationCalendar,
1705-
isBookingRequestedReschedule
1707+
isBookingRequestedReschedule,
1708+
skipDeleteEventsAndMeetings
17061709
);
17071710
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
17081711
// to the default description when we are sending the emails.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Tests for calendar event deletion during round robin organizer change operations.
3+
* Covers scenarios where events need to be deleted from original hosts and
4+
* credential handling during organizer changes.
5+
*/
6+
import {
7+
getDate,
8+
createBookingScenario,
9+
getScenarioData,
10+
getMockBookingAttendee,
11+
TestData,
12+
getOrganizer,
13+
getGoogleCalendarCredential,
14+
mockCalendarToHaveNoBusySlots,
15+
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
16+
import {
17+
expectBookingToBeInDatabase,
18+
expectSuccessfulRoundRobinReschedulingEmails,
19+
} from "@calcom/web/test/utils/bookingScenario/expects";
20+
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
21+
22+
import { describe, expect } from "vitest";
23+
24+
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
25+
import { SchedulingType, BookingStatus } from "@calcom/prisma/enums";
26+
import { test } from "@calcom/web/test/fixtures/fixtures";
27+
28+
describe("roundRobinReassignment test", () => {
29+
setupAndTeardown();
30+
31+
test("should delete calendar events from original host when round robin reassignment changes organizer", async ({
32+
emails,
33+
}) => {
34+
const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
35+
36+
const originalHost = getOrganizer({
37+
name: "Original Host",
38+
email: "originalhost@example.com",
39+
id: 101,
40+
schedules: [TestData.schedules.IstWorkHours],
41+
credentials: [getGoogleCalendarCredential()],
42+
selectedCalendars: [TestData.selectedCalendars.google],
43+
});
44+
45+
const newHost = getOrganizer({
46+
name: "New Host",
47+
email: "newhost@example.com",
48+
id: 102,
49+
schedules: [TestData.schedules.IstWorkHours],
50+
credentials: [getGoogleCalendarCredential()],
51+
selectedCalendars: [TestData.selectedCalendars.google],
52+
});
53+
54+
const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
55+
const bookingToReassignUid = "booking-to-reassign-with-deletion";
56+
57+
await createBookingScenario(
58+
getScenarioData({
59+
eventTypes: [
60+
{
61+
id: 1,
62+
slug: "round-robin-event",
63+
schedulingType: SchedulingType.ROUND_ROBIN,
64+
length: 45,
65+
users: [{ id: 101 }, { id: 102 }],
66+
hosts: [
67+
{ userId: 101, isFixed: false },
68+
{ userId: 102, isFixed: false },
69+
],
70+
},
71+
],
72+
bookings: [
73+
{
74+
id: 123,
75+
eventTypeId: 1,
76+
userId: originalHost.id,
77+
uid: bookingToReassignUid,
78+
status: BookingStatus.ACCEPTED,
79+
startTime: `${dateStringPlusOne}T05:00:00.000Z`,
80+
endTime: `${dateStringPlusOne}T05:45:00.000Z`,
81+
attendees: [
82+
getMockBookingAttendee({
83+
id: 2,
84+
name: "attendee",
85+
email: "attendee@test.com",
86+
locale: "en",
87+
timeZone: "Asia/Kolkata",
88+
}),
89+
],
90+
references: [
91+
{
92+
id: 1,
93+
type: appStoreMetadata.googlecalendar.type,
94+
uid: "ORIGINAL_EVENT_ID",
95+
meetingId: "ORIGINAL_EVENT_ID",
96+
meetingPassword: null,
97+
meetingUrl: null,
98+
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
99+
credentialId: 1,
100+
deleted: null,
101+
},
102+
],
103+
},
104+
],
105+
organizer: originalHost,
106+
usersApartFromOrganizer: [newHost],
107+
apps: [TestData.apps["google-calendar"]],
108+
})
109+
);
110+
111+
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
112+
create: { uid: "NEW_EVENT_ID" },
113+
update: { uid: "UPDATED_EVENT_ID" },
114+
});
115+
116+
await roundRobinReassignment({
117+
bookingId: 123,
118+
});
119+
120+
// Verify that calendar deletion occurred (may be called multiple times due to duplicate references)
121+
expect(calendarMock.deleteEventCalls.length).toBeGreaterThanOrEqual(1);
122+
const deleteCall = calendarMock.deleteEventCalls[0];
123+
expect(deleteCall.args.uid).toBe("ORIGINAL_EVENT_ID");
124+
expect(deleteCall.args.externalCalendarId).toBe("MOCK_EXTERNAL_CALENDAR_ID");
125+
expect(deleteCall.args.event.organizer.email).toBe(newHost.email); // Current implementation uses new host credentials
126+
expect(deleteCall.args.event.uid).toBe(bookingToReassignUid);
127+
128+
// Verify that creation occurred with new host credentials
129+
expect(calendarMock.createEventCalls.length).toBe(1);
130+
const createCall = calendarMock.createEventCalls[0];
131+
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
132+
133+
// Verify the booking was reassigned to the new host
134+
expectBookingToBeInDatabase({
135+
uid: bookingToReassignUid,
136+
userId: newHost.id,
137+
});
138+
139+
expectSuccessfulRoundRobinReschedulingEmails({
140+
prevOrganizer: originalHost,
141+
newOrganizer: newHost,
142+
emails,
143+
});
144+
});
145+
});

packages/lib/EventManager.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,8 @@ export default class EventManager {
570570
newBookingId?: number,
571571
changedOrganizer?: boolean,
572572
previousHostDestinationCalendar?: DestinationCalendar[] | null,
573-
isBookingRequestedReschedule?: boolean
573+
isBookingRequestedReschedule?: boolean,
574+
skipDeleteEventsAndMeetings?: boolean
574575
): Promise<CreateUpdateResult> {
575576
const originalEvt = processLocation(event);
576577
const evt = cloneDeep(originalEvt);
@@ -637,7 +638,7 @@ export default class EventManager {
637638
const shouldUpdateBookingReferences =
638639
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
639640

640-
if (evt.requiresConfirmation && !changedOrganizer) {
641+
if (evt.requiresConfirmation && !skipDeleteEventsAndMeetings) {
641642
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
642643
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
643644
await this.deleteEventsAndMeetings({
@@ -646,6 +647,14 @@ export default class EventManager {
646647
});
647648
} else {
648649
if (changedOrganizer) {
650+
if (!skipDeleteEventsAndMeetings) {
651+
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
652+
await this.deleteEventsAndMeetings({
653+
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
654+
bookingReferences: booking.references,
655+
});
656+
}
657+
649658
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
650659
const createdEvent = await this.create(originalEvt);
651660
results.push(...createdEvent.results);

0 commit comments

Comments
 (0)