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:
3 files changed
+116
-172
No files matched your search
+102
-58
@@ -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.
|
||||
|
||||
+4
-5
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user