refactor: handleNewBooking #4 (#15673)

* refactor: handleNewBooking #3

* refactor: create booking factor

* refactor: handleNewBooking

* refactor: seats and rescheduleUId

* chore: remove comment

* fix: type err

* chore: add missing statement

* chore: use less params and other improvements

* chore: name

* chore: improvement

* fix: type err

* Typo fix

* chore: fix type err

* refactor: more readable

* refactor: improve code

* fix: conflicts

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
Udit Takkar
2024-10-02 10:26:25 -04:00
committed by GitHub
co-authored by Joe Au-Yeung Joe Au-Yeung Syed Ali Shahbaz
parent a99e285d76
commit b4f1b5aebe
22 changed files with 621 additions and 482 deletions
@@ -1,11 +1,9 @@
import type { DestinationCalendar } from "@prisma/client";
import type { Prisma } from "@prisma/client";
// eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash";
import type { NextApiRequest } from "next";
import short, { uuid } from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import type z from "zod";
import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId";
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
@@ -45,7 +43,6 @@ import {
scheduleTrigger,
} from "@calcom/features/webhooks/lib/scheduleTrigger";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
@@ -55,47 +52,42 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
import { getLuckyUser } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { slugify } from "@calcom/lib/slugify";
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { userSelect } from "@calcom/prisma";
import type { BookingReference } from "@calcom/prisma/client";
import prisma from "@calcom/prisma";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type {
AdditionalInformation,
AppsStatus,
CalendarEvent,
IntervalLimit,
Person,
} from "@calcom/types/Calendar";
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { EventPayloadType, EventTypeInfo } from "../../webhooks/lib/sendPayload";
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials";
import getBookingDataSchema from "./getBookingDataSchema";
import { addVideoCallDataToEvent } from "./handleNewBooking/addVideoCallDataToEvent";
import { checkBookingAndDurationLimits } from "./handleNewBooking/checkBookingAndDurationLimits";
import { checkIfBookerEmailIsBlocked } from "./handleNewBooking/checkIfBookerEmailIsBlocked";
import { createBooking } from "./handleNewBooking/createBooking";
import { ensureAvailableUsers } from "./handleNewBooking/ensureAvailableUsers";
import { getBookingData } from "./handleNewBooking/getBookingData";
import { getCustomInputsResponses } from "./handleNewBooking/getCustomInputsResponses";
import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB";
import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB";
import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb";
import { getOriginalRescheduledBooking } from "./handleNewBooking/getOriginalRescheduledBooking";
import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags";
import { getSeatedBooking } from "./handleNewBooking/getSeatedBooking";
import { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails";
import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus";
import { loadUsers } from "./handleNewBooking/loadUsers";
import { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers";
import type {
Invitee,
IEventTypePaymentCredentialType,
@@ -103,62 +95,13 @@ import type {
BookingType,
Booking,
} from "./handleNewBooking/types";
import { validateBookingTimeIsNotOutOfBounds } from "./handleNewBooking/validateBookingTimeIsNotOutOfBounds";
import { validateEventLength } from "./handleNewBooking/validateEventLength";
import handleSeats from "./handleSeats/handleSeats";
import type { BookingSeat } from "./handleSeats/types";
const translator = short();
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
export function getCustomInputsResponses(
reqBody: {
responses?: Record<string, object>;
customInputs?: z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];
},
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
) {
const customInputsResponses = {} as NonNullable<CalendarEvent["customInputs"]>;
if (reqBody.customInputs && (reqBody.customInputs.length || 0) > 0) {
reqBody.customInputs.forEach(({ label, value }) => {
customInputsResponses[label] = value;
});
} else {
const responses = reqBody.responses || {};
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
for (const [fieldName, fieldValue] of Object.entries(responses)) {
const foundACustomInputForTheResponse = eventTypeCustomInputs.find(
(input) => slugify(input.label) === fieldName
);
if (foundACustomInputForTheResponse) {
customInputsResponses[foundACustomInputForTheResponse.label] = fieldValue;
}
}
}
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 addVideoCallDataToEvent = (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,
@@ -184,6 +127,25 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) {
return originalRescheduledBooking.iCalSequence + 1;
}
const getEventType = async ({
eventTypeId,
eventTypeSlug,
}: {
eventTypeId: number;
eventTypeSlug?: string;
}) => {
// handle dynamic user
const eventType =
!eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
return {
...eventType,
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
};
};
type BookingDataSchemaGetter =
| typeof getBookingDataSchema
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
@@ -208,18 +170,10 @@ async function handler(
platformBookingLocation,
} = req;
// handle dynamic user
let eventType =
!req.body.eventTypeId && !!req.body.eventTypeSlug
? getDefaultEvent(req.body.eventTypeSlug)
: await getEventTypesFromDB(req.body.eventTypeId);
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
eventType = {
...eventType,
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
};
const eventType = await getEventType({
eventTypeId: req.body.eventTypeId,
eventTypeSlug: req.body.eventTypeSlug,
});
const bookingDataSchema = bookingDataSchemaGetter({
view: req.body?.rescheduleUid ? "reschedule" : "booking",
@@ -297,192 +251,51 @@ async function handler(
);
const user = eventType.users.find((user) => user.id === eventType.userId);
const userSchedule = user?.schedules.find((schedule) => schedule.id === user?.defaultScheduleId);
const eventTimeZone = eventType.schedule?.timeZone ?? userSchedule?.timeZone;
let timeOutOfBounds = false;
try {
timeOutOfBounds = isOutOfBounds(
reqBody.start,
{
periodType: eventType.periodType,
periodDays: eventType.periodDays,
periodEndDate: eventType.periodEndDate,
periodStartDate: eventType.periodStartDate,
periodCountCalendarDays: eventType.periodCountCalendarDays,
bookerUtcOffset: getUTCOffsetByTimezone(reqBody.timeZone) ?? 0,
eventUtcOffset: eventTimeZone ? getUTCOffsetByTimezone(eventTimeZone) ?? 0 : 0,
},
eventType.minimumBookingNotice
);
} catch (error) {
loggerWithEventDetails.warn({
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
});
if (error instanceof BookingDateInPastError) {
// TODO: HttpError should not bleed through to the console.
loggerWithEventDetails.info(`Booking eventType ${eventTypeId} failed`, JSON.stringify({ error }));
throw new HttpError({ statusCode: 400, message: error.message });
}
}
if (timeOutOfBounds) {
const error = {
errorCode: "BookingTimeOutOfBounds",
message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
};
loggerWithEventDetails.warn({
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
});
throw new HttpError({ statusCode: 400, message: error.message });
}
const reqEventLength = dayjs(reqBody.end).diff(dayjs(reqBody.start), "minutes");
const validEventLengths = eventType.metadata?.multipleDuration?.length
? eventType.metadata.multipleDuration
: [eventType.length];
if (!validEventLengths.includes(reqEventLength)) {
loggerWithEventDetails.warn({ message: "NewBooking: Invalid event length" });
throw new HttpError({ statusCode: 400, message: "Invalid event length" });
}
// loadUsers allows type inferring
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
})[] = await loadUsers(eventType, dynamicUserList, req);
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
if (!isDynamicAllowed && !eventTypeId) {
loggerWithEventDetails.warn({
message: "NewBooking: Some of the users in this group do not allow dynamic booking",
});
throw new HttpError({
message: "Some of the users in this group do not allow dynamic booking",
statusCode: 400,
});
}
// If this event was pre-relationship migration
// TODO: Establish whether this is dead code.
if (!users.length && eventType.userId) {
const eventTypeUser = await prisma.user.findUnique({
where: {
id: eventType.userId,
},
select: {
credentials: {
select: credentialForCalendarServiceSelect,
}, // Don't leak to client
...userSelect.select,
},
});
if (!eventTypeUser) {
loggerWithEventDetails.warn({ message: "NewBooking: eventTypeUser.notFound" });
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
}
users.push(eventTypeUser);
}
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
users = users.map((user) => ({
...user,
isFixed:
user.isFixed === false
? false
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
}));
loggerWithEventDetails.debug(
"Concerned users",
safeStringify({
users: users.map(getPiiFreeUser),
})
await validateBookingTimeIsNotOutOfBounds<typeof eventType>(
reqBody.start,
reqBody.timeZone,
eventType,
eventTimeZone,
loggerWithEventDetails
);
let locationBodyString = location;
validateEventLength({
reqBodyStart: reqBody.start,
reqBodyEnd: reqBody.end,
eventTypeMultipleDuration: eventType.metadata?.multipleDuration,
eventTypeLength: eventType.length,
logger: loggerWithEventDetails,
});
// TODO: It's definition should be moved to getLocationValueForDb
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined;
let users = await loadAndValidateUsers({
req,
eventType,
eventTypeId,
dynamicUserList,
logger: loggerWithEventDetails,
});
if (dynamicUserList.length > 1) {
users = users.sort((a, b) => {
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata);
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
firstUsersMetadata?.defaultConferencingApp?.appLink;
}
let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb(
dynamicUserList,
users,
location
);
let rescheduleUid = reqBody.rescheduleUid;
await checkBookingAndDurationLimits({
eventType,
reqBodyStart: reqBody.start,
reqBodyRescheduleUid: reqBody.rescheduleUid,
});
if (
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
) {
const startAsDate = dayjs(reqBody.start).toDate();
if (
eventType.bookingLimits &&
/* Empty object is truthy */ Object.keys(eventType.bookingLimits).length > 0
) {
await checkBookingLimits(
eventType.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
rescheduleUid,
eventTimeZone
);
}
if (eventType.durationLimits) {
await checkDurationLimits(
eventType.durationLimits as IntervalLimit,
startAsDate,
eventType.id,
rescheduleUid
);
}
}
const bookingSeat = reqBody.rescheduleUid ? await getSeatedBooking(reqBody.rescheduleUid) : null;
const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBody.rescheduleUid;
let bookingSeat: BookingSeat = null;
let originalRescheduledBooking: BookingType = null;
//this gets the original rescheduled booking
if (rescheduleUid) {
// rescheduleUid can be bookingUid and bookingSeatUid
bookingSeat = await prisma.bookingSeat.findUnique({
where: {
referenceUid: rescheduleUid,
},
include: {
booking: true,
attendee: true,
},
});
if (bookingSeat) {
rescheduleUid = bookingSeat.booking.uid;
}
originalRescheduledBooking = await getOriginalRescheduledBooking(
rescheduleUid,
!!eventType.seatsPerTimeSlot
);
if (!originalRescheduledBooking) {
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
}
if (
originalRescheduledBooking.status === BookingStatus.CANCELLED &&
!originalRescheduledBooking.rescheduled
) {
throw new HttpError({ statusCode: 403, message: ErrorCode.CancelledBookingsCannotBeRescheduled });
}
}
let originalRescheduledBooking = rescheduleUid
? await getOriginalRescheduledBooking(rescheduleUid, !!eventType.seatsPerTimeSlot)
: null;
let luckyUserResponse;
let isFirstSeat = true;
@@ -929,7 +742,7 @@ async function handler(
eventDescription: eventType.description,
price: paymentAppData.price,
currency: eventType.currency,
length: reqEventLength,
length: dayjs(reqBody.end).diff(dayjs(reqBody.start), "minutes"),
};
const teamId = await getTeamIdFromEventType({ eventType });
@@ -1087,23 +900,29 @@ async function handler(
try {
booking = await createBooking({
originalRescheduledBooking,
evt,
eventTypeId,
eventTypeSlug,
reqBodyUser: reqBody.user,
reqBodyMetadata: reqBody.metadata,
reqBodyRecurringEventId: reqBody.recurringEventId,
uid,
responses,
isConfirmedByDefault,
smsReminderNumber,
organizerUser,
rescheduleReason,
eventType,
bookerEmail,
paymentAppData,
changedOrganizer,
reqBody: {
user: reqBody.user,
metadata: reqBody.metadata,
recurringEventId: reqBody.recurringEventId,
},
eventType: {
eventTypeData: eventType,
id: eventTypeId,
slug: eventTypeSlug,
organizerUser,
isConfirmedByDefault,
paymentAppData,
},
input: {
bookerEmail,
rescheduleReason,
changedOrganizer,
smsReminderNumber,
responses,
},
evt,
originalRescheduledBooking,
});
// @NOTE: Add specific try catch for all subsequent async calls to avoid error
@@ -1833,28 +1652,3 @@ async function handler(
}
export default handler;
function getVideoCallDetails({
results,
}: {
results: EventResult<AdditionalInformation & { url?: string | undefined; iCalUID?: string | undefined }>[];
}) {
const firstVideoResult = results.find((result) => result.type.includes("_video"));
const metadata: AdditionalInformation = {};
let updatedVideoEvent = null;
if (firstVideoResult && firstVideoResult.success) {
updatedVideoEvent = Array.isArray(firstVideoResult.updatedEvent)
? firstVideoResult.updatedEvent[0]
: firstVideoResult.updatedEvent;
if (updatedVideoEvent) {
metadata.hangoutLink = updatedVideoEvent.hangoutLink;
metadata.conferenceData = updatedVideoEvent.conferenceData;
metadata.entryPoints = updatedVideoEvent.entryPoints;
}
}
const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url;
return { videoCallUrl, metadata, updatedVideoEvent };
}
@@ -0,0 +1,24 @@
import type { BookingReference } from "@calcom/prisma/client";
import type { CalendarEvent } from "@calcom/types/Calendar";
/** 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 addVideoCallDataToEvent = (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;
};
@@ -0,0 +1,43 @@
import dayjs from "@calcom/dayjs";
import { checkBookingLimits, checkDurationLimits } from "@calcom/lib/server";
import type { IntervalLimit } from "@calcom/types/Calendar";
import type { NewBookingEventType } from "./types";
type EventType = Pick<NewBookingEventType, "bookingLimits" | "durationLimits" | "id" | "schedule">;
type InputProps = {
eventType: EventType;
reqBodyStart: string;
reqBodyRescheduleUid?: string;
};
export const checkBookingAndDurationLimits = async ({
eventType,
reqBodyStart,
reqBodyRescheduleUid,
}: InputProps) => {
if (
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
) {
const startAsDate = dayjs(reqBodyStart).toDate();
if (eventType.bookingLimits && Object.keys(eventType.bookingLimits).length > 0) {
await checkBookingLimits(
eventType.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid,
eventType.schedule?.timeZone
);
}
if (eventType.durationLimits) {
await checkDurationLimits(
eventType.durationLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid
);
}
}
};
@@ -12,35 +12,40 @@ import type {
EventTypeId,
AwaitedBookingData,
NewBookingEventType,
IsConfirmedByDefault,
PaymentAppData,
OriginalRescheduledBooking,
AwaitedLoadUsers,
LoadedUsers,
} from "./types";
type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };
type CreateBookingParams = {
originalRescheduledBooking: OriginalRescheduledBooking;
evt: CalendarEvent;
eventType: NewBookingEventType;
eventTypeId: EventTypeId;
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
reqBodyUser: ReqBodyWithEnd["user"];
reqBodyMetadata: ReqBodyWithEnd["metadata"];
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
uid: short.SUUID;
responses: ReqBodyWithEnd["responses"] | null;
isConfirmedByDefault: IsConfirmedByDefault;
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
organizerUser: AwaitedLoadUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
reqBody: {
user: ReqBodyWithEnd["user"];
metadata: ReqBodyWithEnd["metadata"];
recurringEventId: ReqBodyWithEnd["recurringEventId"];
};
rescheduleReason: AwaitedBookingData["rescheduleReason"];
bookerEmail: AwaitedBookingData["email"];
paymentAppData: PaymentAppData;
changedOrganizer: boolean;
eventType: {
eventTypeData: NewBookingEventType;
id: EventTypeId;
slug: AwaitedBookingData["eventTypeSlug"];
organizerUser: LoadedUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
};
isConfirmedByDefault: boolean;
paymentAppData: PaymentAppData;
};
input: {
bookerEmail: AwaitedBookingData["email"];
rescheduleReason: AwaitedBookingData["rescheduleReason"];
changedOrganizer: boolean;
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
responses: ReqBodyWithEnd["responses"] | null;
};
evt: CalendarEvent;
originalRescheduledBooking: OriginalRescheduledBooking;
};
function updateEventDetails(
@@ -57,52 +62,37 @@ function updateEventDetails(
}
export async function createBooking({
originalRescheduledBooking,
evt,
eventTypeId,
eventTypeSlug,
reqBodyUser,
reqBodyMetadata,
reqBodyRecurringEventId,
uid,
responses,
isConfirmedByDefault,
smsReminderNumber,
organizerUser,
rescheduleReason,
reqBody,
eventType,
bookerEmail,
paymentAppData,
changedOrganizer,
input,
evt,
originalRescheduledBooking,
}: CreateBookingParams) {
updateEventDetails(evt, originalRescheduledBooking, changedOrganizer);
updateEventDetails(evt, originalRescheduledBooking, input.changedOrganizer);
const newBookingData = buildNewBookingData({
uid,
evt,
responses,
isConfirmedByDefault,
reqBodyMetadata,
smsReminderNumber,
eventTypeSlug,
organizerUser,
reqBodyRecurringEventId,
originalRescheduledBooking,
bookerEmail,
rescheduleReason,
reqBody,
eventType,
eventTypeId,
reqBodyUser,
input,
evt,
originalRescheduledBooking,
});
return await saveBooking(newBookingData, originalRescheduledBooking, paymentAppData, organizerUser);
return await saveBooking(
newBookingData,
originalRescheduledBooking,
eventType.paymentAppData,
eventType.organizerUser
);
}
async function saveBooking(
newBookingData: Prisma.BookingCreateInput,
originalRescheduledBooking: OriginalRescheduledBooking,
paymentAppData: PaymentAppData,
organizerUser: CreateBookingParams["organizerUser"]
organizerUser: CreateBookingParams["eventType"]["organizerUser"]
) {
const createBookingObj = {
include: {
@@ -143,93 +133,49 @@ function getEventTypeRel(eventTypeId: EventTypeId) {
function getAttendeesData(evt: Pick<CalendarEvent, "attendees" | "team">) {
//if attendee is team member, it should fetch their locale not booker's locale
//perhaps make email fetch request to see if his locale is stored, else
const attendees = evt.attendees.map((attendee) => ({
const teamMembers = evt?.team?.members ?? [];
return evt.attendees.concat(teamMembers).map((attendee) => ({
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
locale: attendee.language.locale,
phoneNumber: attendee.phoneNumber,
}));
if (evt.team?.members) {
attendees.push(
...evt.team.members.map((member) => ({
email: member.email,
name: member.name,
timeZone: member.timeZone,
locale: member.language.locale,
phoneNumber: member.phoneNumber,
}))
);
}
return attendees;
}
function buildNewBookingData(params: {
uid: short.SUUID;
evt: CalendarEvent;
responses: ReqBodyWithEnd["responses"] | null;
isConfirmedByDefault: IsConfirmedByDefault;
reqBodyMetadata: ReqBodyWithEnd["metadata"];
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
organizerUser: CreateBookingParams["organizerUser"];
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
originalRescheduledBooking: OriginalRescheduledBooking | null;
bookerEmail: AwaitedBookingData["email"];
rescheduleReason: AwaitedBookingData["rescheduleReason"];
eventType: NewBookingEventType;
eventTypeId: EventTypeId;
reqBodyUser: ReqBodyWithEnd["user"];
}): Prisma.BookingCreateInput {
const {
uid,
evt,
responses,
isConfirmedByDefault,
reqBodyMetadata,
smsReminderNumber,
eventTypeSlug,
organizerUser,
reqBodyRecurringEventId,
originalRescheduledBooking,
bookerEmail,
rescheduleReason,
eventType,
eventTypeId,
reqBodyUser,
} = params;
function buildNewBookingData(params: CreateBookingParams): Prisma.BookingCreateInput {
const { uid, evt, reqBody, eventType, input, originalRescheduledBooking } = params;
const attendeesData = getAttendeesData(evt);
const eventTypeRel = getEventTypeRel(eventTypeId);
const eventTypeRel = getEventTypeRel(eventType.id);
const newBookingData: Prisma.BookingCreateInput = {
uid,
userPrimaryEmail: evt.organizer.email,
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses,
responses: input.responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : input.responses,
title: evt.title,
startTime: dayjs.utc(evt.startTime).toDate(),
endTime: dayjs.utc(evt.endTime).toDate(),
description: evt.seatsPerTimeSlot ? null : evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
status: eventType.isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
oneTimePassword: evt.oneTimePassword,
location: evt.location,
eventType: eventTypeRel,
smsReminderNumber,
metadata: reqBodyMetadata,
smsReminderNumber: input.smsReminderNumber,
metadata: reqBody.metadata,
attendees: {
createMany: {
data: attendeesData,
},
},
dynamicEventSlugRef: !eventTypeId ? eventTypeSlug : null,
dynamicGroupSlugRef: !eventTypeId ? (reqBodyUser as string).toLowerCase() : null,
dynamicEventSlugRef: !eventType.id ? eventType.slug : null,
dynamicGroupSlugRef: !eventType.id ? (reqBody.user as string).toLowerCase() : null,
iCalUID: evt.iCalUID ?? "",
user: {
connect: {
id: organizerUser.id,
id: eventType.organizerUser.id,
},
},
destinationCalendar:
@@ -240,24 +186,28 @@ function buildNewBookingData(params: {
: undefined,
};
if (reqBodyRecurringEventId) {
newBookingData.recurringEventId = reqBodyRecurringEventId;
if (reqBody.recurringEventId) {
newBookingData.recurringEventId = reqBody.recurringEventId;
}
if (originalRescheduledBooking) {
newBookingData.metadata = {
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
...reqBodyMetadata,
...reqBody.metadata,
};
newBookingData.paid = originalRescheduledBooking.paid;
newBookingData.fromReschedule = originalRescheduledBooking.uid;
if (originalRescheduledBooking.uid) {
newBookingData.cancellationReason = rescheduleReason;
newBookingData.cancellationReason = input.rescheduleReason;
}
// Reschedule logic with booking with seats
if (newBookingData.attendees?.createMany?.data && eventType?.seatsPerTimeSlot && bookerEmail) {
if (
newBookingData.attendees?.createMany?.data &&
eventType?.eventTypeData?.seatsPerTimeSlot &&
input.bookerEmail
) {
newBookingData.attendees.createMany.data = attendeesData.filter(
(attendee) => attendee.email === bookerEmail
(attendee) => attendee.email === input.bookerEmail
);
}
if (originalRescheduledBooking.recurringEventId) {
@@ -0,0 +1,49 @@
import type z from "zod";
import { slugify } from "@calcom/lib/slugify";
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { getEventTypeResponse } from "./types";
type CustomInputs = z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];
type RequestBody = {
responses?: Record<string, object>;
customInputs?: CustomInputs;
};
function mapCustomInputs(
customInputs: { label: string; value: CustomInputs[number]["value"] }[]
): Record<string, CustomInputs[number]["value"]> {
return customInputs.reduce((acc, { label, value }) => {
acc[label] = value;
return acc;
}, {} as Record<string, CustomInputs[number]["value"]>);
}
function mapResponsesToCustomInputs(
responses: Record<string, object>,
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
): NonNullable<CalendarEvent["customInputs"]> {
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
return Object.entries(responses).reduce((acc, [fieldName, fieldValue]) => {
const foundInput = eventTypeCustomInputs.find((input) => slugify(input.label) === fieldName);
if (foundInput) {
acc[foundInput.label] = fieldValue;
}
return acc;
}, {} as NonNullable<CalendarEvent["customInputs"]>);
}
export function getCustomInputsResponses(
reqBody: RequestBody,
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
): NonNullable<CalendarEvent["customInputs"]> {
if (reqBody.customInputs && reqBody.customInputs.length > 0) {
return mapCustomInputs(reqBody.customInputs);
}
const responses = reqBody.responses || {};
return mapResponsesToCustomInputs(responses, eventTypeCustomInputs);
}
@@ -0,0 +1,30 @@
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { loadAndValidateUsers } from "./loadAndValidateUsers";
type Users = Awaited<ReturnType<typeof loadAndValidateUsers>>;
const sortUsersByDynamicList = (users: Users, dynamicUserList: string[]) => {
return users.sort((a, b) => {
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
};
export const getLocationValuesForDb = (
dynamicUserList: string[],
users: Users,
locationBodyString: string
) => {
// TODO: It's definition should be moved to getLocationValueForDb
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl;
if (dynamicUserList.length > 1) {
users = sortUsersByDynamicList(users, dynamicUserList);
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata);
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
firstUsersMetadata?.defaultConferencingApp?.appLink;
}
return { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl };
};
@@ -1,59 +1,14 @@
import type { Prisma } from "@prisma/client";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { validateOriginalRescheduledBooking } from "./validateOriginalRescheduledBooking";
export async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
return prisma.booking.findFirst({
where: {
uid: uid,
status: {
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
},
},
include: {
attendees: {
select: {
name: true,
email: true,
locale: true,
timeZone: true,
phoneNumber: true,
...(seatsEventType && { bookingSeat: true, id: true }),
},
},
user: {
select: {
id: true,
name: true,
email: true,
locale: true,
timeZone: true,
destinationCalendar: true,
credentials: {
select: {
id: true,
userId: true,
key: true,
type: true,
teamId: true,
appId: true,
invalid: true,
user: {
select: {
email: true,
},
},
},
},
},
},
destinationCalendar: true,
payment: true,
references: true,
workflowReminders: true,
},
});
const originalBooking = await BookingRepository.findOriginalRescheduledBooking(uid, seatsEventType);
validateOriginalRescheduledBooking(originalBooking);
return originalBooking;
}
export type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
@@ -69,5 +69,3 @@ function determineIsConfirmedByDefault(
): boolean {
return (!requiresConfirmation && price === 0) || userReschedulingIsOwner;
}
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
@@ -0,0 +1,16 @@
import prisma from "@calcom/prisma";
import type { BookingSeat } from "../handleSeats/types";
export const getSeatedBooking = async (bookingSeatUid: string): Promise<BookingSeat | null> => {
// rescheduleUid can be bookingUid and bookingSeatUid
return prisma.bookingSeat.findUnique({
where: {
referenceUid: bookingSeatUid,
},
include: {
booking: true,
attendee: true,
},
});
};
@@ -0,0 +1,32 @@
import type { AdditionalInformation } from "@calcom/types/Calendar";
import type { EventResult } from "@calcom/types/EventManager";
type ExtraAdditionalInfo = AdditionalInformation & {
url?: string | undefined;
iCalUID?: string | undefined;
};
type VideoResult = EventResult<ExtraAdditionalInfo>;
function extractUpdatedVideoEvent(result: VideoResult | undefined): ExtraAdditionalInfo | undefined {
if (!result || !result.success) return undefined;
return Array.isArray(result.updatedEvent) ? result.updatedEvent[0] : result.updatedEvent;
}
function extractMetadata(event: ExtraAdditionalInfo): AdditionalInformation {
return {
hangoutLink: event.hangoutLink,
conferenceData: event.conferenceData,
entryPoints: event.entryPoints,
};
}
export function getVideoCallDetails({ results }: { results: VideoResult[] }) {
const firstVideoResult = results.find((result) => result.type.includes("_video"));
const updatedVideoEvent = extractUpdatedVideoEvent(firstVideoResult);
const metadata = updatedVideoEvent ? extractMetadata(updatedVideoEvent) : {};
const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url;
return { videoCallUrl, metadata, updatedVideoEvent };
}
@@ -0,0 +1,89 @@
import type { Prisma } from "@prisma/client";
import type { IncomingMessage } from "http";
import type { Logger } from "tslog";
import { HttpError } from "@calcom/lib/http-error";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { userSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { loadUsers } from "./loadUsers";
import type { NewBookingEventType } from "./types";
type Users = (Awaited<ReturnType<typeof loadUsers>>[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
})[];
type EventType = Pick<NewBookingEventType, "hosts" | "users" | "id" | "userId" | "schedulingType">;
type InputProps = {
req: IncomingMessage;
eventType: EventType;
eventTypeId: number;
dynamicUserList: string[];
logger: Logger<unknown>;
};
export async function loadAndValidateUsers({
req,
eventType,
eventTypeId,
dynamicUserList,
logger,
}: InputProps): Promise<Users> {
let users: Users = await loadUsers(eventType, dynamicUserList, req);
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
if (!isDynamicAllowed && !eventTypeId) {
logger.warn({
message: "NewBooking: Some of the users in this group do not allow dynamic booking",
});
throw new HttpError({
message: "Some of the users in this group do not allow dynamic booking",
statusCode: 400,
});
}
// If this event was pre-relationship migration
// TODO: Establish whether this is dead code.
if (!users.length && eventType.userId) {
const eventTypeUser = await prisma.user.findUnique({
where: {
id: eventType.userId,
},
select: {
credentials: {
select: credentialForCalendarServiceSelect,
}, // Don't leak to client
...userSelect.select,
},
});
if (!eventTypeUser) {
logger.warn({ message: "NewBooking: eventTypeUser.notFound" });
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
}
users.push(eventTypeUser);
}
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
users = users.map((user) => ({
...user,
isFixed:
user.isFixed === false
? false
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
}));
logger.debug(
"Concerned users",
safeStringify({
users: users.map(getPiiFreeUser),
})
);
return users;
}
@@ -88,4 +88,4 @@ export const findUsersByUsername = async ({
});
};
export type AwaitedLoadUsers = Awaited<ReturnType<typeof loadUsers>>;
export type LoadedUsers = Awaited<ReturnType<typeof loadUsers>>;
@@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
import type { TFunction } from "next-i18next";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import type { AwaitedGetDefaultEvent } from "@calcom/lib/defaultEvents";
import type { DefaultEvent } from "@calcom/lib/defaultEvents";
import type { PaymentAppData } from "@calcom/lib/getPaymentAppData";
import type { userSelect } from "@calcom/prisma";
import type { CredentialPayload } from "@calcom/types/Credential";
@@ -21,12 +21,11 @@ import type {
} from "./getBookingData";
import type { getEventTypeResponse } from "./getEventTypesFromDB";
import type { BookingType, OriginalRescheduledBooking } from "./getOriginalRescheduledBooking";
import type { getRequiresConfirmationFlags } from "./getRequiresConfirmationFlags";
import type { AwaitedLoadUsers } from "./loadUsers";
import type { LoadedUsers } from "./loadUsers";
type User = Prisma.UserGetPayload<typeof userSelect>;
export type OrganizerUser = AwaitedLoadUsers[number] & {
export type OrganizerUser = LoadedUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
};
@@ -62,9 +61,7 @@ export type IsFixedAwareUser = User & {
weightAdjustment?: number;
};
export type NewBookingEventType = AwaitedGetDefaultEvent | getEventTypeResponse;
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
export type NewBookingEventType = DefaultEvent | getEventTypeResponse;
export type {
AwaitedBookingData,
@@ -79,6 +76,6 @@ export type {
BookingType,
Booking,
OriginalRescheduledBooking,
AwaitedLoadUsers,
LoadedUsers,
getEventTypeResponse,
};
@@ -0,0 +1,62 @@
import type { Logger } from "tslog";
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
import { HttpError } from "@calcom/lib/http-error";
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
import type { EventType } from "@calcom/prisma/client";
type ValidateBookingTimeEventType = Pick<
EventType,
| "periodType"
| "periodDays"
| "periodEndDate"
| "periodStartDate"
| "periodCountCalendarDays"
| "minimumBookingNotice"
| "eventName"
| "id"
>;
export const validateBookingTimeIsNotOutOfBounds = async <T extends ValidateBookingTimeEventType>(
reqBodyStartTime: string,
reqBodyTimeZone: string,
eventType: T,
eventTimeZone: string | null | undefined,
logger: Logger<unknown>
) => {
let timeOutOfBounds = false;
try {
timeOutOfBounds = isOutOfBounds(
reqBodyStartTime,
{
periodType: eventType.periodType,
periodDays: eventType.periodDays,
periodEndDate: eventType.periodEndDate,
periodStartDate: eventType.periodStartDate,
periodCountCalendarDays: eventType.periodCountCalendarDays,
bookerUtcOffset: getUTCOffsetByTimezone(reqBodyTimeZone) ?? 0,
eventUtcOffset: eventTimeZone ? getUTCOffsetByTimezone(eventTimeZone) ?? 0 : 0,
},
eventType.minimumBookingNotice
);
} catch (error) {
logger.warn({
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
});
if (error instanceof BookingDateInPastError) {
logger.info(`Booking eventType ${eventType.id} failed`, JSON.stringify({ error }));
throw new HttpError({ statusCode: 400, message: error.message });
}
}
if (timeOutOfBounds) {
const error = {
errorCode: "BookingTimeOutOfBounds",
message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
};
logger.warn({
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
});
throw new HttpError({ statusCode: 400, message: error.message });
}
};
@@ -0,0 +1,27 @@
import type { Logger } from "tslog";
import dayjs from "@calcom/dayjs";
import { HttpError } from "@calcom/lib/http-error";
type Props = {
reqBodyStart: string;
reqBodyEnd: string;
eventTypeMultipleDuration?: number[];
eventTypeLength: number;
logger: Logger<unknown>;
};
export const validateEventLength = ({
reqBodyStart,
reqBodyEnd,
eventTypeMultipleDuration,
eventTypeLength,
logger,
}: Props) => {
const reqEventLength = dayjs(reqBodyEnd).diff(dayjs(reqBodyStart), "minutes");
const validEventLengths = eventTypeMultipleDuration?.length ? eventTypeMultipleDuration : [eventTypeLength];
if (!validEventLengths.includes(reqEventLength)) {
logger.warn({ message: "NewBooking: Invalid event length" });
throw new HttpError({ statusCode: 400, message: "Invalid event length" });
}
};
@@ -0,0 +1,20 @@
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import { BookingStatus } from "@calcom/prisma/enums";
import { type OriginalRescheduledBooking } from "./getOriginalRescheduledBooking";
export const validateOriginalRescheduledBooking = async (
originalRescheduledBooking: OriginalRescheduledBooking
) => {
if (!originalRescheduledBooking) {
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
}
if (
originalRescheduledBooking.status === BookingStatus.CANCELLED &&
!originalRescheduledBooking.rescheduled
) {
throw new HttpError({ statusCode: 403, message: ErrorCode.CancelledBookingsCannotBeRescheduled });
}
};
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
import type { SeatedBooking, RescheduleSeatedBookingObject, NewTimeSlotBooking } from "../../types";
@@ -6,8 +6,8 @@ import { sendRescheduledEmailsAndSMS } from "@calcom/emails";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus";
import type { Booking } from "../../../handleNewBooking/types";
+1 -1
View File
@@ -27,7 +27,7 @@ export type NewSeatedBookingObject = {
rescheduleReason: RescheduleReason;
reqBodyUser: string | string[] | undefined;
noEmail: NoEmail;
isConfirmedByDefault: IsConfirmedByDefault;
isConfirmedByDefault: boolean;
additionalNotes: AdditionalNotes;
reqAppsStatus: ReqAppsStatus;
attendeeLanguage: string | null;
@@ -8,8 +8,8 @@ import { createInstantMeetingWithCalVideo } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking";
import { getBookingData } from "@calcom/features/bookings/lib/handleNewBooking/getBookingData";
import { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking/getCustomInputsResponses";
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import { getFullName } from "@calcom/features/form-builder/utils";
import { sendNotification } from "@calcom/features/notifications/sendNotification";
+1 -1
View File
@@ -172,4 +172,4 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
export default defaultEvents;
export type AwaitedGetDefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;
export type DefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;
+53
View File
@@ -336,4 +336,57 @@ export class BookingRepository {
return [...collectiveRoundRobinBookingsOwner, ...collectiveRoundRobinBookingsAttendee];
}
static async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
return await prisma.booking.findFirst({
where: {
uid: uid,
status: {
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
},
},
include: {
attendees: {
select: {
name: true,
email: true,
locale: true,
timeZone: true,
phoneNumber: true,
...(seatsEventType && { bookingSeat: true, id: true }),
},
},
user: {
select: {
id: true,
name: true,
email: true,
locale: true,
timeZone: true,
destinationCalendar: true,
credentials: {
select: {
id: true,
userId: true,
key: true,
type: true,
teamId: true,
appId: true,
invalid: true,
user: {
select: {
email: true,
},
},
},
},
},
},
destinationCalendar: true,
payment: true,
references: true,
workflowReminders: true,
},
});
}
}