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 });