From 571e93a19ac7b1bde6f4bd31370246c34369829b Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:54:45 -0400 Subject: [PATCH] chore: `EventManager` add `cancelEvent` method (#14261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix timezone display on booking page to reflect event availability timezone * migrate fetching event owner's schedule to server side * migrate fetching event owner's schedule to server side * fix e2e test errors * Add WEBAPP_URL_FOR_OAUTH to salesforce auth * In event manager constructor include "_crm" credentials as calendar creds * Change crm apps to type to end with `_crm` * Move sendgrid out of CRM * Add zoho bigin to CRM apps * When getting apps, use slug * Add `crm` variants * Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH` * Refactor creating credentials * Fix empty CRM page * Use credentials with `_crm` * Abstract getAppCategoryTitle * Add integration.handler changes * When searching for credential, look for current credentials in class * On cancel, delete 3rd party events * Fix tests * Type fix * Type fixes * Remove apiDeletes * Type fixes * Specific typing --------- Co-authored-by: Shaik-Sirajuddin Co-authored-by: Shaik-Sirajuddin Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Omar López --- packages/core/EventManager.ts | 160 +++++++++++------- .../getAllCredentials.ts | 9 +- .../bookings/lib/handleCancelBooking.ts | 119 ++----------- 3 files changed, 116 insertions(+), 172 deletions(-) diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 251b232587..662a1de8e6 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -1,4 +1,4 @@ -import type { DestinationCalendar } from "@prisma/client"; +import type { DestinationCalendar, BookingReference } from "@prisma/client"; // eslint-disable-next-line no-restricted-imports import { cloneDeep, merge } from "lodash"; import { v5 as uuidv5 } from "uuid"; @@ -246,25 +246,36 @@ export default class EventManager { } private async deleteCalendarEventForBookingReference({ - bookingCalendarReference, + reference, event, + isBookingInRecurringSeries, }: { - bookingCalendarReference: PartialReference; + reference: PartialReference; event: CalendarEvent; + isBookingInRecurringSeries?: boolean; }) { log.debug( "deleteCalendarEventForBookingReference", - safeStringify({ bookingCalendarReference, event: getPiiFreeCalendarEvent(event) }) + safeStringify({ bookingCalendarReference: reference, event: getPiiFreeCalendarEvent(event) }) ); const { - uid: bookingRefUid, + // uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId, credentialId, type: credentialType, - } = bookingCalendarReference; + } = reference; - const calendarCredential = await this.getCredentialAndWarnIfNotFound(credentialId, credentialType); + const bookingRefUid = + isBookingInRecurringSeries && reference?.thirdPartyRecurringEventId + ? reference.thirdPartyRecurringEventId + : reference.uid; + + const calendarCredential = await this.getCredentialAndWarnIfNotFound( + credentialId, + this.calendarCredentials, + credentialType + ); if (calendarCredential) { await deleteEvent({ credential: calendarCredential, @@ -275,17 +286,14 @@ export default class EventManager { } } - private async deleteVideoEventForBookingReference({ - bookingVideoReference, - }: { - bookingVideoReference: PartialReference; - }) { - log.debug("deleteVideoEventForBookingReference", safeStringify({ bookingVideoReference })); - const { uid: bookingRefUid, credentialId } = bookingVideoReference; + private async deleteVideoEventForBookingReference({ reference }: { reference: PartialReference }) { + log.debug("deleteVideoEventForBookingReference", safeStringify({ bookingVideoReference: reference })); + const { uid: bookingRefUid, credentialId } = reference; const videoCredential = await this.getCredentialAndWarnIfNotFound( credentialId, - bookingVideoReference.type + this.videoCredentials, + reference.type ); if (videoCredential) { @@ -293,32 +301,41 @@ export default class EventManager { } } - 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, + private async getCredentialAndWarnIfNotFound( + credentialId: number | null | undefined, + credentials: CredentialPayload[], + type: string + ) { + const credential = credentials.find((cred) => cred.id === credentialId); + if (credential) { + return credential; + } else { + 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, }) - : // 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; } - - return credential; } /** @@ -385,15 +402,15 @@ export default class EventManager { 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: { ...event, destinationCalendar: previousHostDestinationCalendar }, + bookingReferences: booking.references, }); } else { if (changedOrganizer) { log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking"); await this.deleteEventsAndMeetings({ - booking, event: { ...event, destinationCalendar: previousHostDestinationCalendar }, + bookingReferences: booking.references, }); log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking"); @@ -451,30 +468,57 @@ export default class EventManager { }; } + public async cancelEvent( + event: CalendarEvent, + bookingReferences: Pick< + BookingReference, + "uid" | "type" | "externalCalendarId" | "credentialId" | "thirdPartyRecurringEventId" + >[], + isBookingInRecurringSeries?: boolean + ) { + await this.deleteEventsAndMeetings({ + event, + bookingReferences, + isBookingInRecurringSeries, + }); + } + private async deleteEventsAndMeetings({ event, - booking, + bookingReferences, + isBookingInRecurringSeries, }: { event: CalendarEvent; - booking: PartialBooking; + bookingReferences: PartialReference[]; + isBookingInRecurringSeries?: boolean; }) { - const calendarReferences = booking.references.filter((reference) => reference.type.includes("_calendar")); - const videoReferences = booking.references.filter((reference) => reference.type.includes("_video")); + const calendarReferences = [], + videoReferences = [], + allPromises = []; + + for (const reference of bookingReferences) { + if (reference.type.includes("_calendar")) { + calendarReferences.push(reference); + allPromises.push( + this.deleteCalendarEventForBookingReference({ + reference, + event, + isBookingInRecurringSeries, + }) + ); + } + + if (reference.type.includes("_video")) { + videoReferences.push(reference); + allPromises.push( + this.deleteVideoEventForBookingReference({ + reference, + }) + ); + } + } + log.debug("deleteEventsAndMeetings", safeStringify({ calendarReferences, videoReferences })); - const calendarPromises = calendarReferences.map(async (bookingCalendarReference) => { - return this.deleteCalendarEventForBookingReference({ - bookingCalendarReference, - event, - }); - }); - - const videoPromises = videoReferences.map(async (bookingVideoReference) => { - return 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. diff --git a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts index 1f1b7cab6f..b2a3e9b98b 100644 --- a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts +++ b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts @@ -1,6 +1,5 @@ import type { Prisma } from "@prisma/client"; -import type { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking"; import { UserRepository } from "@calcom/lib/server/repository/user"; import type { userSelect } from "@calcom/prisma"; import prisma from "@calcom/prisma"; @@ -14,13 +13,13 @@ type User = Prisma.UserGetPayload; * */ export const getAllCredentials = async ( - user: User & { credentials: CredentialPayload[] }, - eventType: Awaited> + user: { id: number; username: string | null; credentials: CredentialPayload[] }, + eventType: { team: { id: number | null } | null; parentId: number | null } | null ) => { const allCredentials = user.credentials; // If it's a team event type query for team credentials - if (eventType.team?.id) { + if (eventType?.team?.id) { const teamCredentialsQuery = await prisma.credential.findMany({ where: { teamId: eventType.team.id, @@ -31,7 +30,7 @@ export const getAllCredentials = async ( } // If it's a managed event type, query for the parent team's credentials - if (eventType.parentId) { + if (eventType?.parentId) { const teamCredentialsQuery = await prisma.team.findFirst({ where: { eventTypes: { diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 9e66aa57cf..3456170bc4 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -1,10 +1,9 @@ import type { Prisma, WorkflowReminder } from "@prisma/client"; import type { NextApiRequest } from "next"; -import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { DailyLocationType } from "@calcom/app-store/locations"; -import { deleteMeeting } from "@calcom/core/videoClient"; +import EventManager from "@calcom/core/EventManager"; import dayjs from "@calcom/dayjs"; import { sendCancelledEmails } from "@calcom/emails"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; @@ -19,7 +18,6 @@ import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; @@ -29,6 +27,7 @@ import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/crede import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials"; import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat"; async function getBookingToDelete(id: number | undefined, uid: string | undefined) { @@ -44,6 +43,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine user: { select: { id: true, + username: true, credentials: { select: credentialForCalendarServiceSelect }, // Not leaking at the moment, be careful with email: true, timeZone: true, @@ -421,108 +421,16 @@ async function handler(req: CustomRequest) { }); } - const apiDeletes = []; - - const bookingCalendarReference = bookingToDelete.references.filter((reference) => - reference.type.includes("_calendar") + const isBookingInRecurringSeries = !!( + bookingToDelete.eventType?.recurringEvent && + bookingToDelete.recurringEventId && + allRemainingBookings ); + const credentials = await getAllCredentials(bookingToDelete.user, bookingToDelete.eventType); - if (bookingCalendarReference.length > 0) { - for (const reference of bookingCalendarReference) { - const { credentialId, uid, externalCalendarId } = reference; - // If the booking calendar reference contains a credentialId - if (credentialId) { - // Find the correct calendar credential under user credentials - let calendarCredential = bookingToDelete.user.credentials.find( - (credential) => credential.id === credentialId - ); - if (!calendarCredential) { - // get credential from DB - const foundCalendarCredential = await prisma.credential.findUnique({ - where: { - id: credentialId, - }, - select: credentialForCalendarServiceSelect, - }); - if (foundCalendarCredential) { - calendarCredential = foundCalendarCredential; - } - } - if (calendarCredential) { - const calendar = await getCalendar(calendarCredential); - if ( - bookingToDelete.eventType?.recurringEvent && - bookingToDelete.recurringEventId && - allRemainingBookings - ) { - let thirdPartyRecurringEventId; - for (const reference of bookingToDelete.references) { - if (reference.thirdPartyRecurringEventId) { - thirdPartyRecurringEventId = reference.thirdPartyRecurringEventId; - break; - } - } - if (thirdPartyRecurringEventId) { - apiDeletes.push( - calendar?.deleteEvent(thirdPartyRecurringEventId, evt, externalCalendarId) as Promise - ); - } else { - const promises = bookingToDelete.user.credentials - .filter((credential) => credential.type.endsWith("_calendar")) - .map(async (credential) => { - const calendar = await getCalendar(credential); - for (const updBooking of updatedBookings) { - const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar")); - if (bookingRef) { - const { uid, externalCalendarId } = bookingRef; - const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId); - apiDeletes.push(deletedEvent); - } - } - }); - try { - await Promise.all(promises); - } catch (error) { - if (error instanceof Error) { - logger.error(error.message); - } - } - } - } else { - apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); - } - } - } else { - // For bookings made before the refactor we go through the old behavior of running through each calendar credential - const calendarCredentials = bookingToDelete.user.credentials.filter((credential) => - credential.type.endsWith("_calendar") - ); - for (const credential of calendarCredentials) { - const calendar = await getCalendar(credential); - apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); - } - } - } - } + const eventManager = new EventManager({ ...bookingToDelete.user, credentials }); - const bookingVideoReference = bookingToDelete.references.find((reference) => - reference.type.includes("_video") - ); - - // If the video reference has a credentialId find the specific credential - if (bookingVideoReference && bookingVideoReference.credentialId) { - const { credentialId, uid } = bookingVideoReference; - if (credentialId) { - const videoCredential = bookingToDelete.user.credentials.find( - (credential) => credential.id === credentialId - ); - - if (videoCredential) { - logger.debug("videoCredential inside cancel booking handler", videoCredential); - apiDeletes.push(deleteMeeting(videoCredential, uid)); - } - } - } + await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries); const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ where: { @@ -553,13 +461,6 @@ async function handler(req: CustomRequest) { const prismaPromises: Promise[] = [bookingReferenceDeletes]; try { - const temp = prismaPromises.concat(apiDeletes); - const settled = await Promise.allSettled(temp); - const rejected = settled.filter(({ status }) => status === "rejected") as PromiseRejectedResult[]; - if (rejected.length) { - throw new Error(`Reasons: ${rejected.map(({ reason }) => reason)}`); - } - // TODO: if emails fail try to requeue them if (!platformClientId || (platformClientId && arePlatformEmailsEnabled)) await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName });