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:
co-authored by
Tushar Bhatt
sean-brydon
Peer Richelsen
parent
d99ecde0e2
commit
7dfb71ca30
@@ -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(
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user