diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 67012fa09c27e8..5f8164a03dc9ee 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -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; - customInputs?: z.infer["customInputs"]; - }, - eventTypeCustomInputs: getEventTypeResponse["customInputs"] -) { - const customInputsResponses = {} as NonNullable; - 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>[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( + reqBody.start, + reqBody.timeZone, + eventType, + eventTimeZone, + loggerWithEventDetails ); - let locationBodyString = location; - - // TODO: It's definition should be moved to getLocationValueForDb - let organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined; - - 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; - } + validateEventLength({ + reqBodyStart: reqBody.start, + reqBodyEnd: reqBody.end, + eventTypeMultipleDuration: eventType.metadata?.multipleDuration, + eventTypeLength: eventType.length, + logger: loggerWithEventDetails, + }); - let rescheduleUid = 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 - ); - } - } + let users = await loadAndValidateUsers({ + req, + eventType, + eventTypeId, + dynamicUserList, + logger: loggerWithEventDetails, + }); - let bookingSeat: BookingSeat = null; + let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb( + dynamicUserList, + users, + location + ); - let originalRescheduledBooking: BookingType = null; + await checkBookingAndDurationLimits({ + eventType, + reqBodyStart: reqBody.start, + reqBodyRescheduleUid: reqBody.rescheduleUid, + }); - //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" }); - } + const bookingSeat = reqBody.rescheduleUid ? await getSeatedBooking(reqBody.rescheduleUid) : null; + const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBody.rescheduleUid; - 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[]; -}) { - 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 }; -} diff --git a/packages/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent.ts b/packages/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent.ts new file mode 100644 index 00000000000000..624ce91a3ee87d --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent.ts @@ -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; +}; diff --git a/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts b/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts new file mode 100644 index 00000000000000..aad4904eac25f2 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts @@ -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; + +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 + ); + } + } +}; diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index d9c078f6c628d9..089cefbd288b7a 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -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"]; + }; + 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; }; - rescheduleReason: AwaitedBookingData["rescheduleReason"]; - bookerEmail: AwaitedBookingData["email"]; - paymentAppData: PaymentAppData; - changedOrganizer: boolean; + 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, + reqBody, + eventType, + input, evt, - responses, - isConfirmedByDefault, - reqBodyMetadata, - smsReminderNumber, - eventTypeSlug, - organizerUser, - reqBodyRecurringEventId, originalRescheduledBooking, - bookerEmail, - rescheduleReason, - eventType, - eventTypeId, - reqBodyUser, }); - 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) { //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) { diff --git a/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts b/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts new file mode 100644 index 00000000000000..76e34fb83e33f9 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts @@ -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["customInputs"]; + +type RequestBody = { + responses?: Record; + customInputs?: CustomInputs; +}; + +function mapCustomInputs( + customInputs: { label: string; value: CustomInputs[number]["value"] }[] +): Record { + return customInputs.reduce((acc, { label, value }) => { + acc[label] = value; + return acc; + }, {} as Record); +} + +function mapResponsesToCustomInputs( + responses: Record, + eventTypeCustomInputs: getEventTypeResponse["customInputs"] +): NonNullable { + // 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); +} + +export function getCustomInputsResponses( + reqBody: RequestBody, + eventTypeCustomInputs: getEventTypeResponse["customInputs"] +): NonNullable { + if (reqBody.customInputs && reqBody.customInputs.length > 0) { + return mapCustomInputs(reqBody.customInputs); + } + + const responses = reqBody.responses || {}; + return mapResponsesToCustomInputs(responses, eventTypeCustomInputs); +} diff --git a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts new file mode 100644 index 00000000000000..5efa56e02feb80 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts @@ -0,0 +1,30 @@ +import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; + +import type { loadAndValidateUsers } from "./loadAndValidateUsers"; + +type Users = Awaited>; + +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 }; +}; diff --git a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBooking.ts b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBooking.ts index 021541a03704e4..5199885e65201f 100644 --- a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBooking.ts @@ -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; diff --git a/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts b/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts index e2c4f9891f1b97..23a2df53f956c6 100644 --- a/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts +++ b/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts @@ -69,5 +69,3 @@ function determineIsConfirmedByDefault( ): boolean { return (!requiresConfirmation && price === 0) || userReschedulingIsOwner; } - -export type IsConfirmedByDefault = ReturnType["isConfirmedByDefault"]; diff --git a/packages/features/bookings/lib/handleNewBooking/getSeatedBooking.ts b/packages/features/bookings/lib/handleNewBooking/getSeatedBooking.ts new file mode 100644 index 00000000000000..0169cbdd2c5bd3 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getSeatedBooking.ts @@ -0,0 +1,16 @@ +import prisma from "@calcom/prisma"; + +import type { BookingSeat } from "../handleSeats/types"; + +export const getSeatedBooking = async (bookingSeatUid: string): Promise => { + // rescheduleUid can be bookingUid and bookingSeatUid + return prisma.bookingSeat.findUnique({ + where: { + referenceUid: bookingSeatUid, + }, + include: { + booking: true, + attendee: true, + }, + }); +}; diff --git a/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts b/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts new file mode 100644 index 00000000000000..b9f2301350cb3d --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts @@ -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; + +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 }; +} diff --git a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts new file mode 100644 index 00000000000000..94b26433660558 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -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>[number] & { + isFixed?: boolean; + metadata?: Prisma.JsonValue; +})[]; + +type EventType = Pick; + +type InputProps = { + req: IncomingMessage; + eventType: EventType; + eventTypeId: number; + dynamicUserList: string[]; + logger: Logger; +}; + +export async function loadAndValidateUsers({ + req, + eventType, + eventTypeId, + dynamicUserList, + logger, +}: InputProps): Promise { + 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; +} diff --git a/packages/features/bookings/lib/handleNewBooking/loadUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadUsers.ts index 1e2a5a0e46e228..02eaa51e394fc5 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadUsers.ts @@ -88,4 +88,4 @@ export const findUsersByUsername = async ({ }); }; -export type AwaitedLoadUsers = Awaited>; +export type LoadedUsers = Awaited>; diff --git a/packages/features/bookings/lib/handleNewBooking/types.ts b/packages/features/bookings/lib/handleNewBooking/types.ts index 315b2606d08fc2..297e43dabad3f1 100644 --- a/packages/features/bookings/lib/handleNewBooking/types.ts +++ b/packages/features/bookings/lib/handleNewBooking/types.ts @@ -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; -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["isConfirmedByDefault"]; +export type NewBookingEventType = DefaultEvent | getEventTypeResponse; export type { AwaitedBookingData, @@ -79,6 +76,6 @@ export type { BookingType, Booking, OriginalRescheduledBooking, - AwaitedLoadUsers, + LoadedUsers, getEventTypeResponse, }; diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts new file mode 100644 index 00000000000000..d17f39dd94be84 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -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 ( + reqBodyStartTime: string, + reqBodyTimeZone: string, + eventType: T, + eventTimeZone: string | null | undefined, + logger: Logger +) => { + 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 }); + } +}; diff --git a/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts b/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts new file mode 100644 index 00000000000000..62ae8aa65e4db1 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts @@ -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; +}; + +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" }); + } +}; diff --git a/packages/features/bookings/lib/handleNewBooking/validateOriginalRescheduledBooking.ts b/packages/features/bookings/lib/handleNewBooking/validateOriginalRescheduledBooking.ts new file mode 100644 index 00000000000000..98d669155cf866 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/validateOriginalRescheduledBooking.ts @@ -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 }); + } +}; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts index c38e295bf1aacd..42d0e4fc9dffca 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts @@ -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"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts index 7ee29652f6cee1..5a04e20e9f6409 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts @@ -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"; diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index 1e36bd8c62d88b..3eb691c63711b4 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -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; diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index 0e45ad6ba04cfd..d06eee70288139 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -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"; diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 45fae3256f5cce..62a6f80851e182 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -172,4 +172,4 @@ export const getUsernameList = (users: string | string[] | undefined): string[] export default defaultEvents; -export type AwaitedGetDefaultEvent = Awaited>; +export type DefaultEvent = Awaited>; diff --git a/packages/lib/server/repository/booking.ts b/packages/lib/server/repository/booking.ts index 72c2af12235307..c6707bb7df257b 100644 --- a/packages/lib/server/repository/booking.ts +++ b/packages/lib/server/repository/booking.ts @@ -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, + }, + }); + } }