From 98ef45be18ed5e7c6dec09f6bcd8f3a56d12de73 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:08:46 +0530 Subject: [PATCH] fix: prevent calendar event creation for unconfirmed round robin reschedules (#26719) * fix rr booking * fix test * use actual prisma * fix * Refactor --- .../features/bookings/lib/EventManager.ts | 25 ++- .../test/team-bookings/round-robin.test.ts | 153 ++++++++++++++++++ 2 files changed, 171 insertions(+), 7 deletions(-) diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/features/bookings/lib/EventManager.ts index 1726fb4572..fafc7c8fea 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/features/bookings/lib/EventManager.ts @@ -677,13 +677,24 @@ export default class EventManager { const shouldUpdateBookingReferences = !!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired; - if (evt.requiresConfirmation && !skipDeleteEventsAndMeetings) { - log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking"); - // 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. - await this.deleteEventsAndMeetings({ - event: { ...event, destinationCalendar: previousHostDestinationCalendar }, - bookingReferences: booking.references, - }); + if (evt.requiresConfirmation) { + if (!skipDeleteEventsAndMeetings) { + log.debug( + "RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking" + ); + // 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. + await this.deleteEventsAndMeetings({ + event: { + ...event, + destinationCalendar: previousHostDestinationCalendar, + }, + bookingReferences: booking.references, + }); + } else { + log.debug( + "RescheduleRequiresConfirmation: Skipping deletion of Event and Meeting due to skipDeleteEventsAndMeetings flag" + ); + } } else { if (changedOrganizer) { if (!skipDeleteEventsAndMeetings) { diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts index 9e52b9f368..b4017dec06 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts @@ -18,6 +18,7 @@ import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTe import { describe, test, vi, expect } from "vitest"; +import prisma from "@calcom/prisma"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; @@ -1086,4 +1087,156 @@ describe("Round Robin handleNewBooking", () => { }); }); }); + + describe("Round Robin with requiresConfirmation", () => { + test( + "should not create calendar events for unconfirmed round robin bookings on first booking and reschedule", + async () => { + const handleNewBooking = getNewBookingHandler(); + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const roundRobinHost1 = getOrganizer({ + name: "RR Host 1", + email: "rrhost1@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "rrhost1@google-calendar.com", + }, + }); + + const roundRobinHost2 = getOrganizer({ + name: "RR Host 2", + email: "rrhost2@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "rrhost2@google-calendar.com", + }, + }); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 15, + length: 15, + requiresConfirmation: true, + schedulingType: SchedulingType.ROUND_ROBIN, + users: [ + { + id: 101, + }, + { + id: 102, + }, + ], + hosts: [ + { userId: 101, isFixed: false }, + { userId: 102, isFixed: false }, + ], + schedule: TestData.schedules.IstWorkHours, + }, + ], + organizer: roundRobinHost1, + usersApartFromOrganizer: [roundRobinHost2], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + // Mock calendar - we should NOT see calendar events created for unconfirmed bookings + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + uid: "MOCK_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + // First booking with first host - should be PENDING and NO calendar events + const firstBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + user: roundRobinHost1.name, + start: `${plus1DateString}T05:00:00.000Z`, + end: `${plus1DateString}T05:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const firstBooking = await handleNewBooking({ + bookingData: firstBookingData, + }); + + // Verify first booking is PENDING + expect(firstBooking.status).toBe(BookingStatus.PENDING); + + // Verify first booking has NO calendar references (no calendar events created) + const firstBookingInDb = await prisma.booking.findUnique({ + where: { + id: firstBooking.id, + }, + include: { + references: true, + }, + }); + + expect(firstBookingInDb?.references).toHaveLength(0); + expect(firstBookingInDb?.status).toBe(BookingStatus.PENDING); + + // Now reschedule with second host - should still be PENDING and NO calendar events + const rescheduleBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + user: roundRobinHost2.name, + rescheduleUid: firstBooking.uid, + start: `${plus1DateString}T06:00:00.000Z`, + end: `${plus1DateString}T06:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + rescheduledBy: booker.email, + }, + }); + + const rescheduledBooking = await handleNewBooking({ + bookingData: rescheduleBookingData, + }); + + // Verify rescheduled booking is still PENDING + expect(rescheduledBooking.status).toBe(BookingStatus.PENDING); + + // Verify rescheduled booking has NO calendar references (no calendar events created) + // This is the key fix: rescheduling unconfirmed bookings should NOT create calendar events + const rescheduledBookingInDb = await prisma.booking.findUnique({ + where: { + id: rescheduledBooking.id, + }, + include: { + references: true, + }, + }); + + expect(rescheduledBookingInDb?.references).toHaveLength(0); + expect(rescheduledBookingInDb?.status).toBe(BookingStatus.PENDING); + } + ); + }); });