Compare commits
46 Commits
main
...
refactor/h
Author | SHA1 | Date | |
---|---|---|---|
8c081fd6b1 | |||
6e30d9ad65 | |||
3ea89ecc83 | |||
818b367fb1 | |||
25c89f2a8d | |||
64f3a2df45 | |||
afa8d6c5c3 | |||
e48efcac47 | |||
0bb475b173 | |||
1b3612dfdb | |||
64f465d6ef | |||
6ca8ab2877 | |||
97e998f107 | |||
f7630f50db | |||
343e59bd6c | |||
b9dd07b7ef | |||
7a9fca4f9c | |||
1d96a3b8f8 | |||
9175760cd8 | |||
49a7617265 | |||
d375e78ea2 | |||
8e21da36df | |||
76905a7407 | |||
6a9bb9a156 | |||
d80f8c0477 | |||
ffd67c5eb1 | |||
bc8b1e7f45 | |||
94c988101b | |||
776519e262 | |||
9c93891d27 | |||
4cf5e30059 | |||
593ac641b1 | |||
21b86b1e4e | |||
0c54667fad | |||
d4e7ae0858 | |||
b29acc01b8 | |||
d768b6bab8 | |||
34e5b5d917 | |||
d8f9855b7b | |||
ba99a3c8da | |||
63cd63de7e | |||
ad76c74b91 | |||
f02119e47f | |||
010fd62c8e | |||
1e1ceb9bf6 | |||
251146a61c |
|
@ -6,6 +6,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import short, { uuid } from "short-uuid";
|
||||
import type { Logger } from "tslog";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
@ -98,6 +99,7 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
|||
|
||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
import getBookingDataSchema from "./getBookingDataSchema";
|
||||
import type { BookingSeat } from "./handleSeats";
|
||||
|
||||
const translator = short();
|
||||
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
|
@ -105,7 +107,7 @@ const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
|||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
type BufferedBusyTimes = BufferedBusyTime[];
|
||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
export type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
export type NewBookingEventType =
|
||||
| Awaited<ReturnType<typeof getDefaultEvent>>
|
||||
| Awaited<ReturnType<typeof getEventTypesFromDB>>;
|
||||
|
@ -113,8 +115,36 @@ export type NewBookingEventType =
|
|||
// Work with Typescript to require reqBody.end
|
||||
type ReqBodyWithoutEnd = z.infer<ReturnType<typeof getBookingDataSchema>>;
|
||||
type ReqBodyWithEnd = ReqBodyWithoutEnd & { end: string };
|
||||
export type Invitee = {
|
||||
email: string;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
timeZone: string;
|
||||
language: {
|
||||
translate: TFunction;
|
||||
locale: string;
|
||||
};
|
||||
}[];
|
||||
export type OrganizerUser = Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
export type OriginalRescheduledBooking = Awaited<ReturnType<typeof getOriginalRescheduledBooking>>;
|
||||
|
||||
interface IEventTypePaymentCredentialType {
|
||||
type AwaitedBookingData = Awaited<ReturnType<typeof getBookingData>>;
|
||||
export type RescheduleReason = AwaitedBookingData["rescheduleReason"];
|
||||
export type NoEmail = AwaitedBookingData["noEmail"];
|
||||
export type AdditionalNotes = AwaitedBookingData["notes"];
|
||||
export type ReqAppsStatus = AwaitedBookingData["appsStatus"];
|
||||
export type SmsReminderNumber = AwaitedBookingData["smsReminderNumber"];
|
||||
export type EventTypeId = AwaitedBookingData["eventTypeId"];
|
||||
export type ReqBodyMetadata = ReqBodyWithEnd["metadata"];
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
export type PaymentAppData = ReturnType<typeof getPaymentAppData>;
|
||||
|
||||
export interface IEventTypePaymentCredentialType {
|
||||
appId: EventTypeAppsList;
|
||||
app: {
|
||||
categories: App["categories"];
|
||||
|
@ -148,7 +178,9 @@ async function refreshCredential(credential: CredentialPayload): Promise<Credent
|
|||
*
|
||||
* @param credentials
|
||||
*/
|
||||
async function refreshCredentials(credentials: Array<CredentialPayload>): Promise<Array<CredentialPayload>> {
|
||||
export async function refreshCredentials(
|
||||
credentials: Array<CredentialPayload>
|
||||
): Promise<Array<CredentialPayload>> {
|
||||
return await async.mapLimit(credentials, 5, refreshCredential);
|
||||
}
|
||||
|
||||
|
@ -156,7 +188,7 @@ async function refreshCredentials(credentials: Array<CredentialPayload>): Promis
|
|||
* Gets credentials from the user, team, and org if applicable
|
||||
*
|
||||
*/
|
||||
const getAllCredentials = async (
|
||||
export const getAllCredentials = async (
|
||||
user: User & { credentials: CredentialPayload[] },
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
|
||||
) => {
|
||||
|
@ -634,18 +666,18 @@ async function createBooking({
|
|||
paymentAppData,
|
||||
changedOrganizer,
|
||||
}: {
|
||||
originalRescheduledBooking: Awaited<ReturnType<typeof getOriginalRescheduledBooking>>;
|
||||
originalRescheduledBooking: OriginalRescheduledBooking;
|
||||
evt: CalendarEvent;
|
||||
eventType: NewBookingEventType;
|
||||
eventTypeId: Awaited<ReturnType<typeof getBookingData>>["eventTypeId"];
|
||||
eventTypeSlug: Awaited<ReturnType<typeof getBookingData>>["eventTypeSlug"];
|
||||
eventTypeId: EventTypeId;
|
||||
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
|
||||
reqBodyUser: ReqBodyWithEnd["user"];
|
||||
reqBodyMetadata: ReqBodyWithEnd["metadata"];
|
||||
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
|
||||
uid: short.SUUID;
|
||||
responses: ReqBodyWithEnd["responses"] | null;
|
||||
isConfirmedByDefault: ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
smsReminderNumber: Awaited<ReturnType<typeof getBookingData>>["smsReminderNumber"];
|
||||
isConfirmedByDefault: IsConfirmedByDefault;
|
||||
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
|
||||
organizerUser: Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
|
@ -822,6 +854,76 @@ export function getCustomInputsResponses(
|
|||
return customInputsResponses;
|
||||
}
|
||||
|
||||
/** Updates the evt object with video call data found from booking references
|
||||
*
|
||||
* @param bookingReferences
|
||||
* @param evt
|
||||
*
|
||||
* @returns updated evt with video call data
|
||||
*/
|
||||
export const addVideoCallDataToEvt = (bookingReferences: BookingReference[], evt: CalendarEvent) => {
|
||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||
|
||||
if (videoCallReference) {
|
||||
evt.videoCallData = {
|
||||
type: videoCallReference.type,
|
||||
id: videoCallReference.meetingId,
|
||||
password: videoCallReference?.meetingPassword,
|
||||
url: videoCallReference.meetingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return evt;
|
||||
};
|
||||
|
||||
export const createLoggerWithEventDetails = (
|
||||
eventTypeId: number,
|
||||
reqBodyUser: string | string[] | undefined,
|
||||
eventTypeSlug: string | undefined
|
||||
) => {
|
||||
return logger.getSubLogger({
|
||||
prefix: ["book:user", `${eventTypeId}:${reqBodyUser}/${eventTypeSlug}`],
|
||||
});
|
||||
};
|
||||
|
||||
export function handleAppsStatus(
|
||||
results: EventResult<AdditionalInformation>[],
|
||||
booking: (Booking & { appsStatus?: AppsStatus[] }) | null,
|
||||
reqAppsStatus: ReqAppsStatus
|
||||
) {
|
||||
// Taking care of apps status
|
||||
let resultStatus: AppsStatus[] = results.map((app) => ({
|
||||
appName: app.appName,
|
||||
type: app.type,
|
||||
success: app.success ? 1 : 0,
|
||||
failures: !app.success ? 1 : 0,
|
||||
errors: app.calError ? [app.calError] : [],
|
||||
warnings: app.calWarnings,
|
||||
}));
|
||||
|
||||
if (reqAppsStatus === undefined) {
|
||||
if (booking !== null) {
|
||||
booking.appsStatus = resultStatus;
|
||||
}
|
||||
return resultStatus;
|
||||
}
|
||||
// From down here we can assume reqAppsStatus is not undefined anymore
|
||||
// Other status exist, so this is the last booking of a series,
|
||||
// proceeding to prepare the info for the event
|
||||
const calcAppsStatus = reqAppsStatus.concat(resultStatus).reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].success += curr.success;
|
||||
prev[curr.type].errors = prev[curr.type].errors.concat(curr.errors);
|
||||
prev[curr.type].warnings = prev[curr.type].warnings?.concat(curr.warnings || []);
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
resultStatus = Object.values(calcAppsStatus);
|
||||
return resultStatus;
|
||||
}
|
||||
|
||||
function getICalSequence(originalRescheduledBooking: BookingType | null) {
|
||||
// If new booking set the sequence to 0
|
||||
if (!originalRescheduledBooking) {
|
||||
|
@ -837,6 +939,52 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) {
|
|||
return originalRescheduledBooking.iCalSequence + 1;
|
||||
}
|
||||
|
||||
export const findBookingQuery = async (bookingId: number) => {
|
||||
const foundBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
location: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
description: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
currency: true,
|
||||
length: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// This should never happen but it's just typescript safe
|
||||
if (!foundBooking) {
|
||||
throw new Error("Internal Error. Couldn't find booking");
|
||||
}
|
||||
|
||||
// Don't leak any sensitive data
|
||||
return foundBooking;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest & { userId?: number | undefined },
|
||||
{
|
||||
|
@ -884,9 +1032,7 @@ async function handler(
|
|||
eventType,
|
||||
});
|
||||
|
||||
const loggerWithEventDetails = logger.getSubLogger({
|
||||
prefix: ["book:user", `${eventTypeId}:${reqBody.user}/${eventTypeSlug}`],
|
||||
});
|
||||
const loggerWithEventDetails = createLoggerWithEventDetails(eventTypeId, reqBody.user, eventTypeSlug);
|
||||
|
||||
if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) {
|
||||
logger.settings.minLevel = 0;
|
||||
|
@ -1052,7 +1198,7 @@ async function handler(
|
|||
}
|
||||
|
||||
let rescheduleUid = reqBody.rescheduleUid;
|
||||
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
||||
let bookingSeat: BookingSeat = null;
|
||||
|
||||
let originalRescheduledBooking: BookingType = null;
|
||||
|
||||
|
@ -1176,7 +1322,7 @@ async function handler(
|
|||
}
|
||||
}
|
||||
|
||||
const invitee = [
|
||||
const invitee: Invitee = [
|
||||
{
|
||||
email: bookerEmail,
|
||||
name: fullName,
|
||||
|
@ -1201,7 +1347,7 @@ async function handler(
|
|||
language: { translate: tGuests, locale: "en" },
|
||||
});
|
||||
return guestArray;
|
||||
}, [] as typeof invitee);
|
||||
}, [] as Invitee);
|
||||
|
||||
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
|
@ -1314,20 +1460,6 @@ async function handler(
|
|||
evt.destinationCalendar?.push(...teamDestinationCalendars);
|
||||
}
|
||||
|
||||
/* Used for seats bookings to update evt object with video data */
|
||||
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||
|
||||
if (videoCallReference) {
|
||||
evt.videoCallData = {
|
||||
type: videoCallReference.type,
|
||||
id: videoCallReference.meetingId,
|
||||
password: videoCallReference?.meetingPassword,
|
||||
url: videoCallReference.meetingUrl,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/* Check if the original booking has no more attendees, if so delete the booking
|
||||
and any calendar or video integrations */
|
||||
const lastAttendeeDeleteBooking = async (
|
||||
|
@ -1574,7 +1706,7 @@ async function handler(
|
|||
},
|
||||
});
|
||||
|
||||
addVideoCallDataToEvt(newBooking.references);
|
||||
evt = addVideoCallDataToEvt(newBooking.references, evt);
|
||||
|
||||
const copyEvent = cloneDeep(evt);
|
||||
|
||||
|
@ -1611,7 +1743,7 @@ async function handler(
|
|||
metadata.hangoutLink = updatedEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
evt.appsStatus = handleAppsStatus(results, newBooking);
|
||||
evt.appsStatus = handleAppsStatus(results, newBooking, reqAppsStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1719,7 +1851,7 @@ async function handler(
|
|||
|
||||
evt.attendees = updatedBookingAttendees;
|
||||
|
||||
addVideoCallDataToEvt(updatedNewBooking.references);
|
||||
evt = addVideoCallDataToEvt(updatedNewBooking.references, evt);
|
||||
|
||||
const copyEvent = cloneDeep(evt);
|
||||
|
||||
|
@ -2167,43 +2299,6 @@ async function handler(
|
|||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
|
||||
function handleAppsStatus(
|
||||
results: EventResult<AdditionalInformation>[],
|
||||
booking: (Booking & { appsStatus?: AppsStatus[] }) | null
|
||||
) {
|
||||
// Taking care of apps status
|
||||
let resultStatus: AppsStatus[] = results.map((app) => ({
|
||||
appName: app.appName,
|
||||
type: app.type,
|
||||
success: app.success ? 1 : 0,
|
||||
failures: !app.success ? 1 : 0,
|
||||
errors: app.calError ? [app.calError] : [],
|
||||
warnings: app.calWarnings,
|
||||
}));
|
||||
|
||||
if (reqAppsStatus === undefined) {
|
||||
if (booking !== null) {
|
||||
booking.appsStatus = resultStatus;
|
||||
}
|
||||
return resultStatus;
|
||||
}
|
||||
// From down here we can assume reqAppsStatus is not undefined anymore
|
||||
// Other status exist, so this is the last booking of a series,
|
||||
// proceeding to prepare the info for the event
|
||||
const calcAppsStatus = reqAppsStatus.concat(resultStatus).reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].success += curr.success;
|
||||
prev[curr.type].errors = prev[curr.type].errors.concat(curr.errors);
|
||||
prev[curr.type].warnings = prev[curr.type].warnings?.concat(curr.warnings || []);
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
resultStatus = Object.values(calcAppsStatus);
|
||||
return resultStatus;
|
||||
}
|
||||
|
||||
let videoCallUrl;
|
||||
|
||||
//this is the actual rescheduling logic
|
||||
|
@ -2219,7 +2314,7 @@ async function handler(
|
|||
);
|
||||
}
|
||||
|
||||
addVideoCallDataToEvt(originalRescheduledBooking.references);
|
||||
addVideoCallDataToEvt(originalRescheduledBooking.references, evt);
|
||||
|
||||
//update original rescheduled booking (no seats event)
|
||||
if (!eventType.seatsPerTimeSlot) {
|
||||
|
@ -2346,7 +2441,7 @@ async function handler(
|
|||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
evt.appsStatus = handleAppsStatus(results, booking);
|
||||
evt.appsStatus = handleAppsStatus(results, booking, reqAppsStatus);
|
||||
videoCallUrl =
|
||||
metadata.hangoutLink ||
|
||||
results[0].createdEvent?.url ||
|
||||
|
@ -2361,7 +2456,7 @@ async function handler(
|
|||
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||
}
|
||||
|
||||
evt.appsStatus = handleAppsStatus(results, booking);
|
||||
evt.appsStatus = handleAppsStatus(results, booking, reqAppsStatus);
|
||||
|
||||
// If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient
|
||||
if (noEmail !== true && isConfirmedByDefault && !isThereAnIntegrationError) {
|
||||
|
@ -2516,7 +2611,7 @@ async function handler(
|
|||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
evt.appsStatus = handleAppsStatus(results, booking);
|
||||
evt.appsStatus = handleAppsStatus(results, booking, reqAppsStatus);
|
||||
videoCallUrl =
|
||||
metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl;
|
||||
|
||||
|
@ -2911,49 +3006,3 @@ function handleCustomInputs(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const findBookingQuery = async (bookingId: number) => {
|
||||
const foundBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
location: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
description: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
currency: true,
|
||||
length: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// This should never happen but it's just typescript safe
|
||||
if (!foundBooking) {
|
||||
throw new Error("Internal Error. Couldn't find booking");
|
||||
}
|
||||
|
||||
// Don't leak any sensitive data
|
||||
return foundBooking;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,796 @@
|
|||
import type { Prisma, Attendee } from "@prisma/client";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import type short from "short-uuid";
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendRescheduledEmails, sendRescheduledSeatEmail, sendScheduledSeatsEmails } from "@calcom/emails";
|
||||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import type { getFullName } from "@calcom/features/form-builder/utils";
|
||||
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { handlePayment } from "@calcom/lib/payment/handlePayment";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
import {
|
||||
refreshCredentials,
|
||||
addVideoCallDataToEvt,
|
||||
createLoggerWithEventDetails,
|
||||
handleAppsStatus,
|
||||
findBookingQuery,
|
||||
} from "./handleNewBooking";
|
||||
import type {
|
||||
Booking,
|
||||
Invitee,
|
||||
NewBookingEventType,
|
||||
getAllCredentials,
|
||||
OrganizerUser,
|
||||
OriginalRescheduledBooking,
|
||||
RescheduleReason,
|
||||
NoEmail,
|
||||
IsConfirmedByDefault,
|
||||
AdditionalNotes,
|
||||
ReqAppsStatus,
|
||||
PaymentAppData,
|
||||
IEventTypePaymentCredentialType,
|
||||
SmsReminderNumber,
|
||||
EventTypeId,
|
||||
ReqBodyMetadata,
|
||||
} from "./handleNewBooking";
|
||||
|
||||
export type BookingSeat = Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null;
|
||||
|
||||
/* Check if the original booking has no more attendees, if so delete the booking
|
||||
and any calendar or video integrations */
|
||||
const lastAttendeeDeleteBooking = async (
|
||||
originalRescheduledBooking: OriginalRescheduledBooking,
|
||||
filteredAttendees: Partial<Attendee>[],
|
||||
originalBookingEvt?: CalendarEvent
|
||||
) => {
|
||||
let deletedReferences = false;
|
||||
if (filteredAttendees && filteredAttendees.length === 0 && originalRescheduledBooking) {
|
||||
const integrationsToDelete = [];
|
||||
|
||||
for (const reference of originalRescheduledBooking.references) {
|
||||
if (reference.credentialId) {
|
||||
const credential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (credential) {
|
||||
if (reference.type.includes("_video")) {
|
||||
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
|
||||
}
|
||||
if (reference.type.includes("_calendar") && originalBookingEvt) {
|
||||
const calendar = await getCalendar(credential);
|
||||
if (calendar) {
|
||||
integrationsToDelete.push(
|
||||
calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(integrationsToDelete).then(async () => {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: originalRescheduledBooking.id,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
},
|
||||
});
|
||||
});
|
||||
deletedReferences = true;
|
||||
}
|
||||
return deletedReferences;
|
||||
};
|
||||
|
||||
const handleSeats = async ({
|
||||
rescheduleUid,
|
||||
reqBookingUid,
|
||||
eventType,
|
||||
evt,
|
||||
invitee,
|
||||
allCredentials,
|
||||
organizerUser,
|
||||
originalRescheduledBooking,
|
||||
bookerEmail,
|
||||
tAttendees,
|
||||
bookingSeat,
|
||||
reqUserId,
|
||||
rescheduleReason,
|
||||
reqBodyUser,
|
||||
noEmail,
|
||||
isConfirmedByDefault,
|
||||
additionalNotes,
|
||||
reqAppsStatus,
|
||||
attendeeLanguage,
|
||||
paymentAppData,
|
||||
fullName,
|
||||
smsReminderNumber,
|
||||
eventTypeInfo,
|
||||
uid,
|
||||
eventTypeId,
|
||||
reqBodyMetadata,
|
||||
subscriberOptions,
|
||||
eventTrigger,
|
||||
}: {
|
||||
rescheduleUid: string;
|
||||
reqBookingUid: string;
|
||||
eventType: NewBookingEventType;
|
||||
evt: CalendarEvent;
|
||||
invitee: Invitee;
|
||||
allCredentials: Awaited<ReturnType<typeof getAllCredentials>>;
|
||||
organizerUser: OrganizerUser;
|
||||
originalRescheduledBooking: OriginalRescheduledBooking;
|
||||
bookerEmail: string;
|
||||
tAttendees: TFunction;
|
||||
bookingSeat: BookingSeat;
|
||||
reqUserId: number | undefined;
|
||||
rescheduleReason: RescheduleReason;
|
||||
reqBodyUser: string | string[] | undefined;
|
||||
noEmail: NoEmail;
|
||||
isConfirmedByDefault: IsConfirmedByDefault;
|
||||
additionalNotes: AdditionalNotes;
|
||||
reqAppsStatus: ReqAppsStatus;
|
||||
attendeeLanguage: string | null;
|
||||
paymentAppData: PaymentAppData;
|
||||
fullName: ReturnType<typeof getFullName>;
|
||||
smsReminderNumber: SmsReminderNumber;
|
||||
eventTypeInfo: EventTypeInfo;
|
||||
uid: short.SUUID;
|
||||
eventTypeId: EventTypeId;
|
||||
reqBodyMetadata: ReqBodyMetadata;
|
||||
subscriberOptions: GetSubscriberOptions;
|
||||
eventTrigger: WebhookTriggerEvents;
|
||||
}) => {
|
||||
const loggerWithEventDetails = createLoggerWithEventDetails(eventType.id, reqBodyUser, eventType.slug);
|
||||
|
||||
let resultBooking:
|
||||
| (Partial<Booking> & {
|
||||
appsStatus?: AppsStatus[];
|
||||
seatReferenceUid?: string;
|
||||
paymentUid?: string;
|
||||
message?: string;
|
||||
paymentId?: number;
|
||||
})
|
||||
| null = null;
|
||||
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
uid: rescheduleUid || reqBookingUid,
|
||||
},
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
startTime: evt.startTime,
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
id: true,
|
||||
attendees: { include: { bookingSeat: true } },
|
||||
userId: true,
|
||||
references: true,
|
||||
startTime: true,
|
||||
user: true,
|
||||
status: true,
|
||||
smsReminderNumber: true,
|
||||
endTime: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find booking" });
|
||||
}
|
||||
|
||||
// See if attendee is already signed up for timeslot
|
||||
if (
|
||||
booking.attendees.find((attendee) => attendee.email === invitee[0].email) &&
|
||||
dayjs.utc(booking.startTime).format() === evt.startTime
|
||||
) {
|
||||
throw new HttpError({ statusCode: 409, message: "Already signed up for this booking." });
|
||||
}
|
||||
|
||||
// There are two paths here, reschedule a booking with seats and booking seats without reschedule
|
||||
if (rescheduleUid) {
|
||||
// See if the new date has a booking already
|
||||
const newTimeSlotBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
startTime: evt.startTime,
|
||||
eventTypeId: eventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
|
||||
if (!originalRescheduledBooking) {
|
||||
// typescript isn't smart enough;
|
||||
throw new Error("Internal Error.");
|
||||
}
|
||||
|
||||
const updatedBookingAttendees = originalRescheduledBooking.attendees.reduce(
|
||||
(filteredAttendees, attendee) => {
|
||||
if (attendee.email === bookerEmail) {
|
||||
return filteredAttendees; // skip current booker, as we know the language already.
|
||||
}
|
||||
filteredAttendees.push({
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: { translate: tAttendees, locale: attendee.locale ?? "en" },
|
||||
});
|
||||
return filteredAttendees;
|
||||
},
|
||||
[] as Person[]
|
||||
);
|
||||
|
||||
// If original booking has video reference we need to add the videoCallData to the new evt
|
||||
const videoReference = originalRescheduledBooking.references.find((reference) =>
|
||||
reference.type.includes("_video")
|
||||
);
|
||||
|
||||
const originalBookingEvt = {
|
||||
...evt,
|
||||
title: originalRescheduledBooking.title,
|
||||
startTime: dayjs(originalRescheduledBooking.startTime).utc().format(),
|
||||
endTime: dayjs(originalRescheduledBooking.endTime).utc().format(),
|
||||
attendees: updatedBookingAttendees,
|
||||
// If the location is a video integration then include the videoCallData
|
||||
...(videoReference && {
|
||||
videoCallData: {
|
||||
type: videoReference.type,
|
||||
id: videoReference.meetingId,
|
||||
password: videoReference.meetingPassword,
|
||||
url: videoReference.meetingUrl,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
if (!bookingSeat) {
|
||||
// if no bookingSeat is given and the userId != owner, 401.
|
||||
// TODO: Next step; Evaluate ownership, what about teams?
|
||||
if (booking.user?.id !== reqUserId) {
|
||||
throw new HttpError({ statusCode: 401 });
|
||||
}
|
||||
|
||||
// Moving forward in this block is the owner making changes to the booking. All attendees should be affected
|
||||
evt.attendees = originalRescheduledBooking.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: { translate: tAttendees, locale: attendee.locale ?? "en" },
|
||||
};
|
||||
});
|
||||
|
||||
// If owner reschedules the event we want to update the entire booking
|
||||
// Also if owner is rescheduling there should be no bookingSeat
|
||||
|
||||
// If there is no booking during the new time slot then update the current booking to the new date
|
||||
if (!newTimeSlotBooking) {
|
||||
const newBooking: (Booking & { appsStatus?: AppsStatus[] }) | null = await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
startTime: evt.startTime,
|
||||
endTime: evt.endTime,
|
||||
cancellationReason: rescheduleReason,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
references: true,
|
||||
payment: true,
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
|
||||
evt = addVideoCallDataToEvt(newBooking.references, evt);
|
||||
|
||||
const copyEvent = cloneDeep(evt);
|
||||
|
||||
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newBooking.id);
|
||||
|
||||
// @NOTE: This code is duplicated and should be moved to a function
|
||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||
// to the default description when we are sending the emails.
|
||||
evt.description = eventType.description;
|
||||
|
||||
const results = updateManager.results;
|
||||
|
||||
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||
|
||||
evt.iCalUID = calendarResult?.updatedEvent.iCalUID || undefined;
|
||||
|
||||
if (results.length > 0 && results.some((res) => !res.success)) {
|
||||
const error = {
|
||||
errorCode: "BookingReschedulingMeetingFailed",
|
||||
message: "Booking Rescheduling failed",
|
||||
};
|
||||
loggerWithEventDetails.error(
|
||||
`Booking ${organizerUser.name} failed`,
|
||||
JSON.stringify({ error, results })
|
||||
);
|
||||
} else {
|
||||
const metadata: AdditionalInformation = {};
|
||||
if (results.length) {
|
||||
// TODO: Handle created event metadata more elegantly
|
||||
const [updatedEvent] = Array.isArray(results[0].updatedEvent)
|
||||
? results[0].updatedEvent
|
||||
: [results[0].updatedEvent];
|
||||
if (updatedEvent) {
|
||||
metadata.hangoutLink = updatedEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
evt.appsStatus = handleAppsStatus(results, newBooking, reqAppsStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (noEmail !== true && isConfirmedByDefault) {
|
||||
const copyEvent = cloneDeep(evt);
|
||||
loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats");
|
||||
await sendRescheduledEmails({
|
||||
...copyEvent,
|
||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||
});
|
||||
}
|
||||
const foundBooking = await findBookingQuery(newBooking.id);
|
||||
|
||||
resultBooking = { ...foundBooking, appsStatus: newBooking.appsStatus };
|
||||
} else {
|
||||
// Merge two bookings together
|
||||
const attendeesToMove = [],
|
||||
attendeesToDelete = [];
|
||||
|
||||
for (const attendee of booking.attendees) {
|
||||
// If the attendee already exists on the new booking then delete the attendee record of the old booking
|
||||
if (
|
||||
newTimeSlotBooking.attendees.some(
|
||||
(newBookingAttendee) => newBookingAttendee.email === attendee.email
|
||||
)
|
||||
) {
|
||||
attendeesToDelete.push(attendee.id);
|
||||
// If the attendee does not exist on the new booking then move that attendee record to the new booking
|
||||
} else {
|
||||
attendeesToMove.push({ id: attendee.id, seatReferenceId: attendee.bookingSeat?.id });
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm that the new event will have enough available seats
|
||||
if (
|
||||
!eventType.seatsPerTimeSlot ||
|
||||
attendeesToMove.length +
|
||||
newTimeSlotBooking.attendees.filter((attendee) => attendee.bookingSeat).length >
|
||||
eventType.seatsPerTimeSlot
|
||||
) {
|
||||
throw new HttpError({ statusCode: 409, message: "Booking does not have enough available seats" });
|
||||
}
|
||||
|
||||
const moveAttendeeCalls = [];
|
||||
for (const attendeeToMove of attendeesToMove) {
|
||||
moveAttendeeCalls.push(
|
||||
prisma.attendee.update({
|
||||
where: {
|
||||
id: attendeeToMove.id,
|
||||
},
|
||||
data: {
|
||||
bookingId: newTimeSlotBooking.id,
|
||||
bookingSeat: {
|
||||
upsert: {
|
||||
create: {
|
||||
referenceUid: uuid(),
|
||||
bookingId: newTimeSlotBooking.id,
|
||||
},
|
||||
update: {
|
||||
bookingId: newTimeSlotBooking.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...moveAttendeeCalls,
|
||||
// Delete any attendees that are already a part of that new time slot booking
|
||||
prisma.attendee.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: attendeesToDelete,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const updatedNewBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: newTimeSlotBooking.id,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
references: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedNewBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Updated booking not found" });
|
||||
}
|
||||
|
||||
// Update the evt object with the new attendees
|
||||
const updatedBookingAttendees = updatedNewBooking.attendees.map((attendee) => {
|
||||
const evtAttendee = {
|
||||
...attendee,
|
||||
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
|
||||
};
|
||||
return evtAttendee;
|
||||
});
|
||||
|
||||
evt.attendees = updatedBookingAttendees;
|
||||
|
||||
evt = addVideoCallDataToEvt(updatedNewBooking.references, evt);
|
||||
|
||||
const copyEvent = cloneDeep(evt);
|
||||
|
||||
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
||||
|
||||
const results = updateManager.results;
|
||||
|
||||
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||
|
||||
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||
|
||||
if (noEmail !== true && isConfirmedByDefault) {
|
||||
// TODO send reschedule emails to attendees of the old booking
|
||||
loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats");
|
||||
await sendRescheduledEmails({
|
||||
...copyEvent,
|
||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||
});
|
||||
}
|
||||
|
||||
// Update the old booking with the cancelled status
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
},
|
||||
});
|
||||
|
||||
const foundBooking = await findBookingQuery(newTimeSlotBooking.id);
|
||||
|
||||
resultBooking = { ...foundBooking };
|
||||
}
|
||||
}
|
||||
|
||||
// seatAttendee is null when the organizer is rescheduling.
|
||||
const seatAttendee: Partial<Person> | null = bookingSeat?.attendee || null;
|
||||
if (seatAttendee) {
|
||||
seatAttendee["language"] = { translate: tAttendees, locale: bookingSeat?.attendee.locale ?? "en" };
|
||||
|
||||
// If there is no booking then remove the attendee from the old booking and create a new one
|
||||
if (!newTimeSlotBooking) {
|
||||
await prisma.attendee.delete({
|
||||
where: {
|
||||
id: seatAttendee?.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Update the original calendar event by removing the attendee that is rescheduling
|
||||
if (originalBookingEvt && originalRescheduledBooking) {
|
||||
// Event would probably be deleted so we first check than instead of updating references
|
||||
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
|
||||
return attendee.email !== bookerEmail;
|
||||
});
|
||||
const deletedReference = await lastAttendeeDeleteBooking(
|
||||
originalRescheduledBooking,
|
||||
filteredAttendees,
|
||||
originalBookingEvt
|
||||
);
|
||||
|
||||
if (!deletedReference) {
|
||||
await eventManager.updateCalendarAttendees(originalBookingEvt, originalRescheduledBooking);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want to trigger rescheduling logic of the original booking
|
||||
originalRescheduledBooking = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Need to change the new seat reference and attendee record to remove it from the old booking and add it to the new booking
|
||||
// https://stackoverflow.com/questions/4980963/database-insert-new-rows-or-update-existing-ones
|
||||
if (seatAttendee?.id && bookingSeat?.id) {
|
||||
await Promise.all([
|
||||
await prisma.attendee.update({
|
||||
where: {
|
||||
id: seatAttendee.id,
|
||||
},
|
||||
data: {
|
||||
bookingId: newTimeSlotBooking.id,
|
||||
},
|
||||
}),
|
||||
await prisma.bookingSeat.update({
|
||||
where: {
|
||||
id: bookingSeat.id,
|
||||
},
|
||||
data: {
|
||||
bookingId: newTimeSlotBooking.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
const copyEvent = cloneDeep(evt);
|
||||
|
||||
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
||||
|
||||
const results = updateManager.results;
|
||||
|
||||
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||
|
||||
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||
|
||||
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
|
||||
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
|
||||
return attendee.email !== bookerEmail;
|
||||
});
|
||||
await lastAttendeeDeleteBooking(originalRescheduledBooking, filteredAttendees, originalBookingEvt);
|
||||
|
||||
const foundBooking = await findBookingQuery(newTimeSlotBooking.id);
|
||||
|
||||
resultBooking = { ...foundBooking, seatReferenceUid: bookingSeat?.referenceUid };
|
||||
}
|
||||
} else {
|
||||
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
|
||||
const bookingAttendees = booking.attendees.map((attendee) => {
|
||||
return { ...attendee, language: { translate: tAttendees, locale: attendeeLanguage ?? "en" } };
|
||||
});
|
||||
|
||||
evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] };
|
||||
|
||||
if (eventType.seatsPerTimeSlot && eventType.seatsPerTimeSlot <= booking.attendees.length) {
|
||||
throw new HttpError({ statusCode: 409, message: "Booking seats are full" });
|
||||
}
|
||||
|
||||
const videoCallReference = booking.references.find((reference) => reference.type.includes("_video"));
|
||||
|
||||
if (videoCallReference) {
|
||||
evt.videoCallData = {
|
||||
type: videoCallReference.type,
|
||||
id: videoCallReference.meetingId,
|
||||
password: videoCallReference?.meetingPassword,
|
||||
url: videoCallReference.meetingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const attendeeUniqueId = uuid();
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: reqBookingUid,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
data: {
|
||||
attendees: {
|
||||
create: {
|
||||
email: invitee[0].email,
|
||||
name: invitee[0].name,
|
||||
timeZone: invitee[0].timeZone,
|
||||
locale: invitee[0].language.locale,
|
||||
bookingSeat: {
|
||||
create: {
|
||||
referenceUid: attendeeUniqueId,
|
||||
data: {
|
||||
description: additionalNotes,
|
||||
},
|
||||
booking: {
|
||||
connect: {
|
||||
id: booking.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(booking.status === BookingStatus.CANCELLED && { status: BookingStatus.ACCEPTED }),
|
||||
},
|
||||
});
|
||||
|
||||
evt.attendeeSeatId = attendeeUniqueId;
|
||||
|
||||
const newSeat = booking.attendees.length !== 0;
|
||||
|
||||
/**
|
||||
* Remember objects are passed into functions as references
|
||||
* so if you modify it in a inner function it will be modified in the outer function
|
||||
* deep cloning evt to avoid this
|
||||
*/
|
||||
if (!evt?.uid) {
|
||||
evt.uid = booking?.uid ?? null;
|
||||
}
|
||||
const copyEvent = cloneDeep(evt);
|
||||
copyEvent.uid = booking.uid;
|
||||
if (noEmail !== true) {
|
||||
let isHostConfirmationEmailsDisabled = false;
|
||||
let isAttendeeConfirmationEmailDisabled = false;
|
||||
|
||||
const workflows = eventType.workflows.map((workflow) => workflow.workflow);
|
||||
|
||||
if (eventType.workflows) {
|
||||
isHostConfirmationEmailsDisabled =
|
||||
eventType.metadata?.disableStandardEmails?.confirmation?.host || false;
|
||||
isAttendeeConfirmationEmailDisabled =
|
||||
eventType.metadata?.disableStandardEmails?.confirmation?.attendee || false;
|
||||
|
||||
if (isHostConfirmationEmailsDisabled) {
|
||||
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows);
|
||||
}
|
||||
|
||||
if (isAttendeeConfirmationEmailDisabled) {
|
||||
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows);
|
||||
}
|
||||
}
|
||||
await sendScheduledSeatsEmails(
|
||||
copyEvent,
|
||||
invitee[0],
|
||||
newSeat,
|
||||
!!eventType.seatsShowAttendees,
|
||||
isHostConfirmationEmailsDisabled,
|
||||
isAttendeeConfirmationEmailDisabled
|
||||
);
|
||||
}
|
||||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
await eventManager.updateCalendarAttendees(evt, booking);
|
||||
|
||||
const foundBooking = await findBookingQuery(booking.id);
|
||||
|
||||
if (!Number.isNaN(paymentAppData.price) && paymentAppData.price > 0 && !!booking) {
|
||||
const credentialPaymentAppCategories = await prisma.credential.findMany({
|
||||
where: {
|
||||
...(paymentAppData.credentialId
|
||||
? { id: paymentAppData.credentialId }
|
||||
: { userId: organizerUser.id }),
|
||||
app: {
|
||||
categories: {
|
||||
hasSome: ["payment"],
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
key: true,
|
||||
appId: true,
|
||||
app: {
|
||||
select: {
|
||||
categories: true,
|
||||
dirName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypePaymentAppCredential = credentialPaymentAppCategories.find((credential) => {
|
||||
return credential.appId === paymentAppData.appId;
|
||||
});
|
||||
|
||||
if (!eventTypePaymentAppCredential) {
|
||||
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
|
||||
}
|
||||
if (!eventTypePaymentAppCredential?.appId) {
|
||||
throw new HttpError({ statusCode: 400, message: "Missing payment app id" });
|
||||
}
|
||||
|
||||
const payment = await handlePayment(
|
||||
evt,
|
||||
eventType,
|
||||
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
|
||||
booking,
|
||||
fullName,
|
||||
bookerEmail
|
||||
);
|
||||
|
||||
resultBooking = { ...foundBooking };
|
||||
resultBooking["message"] = "Payment required";
|
||||
resultBooking["paymentUid"] = payment?.uid;
|
||||
resultBooking["id"] = payment?.id;
|
||||
} else {
|
||||
resultBooking = { ...foundBooking };
|
||||
}
|
||||
|
||||
resultBooking["seatReferenceUid"] = evt.attendeeSeatId;
|
||||
}
|
||||
|
||||
// Here we should handle every after action that needs to be done after booking creation
|
||||
|
||||
// Obtain event metadata that includes videoCallUrl
|
||||
const metadata = evt.videoCallData?.url ? { videoCallUrl: evt.videoCallData.url } : undefined;
|
||||
try {
|
||||
await scheduleWorkflowReminders({
|
||||
workflows: eventType.workflows,
|
||||
smsReminderNumber: smsReminderNumber || null,
|
||||
calendarEvent: { ...evt, ...{ metadata, eventType: { slug: eventType.slug } } },
|
||||
isNotConfirmed: evt.requiresConfirmation || false,
|
||||
isRescheduleEvent: !!rescheduleUid,
|
||||
isFirstRecurringEvent: true,
|
||||
emailAttendeeSendToOverride: bookerEmail,
|
||||
seatReferenceUid: evt.attendeeSeatId,
|
||||
eventTypeRequiresConfirmation: eventType.requiresConfirmation,
|
||||
});
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
|
||||
}
|
||||
|
||||
const webhookData = {
|
||||
...evt,
|
||||
...eventTypeInfo,
|
||||
uid: resultBooking?.uid || uid,
|
||||
bookingId: booking?.id,
|
||||
rescheduleUid,
|
||||
rescheduleStartTime: originalRescheduledBooking?.startTime
|
||||
? dayjs(originalRescheduledBooking?.startTime).utc().format()
|
||||
: undefined,
|
||||
rescheduleEndTime: originalRescheduledBooking?.endTime
|
||||
? dayjs(originalRescheduledBooking?.endTime).utc().format()
|
||||
: undefined,
|
||||
metadata: { ...metadata, ...reqBodyMetadata },
|
||||
eventTypeId,
|
||||
status: "ACCEPTED",
|
||||
smsReminderNumber: booking?.smsReminderNumber || undefined,
|
||||
};
|
||||
|
||||
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
|
||||
|
||||
return resultBooking;
|
||||
};
|
||||
|
||||
export default handleSeats;
|
Loading…
Reference in New Issue
Block a user