chore: EventManager add cancelEvent method (#14261)

* 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 <sirajuddinshaik30gmail.com>
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
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 <zomars@me.com>
This commit is contained in:
authored and GitHub committed 2024-04-23 17:54:45 +00:00
1 parent d16428da6c
commit 571e93a19a
3 files changed
+116 -172

No files matched your search

+102 -58
View File
@@ -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.
@@ -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<typeof userSelect>;
*
*/
export const getAllCredentials = async (
user: User & { credentials: CredentialPayload[] },
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
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: {
@@ -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<unknown>
);
} 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<unknown>);
}
}
} 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<unknown>);
}
}
}
}
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<unknown>[] = [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 });