From 47cb39c7caff20d59339244593fc0b877255dd67 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 28 Jun 2024 15:18:36 +0530 Subject: [PATCH 01/16] refactor: handleNewBooking #3 --- .../bookings/lib/handleNewBooking/createBooking.ts | 1 - packages/features/bookings/lib/handleSeats/types.d.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index d9c078f6c628d9..f902ff8940efee 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -243,7 +243,6 @@ function buildNewBookingData(params: { if (reqBodyRecurringEventId) { newBookingData.recurringEventId = reqBodyRecurringEventId; } - if (originalRescheduledBooking) { newBookingData.metadata = { ...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata), diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index ef639438c62d5f..df8155baf39190 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -4,7 +4,15 @@ import type z from "zod"; import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; import type { AppsStatus } from "@calcom/types/Calendar"; -import type { Booking, NewBookingEventType, OriginalRescheduledBooking } from "../handleNewBooking/types"; +import type { + RescheduleReason, + NoEmail, + AdditionalNotes, + ReqAppsStatus, + SmsReminderNumber, + EventTypeId, + ReqBodyMetadata, +} from "../handleNewBooking/types"; export type BookingSeat = Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null; From 619f54e98f546ef1073d3fa88179a294571e67fd Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 28 Jun 2024 19:14:09 +0530 Subject: [PATCH 02/16] refactor: create booking factor --- packages/features/bookings/lib/handleNewBooking/createBooking.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index f902ff8940efee..d9c078f6c628d9 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -243,6 +243,7 @@ function buildNewBookingData(params: { if (reqBodyRecurringEventId) { newBookingData.recurringEventId = reqBodyRecurringEventId; } + if (originalRescheduledBooking) { newBookingData.metadata = { ...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata), From 717084b6befe0b5d7bf922952a569b122c454e4f Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 5 Jul 2024 23:03:42 +0530 Subject: [PATCH 03/16] refactor: handleNewBooking --- .../features/bookings/lib/handleNewBooking.ts | 318 ++++-------------- .../addVideoCallDataToEvent.ts | 24 ++ .../checkBookingAndDurationLimits.ts | 43 +++ .../getCustomInputsResponses.ts | 49 +++ .../getLocationValuesForDb.ts | 30 ++ .../handleNewBooking/getVideoCallDetails.ts | 32 ++ .../handleNewBooking/loadAndValidateUsers.ts | 98 ++++++ .../validateBookingTimeIsNotOutOfBounds.ts | 64 ++++ .../handleNewBooking/validateEventLength.ts | 27 ++ .../instant-meeting/handleInstantMeeting.ts | 2 +- 10 files changed, 434 insertions(+), 253 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/validateEventLength.ts diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 6f4be8fc2519ab..cb1dd806a039d3 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,49 +52,43 @@ 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 { 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 { deleteAllWorkflowReminders, 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 { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus"; -import { loadUsers } from "./handleNewBooking/loadUsers"; +import { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers"; import type { Invitee, IEventTypePaymentCredentialType, @@ -105,62 +96,14 @@ 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, @@ -186,6 +129,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; @@ -210,18 +172,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", @@ -298,159 +252,43 @@ 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); - } + await validateBookingTimeIsNotOutOfBounds( + reqBody.start, + reqBody.timeZone, + eventType, + loggerWithEventDetails + ); - if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" }); + validateEventLength({ + reqBodyStart: reqBody.start, + reqBodyEnd: reqBody.end, + eventTypeMutipleDuration: eventType.metadata?.multipleDuration, + eventTypeLength: eventType.length, + logger: loggerWithEventDetails, + }); - users = users.map((user) => ({ - ...user, - isFixed: - user.isFixed === false - ? false - : user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN, - })); + let users = await loadAndValidateUsers({ + req, + eventType, + eventTypeId, + dynamicUserList, + logger: loggerWithEventDetails, + }); - loggerWithEventDetails.debug( - "Concerned users", - safeStringify({ - users: users.map(getPiiFreeUser), - }) + let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb( + dynamicUserList, + users, + location ); - 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; - } + await checkBookingAndDurationLimits({ + eventType, + reqBodyStart: reqBody.start, + reqBodyRescheduleUid: reqBody.rescheduleUid, + }); 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 bookingSeat: BookingSeat = null; let originalRescheduledBooking: BookingType = null; @@ -677,6 +515,7 @@ async function handler( throw new Error(ErrorCode.NoAvailableUsersFound); } + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // If the team member is requested then they should be the organizer const organizerUser = reqBody.teamMemberEmail ? users.find((user) => user.email === reqBody.teamMemberEmail) ?? users[0] @@ -930,7 +769,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 }); @@ -1833,28 +1672,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/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/getVideoCallDetails.ts b/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts new file mode 100644 index 00000000000000..fec95de539b46e --- /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) 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..5ddf37a176ff39 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -0,0 +1,98 @@ +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 prisma, { userSelect } 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 getUserById(eventType.userId, logger); + 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; +} + +// TODO: use repository +async function getUserById(userId: number, logger: Logger) { + const eventTypeUser = await prisma.user.findUnique({ + where: { + id: 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" }); + } + return eventTypeUser; +} diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts new file mode 100644 index 00000000000000..7ec6c92ad7a243 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -0,0 +1,64 @@ +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" +>; + +export const validateBookingTimeIsNotOutOfBounds = async ( + reqBodyStartTime: string, + reqBodyTimeZone: string, + eventType: T, + logger: Logger +) => { + 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( + reqBodyStartTime, + { + 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) { + logger.warn({ + message: "NewBooking: Unable set timeOutOfBounds. Using false. ", + }); + if (error instanceof BookingDateInPastError) { + logger.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.`, + }; + 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..629b13c02e1449 --- /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; + eventTypeMutipleDuration?: number[]; + eventTypeLength: number; + logger: Logger; +}; + +export const validateEventLength = ({ + reqBodyStart, + reqBodyEnd, + eventTypeMutipleDuration, + eventTypeLength, + logger, +}: Props) => { + const reqEventLength = dayjs(reqBodyEnd).diff(dayjs(reqBodyStart), "minutes"); + const validEventLengths = eventTypeMutipleDuration?.length ? eventTypeMutipleDuration : [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/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"; From 801d1c8cdf5a23487ee44f2303ad6f98444c0d57 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 5 Jul 2024 23:58:27 +0530 Subject: [PATCH 04/16] refactor: seats and rescheduleUId --- .../features/bookings/lib/handleNewBooking.ts | 47 ++++----------- .../getOriginalRescheduledBookingAndSeat.ts | 60 +++++++++++++++++++ .../owner/combineTwoSeatedBookings.ts | 2 +- .../owner/moveSeatedBookingToNewTimeSlot.ts | 2 +- 4 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index cb1dd806a039d3..3c9f744733f05e 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -84,7 +84,7 @@ import { getCustomInputsResponses } from "./handleNewBooking/getCustomInputsResp import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB"; import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB"; import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb"; -import { getOriginalRescheduledBooking } from "./handleNewBooking/getOriginalRescheduledBooking"; +import { getOriginalRescheduledBookingAndSeat } from "./handleNewBooking/getOriginalRescheduledBookingAndSeat"; import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags"; import { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus"; @@ -99,7 +99,6 @@ import type { 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"] }); @@ -287,42 +286,16 @@ async function handler( reqBodyRescheduleUid: reqBody.rescheduleUid, }); - let rescheduleUid = 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" }); - } + const { + rescheduleUid, + originalRescheduledBooking: originalBooking, + bookingSeat, + } = await getOriginalRescheduledBookingAndSeat({ + reqBodyRescheduleUid: reqBody.rescheduleUid, + seatsPerTimeSlot: eventType.seatsPerTimeSlot, + }); - if ( - originalRescheduledBooking.status === BookingStatus.CANCELLED && - !originalRescheduledBooking.rescheduled - ) { - throw new HttpError({ statusCode: 403, message: ErrorCode.CancelledBookingsCannotBeRescheduled }); - } - } + let originalRescheduledBooking = originalBooking; let luckyUserResponse; let isFirstSeat = true; diff --git a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts new file mode 100644 index 00000000000000..e799f0746651cd --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts @@ -0,0 +1,60 @@ +import { ErrorCode } from "@calcom/lib/errorCodes"; +import { HttpError } from "@calcom/lib/http-error"; +import prisma from "@calcom/prisma"; +import { BookingStatus } from "@calcom/prisma/enums"; + +import type { BookingSeat } from "../handleSeats/types"; +import { + getOriginalRescheduledBooking, + type OriginalRescheduledBooking, +} from "./getOriginalRescheduledBooking"; +import type { BookingType } from "./types"; + +type InputProps = { + reqBodyRescheduleUid?: string; + seatsPerTimeSlot?: number | null; +}; + +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 }); + } +}; + +export const getOriginalRescheduledBookingAndSeat = async ({ + reqBodyRescheduleUid, + seatsPerTimeSlot, +}: InputProps) => { + let rescheduleUid = reqBodyRescheduleUid; + 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, !!seatsPerTimeSlot); + + validateOriginalRescheduledBooking(originalRescheduledBooking); + } + + return { rescheduleUid, originalRescheduledBooking, bookingSeat }; +}; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts index 5d1132f4754302..2b76582b9d0523 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 524fc243f36e45..6ec5e181a51590 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"; From 9fbec20409149dd2896eabd82ebd49373c58c42c Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Sat, 6 Jul 2024 00:01:25 +0530 Subject: [PATCH 05/16] chore: remove comment --- packages/features/bookings/lib/handleNewBooking.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 3c9f744733f05e..3403c3700e3070 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -488,7 +488,6 @@ async function handler( throw new Error(ErrorCode.NoAvailableUsersFound); } - // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // If the team member is requested then they should be the organizer const organizerUser = reqBody.teamMemberEmail ? users.find((user) => user.email === reqBody.teamMemberEmail) ?? users[0] From c4e427807ebf1ef660231bb9775a2687f015d92b Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 11 Jul 2024 01:39:44 +0530 Subject: [PATCH 06/16] fix: type err --- packages/features/bookings/lib/handleSeats/types.d.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index df8155baf39190..ef639438c62d5f 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -4,15 +4,7 @@ import type z from "zod"; import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; import type { AppsStatus } from "@calcom/types/Calendar"; -import type { - RescheduleReason, - NoEmail, - AdditionalNotes, - ReqAppsStatus, - SmsReminderNumber, - EventTypeId, - ReqBodyMetadata, -} from "../handleNewBooking/types"; +import type { Booking, NewBookingEventType, OriginalRescheduledBooking } from "../handleNewBooking/types"; export type BookingSeat = Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null; From 03d7dd729dde6f3b961ecbc2d5142496983755fa Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 11 Jul 2024 02:16:55 +0530 Subject: [PATCH 07/16] chore: add missing statement --- .../bookings/lib/handleNewBooking/getVideoCallDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts b/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts index fec95de539b46e..b9f2301350cb3d 100644 --- a/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts +++ b/packages/features/bookings/lib/handleNewBooking/getVideoCallDetails.ts @@ -9,7 +9,7 @@ type ExtraAdditionalInfo = AdditionalInformation & { type VideoResult = EventResult; function extractUpdatedVideoEvent(result: VideoResult | undefined): ExtraAdditionalInfo | undefined { - if (!result) return undefined; + if (!result || !result.success) return undefined; return Array.isArray(result.updatedEvent) ? result.updatedEvent[0] : result.updatedEvent; } From 07842088bee18065c7ff1834a052eebf30b04a18 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 12 Jul 2024 02:14:40 +0530 Subject: [PATCH 08/16] chore: use less params and other improvements --- .../features/bookings/lib/handleNewBooking.ts | 38 ++-- .../lib/handleNewBooking/createBooking.ts | 174 +++++++----------- .../getRequiresConfirmationFlags.ts | 2 - .../lib/handleNewBooking/loadUsers.ts | 2 +- .../bookings/lib/handleNewBooking/types.ts | 9 +- .../bookings/lib/handleSeats/types.d.ts | 2 +- 6 files changed, 89 insertions(+), 138 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 3403c3700e3070..9e59e983440df9 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -899,23 +899,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 diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index d9c078f6c628d9..9ee1490a078642 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,8 +186,8 @@ function buildNewBookingData(params: { : undefined, }; - if (reqBodyRecurringEventId) { - newBookingData.recurringEventId = reqBodyRecurringEventId; + if (reqBody.recurringEventId) { + newBookingData.recurringEventId = reqBody.recurringEventId; } if (originalRescheduledBooking) { @@ -252,12 +198,16 @@ function buildNewBookingData(params: { 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/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/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..abeb914831c554 100644 --- a/packages/features/bookings/lib/handleNewBooking/types.ts +++ b/packages/features/bookings/lib/handleNewBooking/types.ts @@ -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; }; @@ -64,8 +63,6 @@ export type IsFixedAwareUser = User & { export type NewBookingEventType = AwaitedGetDefaultEvent | getEventTypeResponse; -export type IsConfirmedByDefault = ReturnType["isConfirmedByDefault"]; - export type { AwaitedBookingData, RescheduleReason, @@ -79,6 +76,6 @@ export type { BookingType, Booking, OriginalRescheduledBooking, - AwaitedLoadUsers, + LoadedUsers, getEventTypeResponse, }; diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index ef639438c62d5f..432847d94332ae 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -25,7 +25,7 @@ export type NewSeatedBookingObject = { rescheduleReason: RescheduleReason; reqBodyUser: string | string[] | undefined; noEmail: NoEmail; - isConfirmedByDefault: IsConfirmedByDefault; + isConfirmedByDefault: boolean; additionalNotes: AdditionalNotes; reqAppsStatus: ReqAppsStatus; attendeeLanguage: string | null; From 2a92ce6e90532d4cd50898b36ddd794d22d9914a Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Fri, 12 Jul 2024 02:15:48 +0530 Subject: [PATCH 09/16] chore: name --- packages/features/bookings/lib/handleNewBooking/types.ts | 4 ++-- packages/lib/defaultEvents.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/types.ts b/packages/features/bookings/lib/handleNewBooking/types.ts index abeb914831c554..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"; @@ -61,7 +61,7 @@ export type IsFixedAwareUser = User & { weightAdjustment?: number; }; -export type NewBookingEventType = AwaitedGetDefaultEvent | getEventTypeResponse; +export type NewBookingEventType = DefaultEvent | getEventTypeResponse; export type { AwaitedBookingData, diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index f28cebcaf1f4c9..4dd5ce3791d1b3 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -171,4 +171,4 @@ export const getUsernameList = (users: string | string[] | undefined): string[] export default defaultEvents; -export type AwaitedGetDefaultEvent = Awaited>; +export type DefaultEvent = Awaited>; From 1e7e99198b3f1b5ae611561cef7856c94bbedfd0 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 12 Sep 2024 15:17:18 +0530 Subject: [PATCH 10/16] chore: improvement --- .../lib/handleNewBooking/createBooking.ts | 2 +- .../handleNewBooking/loadAndValidateUsers.ts | 29 ++++--------------- .../validateBookingTimeIsNotOutOfBounds.ts | 6 ++-- packages/lib/server/repository/user.ts | 21 ++++++++++++++ 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index 9ee1490a078642..089cefbd288b7a 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -193,7 +193,7 @@ function buildNewBookingData(params: CreateBookingParams): Prisma.BookingCreateI if (originalRescheduledBooking) { newBookingData.metadata = { ...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata), - ...reqBodyMetadata, + ...reqBody.metadata, }; newBookingData.paid = originalRescheduledBooking.paid; newBookingData.fromReschedule = originalRescheduledBooking.uid; diff --git a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts index 5ddf37a176ff39..0f4bf0271e21ae 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -5,9 +5,9 @@ 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 prisma, { userSelect } from "@calcom/prisma"; +import { UserRepository } from "@calcom/lib/server/repository/user"; +import { userSelect } 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"; @@ -49,7 +49,10 @@ export async function loadAndValidateUsers({ // If this event was pre-relationship migration // TODO: Establish whether this is dead code. if (!users.length && eventType.userId) { - const eventTypeUser = await getUserById(eventType.userId, logger); + const eventTypeUser = await UserRepository.findUserWithCredentialsById({ + userId: eventType.userId, + select: userSelect.select, + }); if (!eventTypeUser) { logger.warn({ message: "NewBooking: eventTypeUser.notFound" }); throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" }); @@ -76,23 +79,3 @@ export async function loadAndValidateUsers({ return users; } - -// TODO: use repository -async function getUserById(userId: number, logger: Logger) { - const eventTypeUser = await prisma.user.findUnique({ - where: { - id: 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" }); - } - return eventTypeUser; -} diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts index 7ec6c92ad7a243..efe021e952fde6 100644 --- a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -14,6 +14,8 @@ type ValidateBookingTimeEventType = Pick< | "periodCountCalendarDays" | "minimumBookingNotice" | "eventName" + | "users" + | "id" >; export const validateBookingTimeIsNotOutOfBounds = async ( @@ -36,7 +38,7 @@ export const validateBookingTimeIsNotOutOfBounds = async Date: Fri, 13 Sep 2024 22:56:01 +0530 Subject: [PATCH 11/16] fix: type err --- .../features/bookings/lib/handleNewBooking.ts | 5 +++++ .../handleNewBooking/loadAndValidateUsers.ts | 16 ++++++++++---- .../validateBookingTimeIsNotOutOfBounds.ts | 6 +----- packages/lib/server/repository/user.ts | 21 ------------------- 4 files changed, 18 insertions(+), 30 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 9e59e983440df9..98014d7ab3ca27 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -251,10 +251,15 @@ 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; + await validateBookingTimeIsNotOutOfBounds( reqBody.start, reqBody.timeZone, eventType, + eventTimeZone, loggerWithEventDetails ); diff --git a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts index 0f4bf0271e21ae..94b26433660558 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -5,9 +5,10 @@ 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 { UserRepository } from "@calcom/lib/server/repository/user"; 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"; @@ -49,9 +50,16 @@ export async function loadAndValidateUsers({ // If this event was pre-relationship migration // TODO: Establish whether this is dead code. if (!users.length && eventType.userId) { - const eventTypeUser = await UserRepository.findUserWithCredentialsById({ - userId: eventType.userId, - select: userSelect.select, + 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" }); diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts index efe021e952fde6..d17f39dd94be84 100644 --- a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -14,7 +14,6 @@ type ValidateBookingTimeEventType = Pick< | "periodCountCalendarDays" | "minimumBookingNotice" | "eventName" - | "users" | "id" >; @@ -22,12 +21,9 @@ export const validateBookingTimeIsNotOutOfBounds = async ) => { - 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( diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 8c01df65ba8dd5..3ecea0851286d6 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -9,7 +9,6 @@ import prisma from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import type { User as UserType } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { userMetadata } from "@calcom/prisma/zod-utils"; import type { UpId, UserProfile } from "@calcom/types/UserProfile"; @@ -97,26 +96,6 @@ export class UserRepository { }; } - static async findUserWithCredentialsById({ - userId, - select, - }: { - userId: number; - select: Prisma.UserSelect; - }) { - return await prisma.user.findUnique({ - where: { - id: userId, - }, - select: { - credentials: { - select: credentialForCalendarServiceSelect, - }, // Don't leak to client - ...select, - }, - }); - } - static async findOrganizations({ userId }: { userId: UserType["id"] }) { const { acceptedTeamMemberships } = await UserRepository.findTeamsByUserId({ userId, From d4586c94305f3725f3fb0afb61fc3c24c88281a8 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung Date: Wed, 18 Sep 2024 11:53:19 -0400 Subject: [PATCH 12/16] Typo fix --- .../bookings/lib/handleNewBooking/validateEventLength.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts b/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts index 629b13c02e1449..62ae8aa65e4db1 100644 --- a/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts +++ b/packages/features/bookings/lib/handleNewBooking/validateEventLength.ts @@ -6,7 +6,7 @@ import { HttpError } from "@calcom/lib/http-error"; type Props = { reqBodyStart: string; reqBodyEnd: string; - eventTypeMutipleDuration?: number[]; + eventTypeMultipleDuration?: number[]; eventTypeLength: number; logger: Logger; }; @@ -14,12 +14,12 @@ type Props = { export const validateEventLength = ({ reqBodyStart, reqBodyEnd, - eventTypeMutipleDuration, + eventTypeMultipleDuration, eventTypeLength, logger, }: Props) => { const reqEventLength = dayjs(reqBodyEnd).diff(dayjs(reqBodyStart), "minutes"); - const validEventLengths = eventTypeMutipleDuration?.length ? eventTypeMutipleDuration : [eventTypeLength]; + 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" }); From 1f5eefaee6438858df2a64aef206eb1c89d3d91e Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 19 Sep 2024 20:27:37 +0530 Subject: [PATCH 13/16] chore: fix type err --- packages/features/bookings/lib/handleNewBooking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 98014d7ab3ca27..db349e27e847f9 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -266,7 +266,7 @@ async function handler( validateEventLength({ reqBodyStart: reqBody.start, reqBodyEnd: reqBody.end, - eventTypeMutipleDuration: eventType.metadata?.multipleDuration, + eventTypeMultipleDuration: eventType.metadata?.multipleDuration, eventTypeLength: eventType.length, logger: loggerWithEventDetails, }); From fb1bafeb5d5b7f07702e11215bf06f51c143c591 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 19 Sep 2024 20:45:02 +0530 Subject: [PATCH 14/16] refactor: more readable --- .../getOriginalRescheduledBookingAndSeat.ts | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts index e799f0746651cd..e6ab94c0a76493 100644 --- a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts +++ b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts @@ -8,7 +8,6 @@ import { getOriginalRescheduledBooking, type OriginalRescheduledBooking, } from "./getOriginalRescheduledBooking"; -import type { BookingType } from "./types"; type InputProps = { reqBodyRescheduleUid?: string; @@ -28,33 +27,31 @@ const validateOriginalRescheduledBooking = async (originalRescheduledBooking: Or } }; +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, + }, + }); +}; + export const getOriginalRescheduledBookingAndSeat = async ({ reqBodyRescheduleUid, seatsPerTimeSlot, }: InputProps) => { - let rescheduleUid = reqBodyRescheduleUid; - 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, !!seatsPerTimeSlot); - - validateOriginalRescheduledBooking(originalRescheduledBooking); + if (!reqBodyRescheduleUid) { + return { rescheduleUid: undefined, originalRescheduledBooking: null, bookingSeat: null }; } + const bookingSeat = await getSeatedBooking(reqBodyRescheduleUid); + const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBodyRescheduleUid; + const originalRescheduledBooking = await getOriginalRescheduledBooking(rescheduleUid, !!seatsPerTimeSlot); + validateOriginalRescheduledBooking(originalRescheduledBooking); + return { rescheduleUid, originalRescheduledBooking, bookingSeat }; }; From 074b0b2db87f165a59e5a7c3f9759fb0445ea1c2 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 19 Sep 2024 21:42:39 +0530 Subject: [PATCH 15/16] refactor: improve code --- .../features/bookings/lib/handleNewBooking.ts | 17 +++--- .../getOriginalRescheduledBooking.ts | 59 +++---------------- .../getOriginalRescheduledBookingAndSeat.ts | 57 ------------------ .../lib/handleNewBooking/getSeatedBooking.ts | 16 +++++ .../validateOriginalRescheduledBooking.ts | 20 +++++++ packages/lib/server/repository/booking.ts | 53 +++++++++++++++++ 6 files changed, 103 insertions(+), 119 deletions(-) delete mode 100644 packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/getSeatedBooking.ts create mode 100644 packages/features/bookings/lib/handleNewBooking/validateOriginalRescheduledBooking.ts diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index db349e27e847f9..06464597e532ce 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -84,8 +84,9 @@ import { getCustomInputsResponses } from "./handleNewBooking/getCustomInputsResp import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB"; import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB"; import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb"; -import { getOriginalRescheduledBookingAndSeat } from "./handleNewBooking/getOriginalRescheduledBookingAndSeat"; +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 { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers"; @@ -291,16 +292,12 @@ async function handler( reqBodyRescheduleUid: reqBody.rescheduleUid, }); - const { - rescheduleUid, - originalRescheduledBooking: originalBooking, - bookingSeat, - } = await getOriginalRescheduledBookingAndSeat({ - reqBodyRescheduleUid: reqBody.rescheduleUid, - seatsPerTimeSlot: eventType.seatsPerTimeSlot, - }); + const bookingSeat = reqBody.rescheduleUid ? await getSeatedBooking(reqBody.rescheduleUid) : null; + const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBody.rescheduleUid; - let originalRescheduledBooking = originalBooking; + let originalRescheduledBooking = rescheduleUid + ? await getOriginalRescheduledBooking(rescheduleUid, !!eventType.seatsPerTimeSlot) + : null; let luckyUserResponse; let isFirstSeat = true; 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/getOriginalRescheduledBookingAndSeat.ts b/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts deleted file mode 100644 index e6ab94c0a76493..00000000000000 --- a/packages/features/bookings/lib/handleNewBooking/getOriginalRescheduledBookingAndSeat.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ErrorCode } from "@calcom/lib/errorCodes"; -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import type { BookingSeat } from "../handleSeats/types"; -import { - getOriginalRescheduledBooking, - type OriginalRescheduledBooking, -} from "./getOriginalRescheduledBooking"; - -type InputProps = { - reqBodyRescheduleUid?: string; - seatsPerTimeSlot?: number | null; -}; - -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 }); - } -}; - -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, - }, - }); -}; - -export const getOriginalRescheduledBookingAndSeat = async ({ - reqBodyRescheduleUid, - seatsPerTimeSlot, -}: InputProps) => { - if (!reqBodyRescheduleUid) { - return { rescheduleUid: undefined, originalRescheduledBooking: null, bookingSeat: null }; - } - - const bookingSeat = await getSeatedBooking(reqBodyRescheduleUid); - const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBodyRescheduleUid; - const originalRescheduledBooking = await getOriginalRescheduledBooking(rescheduleUid, !!seatsPerTimeSlot); - validateOriginalRescheduledBooking(originalRescheduledBooking); - - return { rescheduleUid, originalRescheduledBooking, bookingSeat }; -}; 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/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/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, + }, + }); + } } From 6626cb7d56784734327ad48281e1963e343bb0eb Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Mon, 30 Sep 2024 01:02:49 +0530 Subject: [PATCH 16/16] fix: conflicts --- packages/features/bookings/lib/handleNewBooking.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 600e83afe396ae..6204fa6b7b10d2 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -64,10 +64,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; -import { - deleteAllWorkflowReminders, - getAllWorkflowsFromEventType, -} from "@calcom/trpc/server/routers/viewer/workflows/util"; +import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util"; import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar"; import type { EventResult, PartialReference } from "@calcom/types/EventManager";