diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 9a18537ecf..44b6825c42 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1218,6 +1218,9 @@ async function handler( eventType.schedulingType === SchedulingType.ROUND_ROBIN && originalRescheduledBooking.userId !== evt.organizer.id; + const isLocationChanged = + !!originalRescheduledBooking && originalRescheduledBooking.location !== evt.location; + let results: EventResult[] = []; let referencesToCreate: PartialReference[] = []; @@ -1257,7 +1260,6 @@ async function handler( input: { bookerEmail, rescheduleReason, - changedOrganizer, smsReminderNumber, responses, }, diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index 303812da5a..35434cd634 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -43,7 +43,6 @@ type CreateBookingParams = { input: { bookerEmail: AwaitedBookingData["email"]; rescheduleReason: AwaitedBookingData["rescheduleReason"]; - changedOrganizer: boolean; smsReminderNumber: AwaitedBookingData["smsReminderNumber"]; responses: ReqBodyWithEnd["responses"] | null; }; @@ -55,14 +54,12 @@ type CreateBookingParams = { function updateEventDetails( evt: CalendarEvent, - originalRescheduledBooking: OriginalRescheduledBooking | null, - changedOrganizer: boolean + originalRescheduledBooking: OriginalRescheduledBooking | null ) { if (originalRescheduledBooking) { evt.title = originalRescheduledBooking?.title || evt.title; evt.description = originalRescheduledBooking?.description || evt.description; - evt.location = originalRescheduledBooking?.location || evt.location; - evt.location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt.location; + evt.location = evt.location || originalRescheduledBooking?.location; } } @@ -88,7 +85,7 @@ export async function createBooking({ creationSource, tracking, }: CreateBookingParams & { rescheduledBy: string | undefined }) { - updateEventDetails(evt, originalRescheduledBooking, input.changedOrganizer); + updateEventDetails(evt, originalRescheduledBooking); const associatedBookingForFormResponse = routingFormResponseId ? await getAssociatedBookingForFormResponse(routingFormResponseId) : null; diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index 63f3754801..c3bb7489b0 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -1848,6 +1848,161 @@ describe("handleNewBooking", () => { timeout ); }); + test( + `should reschedule a booking successfully with a different location option (change to Cal Video) + 1. Should cancel the existing booking + 2. Should create a new booking with the new location + 3. Should send appropriate notifications + 4. Should update/create necessary video conference links + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential(), getGoogleMeetCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP"; + const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`; + + // Original booking has a different location (Google Meet) + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + workflows: [ + { + userId: organizer.id, + trigger: "RESCHEDULE_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeOn: [1], + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 15, + length: 15, + locations: [{ type: BookingLocations.GoogleMeet }, { type: BookingLocations.CalVideo }], + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + uid: uidOfBookingToBeRescheduled, + eventTypeId: 1, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:15:00.000Z`, + location: BookingLocations.GoogleMeet, + metadata: { + videoCallUrl: "https://meet.google.com/existing-meeting", + }, + references: [ + { + type: appStoreMetadata.googlevideo.type, + uid: "GOOGLE_MEET_ID", + meetingId: "GOOGLE_MEET_ID", + meetingPassword: "", + meetingUrl: "https://meet.google.com/existing-meeting", + }, + ], + iCalUID, + }, + ], + organizer, + apps: [TestData.apps["daily-video"], TestData.apps["google-meet"]], + }) + ); + + // Mock video meeting creation for Cal Video + const videoMock = mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + // Request data for rescheduling - with Cal Video as the new location + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + rescheduleUid: uidOfBookingToBeRescheduled, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + // Verify that previous booking gets cancelled + await expectBookingToBeInDatabase({ + uid: uidOfBookingToBeRescheduled, + status: BookingStatus.CANCELLED, + }); + + // Validate new booking time and location + expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T04:00:00.000Z`); + expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T04:15:00.000Z`); + expect(createdBooking.location).toBe(BookingLocations.CalVideo); + + // Verify booking details in database + await expectBookingInDBToBeRescheduledFromTo({ + from: { + uid: uidOfBookingToBeRescheduled, + location: BookingLocations.GoogleMeet, + }, + to: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + location: BookingLocations.CalVideo, + responses: expect.objectContaining({ + email: booker.email, + name: booker.name, + }), + references: [ + { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + ], + }, + }); + }, + timeout + ); }); describe("Team event-type", () => { test( diff --git a/packages/lib/EventManager.ts b/packages/lib/EventManager.ts index f287914e8a..facf3dbf65 100644 --- a/packages/lib/EventManager.ts +++ b/packages/lib/EventManager.ts @@ -325,7 +325,7 @@ export default class EventManager { meetingPassword: result.createdEvent?.password, meetingUrl: result.createdEvent?.url, externalCalendarId: result.externalId, - credentialId: result.credentialId ?? undefined, + ...(result.credentialId && result.credentialId > 0 ? { credentialId: result.credentialId } : {}), }; }); @@ -457,6 +457,7 @@ export default class EventManager { id: true, userId: true, attendees: true, + location: true, references: { where: { deleted: null, @@ -490,7 +491,8 @@ export default class EventManager { } const results: Array> = []; - const bookingReferenceChangedOrganizer: Array = []; + const updatedBookingReferences: Array = []; + const isLocationChanged = evt.location && booking.location && evt.location !== booking.location; if (evt.requiresConfirmation) { log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking"); @@ -511,31 +513,37 @@ export default class EventManager { const createdEvent = await this.create(originalEvt); results.push(...createdEvent.results); - bookingReferenceChangedOrganizer.push(...createdEvent.referencesToCreate); + updatedBookingReferences.push(...createdEvent.referencesToCreate); } else { // If the reschedule doesn't require confirmation, we can "update" the events and meetings to new time. - const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null; - // If and only if event type is a dedicated meeting, update the dedicated video meeting. - if (isDedicated) { - const result = await this.updateVideoEvent(evt, booking); - const [updatedEvent] = Array.isArray(result.updatedEvent) - ? result.updatedEvent - : [result.updatedEvent]; + if (isLocationChanged) { + const updatedLocation = await this.updateLocation(evt, booking); + results.push(...updatedLocation.results); + updatedBookingReferences.push(...updatedLocation.referencesToCreate); + } else { + const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null; + // If and only if event type is a dedicated meeting, update the dedicated video meeting. + if (isDedicated) { + const result = await this.updateVideoEvent(evt, booking); + const [updatedEvent] = Array.isArray(result.updatedEvent) + ? result.updatedEvent + : [result.updatedEvent]; - if (updatedEvent) { - evt.videoCallData = updatedEvent; - evt.location = updatedEvent.url; + if (updatedEvent) { + evt.videoCallData = updatedEvent; + evt.location = updatedEvent.url; + } + results.push(result); } - results.push(result); - } - const bookingCalendarReference = booking.references.find((reference) => - reference.type.includes("_calendar") - ); - // There was a case that booking didn't had any reference and we don't want to throw error on function - if (bookingCalendarReference) { - // Update all calendar events. - results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId))); + const bookingCalendarReference = booking.references.find((reference) => + reference.type.includes("_calendar") + ); + // There was a case that booking didn't had any reference and we don't want to throw error on function + if (bookingCalendarReference) { + // Update all calendar events. + results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId))); + } } results.push(...(await this.updateAllCRMEvents(evt, booking))); @@ -560,7 +568,8 @@ export default class EventManager { return { results, - referencesToCreate: changedOrganizer ? bookingReferenceChangedOrganizer : [...booking.references], + referencesToCreate: + changedOrganizer || isLocationChanged ? updatedBookingReferences : [...booking.references], }; }