fix: Location not changing when rescheduling (#20404)

* fix: location not chnging

* update

* update

* Update EventManager.ts

* update

* update

* Update reschedule.test.ts

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Anik Dhabal Babu
2025-04-21 14:45:59 +01:00
committed by GitHub
co-authored by Tushar Bhatt sean-brydon Peer Richelsen
parent d99ecde0e2
commit 7dfb71ca30
4 changed files with 193 additions and 30 deletions
@@ -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<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
let referencesToCreate: PartialReference[] = [];
@@ -1257,7 +1260,6 @@ async function handler(
input: {
bookerEmail,
rescheduleReason,
changedOrganizer,
smsReminderNumber,
responses,
},
@@ -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;
@@ -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(
+32 -23
View File
@@ -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<EventResult<Event>> = [];
const bookingReferenceChangedOrganizer: Array<PartialReference> = [];
const updatedBookingReferences: Array<PartialReference> = [];
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],
};
}