fix: Duplicate Calendar Invites on rescheduling an accepted booking that requires confirmation (#11827)
parent
e3a9e61046
commit
db059d84c3
|
@ -2,6 +2,7 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store";
|
|||
import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n";
|
||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { BookingReference, Attendee } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
|
@ -111,17 +112,10 @@ type InputBooking = {
|
|||
title?: string;
|
||||
status: BookingStatus;
|
||||
attendees?: { email: string }[];
|
||||
references?: {
|
||||
type: string;
|
||||
uid: string;
|
||||
meetingId?: string;
|
||||
meetingPassword?: string;
|
||||
meetingUrl?: string;
|
||||
bookingId?: number;
|
||||
externalCalendarId?: string;
|
||||
deleted?: boolean;
|
||||
credentialId?: number;
|
||||
}[];
|
||||
references?: (Omit<ReturnType<typeof getMockBookingReference>, "credentialId"> & {
|
||||
// TODO: Make sure that all references start providing credentialId and then remove this intersection of optional credentialId
|
||||
credentialId?: number | null;
|
||||
})[];
|
||||
};
|
||||
|
||||
export const Timezones = {
|
||||
|
@ -267,15 +261,17 @@ async function addBookingsToDb(
|
|||
references: any[];
|
||||
})[]
|
||||
) {
|
||||
log.silly("TestData: Creating Bookings", JSON.stringify(bookings));
|
||||
await prismock.booking.createMany({
|
||||
data: bookings,
|
||||
});
|
||||
log.silly(
|
||||
"TestData: Booking as in DB",
|
||||
"TestData: Bookings as in DB",
|
||||
JSON.stringify({
|
||||
bookings: await prismock.booking.findMany({
|
||||
include: {
|
||||
references: true,
|
||||
attendees: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
@ -318,6 +314,15 @@ async function addBookings(bookings: InputBooking[]) {
|
|||
},
|
||||
};
|
||||
}
|
||||
if (booking.attendees) {
|
||||
bookingCreate.attendees = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
createMany: {
|
||||
data: booking.attendees,
|
||||
},
|
||||
};
|
||||
}
|
||||
return bookingCreate;
|
||||
})
|
||||
);
|
||||
|
@ -839,6 +844,8 @@ export function mockCalendar(
|
|||
const createEventCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const updateEventCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const deleteEventCalls: any[] = [];
|
||||
const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata];
|
||||
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({
|
||||
lib: {
|
||||
|
@ -888,6 +895,11 @@ export function mockCalendar(
|
|||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
deleteEvent: async (...rest: any[]) => {
|
||||
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
deleteEventCalls.push(rest);
|
||||
},
|
||||
getAvailability: async (): Promise<EventBusyDate[]> => {
|
||||
if (calendarData?.getAvailabilityCrash) {
|
||||
throw new Error("MockCalendarService.getAvailability fake error");
|
||||
|
@ -902,6 +914,7 @@ export function mockCalendar(
|
|||
});
|
||||
return {
|
||||
createEventCalls,
|
||||
deleteEventCalls,
|
||||
updateEventCalls,
|
||||
};
|
||||
}
|
||||
|
@ -952,11 +965,13 @@ export function mockVideoApp({
|
|||
password: "MOCK_PASS",
|
||||
url: `http://mock-${metadataLookupKey}.example.com`,
|
||||
};
|
||||
log.silly("mockSuccessfulVideoMeetingCreation", JSON.stringify({ metadataLookupKey, appStoreLookupKey }));
|
||||
log.silly("mockVideoApp", JSON.stringify({ metadataLookupKey, appStoreLookupKey }));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createMeetingCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const updateMeetingCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const deleteMeetingCalls: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => {
|
||||
|
@ -998,15 +1013,19 @@ export function mockVideoApp({
|
|||
if (!calEvent.organizer) {
|
||||
throw new Error("calEvent.organizer is not defined");
|
||||
}
|
||||
log.silly(
|
||||
"mockSuccessfulVideoMeetingCreation.updateMeeting",
|
||||
JSON.stringify({ bookingRef, calEvent })
|
||||
);
|
||||
log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent }));
|
||||
return Promise.resolve({
|
||||
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
|
||||
...videoMeetingData,
|
||||
});
|
||||
},
|
||||
deleteMeeting: async (...rest: any[]) => {
|
||||
log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
|
||||
deleteMeetingCalls.push({
|
||||
credential,
|
||||
args: rest,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -1016,6 +1035,7 @@ export function mockVideoApp({
|
|||
return {
|
||||
createMeetingCalls,
|
||||
updateMeetingCalls,
|
||||
deleteMeetingCalls,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1154,6 +1174,31 @@ export function getExpectedCalEventForBookingRequest({
|
|||
};
|
||||
}
|
||||
|
||||
export function getMockBookingReference(
|
||||
bookingReference: Partial<BookingReference> & Pick<BookingReference, "type" | "uid" | "credentialId">
|
||||
) {
|
||||
let credentialId = bookingReference.credentialId;
|
||||
if (bookingReference.type === appStoreMetadata.dailyvideo.type) {
|
||||
// Right now we seems to be storing credentialId for `dailyvideo` in BookingReference as null. Another possible value is 0 in there.
|
||||
credentialId = null;
|
||||
log.debug("Ensuring null credentialId for dailyvideo");
|
||||
}
|
||||
return {
|
||||
...bookingReference,
|
||||
credentialId,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockBookingAttendee(attendee: Omit<Attendee, "bookingId">) {
|
||||
return {
|
||||
id: attendee.id,
|
||||
timeZone: attendee.timeZone,
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
locale: attendee.locale,
|
||||
};
|
||||
}
|
||||
|
||||
export const enum BookingLocations {
|
||||
CalVideo = "integrations:daily",
|
||||
ZoomVideo = "integrations:zoom",
|
||||
|
|
|
@ -624,6 +624,31 @@ export function expectSuccessfulCalendarEventUpdationInCalendar(
|
|||
expect(externalId).toBe(expected.externalCalendarId);
|
||||
}
|
||||
|
||||
export function expectSuccessfulCalendarEventDeletionInCalendar(
|
||||
calendarMock: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createEventCalls: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateEventCalls: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteEventCalls: any[];
|
||||
},
|
||||
expected: {
|
||||
externalCalendarId: string;
|
||||
calEvent: Partial<CalendarEvent>;
|
||||
uid: string;
|
||||
}
|
||||
) {
|
||||
expect(calendarMock.deleteEventCalls.length).toBe(1);
|
||||
const call = calendarMock.deleteEventCalls[0];
|
||||
const uid = call[0];
|
||||
const calendarEvent = call[1];
|
||||
const externalId = call[2];
|
||||
expect(uid).toBe(expected.uid);
|
||||
expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent));
|
||||
expect(externalId).toBe(expected.externalCalendarId);
|
||||
}
|
||||
|
||||
export function expectSuccessfulVideoMeetingCreation(
|
||||
videoMock: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -669,6 +694,26 @@ export function expectSuccessfulVideoMeetingUpdationInCalendar(
|
|||
expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent));
|
||||
}
|
||||
|
||||
export function expectSuccessfulVideoMeetingDeletionInCalendar(
|
||||
videoMock: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createMeetingCalls: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateMeetingCalls: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteMeetingCalls: any[];
|
||||
},
|
||||
expected: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
bookingRef: any;
|
||||
}
|
||||
) {
|
||||
expect(videoMock.deleteMeetingCalls.length).toBe(1);
|
||||
const call = videoMock.deleteMeetingCalls[0];
|
||||
const bookingRefUid = call.args[0];
|
||||
expect(bookingRefUid).toEqual(expected.bookingRef.uid);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { from: any; to: any }) {
|
||||
// Expect previous booking to be cancelled
|
||||
|
@ -678,10 +723,9 @@ export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { fro
|
|||
status: BookingStatus.CANCELLED,
|
||||
});
|
||||
|
||||
// Expect new booking to be created
|
||||
// Expect new booking to be created but status would depend on whether the new booking requires confirmation or not.
|
||||
await expectBookingToBeInDatabase({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...to,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import getApps from "@calcom/app-store/utils";
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeCalendarEvent } from "@calcom/lib/piiFreeData";
|
||||
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import type {
|
||||
|
@ -366,14 +366,34 @@ export const updateEvent = async (
|
|||
};
|
||||
};
|
||||
|
||||
export const deleteEvent = async (
|
||||
credential: CredentialPayload,
|
||||
uid: string,
|
||||
event: CalendarEvent
|
||||
): Promise<unknown> => {
|
||||
export const deleteEvent = async ({
|
||||
credential,
|
||||
bookingRefUid,
|
||||
event,
|
||||
externalCalendarId,
|
||||
}: {
|
||||
credential: CredentialPayload;
|
||||
bookingRefUid: string;
|
||||
event: CalendarEvent;
|
||||
externalCalendarId?: string | null;
|
||||
}): Promise<unknown> => {
|
||||
const calendar = await getCalendar(credential);
|
||||
log.debug(
|
||||
"Deleting calendar event",
|
||||
safeStringify({
|
||||
bookingRefUid,
|
||||
event: getPiiFreeCalendarEvent(event),
|
||||
})
|
||||
);
|
||||
if (calendar) {
|
||||
return calendar.deleteEvent(uid, event);
|
||||
return calendar.deleteEvent(bookingRefUid, event, externalCalendarId);
|
||||
} else {
|
||||
log.warn(
|
||||
"Could not do deleteEvent - No calendar adapter found",
|
||||
safeStringify({
|
||||
credential: getPiiFreeCredential(credential),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
|
|
|
@ -10,7 +10,12 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide
|
|||
import { getEventLocationTypeFromApp, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeDestinationCalendar, getPiiFreeUser, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
|
||||
import {
|
||||
getPiiFreeDestinationCalendar,
|
||||
getPiiFreeUser,
|
||||
getPiiFreeCredential,
|
||||
getPiiFreeCalendarEvent,
|
||||
} from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
@ -25,8 +30,8 @@ import type {
|
|||
PartialReference,
|
||||
} from "@calcom/types/EventManager";
|
||||
|
||||
import { createEvent, updateEvent } from "./CalendarManager";
|
||||
import { createMeeting, updateMeeting } from "./videoClient";
|
||||
import { createEvent, updateEvent, deleteEvent } from "./CalendarManager";
|
||||
import { createMeeting, updateMeeting, deleteMeeting } from "./videoClient";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["EventManager"] });
|
||||
export const isDedicatedIntegration = (location: string): boolean => {
|
||||
|
@ -91,7 +96,14 @@ export default class EventManager {
|
|||
// (type google_calendar) and non-traditional calendars such as CRMs like Close.com
|
||||
// (type closecom_other_calendar)
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
this.videoCredentials = appCredentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
this.videoCredentials = appCredentials
|
||||
.filter((cred) => cred.type.endsWith("_video"))
|
||||
// Whenever a new video connection is added, latest credentials are added with the highest ID.
|
||||
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
|
||||
// We also don't have updatedAt or createdAt dates on credentials so this is the best we can do
|
||||
.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,6 +238,82 @@ export default class EventManager {
|
|||
};
|
||||
}
|
||||
|
||||
private async deleteCalendarEventForBookingReference({
|
||||
bookingCalendarReference,
|
||||
event,
|
||||
}: {
|
||||
bookingCalendarReference: PartialReference;
|
||||
event: CalendarEvent;
|
||||
}) {
|
||||
log.debug(
|
||||
"deleteCalendarEventForBookingReference",
|
||||
safeStringify({ bookingCalendarReference, event: getPiiFreeCalendarEvent(event) })
|
||||
);
|
||||
|
||||
const {
|
||||
uid: bookingRefUid,
|
||||
externalCalendarId: bookingExternalCalendarId,
|
||||
credentialId,
|
||||
type: credentialType,
|
||||
} = bookingCalendarReference;
|
||||
|
||||
const calendarCredential = await this.getCredentialAndWarnIfNotFound(credentialId, credentialType);
|
||||
if (calendarCredential) {
|
||||
await deleteEvent({
|
||||
credential: calendarCredential,
|
||||
bookingRefUid,
|
||||
event,
|
||||
externalCalendarId: bookingExternalCalendarId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteVideoEventForBookingReference({
|
||||
bookingVideoReference,
|
||||
}: {
|
||||
bookingVideoReference: PartialReference;
|
||||
}) {
|
||||
log.debug("deleteVideoEventForBookingReference", safeStringify({ bookingVideoReference }));
|
||||
const { uid: bookingRefUid, credentialId } = bookingVideoReference;
|
||||
|
||||
const videoCredential = await this.getCredentialAndWarnIfNotFound(
|
||||
credentialId,
|
||||
bookingVideoReference.type
|
||||
);
|
||||
|
||||
if (videoCredential) {
|
||||
await deleteMeeting(videoCredential, bookingRefUid);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCredentialAndWarnIfNotFound(credentialId: number | null | undefined, type: string) {
|
||||
const credential =
|
||||
typeof credentialId === "number" && credentialId > 0
|
||||
? await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
})
|
||||
: // Fallback for zero or nullish credentialId which could be the case of Global App e.g. dailyVideo
|
||||
this.videoCredentials.find((cred) => cred.type === type) ||
|
||||
this.calendarCredentials.find((cred) => cred.type === type) ||
|
||||
null;
|
||||
|
||||
if (!credential) {
|
||||
log.error(
|
||||
"getCredentialAndWarnIfNotFound: Could not find credential",
|
||||
safeStringify({
|
||||
credentialId,
|
||||
type,
|
||||
videoCredentials: this.videoCredentials,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a calendarEvent and a rescheduleUid and updates the event that has the
|
||||
* given uid using the data delivered in the given CalendarEvent.
|
||||
|
@ -281,24 +369,37 @@ export default class EventManager {
|
|||
throw new Error("booking not found");
|
||||
}
|
||||
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
const results: Array<EventResult<Event>> = [];
|
||||
// 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 (evt.requiresConfirmation) {
|
||||
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({ booking, event });
|
||||
} 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 (updatedEvent) {
|
||||
evt.videoCallData = updatedEvent;
|
||||
evt.location = updatedEvent.url;
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// There was a case that booking didn't had any reference and we don't want to throw error on function
|
||||
if (booking.references.find((reference) => reference.type.includes("_calendar"))) {
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
|
||||
const bookingPayment = booking?.payment;
|
||||
|
@ -317,12 +418,54 @@ export default class EventManager {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
referencesToCreate: [...booking.references],
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteEventsAndMeetings({
|
||||
event,
|
||||
booking,
|
||||
}: {
|
||||
event: CalendarEvent;
|
||||
booking: PartialBooking;
|
||||
}) {
|
||||
const calendarReferences = booking.references.filter((reference) => reference.type.includes("_calendar"));
|
||||
const videoReferences = booking.references.filter((reference) => reference.type.includes("_video"));
|
||||
log.debug("deleteEventsAndMeetings", safeStringify({ calendarReferences, videoReferences }));
|
||||
const calendarPromises = calendarReferences.map(async (bookingCalendarReference) => {
|
||||
return await this.deleteCalendarEventForBookingReference({
|
||||
bookingCalendarReference,
|
||||
event,
|
||||
});
|
||||
});
|
||||
|
||||
const videoPromises = videoReferences.map(async (bookingVideoReference) => {
|
||||
return await this.deleteVideoEventForBookingReference({
|
||||
bookingVideoReference,
|
||||
});
|
||||
});
|
||||
|
||||
const allPromises = [...calendarPromises, ...videoPromises];
|
||||
|
||||
// Using allSettled to ensure that if one of the promises rejects, the others will still be executed.
|
||||
// Because we are just cleaning up the events and meetings, we don't want to throw an error if one of them fails.
|
||||
(await Promise.allSettled(allPromises)).some((result) => {
|
||||
if (result.status === "rejected") {
|
||||
log.error(
|
||||
"Error deleting calendar event or video meeting for booking",
|
||||
safeStringify({ error: result.reason })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!allPromises.length) {
|
||||
log.warn("No calendar or video references found for booking - Couldn't delete events or meetings");
|
||||
}
|
||||
}
|
||||
|
||||
public async updateCalendarAttendees(event: CalendarEvent, booking: PartialBooking) {
|
||||
if (booking.references.length === 0) {
|
||||
console.error("Tried to update references but there wasn't any.");
|
||||
|
@ -466,13 +609,9 @@ export default class EventManager {
|
|||
(credential) => credential.id === event.conferenceCredentialId
|
||||
);
|
||||
} else {
|
||||
videoCredential = this.videoCredentials
|
||||
// Whenever a new video connection is added, latest credentials are added with the highest ID.
|
||||
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
|
||||
.sort((a, b) => {
|
||||
return b.id - a.id;
|
||||
})
|
||||
.find((credential: CredentialPayload) => credential.type.includes(integrationName));
|
||||
videoCredential = this.videoCredentials.find((credential: CredentialPayload) =>
|
||||
credential.type.includes(integrationName)
|
||||
);
|
||||
log.warn(
|
||||
`Could not find conferenceCredentialId for event with location: ${event.location}, trying to use last added video credential`
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
mockCalendarToHaveNoBusySlots,
|
||||
mockCalendarToCrashOnUpdateEvent,
|
||||
BookingLocations,
|
||||
getMockBookingReference,
|
||||
getMockBookingAttendee,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import {
|
||||
expectWorkflowToBeTriggered,
|
||||
|
@ -28,6 +30,10 @@ import {
|
|||
expectSuccessfulCalendarEventUpdationInCalendar,
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar,
|
||||
expectBookingInDBToBeRescheduledFromTo,
|
||||
expectBookingRequestedEmails,
|
||||
expectBookingRequestedWebhookToHaveBeenFired,
|
||||
expectSuccessfulCalendarEventDeletionInCalendar,
|
||||
expectSuccessfulVideoMeetingDeletionInCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
|
||||
|
@ -104,6 +110,7 @@ describe("handleNewBooking", () => {
|
|||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
credentialId: null,
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
|
@ -248,6 +255,7 @@ describe("handleNewBooking", () => {
|
|||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
});
|
||||
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
|
@ -258,6 +266,7 @@ describe("handleNewBooking", () => {
|
|||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`should rechedule a booking successfully and update the event in the same externalCalendarId as was used in the booking earlier.
|
||||
1. Should cancel the existing booking
|
||||
|
@ -604,5 +613,700 @@ describe("handleNewBooking", () => {
|
|||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
describe("Event Type that requires confirmation", () => {
|
||||
test(
|
||||
`should reschedule a booking that requires confirmation in PENDING state - When a booker(who is not the organizer himself) is doing the schedule
|
||||
1. Should cancel the existing booking
|
||||
2. Should delete existing calendar invite and Video meeting
|
||||
2. Should create a new booking in the database in PENDING state
|
||||
3. Should send BOOKING Requested scenario emails to the booker as well as organizer
|
||||
4. Should trigger BOOKING_REQUESTED webhook instead of BOOKING_RESCHEDULED
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const subscriberUrl = "http://my-webhook.example.com";
|
||||
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()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
webhooks: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
eventTriggers: ["BOOKING_CREATED"],
|
||||
subscriberUrl,
|
||||
active: true,
|
||||
eventTypeId: 1,
|
||||
appId: null,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
requiresConfirmation: true,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
getMockBookingReference({
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
credentialId: 0,
|
||||
}),
|
||||
getMockBookingReference({
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
credentialId: 1,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
});
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const videoMock = mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
update: {
|
||||
uid: "UPDATED_MOCK_ID",
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
},
|
||||
});
|
||||
|
||||
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 { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
// Rescheduled booking sill stays in pending state
|
||||
status: BookingStatus.PENDING,
|
||||
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",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectBookingRequestedEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
});
|
||||
|
||||
expectBookingRequestedWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl,
|
||||
eventType: scenarioData.eventTypes[0],
|
||||
});
|
||||
|
||||
expectSuccessfulVideoMeetingDeletionInCalendar(videoMock, {
|
||||
bookingRef: {
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventDeletionInCalendar(calendarMock, {
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
calEvent: {
|
||||
videoCallData: expect.objectContaining({
|
||||
url: "http://mock-dailyvideo.example.com",
|
||||
}),
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
`should rechedule a booking, that requires confirmation, without confirmation - When Organizer is doing the reschedule
|
||||
1. Should cancel the existing booking
|
||||
2. Should delete existing calendar invite and Video meeting
|
||||
2. Should create a new booking in the database in ACCEPTED state
|
||||
3. Should send rescheduled emails to the booker as well as organizer
|
||||
4. Should trigger BOOKING_RESCHEDULED webhook
|
||||
`,
|
||||
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()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
webhooks: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
eventTriggers: ["BOOKING_CREATED"],
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
active: true,
|
||||
eventTypeId: 1,
|
||||
appId: null,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
requiresConfirmation: true,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
credentialId: undefined,
|
||||
},
|
||||
],
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 1,
|
||||
name: organizer.name,
|
||||
email: organizer.email,
|
||||
locale: "en",
|
||||
timeZone: "Europe/London",
|
||||
}),
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: booker.name,
|
||||
email: booker.email,
|
||||
// Booker's locale when the fresh booking happened earlier
|
||||
locale: "hi",
|
||||
// Booker's timezone when the fresh booking happened earlier
|
||||
timeZone: "Asia/Kolkata",
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const videoMock = mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
update: {
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
uid: "UPDATED_MOCK_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:15:00.000Z`,
|
||||
// Organizer is doing the rescheduling from his timezone which is different from Booker Timezone as per the booking being rescheduled
|
||||
timeZone: "Europe/London",
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
// Fake the request to be from organizer
|
||||
req.userId = organizer.id;
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
/**
|
||||
* Booking Time should be new time
|
||||
*/
|
||||
expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T04:00:00.000Z`);
|
||||
expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T04:15:00.000Z`);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// 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",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
bookingRef: {
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
// updateEvent uses existing booking's externalCalendarId to update the event in calendar.
|
||||
// and not the event-type's organizer's which is event-type-1@example.com
|
||||
expectSuccessfulCalendarEventUpdationInCalendar(calendarMock, {
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
attendees: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
// Expect that the booker timezone is his earlier timezone(from original booking), even though the rescheduling is done by organizer from his timezone
|
||||
timeZone: "Asia/Kolkata",
|
||||
language: expect.objectContaining({
|
||||
// Expect that the booker locale is his earlier locale(from original booking), even though the rescheduling is done by organizer with his locale
|
||||
locale: "hi",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
|
||||
expectSuccessfulBookingRescheduledEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
});
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(
|
||||
`should rechedule a booking, that requires confirmation, without confirmation - When the owner of the previous booking is doing the reschedule
|
||||
1. Should cancel the existing booking
|
||||
2. Should delete existing calendar invite and Video meeting
|
||||
2. Should create a new booking in the database in ACCEPTED state
|
||||
3. Should send rescheduled emails to the booker as well as organizer
|
||||
4. Should trigger BOOKING_RESCHEDULED webhook
|
||||
`,
|
||||
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()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
|
||||
const previousOrganizerIdForTheBooking = 1001;
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
webhooks: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
eventTriggers: ["BOOKING_CREATED"],
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
active: true,
|
||||
eventTypeId: 1,
|
||||
appId: null,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
requiresConfirmation: true,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
// Make sure that the earlier booking owner is some user with ID 10001
|
||||
userId: previousOrganizerIdForTheBooking,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
credentialId: undefined,
|
||||
},
|
||||
],
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 1,
|
||||
name: organizer.name,
|
||||
email: organizer.email,
|
||||
locale: "en",
|
||||
timeZone: "Europe/London",
|
||||
}),
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: booker.name,
|
||||
email: booker.email,
|
||||
// Booker's locale when the fresh booking happened earlier
|
||||
locale: "hi",
|
||||
// Booker's timezone when the fresh booking happened earlier
|
||||
timeZone: "Asia/Kolkata",
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: [
|
||||
{
|
||||
id: previousOrganizerIdForTheBooking,
|
||||
name: "Previous Organizer",
|
||||
username: "prev-organizer",
|
||||
email: "",
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
timeZone: "Europe/London",
|
||||
},
|
||||
],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const videoMock = mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
update: {
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
uid: "UPDATED_MOCK_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
start: `${plus1DateString}T04:00:00.000Z`,
|
||||
end: `${plus1DateString}T04:15:00.000Z`,
|
||||
// Organizer is doing the rescheduling from his timezone which is different from Booker Timezone as per the booking being rescheduled
|
||||
timeZone: "Europe/London",
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
// Fake the request to be from organizer
|
||||
req.userId = previousOrganizerIdForTheBooking;
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
|
||||
/**
|
||||
* Booking Time should be new time
|
||||
*/
|
||||
expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T04:00:00.000Z`);
|
||||
expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T04:15:00.000Z`);
|
||||
|
||||
await expectBookingInDBToBeRescheduledFromTo({
|
||||
from: {
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
to: {
|
||||
description: "",
|
||||
// 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",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, {
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
bookingRef: {
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
// updateEvent uses existing booking's externalCalendarId to update the event in calendar.
|
||||
// and not the event-type's organizer's which is event-type-1@example.com
|
||||
expectSuccessfulCalendarEventUpdationInCalendar(calendarMock, {
|
||||
externalCalendarId: "existing-event-type@example.com",
|
||||
calEvent: {
|
||||
location: "http://mock-dailyvideo.example.com",
|
||||
attendees: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
// Expect that the booker timezone is his earlier timezone(from original booking), even though the rescheduling is done by organizer from his timezone
|
||||
timeZone: "Asia/Kolkata",
|
||||
language: expect.objectContaining({
|
||||
// Expect that the booker locale is his earlier locale(from original booking), even though the rescheduling is done by organizer with his locale
|
||||
locale: "hi",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
|
||||
expectSuccessfulBookingRescheduledEmails({
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
|
||||
});
|
||||
expectBookingRescheduledWebhookToHaveBeenFired({
|
||||
booker,
|
||||
organizer,
|
||||
location: BookingLocations.CalVideo,
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue