Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Commit

Permalink
fix: Duplicate Calendar Invites on rescheduling an accepted booking t…
Browse files Browse the repository at this point in the history
…hat requires confirmation (calcom#11827)
  • Loading branch information
hariombalhara authored Oct 12, 2023
1 parent e3a9e61 commit db059d8
Show file tree
Hide file tree
Showing 5 changed files with 1,003 additions and 51 deletions.
79 changes: 62 additions & 17 deletions apps/web/test/utils/bookingScenario/bookingScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
},
}),
})
Expand Down Expand Up @@ -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;
})
);
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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");
Expand All @@ -902,6 +914,7 @@ export function mockCalendar(
});
return {
createEventCalls,
deleteEventCalls,
updateEventCalls,
};
}
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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,
});
},
};
},
},
Expand All @@ -1016,6 +1035,7 @@ export function mockVideoApp({
return {
createMeetingCalls,
updateMeetingCalls,
deleteMeetingCalls,
};
}

Expand Down Expand Up @@ -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",
Expand Down
48 changes: 46 additions & 2 deletions apps/web/test/utils/bookingScenario/expects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
});
}
34 changes: 27 additions & 7 deletions packages/core/CalendarManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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({});
Expand Down
Loading

0 comments on commit db059d8

Please sign in to comment.