From 417a612aa380d15b476509edcd4e69f5bdf4eeca Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:57:58 +0100 Subject: [PATCH] refactor: extract NextRequest from handle new booking (#20082) * extract NextRequest * update api and tests * booking limits tests * fix more tests to use new approach * update more tests with new format * extract getOrgDomainConfig to not use req * extract req from loadNewUsers and pass in hostname/forcedSlug * fix instant meeting types and hostname fixes * fix handleNewBookingReq * fix type errors in tests * make hostName and forcedSlug optional * fix type err * Revert "fix type err" This reverts commit 9d5de9019d9dafe348c97b876baaa1d0675967e5. * wip fix e2e * fix: add missing headers * migrate handle recurring event and also create tests specific to fn * platform recurringbooking * fix type * hard code types on request object * bump libraries * fixup! bump libraries * fix: accessing host if headers not passed * fix: v2 recurring booking * fix: accessing host if headers not passed * chore: bump platform libraries * fix tests * push * chore: bump libraries * push lock changes * bump libraries --------- Co-authored-by: amrit Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay Co-authored-by: supalarry Co-authored-by: Lauris Skraucis --- apps/api/v1/pages/api/bookings/_post.ts | 18 +- apps/api/v2/package.json | 2 +- .../controllers/bookings.controller.ts | 31 +- .../2024-08-13/services/bookings.service.ts | 56 +- .../2024-08-13/services/input.service.ts | 13 +- apps/web/pages/api/book/event.ts | 9 +- apps/web/pages/api/book/recurring-event.ts | 29 +- packages/features/apps/components/AppList.tsx | 2 +- .../features/bookings/lib/handleNewBooking.ts | 94 +-- .../lib/handleNewBooking/getBookingData.ts | 31 +- .../global-booking-limits.test.ts | 86 +-- .../handleNewBooking/loadAndValidateUsers.ts | 13 +- .../lib/handleNewBooking/loadUsers.ts | 18 +- .../test/booking-limits.test.ts | 145 ++--- .../test/complex-schedules.test.ts | 8 +- .../test/date-overrides.test.ts | 13 +- .../test/delegation-credential.test.ts | 29 +- .../test/dynamic-group-booking.test.ts | 36 +- .../test/fresh-booking.test.ts | 408 ++++++------- .../test/handleNewRecurringBooking.test.ts | 566 ++++++++++++++++++ .../handleNewBooking/test/reschedule.test.ts | 116 ++-- .../collective-scheduling.test.ts | 92 +-- .../test/workflow-notifications.test.ts | 43 +- .../bookings/lib/handleNewRecurringBooking.ts | 71 ++- .../lib/handleSeats/test/handleSeats.test.ts | 127 ++-- .../ee/organizations/lib/orgDomains.ts | 28 + .../instant-meeting/handleInstantMeeting.ts | 2 +- 27 files changed, 1301 insertions(+), 785 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts diff --git a/apps/api/v1/pages/api/bookings/_post.ts b/apps/api/v1/pages/api/bookings/_post.ts index f6f1ccd0e0..9b6628d04a 100644 --- a/apps/api/v1/pages/api/bookings/_post.ts +++ b/apps/api/v1/pages/api/bookings/_post.ts @@ -213,12 +213,14 @@ import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; * description: Authorization information is missing or invalid. */ async function handler(req: NextApiRequest) { - const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; + const { isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req; + let userId = req.userId; + req.body = { ...req.body, creationSource: CreationSource.API_V1, }; - if (isSystemWideAdmin) req.userId = req.body.userId || userId; + if (isSystemWideAdmin) userId = req.body.userId || userId; if (isOrganizationOwnerOrAdmin) { const accessibleUsersIds = await getAccessibleUsers({ @@ -226,11 +228,19 @@ async function handler(req: NextApiRequest) { memberUserIds: [req.body.userId || userId], }); const [requestedUserId] = accessibleUsersIds; - req.userId = requestedUserId || userId; + userId = requestedUserId || userId; } try { - return await handleNewBooking(req, getBookingDataSchemaForApi); + return await handleNewBooking( + { + bookingData: req.body, + userId, + hostname: req.headers.host || "", + forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + }, + getBookingDataSchemaForApi + ); } catch (error: unknown) { const knownError = error as Error; if (knownError?.message === ErrorCode.NoAvailableUsersFound) { diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index d2ecc5c251..2cdb161519 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.166", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.167", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 226cf89ed3..401375b111 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -174,11 +174,19 @@ export class BookingsController_2024_04_15 { const oAuthClientId = clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId)); const { orgSlug, locationUrl } = body; - req.headers["x-cal-force-slug"] = orgSlug; try { - const booking = await handleNewBooking( - await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed) - ); + const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed); + const booking = await handleNewBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + forcedSlug: orgSlug, + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); if (booking.userId && booking.uid && booking.startTime) { void (await this.billingService.increaseUsageByUserId(booking.userId, { uid: booking.uid, @@ -284,9 +292,18 @@ export class BookingsController_2024_04_15 { } } - const createdBookings: BookingResponse[] = await handleNewRecurringBooking( - await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed) - ); + const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed); + + const createdBookings: BookingResponse[] = await handleNewRecurringBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); createdBookings.forEach(async (booking) => { if (booking.userId && booking.uid && booking.startTime) { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index d8560e103e..1475eaedda 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -149,14 +149,33 @@ export class BookingsService_2024_08_13 { async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); - const bookings = await handleNewRecurringBooking(bookingRequest); + const bookings = await handleNewRecurringBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + noEmail: bookingRequest.noEmail, + }); const ids = bookings.map((booking) => booking.id || 0); return this.outputService.getOutputRecurringBookings(ids); } async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); - const bookings = await handleNewRecurringBooking(bookingRequest); + const bookings = await handleNewRecurringBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); return this.outputService.getOutputCreateRecurringSeatedBookings( bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })) ); @@ -164,7 +183,16 @@ export class BookingsService_2024_08_13 { async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createBookingRequest(request, body); - const booking = await handleNewBooking(bookingRequest); + const booking = await handleNewBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); if (!booking.uid) { throw new Error("Booking missing uid"); @@ -180,7 +208,16 @@ export class BookingsService_2024_08_13 { async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createBookingRequest(request, body); - const booking = await handleNewBooking(bookingRequest); + const booking = await handleNewBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); if (!booking.uid) { throw new Error("Booking missing uid"); @@ -324,7 +361,16 @@ export class BookingsService_2024_08_13 { bookingUid, body ); - const booking = await handleNewBooking(bookingRequest); + const booking = await handleNewBooking({ + bookingData: bookingRequest.body, + userId: bookingRequest.userId, + hostname: bookingRequest.headers?.host || "", + platformClientId: bookingRequest.platformClientId, + platformRescheduleUrl: bookingRequest.platformRescheduleUrl, + platformCancelUrl: bookingRequest.platformCancelUrl, + platformBookingUrl: bookingRequest.platformBookingUrl, + platformBookingLocation: bookingRequest.platformBookingLocation, + }); if (!booking.uid) { throw new Error("Booking missing uid"); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 130e5d9026..18a19b3569 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -45,7 +45,10 @@ import { import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input"; import { EventType } from "@calcom/prisma/client"; -type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams; +type BookingRequest = NextApiRequest & { + userId: number | undefined; + noEmail: boolean | undefined; +} & OAuthRequestParams; type OAuthRequestParams = { platformClientId: string; @@ -223,7 +226,13 @@ export class InputBookingsService_2024_08_13 { creationSource: CreationSource.API_V2, })); - return newRequest as unknown as BookingRequest; + return { + ...newRequest, + headers: { + hostname: request.headers["host"] || "", + forcedSlug: request.headers["x-cal-force-slug"] as string | undefined, + }, + } as unknown as BookingRequest; } transformLocation(location: string | BookingInputLocation_2024_08_13): { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index cb02fb0434..2ef22ba9d8 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -25,12 +25,17 @@ async function handler(req: NextApiRequest & { userId?: number }) { const session = await getServerSession({ req }); /* To mimic API behavior and comply with types */ - req.userId = session?.user?.id || -1; req.body = { ...req.body, creationSource: CreationSource.WEBAPP, }; - const booking = await handleNewBooking(req); + + const booking = await handleNewBooking({ + bookingData: req.body, + userId: session?.user?.id || -1, + hostname: req.headers.host || "", + forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + }); return booking; } diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index bd99ccef6a..840a1da936 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -10,7 +10,22 @@ import { defaultResponder } from "@calcom/lib/server/defaultResponder"; // @TODO: Didn't look at the contents of this function in order to not break old booking page. -async function handler(req: NextApiRequest & { userId?: number }) { +type PlatformParams = { + platformClientId?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformRescheduleUrl?: string; + platformBookingLocation?: string; +}; + +type RequestMeta = { + userId?: number; + hostname?: string; + forcedSlug?: string; + noEmail?: boolean; +} & PlatformParams; + +async function handler(req: NextApiRequest & RequestMeta) { const userIp = getIP(req); if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { @@ -26,9 +41,17 @@ async function handler(req: NextApiRequest & { userId?: number }) { }); const session = await getServerSession({ req }); /* To mimic API behavior and comply with types */ - req.userId = session?.user?.id || -1; - const createdBookings: BookingResponse[] = await handleNewRecurringBooking(req); + const createdBookings: BookingResponse[] = await handleNewRecurringBooking({ + bookingData: req.body, + userId: session?.user?.id || -1, + platformClientId: req.platformClientId, + platformCancelUrl: req.platformCancelUrl, + platformBookingUrl: req.platformBookingUrl, + platformRescheduleUrl: req.platformRescheduleUrl, + platformBookingLocation: req.platformBookingLocation, + noEmail: req.noEmail, + }); return createdBookings; } diff --git a/packages/features/apps/components/AppList.tsx b/packages/features/apps/components/AppList.tsx index 92f00ca0a8..8b556ce75f 100644 --- a/packages/features/apps/components/AppList.tsx +++ b/packages/features/apps/components/AppList.tsx @@ -17,7 +17,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AppCategories } from "@calcom/prisma/enums"; import { type RouterOutputs } from "@calcom/trpc"; import type { App } from "@calcom/types/App"; -import { List } from "@calcom/ui/components/list"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { @@ -27,6 +26,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@calcom/ui/components/dropdown"; +import { List } from "@calcom/ui/components/list"; import { showToast } from "@calcom/ui/components/toast"; export type HandleDisconnect = (credentialId: number, app: App["slug"], teamId?: number) => void; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index a5084f426f..929f52bc9d 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1,7 +1,6 @@ import type { DestinationCalendar } 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"; @@ -77,7 +76,6 @@ import { eventTypeAppMetadataOptionalSchema, eventTypeMetaDataSchemaWithTypedApps, } from "@calcom/prisma/zod-utils"; -import type { PlatformClientParams } 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, Person } from "@calcom/types/Calendar"; @@ -314,33 +312,52 @@ function buildTroubleshooterData({ return troubleshooterData; } +export type PlatformParams = { + platformClientId?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformRescheduleUrl?: string; + platformBookingLocation?: string; +}; + +export type BookingHandlerInput = { + bookingData: Record; + userId?: number; + // These used to come from headers but now we're passing them as params + hostname?: string; + forcedSlug?: string; +} & PlatformParams; + async function handler( - req: NextApiRequest & - PlatformClientParams & { - userId?: number | undefined; - }, + input: BookingHandlerInput, bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema ) { const { + bookingData: rawBookingData, userId, platformClientId, platformCancelUrl, platformBookingUrl, platformRescheduleUrl, platformBookingLocation, - } = req; + hostname, + forcedSlug, + } = input; + + const isPlatformBooking = !!platformClientId; const eventType = await monitorCallbackAsync(getEventType, { - eventTypeId: req.body.eventTypeId, - eventTypeSlug: req.body.eventTypeSlug, + eventTypeId: rawBookingData.eventTypeId, + eventTypeSlug: rawBookingData.eventTypeSlug, }); const bookingDataSchema = bookingDataSchemaGetter({ - view: req.body?.rescheduleUid ? "reschedule" : "booking", + view: rawBookingData.rescheduleUid ? "reschedule" : "booking", bookingFields: eventType.bookingFields, }); + const bookingData = await getBookingData({ - req, + reqBody: rawBookingData, eventType, schema: bookingDataSchema, }); @@ -480,7 +497,9 @@ async function handler( const { qualifiedRRUsers, additionalFallbackRRUsers, fixedUsers } = await monitorCallbackAsync( loadAndValidateUsers, { - req, + hostname, + forcedSlug, + isPlatform: isPlatformBooking, eventType, eventTypeId, dynamicUserList, @@ -545,7 +564,7 @@ async function handler( }, }), }; - if (req.body.allRecurringDates && req.body.isFirstRecurringSlot) { + if (input.bookingData.allRecurringDates && input.bookingData.isFirstRecurringSlot) { const isTeamEvent = eventType.schedulingType === SchedulingType.COLLECTIVE || eventType.schedulingType === SchedulingType.ROUND_ROBIN; @@ -556,11 +575,12 @@ async function handler( for ( let i = 0; - i < req.body.allRecurringDates.length && i < req.body.numSlotsToCheckForAvailability; + i < input.bookingData.allRecurringDates.length && + i < input.bookingData.numSlotsToCheckForAvailability; i++ ) { - const start = req.body.allRecurringDates[i].start; - const end = req.body.allRecurringDates[i].end; + const start = input.bookingData.allRecurringDates[i].start; + const end = input.bookingData.allRecurringDates[i].end; if (isTeamEvent) { // each fixed user must be available for (const key in fixedUsers) { @@ -593,7 +613,7 @@ async function handler( } } - if (!req.body.allRecurringDates || req.body.isFirstRecurringSlot) { + if (!input.bookingData.allRecurringDates || input.bookingData.isFirstRecurringSlot) { let availableUsers: IsFixedAwareUser[] = []; try { availableUsers = await ensureAvailableUsers( @@ -693,16 +713,20 @@ async function handler( if (!newLuckyUser) { break; // prevent infinite loop } - if (req.body.isFirstRecurringSlot && eventType.schedulingType === SchedulingType.ROUND_ROBIN) { + if ( + input.bookingData.isFirstRecurringSlot && + eventType.schedulingType === SchedulingType.ROUND_ROBIN + ) { // for recurring round robin events check if lucky user is available for next slots try { for ( let i = 0; - i < req.body.allRecurringDates.length && i < req.body.numSlotsToCheckForAvailability; + i < input.bookingData.allRecurringDates.length && + i < input.bookingData.numSlotsToCheckForAvailability; i++ ) { - const start = req.body.allRecurringDates[i].start; - const end = req.body.allRecurringDates[i].end; + const start = input.bookingData.allRecurringDates[i].start; + const end = input.bookingData.allRecurringDates[i].end; await ensureAvailableUsers( { ...eventTypeWithUsers, users: [newLuckyUser] }, @@ -741,7 +765,10 @@ async function handler( fixedUsers: fixedUserPool.map((u) => u.id), luckyUserPool: luckyUserPool.map((u) => u.id), }; - } else if (req.body.allRecurringDates && eventType.schedulingType === SchedulingType.ROUND_ROBIN) { + } else if ( + input.bookingData.allRecurringDates && + eventType.schedulingType === SchedulingType.ROUND_ROBIN + ) { // all recurring slots except the first one const luckyUsersFromFirstBooking = luckyUsers ? eventTypeWithUsers.users.filter((user) => luckyUsers.find((luckyUserId) => luckyUserId === user.id)) @@ -1024,9 +1051,9 @@ async function handler( }) .build(); - if (req.body.thirdPartyRecurringEventId) { + if (input.bookingData.thirdPartyRecurringEventId) { evt = CalendarEventBuilder.fromEvent(evt) - .withRecurringEventId(req.body.thirdPartyRecurringEventId) + .withRecurringEventId(input.bookingData.thirdPartyRecurringEventId) .build(); } @@ -1119,7 +1146,7 @@ async function handler( bookerPhoneNumber, tAttendees, bookingSeat, - reqUserId: req.userId, + reqUserId: input.userId, rescheduleReason, reqBodyUser: reqBody.user, noEmail, @@ -1143,7 +1170,6 @@ async function handler( }); if (newBooking) { - req.statusCode = 201; const bookingResponse = { ...newBooking, user: { @@ -1229,7 +1255,7 @@ async function handler( }, evt, originalRescheduledBooking, - creationSource: req.body.creationSource, + creationSource: input.bookingData.creationSource, tracking: reqBody.tracking, }); @@ -1269,9 +1295,9 @@ async function handler( if (booking && booking.id && eventType.seatsPerTimeSlot) { const currentAttendee = booking.attendees.find( (attendee) => - attendee.email === req.body.responses.email || - (req.body.responses.attendeePhoneNumber && - attendee.phoneNumber === req.body.responses.attendeePhoneNumber) + attendee.email === input.bookingData.responses.email || + (input.bookingData.responses.attendeePhoneNumber && + attendee.phoneNumber === input.bookingData.responses.attendeePhoneNumber) ); // Save description to bookingSeat @@ -1853,7 +1879,6 @@ async function handler( isDryRun, }); - req.statusCode = 201; // TODO: Refactor better so this booking object is not passed // all around and instead the individual fields are sent as args. const bookingResponse = { @@ -2019,7 +2044,9 @@ async function handler( calendarEvent: evtWithMetadata, isNotConfirmed: rescheduleUid ? false : !isConfirmedByDefault, isRescheduleEvent: !!rescheduleUid, - isFirstRecurringEvent: req.body.allRecurringDates ? req.body.isFirstRecurringSlot : undefined, + isFirstRecurringEvent: input.bookingData.allRecurringDates + ? input.bookingData.isFirstRecurringSlot + : undefined, hideBranding: !!eventType.owner?.hideBranding, seatReferenceUid: evt.attendeeSeatId, isDryRun, @@ -2044,9 +2071,6 @@ async function handler( loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error })); } - // booking successful - req.statusCode = 201; - // TODO: Refactor better so this booking object is not passed // all around and instead the individual fields are sent as args. const bookingResponse = { diff --git a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts index ee37343032..6c5a77a899 100644 --- a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts +++ b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts @@ -1,5 +1,4 @@ import type { EventTypeCustomInput } from "@prisma/client"; -import type { NextApiRequest } from "next"; import type z from "zod"; import dayjs from "@calcom/dayjs"; @@ -14,32 +13,32 @@ import { handleCustomInputs } from "./handleCustomInputs"; type ReqBodyWithEnd = TgetBookingDataSchema & { end: string }; export async function getBookingData({ - req, + reqBody, eventType, schema, }: { - req: NextApiRequest; + reqBody: Record; eventType: getEventTypeResponse; schema: T; }) { - const reqBody = await schema.parseAsync(req.body); - const reqBodyWithEnd = (reqBody: TgetBookingDataSchema): reqBody is ReqBodyWithEnd => { + const parsedBody = await schema.parseAsync(reqBody); + const parsedBodyWithEnd = (body: TgetBookingDataSchema): body is ReqBodyWithEnd => { // Use the event length to auto-set the event end time. - if (!Object.prototype.hasOwnProperty.call(reqBody, "end")) { - reqBody.end = dayjs.utc(reqBody.start).add(eventType.length, "minutes").format(); + if (!Object.prototype.hasOwnProperty.call(body, "end")) { + body.end = dayjs.utc(body.start).add(eventType.length, "minutes").format(); } return true; }; - if (!reqBodyWithEnd(reqBody)) { + if (!parsedBodyWithEnd(parsedBody)) { throw new Error(ErrorCode.RequestBodyWithouEnd); } - // reqBody.end is no longer an optional property. - if (reqBody.customInputs) { + // parsedBody.end is no longer an optional property. + if (parsedBody.customInputs) { // Check if required custom inputs exist - handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs); - const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi.parse(reqBody); + handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], parsedBody.customInputs); + const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi.parse(parsedBody); return { - ...reqBody, + ...parsedBody, name: reqBodyWithLegacyProps.name, email: reqBodyWithLegacyProps.email, guests: reqBodyWithLegacyProps.guests, @@ -54,10 +53,10 @@ export async function getBookingData({ attendeePhoneNumber: undefined, }; } - if (!reqBody.responses) { + if (!parsedBody.responses) { throw new Error("`responses` must not be nullish"); } - const responses = reqBody.responses; + const responses = parsedBody.responses; const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } = getCalEventResponses({ @@ -65,7 +64,7 @@ export async function getBookingData({ responses, }); return { - ...reqBody, + ...parsedBody, name: responses.name, email: responses.email, attendeePhoneNumber: responses.attendeePhoneNumber, diff --git a/packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts b/packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts index c37181e28d..a79f14f0ed 100644 --- a/packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts @@ -7,7 +7,7 @@ import { getGoogleCalendarCredential, Timezones, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; +// TODO: we should rename this - it doesnt get a mockRequestDataForBooking - it just gets mock booking data. import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; @@ -173,13 +173,10 @@ describe( }, }); - const { req: req1 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingWithinLimit, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingWithinLimit, }); - const createdBooking = await handleNewBooking(req1); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -200,15 +197,13 @@ describe( }, }); - const { req: req2 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingAboveLimit, - }); - // this is the third team booking of this week for user 101, limit reached - await expect(async () => await handleNewBooking(req2)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingAboveLimit, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); test(`Booking limits per day`, async ({}) => { @@ -264,13 +259,10 @@ describe( }, }); - const { req: req1 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingWithinLimit, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingWithinLimit, }); - const createdBooking = await handleNewBooking(req1); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -291,15 +283,13 @@ describe( }, }); - const { req: req2 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingAboveLimit, - }); - // this is the second team booking of this day for user 101, limit reached - await expect(async () => await handleNewBooking(req2)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingAboveLimit, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); test(`Booking limits per month`, async ({}) => { @@ -400,13 +390,10 @@ describe( }, }); - const { req: req1 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingWithinLimit, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingWithinLimit, }); - const createdBooking = await handleNewBooking(req1); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -427,15 +414,13 @@ describe( }, }); - const { req: req2 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingAboveLimit, - }); - // this is the firth team booking (incl. managed) of this month for user 101, limit reached - await expect(async () => await handleNewBooking(req2)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingAboveLimit, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); test(`Booking limits per year`, async ({}) => { @@ -528,13 +513,10 @@ describe( }, }); - const { req: req1 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingWithinLimit, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingWithinLimit, }); - const createdBooking = await handleNewBooking(req1); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -555,14 +537,12 @@ describe( }, }); - const { req: req2 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingAboveLimit, - }); - - await expect(async () => await handleNewBooking(req2)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingAboveLimit, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); }); }, diff --git a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts index 445562af61..7dbe84b51d 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -1,5 +1,4 @@ import type { Prisma } from "@prisma/client"; -import type { IncomingMessage } from "http"; import type { Logger } from "tslog"; import { checkIfUsersAreBlocked } from "@calcom/features/watchlist/operations/check-if-users-are-blocked.controller"; @@ -54,7 +53,6 @@ type EventType = Pick< >; type InputProps = { - req: IncomingMessage; eventType: EventType; eventTypeId: number; dynamicUserList: string[]; @@ -63,10 +61,12 @@ type InputProps = { contactOwnerEmail: string | null; rescheduleUid: string | null; routingFormResponse: RoutingFormResponse | null; + isPlatform: boolean; + hostname: string | undefined; + forcedSlug: string | undefined; }; export async function loadAndValidateUsers({ - req, eventType, eventTypeId, dynamicUserList, @@ -75,6 +75,9 @@ export async function loadAndValidateUsers({ contactOwnerEmail, rescheduleUid, routingFormResponse, + isPlatform, + hostname, + forcedSlug, }: InputProps): Promise<{ qualifiedRRUsers: UsersWithDelegationCredentials; additionalFallbackRRUsers: UsersWithDelegationCredentials; @@ -83,7 +86,9 @@ export async function loadAndValidateUsers({ let users: Users = await loadUsers({ eventType, dynamicUserList, - req, + hostname: hostname || "", + forcedSlug, + isPlatform, routedTeamMemberIds, contactOwnerEmail, }); diff --git a/packages/features/bookings/lib/handleNewBooking/loadUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadUsers.ts index c3b7e3f19b..ac53ed2807 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadUsers.ts @@ -1,7 +1,6 @@ import { Prisma } from "@prisma/client"; -import type { IncomingMessage } from "http"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { getRoutedUsersWithContactOwnerAndFixedUsers, findMatchingHostsWithEventSegment, @@ -33,18 +32,27 @@ type EventType = Pick< export const loadUsers = async ({ eventType, dynamicUserList, - req, + hostname, + forcedSlug, + isPlatform, routedTeamMemberIds, contactOwnerEmail, }: { eventType: EventType; dynamicUserList: string[]; - req: IncomingMessage; routedTeamMemberIds: number[] | null; contactOwnerEmail: string | null; + hostname: string; + forcedSlug: string | undefined; + isPlatform: boolean; }) => { try { - const { currentOrgDomain } = orgDomainConfig(req); + const { currentOrgDomain } = getOrgDomainConfig({ + hostname, + forcedSlug, + isPlatform, + }); + const users = eventType.id ? await loadUsersByEventType(eventType) : await loadDynamicUsers(dynamicUserList, currentOrgDomain); diff --git a/packages/features/bookings/lib/handleNewBooking/test/booking-limits.test.ts b/packages/features/bookings/lib/handleNewBooking/test/booking-limits.test.ts index 37342f057c..eab7d1bcfb 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/booking-limits.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/booking-limits.test.ts @@ -20,7 +20,6 @@ import { mockSuccessfulVideoMeetingCreation, mockCalendarToHaveNoBusySlots, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScenario/expects"; import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; @@ -116,12 +115,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("booking_limit_reached"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("booking_limit_reached"); const { dateString: yearWithoutBookingsDateString } = getDate({ yearIncrement: 2 }); @@ -138,13 +137,10 @@ describe("handleNewBooking", () => { }, }); - const { req: reqFollowingYear } = createMockNextJsRequest({ - method: "POST", - body: mockBookingDataFollowingYear, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingDataFollowingYear, }); - const createdBooking = await handleNewBooking(reqFollowingYear); - expect(createdBooking.responses).toContain({ email: booker.email, name: booker.name, @@ -239,16 +235,14 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - vi.spyOn(prismock, "$queryRaw").mockResolvedValue([{ totalMinutes: yearlyDurationLimit }]); - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "duration_limit_reached" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("duration_limit_reached"); const { dateString: yearWithoutBookingsDateString } = getDate({ yearIncrement: 2 }); @@ -265,14 +259,11 @@ describe("handleNewBooking", () => { }, }); - const { req: reqFollowingYear } = createMockNextJsRequest({ - method: "POST", - body: mockBookingDataFollowingYear, - }); - vi.spyOn(prismock, "$queryRaw").mockResolvedValue([{ totalMinutes: 0 }]); - const createdBooking = await handleNewBooking(reqFollowingYear); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingDataFollowingYear, + }); expect(createdBooking.responses).toEqual( expect.objectContaining({ @@ -373,12 +364,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("booking_limit_reached"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("booking_limit_reached"); }, timeout ); @@ -460,12 +451,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("booking_limit_reached"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("booking_limit_reached"); }, timeout ); @@ -536,13 +527,12 @@ describe("handleNewBooking", () => { }, }); - const { req: reqBookingBefore } = createMockNextJsRequest({ - method: "POST", - body: mockBookingBeforeData, - }); - await expect(async () => await handleNewBooking(reqBookingBefore)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingBeforeData, + }) + ).rejects.toThrowError("no_available_users_found_error"); // 7:00 - 7:15 busy // 7:17 - 8:15 after event buffer @@ -560,13 +550,12 @@ describe("handleNewBooking", () => { }, }); - const { req: reqBookingAfter } = createMockNextJsRequest({ - method: "POST", - body: mockBookingAfterData, - }); - await expect(async () => await handleNewBooking(reqBookingAfter)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingAfterData, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); test(`should throw error when booking is within a before event buffer of an existing booking @@ -628,13 +617,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); }); test(`should throw error when booking is within a after event buffer of an existing booking @@ -696,13 +684,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "no_available_users_found_error" - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("no_available_users_found_error"); }); test( @@ -737,11 +724,6 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const scenarioData = getScenarioData({ webhooks: [ { @@ -772,9 +754,12 @@ describe("handleNewBooking", () => { mockCalendarToHaveNoBusySlots("googlecalendar", {}); await createBookingScenario(scenarioData); - await expect(() => handleNewBooking(req)).rejects.toThrowError( - "Attempting to book a meeting in the past." - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("Attempting to book a meeting in the past."); }, timeout ); @@ -847,11 +832,6 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - mockCalendarToHaveNoBusySlots("googlecalendar", { create: { id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", @@ -867,7 +847,12 @@ describe("handleNewBooking", () => { }, }); - expect(() => handleNewBooking(req)).rejects.toThrowError("booking_time_out_of_bounds_error"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("booking_time_out_of_bounds_error"); }, timeout ); diff --git a/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts b/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts index 91910bc00d..6e89b6d986 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts @@ -10,7 +10,6 @@ import { mockCalendarToHaveNoBusySlots, BookingLocations, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectSuccessfulBookingCreationEmails, expectBookingToBeInDatabase, @@ -157,12 +156,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, diff --git a/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts b/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts index ea3c7cfc99..19f4292d4b 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/date-overrides.test.ts @@ -10,7 +10,6 @@ import { mockCalendarToHaveNoBusySlots, BookingLocations, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectSuccessfulBookingCreationEmails, expectBookingToBeInDatabase, @@ -142,12 +141,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -322,12 +319,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, diff --git a/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts b/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts index 0fb1dfe11e..0568508d55 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/delegation-credential.test.ts @@ -21,7 +21,6 @@ import { createOrganization, buildDelegationCredential, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, @@ -160,13 +159,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -350,13 +346,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -552,13 +545,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -718,13 +708,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, diff --git a/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts index 6ef1648c8d..c99a86be82 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts @@ -17,7 +17,6 @@ import { getScenarioData, mockCalendar, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScenario/expects"; import { getMockRequestDataForDynamicGroupBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; @@ -118,13 +117,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -208,14 +204,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.HostsUnavailableForBooking - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking); }, timeout ); @@ -299,14 +293,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.HostsUnavailableForBooking - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking); }, timeout ); diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index e21648924a..d3b27d4148 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -29,7 +29,6 @@ import { mockVideoAppToCrashOnCreateMeeting, BookingLocations, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectWorkflowToBeNotTriggered, @@ -177,13 +176,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -346,12 +342,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -512,12 +505,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -657,12 +647,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -804,12 +791,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -966,12 +951,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -1056,17 +1039,14 @@ describe("handleNewBooking", () => { selectedCalendars: [TestData.selectedCalendars.google], }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - }, + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, }, - }), + }, }); const scenarioData = getScenarioData({ @@ -1104,7 +1084,9 @@ describe("handleNewBooking", () => { metadataLookupKey: "zoomvideo", }); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); expect(createdBooking).toEqual( expect.objectContaining({ location: BookingLocations.ZoomVideo, @@ -1150,17 +1132,14 @@ describe("handleNewBooking", () => { selectedCalendars: [TestData.selectedCalendars.google], }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - }, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, }, - }), + }, }); const scenarioData = getScenarioData({ @@ -1191,7 +1170,9 @@ describe("handleNewBooking", () => { apps: [TestData.apps["daily-video"]], }); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, + }); expect(createdBooking).toEqual( expect.objectContaining({ location: "Seoul", @@ -1239,17 +1220,14 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - }, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, }, - }), + }, }); const scenarioData = getScenarioData({ @@ -1282,7 +1260,9 @@ describe("handleNewBooking", () => { metadataLookupKey: "zoomvideo", }); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, + }); expect(createdBooking).toEqual( expect.objectContaining({ location: BookingLocations.ZoomVideo, @@ -1361,20 +1341,20 @@ describe("handleNewBooking", () => { metadataLookupKey: "zoomvideo", }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.ZoomVideo }, - }, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.ZoomVideo }, }, - }), + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, }); - const createdBooking = await handleNewBooking(req); const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); @@ -1459,20 +1439,20 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.ZoomVideo }, - }, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.ZoomVideo }, }, - }), + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking).toEqual( expect.objectContaining({ @@ -1528,7 +1508,7 @@ describe("handleNewBooking", () => { }) ); - const mockBookingData = getMockRequestDataForBooking({ + const mockedBookingData = getMockRequestDataForBooking({ data: { start: `${getDate({ dateIncrement: 1 }).dateString}T05:00:00.000Z`, end: `${getDate({ dateIncrement: 1 }).dateString}T05:15:00.000Z`, @@ -1541,12 +1521,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("Invalid event length"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockedBookingData, + }) + ).rejects.toThrowError("Invalid event length"); }, timeout ); @@ -1579,18 +1559,15 @@ describe("handleNewBooking", () => { utm_content: "content test", }; - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - }, - tracking, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, }, - }), + tracking, + }, }); const scenarioData = getScenarioData({ @@ -1629,7 +1606,9 @@ describe("handleNewBooking", () => { metadataLookupKey: "zoomvideo", }); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, + }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expectBookingTrackingToBeInDatabase(tracking, createdBooking.uid!); @@ -1657,18 +1636,15 @@ describe("handleNewBooking", () => { selectedCalendars: [TestData.selectedCalendars.google], }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - }, - creationSource: CreationSource.WEBAPP, + const mockedBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, }, - }), + creationSource: CreationSource.WEBAPP, + }, }); const scenarioData = getScenarioData({ @@ -1707,7 +1683,9 @@ describe("handleNewBooking", () => { metadataLookupKey: "zoomvideo", }); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockedBookingData, + }); expect(createdBooking).toEqual( expect.objectContaining({ creationSource: CreationSource.WEBAPP, @@ -1783,14 +1761,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.NoAvailableUsersFound - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); @@ -1844,7 +1820,7 @@ describe("handleNewBooking", () => { }) ); - const _calendarMock = mockCalendar("googlecalendar", { + mockCalendar("googlecalendar", { create: { uid: "MOCK_ID", iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", @@ -1870,14 +1846,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.NoAvailableUsersFound - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); @@ -1951,12 +1925,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expectBookingToBeInDatabase({ description: "", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2039,14 +2010,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.NoAvailableUsersFound - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); @@ -2125,12 +2094,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expectBookingToBeInDatabase({ description: "", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2228,12 +2194,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -2358,15 +2322,11 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, }); - req.userId = organizer.id; - - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2484,12 +2444,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -2621,12 +2578,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -2707,22 +2661,21 @@ describe("handleNewBooking", () => { mockCalendarToHaveNoBusySlots("googlecalendar"); - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, }, - }), + }, }); try { - await handleNewBooking(req); + await handleNewBooking({ + bookingData: mockBookingData, + }); } catch (e) { expect(e).toBeInstanceOf(MockError); expect((e as { message: string }).message).toBe("Error creating Video meeting"); @@ -2765,11 +2718,6 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const scenarioData = getScenarioData({ webhooks: [ { @@ -2809,7 +2757,9 @@ describe("handleNewBooking", () => { mockCalendarToHaveNoBusySlots("googlecalendar", {}); await createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -2952,11 +2902,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking).toEqual( expect.objectContaining({ @@ -3110,11 +3058,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ @@ -3261,13 +3207,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -3325,9 +3268,12 @@ describe("handleNewBooking", () => { destinationEmail: organizerDestinationCalendarEmailOnEventType, }); - await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - ErrorCode.NoAvailableUsersFound - ); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); @@ -3382,12 +3328,12 @@ describe("handleNewBooking", () => { await createBookingScenario(scenarioData); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("eventTypeUser.notFound"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("eventTypeUser.notFound"); }); test("username is critical in watchlist", async () => { @@ -3444,12 +3390,12 @@ describe("handleNewBooking", () => { value: organizer.username, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("eventTypeUser.notFound"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("eventTypeUser.notFound"); }); test("domain is critical in watchlist", async () => { @@ -3506,12 +3452,12 @@ describe("handleNewBooking", () => { value: "spammer.com", }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("eventTypeUser.notFound"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("eventTypeUser.notFound"); }); test("domain is critical in watchlist", async () => { @@ -3568,12 +3514,12 @@ describe("handleNewBooking", () => { value: "spam@spammer.com", }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(async () => await handleNewBooking(req)).rejects.toThrowError("eventTypeUser.notFound"); + await expect( + async () => + await handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError("eventTypeUser.notFound"); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts new file mode 100644 index 0000000000..08c6dd60f5 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/test/handleNewRecurringBooking.test.ts @@ -0,0 +1,566 @@ +import { + createBookingScenario, + getBooker, + getDate, + getGoogleCalendarCredential, + getOrganizer, + getScenarioData, + mockCalendarToHaveNoBusySlots, + mockSuccessfulVideoMeetingCreation, + TestData, + Timezones, +} from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { + expectBookingCreatedWebhookToHaveBeenFired, + expectBookingToBeInDatabase, + expectSuccessfulBookingCreationEmails, + expectSuccessfulCalendarEventCreationInCalendar, +} from "@calcom/web/test/utils/bookingScenario/expects"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; + +import { v4 as uuidv4 } from "uuid"; +import { describe, expect } from "vitest"; + +import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import logger from "@calcom/lib/logger"; +import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; +import { test } from "@calcom/web/test/fixtures/fixtures"; + +import { handleNewRecurringBooking } from "../../handleNewRecurringBooking"; + +const DAY_IN_MS = 1000 * 60 * 60 * 24; + +function getPlusDayDate(date: string, days: number) { + return new Date(new Date(date).getTime() + days * DAY_IN_MS); +} + +// Local test runs sometime gets too slow +const timeout = process.env.CI ? 5000 : 20000; +describe("handleNewRecurringBooking", () => { + setupAndTeardown(); + + describe("Recurring EventType:", () => { + describe("User event type:", () => { + test( + `should create successful bookings for the number of slots requested + 1. Should create the same number of bookings as requested slots in the database + 2. Should send emails for the first booking only to the booker as well as organizer + 3. Should create a calendar event for every booking in the destination calendar + 3. Should trigger BOOKING_CREATED webhook for every booking + `, + async ({ emails }) => { + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + }, + }); + + const recurrence = getRecurrence({ + type: "weekly", + numberOfOccurrences: 3, + }); + const plus1DateString = getDate({ dateIncrement: 1 }).dateString; + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + recurringEvent: recurrence, + users: [ + { + id: 101, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const recurringCountInRequest = 2; + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:30:00.000Z`, + recurringEventId: uuidv4(), + recurringCount: recurringCountInRequest, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const numOfSlotsToBeBooked = 4; + + // Create an array of booking data for multiple slots + const bookingDataArray = Array(numOfSlotsToBeBooked) + .fill(mockBookingData) + .map((mockBookingData, index) => { + return { + ...mockBookingData, + start: getPlusDayDate(mockBookingData.start, index).toISOString(), + end: getPlusDayDate(mockBookingData.end, index).toISOString(), + }; + }); + + // Call handleNewRecurringBooking directly instead of through API + const createdBookings = await handleNewRecurringBooking({ + bookingData: bookingDataArray, + userId: -1, // Simulating anonymous user like in the API test + }); + + expect(createdBookings.length).toBe(numOfSlotsToBeBooked); + for (const [index, createdBooking] of Object.entries(createdBookings)) { + logger.debug("Assertion for Booking with index:", index, { createdBooking }); + expect(createdBooking.responses).toEqual( + expect.objectContaining({ + email: booker.email, + name: booker.name, + }) + ); + + expect(createdBooking).toEqual( + expect.objectContaining({ + location: "integrations:daily", + }) + ); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + recurringEventId: mockBookingData.recurringEventId, + references: [ + { + type: "daily_video", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + { + type: "google_calendar", + uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + }, + ], + }); + + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`, + }); + } + + expectSuccessfulBookingCreationEmails({ + booker, + booking: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBookings[0].uid!, + urlOrigin: WEBSITE_URL, + }, + organizer, + emails, + bookingTimeRange: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start: createdBookings[0].startTime!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + end: createdBookings[0].endTime!, + }, + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + recurrence: { + ...recurrence, + count: recurringCountInRequest, + }, + }); + + expect(emails.get().length).toBe(2); + + expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [ + { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + ]); + }, + timeout + ); + + test( + `should fail recurring booking if second slot is already booked`, + async ({}) => { + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + }, + }); + + const recurrence = getRecurrence({ + type: "weekly", + numberOfOccurrences: 3, + }); + const plus1DateString = getDate({ dateIncrement: 1 }).dateString; + const plus2DateString = getDate({ dateIncrement: 2 }).dateString; + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + recurringEvent: recurrence, + users: [ + { + id: 101, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + bookings: [ + { + uid: "booking-1-uid", + eventTypeId: 1, + userId: organizer.id, + status: BookingStatus.ACCEPTED, + startTime: `${plus2DateString}T04:00:00.000Z`, + endTime: `${plus2DateString}T04:30:00.000Z`, + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const recurringCountInRequest = 2; + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:30:00.000Z`, + recurringEventId: uuidv4(), + recurringCount: recurringCountInRequest, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const numOfSlotsToBeBooked = 4; + + // Create an array of booking data for multiple slots + const bookingDataArray = Array(numOfSlotsToBeBooked) + .fill(mockBookingData) + .map((mockBookingData, index) => { + return { + ...mockBookingData, + start: getPlusDayDate(mockBookingData.start, index).toISOString(), + end: getPlusDayDate(mockBookingData.end, index).toISOString(), + }; + }); + + await expect(() => + handleNewRecurringBooking({ + bookingData: bookingDataArray, + userId: -1, + }) + ).rejects.toThrow(ErrorCode.NoAvailableUsersFound); + }, + timeout + ); + }); + + describe("Round robin event type:", () => { + test("should fail recurring booking if a fixed host is not available on the second slot", async () => { + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + defaultScheduleId: null, + teams: [ + { + membership: { + accepted: true, + }, + team: { + id: 1, + name: "Team 1", + slug: "team-1", + }, + }, + ], + schedules: [TestData.schedules.IstMorningShift], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "organizer@google-calendar.com", + }, + }); + + const otherTeamMembers = [ + { + name: "Other Team Member 1", + username: "other-team-member-1", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "other-team-member-1@example.com", + id: 102, + schedules: [TestData.schedules.IstMorningShift], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "other-team-member-1@google-calendar.com", + }, + }, + ]; + + const recurrence = getRecurrence({ + type: "weekly", + numberOfOccurrences: 3, + }); + + const plus1DateString = getDate({ dateIncrement: 1 }).dateString; + const plus2DateString = getDate({ dateIncrement: 2 }).dateString; + + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 30, + schedulingType: SchedulingType.ROUND_ROBIN, + length: 30, + recurringEvent: recurrence, + hosts: [ + { + userId: 101, + isFixed: false, + }, + { + userId: 102, + isFixed: true, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + bookings: [ + { + userId: 102, + attendees: [ + { + email: "IntegrationTestUser102@example.com", + }, + ], + eventTypeId: 1, + status: "ACCEPTED", + startTime: `${plus2DateString}T04:00:00.000Z`, + endTime: `${plus2DateString}T04:30:00.000Z`, + }, + ], + organizer, + usersApartFromOrganizer: otherTeamMembers, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const recurringCountInRequest = 4; + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:30:00.000Z`, + recurringEventId: uuidv4(), + recurringCount: recurringCountInRequest, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const numOfSlotsToBeBooked = 4; + + // Create an array of booking data for multiple slots + const bookingDataArray = Array(numOfSlotsToBeBooked) + .fill(mockBookingData) + .map((mockBookingData, index) => { + return { + ...mockBookingData, + schedulingType: SchedulingType.ROUND_ROBIN, + start: getPlusDayDate(mockBookingData.start, index).toISOString(), + end: getPlusDayDate(mockBookingData.end, index).toISOString(), + }; + }); + + await expect(() => + handleNewRecurringBooking({ + bookingData: bookingDataArray, + userId: -1, + }) + ).rejects.toThrow(ErrorCode.NoAvailableUsersFound); + }); + }); + }); + + function getRecurrence({ + type, + numberOfOccurrences, + }: { + type: "weekly" | "monthly" | "yearly"; + numberOfOccurrences: number; + }) { + const freq = type === "yearly" ? 0 : type === "monthly" ? 1 : 2; + return { freq, count: numberOfOccurrences, interval: 1 }; + } +}); diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index 3df591bc87..63f3754801 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -19,7 +19,6 @@ import { getMockPassingAppStatus, getDefaultBookingFields, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectBookingToBeInDatabase, @@ -183,13 +182,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - const previousBooking = await prismaMock.booking.findUnique({ where: { uid: uidOfBookingToBeRescheduled, @@ -424,13 +420,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - /** * Booking Time should be new time */ @@ -623,13 +616,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingInDBToBeRescheduledFromTo({ from: { uid: uidOfBookingToBeRescheduled, @@ -819,12 +809,9 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -1052,16 +1039,11 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, }); - // Fake the request to be from organizer - req.userId = organizer.id; - - const createdBooking = await handleNewBooking(req); - /** * Booking Time should be new time */ @@ -1305,16 +1287,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + // Fake the request to be from organizer + userId: organizer.id, }); - // Fake the request to be from organizer - req.userId = organizer.id; - - const createdBooking = await handleNewBooking(req); - /** * Booking Time should be new time */ @@ -1524,15 +1502,11 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + // Fake the request to be from organizer + userId: organizer.id, }); - - // Fake the request to be from organizer - req.userId = organizer.id; - - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toEqual( expect.objectContaining({ email: booker.email, @@ -1772,16 +1746,12 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + // Fake the request to be from organizer + userId: previousOrganizerIdForTheBooking, }); - // Fake the request to be from organizer - req.userId = previousOrganizerIdForTheBooking; - - const createdBooking = await handleNewBooking(req); - /** * Booking Time should be new time */ @@ -1975,12 +1945,10 @@ describe("handleNewBooking", () => { rescheduledBy: booker.email, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); const previousBooking = await prismaMock.booking.findUnique({ where: { @@ -2126,12 +2094,10 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); const previousBooking = await prismaMock.booking.findUnique({ where: { @@ -2328,12 +2294,10 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); const previousBooking = await prismaMock.booking.findUnique({ where: { @@ -2485,12 +2449,10 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); const previousBooking = await prismaMock.booking.findUnique({ where: { @@ -2646,12 +2608,10 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); const previousBooking = await prismaMock.booking.findUnique({ where: { diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index 516f65f03b..ba238ad180 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -15,7 +15,6 @@ import { getZoomAppCredential, getDefaultBookingFields, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { // expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, @@ -174,13 +173,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -360,13 +356,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - await expect(async () => { - await handleNewBooking(req); + await handleNewBooking({ + bookingData: mockBookingData, + }); }).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking); }, timeout @@ -483,11 +476,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -713,13 +704,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -951,13 +939,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -1194,13 +1179,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -1337,12 +1319,10 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); await expect(async () => { - await handleNewBooking(req); + await handleNewBooking({ + bookingData: mockBookingData, + }); }).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout @@ -1450,11 +1430,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -1653,11 +1631,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); await expectBookingToBeInDatabase({ description: "", location: BookingLocations.ZoomVideo, @@ -1861,11 +1837,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -2058,11 +2032,9 @@ describe("handleNewBooking", () => { }, }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, @@ -2227,21 +2199,15 @@ describe("handleNewBooking", () => { }, }); - const { req: req1 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData1, + const createdBooking1 = await handleNewBooking({ + bookingData: mockBookingData1, }); - const { req: req2 } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData2, - }); - - const createdBooking1 = await handleNewBooking(req1); - expect(createdBooking1.userId).toBe(102); - const createdBooking2 = await handleNewBooking(req2); + const createdBooking2 = await handleNewBooking({ + bookingData: mockBookingData2, + }); expect(createdBooking2.userId).toBe(102); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts b/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts index 95fcc17243..cfbbd9fe4d 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts @@ -12,7 +12,6 @@ import { Timezones, createOrganization, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectSMSWorkflowToBeTriggered, @@ -133,13 +132,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectSMSWorkflowToBeTriggered({ sms, toNumber: "000", @@ -232,13 +228,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectSMSWorkflowToBeNotTriggered({ sms, toNumber: "000", @@ -379,13 +372,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectSMSWorkflowToBeTriggered({ sms, toNumber: "000", @@ -524,13 +514,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectSMSWorkflowToBeNotTriggered({ sms, toNumber: "000", @@ -644,13 +631,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectWorkflowToBeTriggered({ emailsToReceive: ["booker@example.com"], emails, @@ -760,13 +744,10 @@ describe("handleNewBooking", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expectSMSWorkflowToBeTriggered({ sms, toNumber: "000", diff --git a/packages/features/bookings/lib/handleNewRecurringBooking.ts b/packages/features/bookings/lib/handleNewRecurringBooking.ts index 36f08bee73..dca7019257 100644 --- a/packages/features/bookings/lib/handleNewRecurringBooking.ts +++ b/packages/features/bookings/lib/handleNewRecurringBooking.ts @@ -1,22 +1,27 @@ -import type { NextApiRequest } from "next"; - import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; -import type { RecurringBookingCreateBody, BookingResponse } from "@calcom/features/bookings/types"; +import type { BookingResponse } from "@calcom/features/bookings/types"; import { SchedulingType } from "@calcom/prisma/client"; import type { AppsStatus } from "@calcom/types/Calendar"; -export const handleNewRecurringBooking = async ( - req: NextApiRequest & { - userId?: number | undefined; - platformClientId?: string; - platformRescheduleUrl?: string; - platformCancelUrl?: string; - platformBookingUrl?: string; - platformBookingLocation?: string; - noEmail?: boolean; - } -): Promise => { - const data: RecurringBookingCreateBody[] = req.body; +export type PlatformParams = { + platformClientId?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformRescheduleUrl?: string; + platformBookingLocation?: string; +}; + +export type BookingHandlerInput = { + bookingData: Record[]; + userId?: number; + // These used to come from headers but now we're passing them as params + hostname?: string; + forcedSlug?: string; + noEmail?: boolean; +} & PlatformParams; + +export const handleNewRecurringBooking = async (input: BookingHandlerInput): Promise => { + const data = input.bookingData; const createdBookings: BookingResponse[] = []; const allRecurringDates: { start: string | undefined; end: string | undefined }[] = data.map((booking) => { return { start: booking.start, end: booking.end }; @@ -33,10 +38,17 @@ export const handleNewRecurringBooking = async ( let luckyUsers = undefined; - if (isRoundRobin) { - const recurringEventReq: NextApiRequest & { userId?: number } = req; + const handleBookingMeta = { + userId: input.userId, + platformClientId: input.platformClientId, + platformRescheduleUrl: input.platformRescheduleUrl, + platformCancelUrl: input.platformCancelUrl, + platformBookingUrl: input.platformBookingUrl, + platformBookingLocation: input.platformBookingLocation, + }; - recurringEventReq.body = { + if (isRoundRobin) { + const recurringEventData = { ...firstBooking, appsStatus, allRecurringDates, @@ -44,10 +56,15 @@ export const handleNewRecurringBooking = async ( thirdPartyRecurringEventId, numSlotsToCheckForAvailability, currentRecurringIndex: 0, - noEmail: req.noEmail !== undefined ? req.noEmail : false, + noEmail: input.noEmail !== undefined ? input.noEmail : false, }; - const firstBookingResult = await handleNewBooking(recurringEventReq); + const firstBookingResult = await handleNewBooking({ + bookingData: recurringEventData, + hostname: input.hostname || "", + forcedSlug: input.forcedSlug as string | undefined, + ...handleBookingMeta, + }); luckyUsers = firstBookingResult.luckyUsers; } @@ -71,9 +88,7 @@ export const handleNewRecurringBooking = async ( // appsStatus = Object.values(calcAppsStatus); // } - const recurringEventReq: NextApiRequest & { userId?: number } = req; - - recurringEventReq.body = { + const recurringEventData = { ...booking, appsStatus, allRecurringDates, @@ -81,12 +96,16 @@ export const handleNewRecurringBooking = async ( thirdPartyRecurringEventId, numSlotsToCheckForAvailability, currentRecurringIndex: key, - noEmail: req.noEmail !== undefined ? req.noEmail : key !== 0, + noEmail: input.noEmail !== undefined ? input.noEmail : key !== 0, luckyUsers, }; - const promiseEachRecurringBooking: ReturnType = - handleNewBooking(recurringEventReq); + const promiseEachRecurringBooking: ReturnType = handleNewBooking({ + hostname: input.hostname || "", + forcedSlug: input.forcedSlug as string | undefined, + bookingData: recurringEventData, + ...handleBookingMeta, + }); const eachRecurringBooking = await promiseEachRecurringBooking; diff --git a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts index 17cabef47d..b20fb5012d 100644 --- a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts +++ b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts @@ -87,13 +87,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - expect(spy).toHaveBeenCalledTimes(1); }); @@ -191,7 +188,9 @@ describe("handleSeats", () => { body: mockBookingData, }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); expect(createdBooking.metadata).toHaveProperty("videoCallUrl"); @@ -330,13 +329,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - const handleSeatsCall = spy.mock.calls[0][0]; expect(handleSeatsCall).toEqual( @@ -488,13 +484,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - const newAttendee = await prismaMock.attendee.findFirst({ where: { email: booker.email, @@ -617,13 +610,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - const newAttendee = await prismaMock.attendee.findFirst({ where: { email: booker.email, @@ -746,12 +736,11 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.AlreadySignedUpForBooking); + await expect(() => + handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.AlreadySignedUpForBooking); }); test("If event is already full, fail", async () => { @@ -869,12 +858,11 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.BookingSeatsFull); + await expect(() => + handleNewBooking({ + bookingData: mockBookingData, + }) + ).rejects.toThrowError(ErrorCode.BookingSeatsFull); }); test("Verify Seat Availability Calculation Based on Booked Seats, Not Total Attendees", async () => { @@ -1021,13 +1009,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - const newAttendee = await prismaMock.attendee.findFirst({ where: { email: booker.email, @@ -1219,13 +1204,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + await handleNewBooking({ + bookingData: mockBookingData, }); - await handleNewBooking(req); - // Ensure that the attendee is no longer a part of the old booking const oldBookingAttendees = await prismaMock.attendee.findMany({ where: { @@ -1393,7 +1375,9 @@ describe("handleSeats", () => { body: mockBookingData, }); - const createdBooking = await handleNewBooking(req); + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); // Ensure that the attendee is no longer a part of the old booking const oldBookingAttendees = await prismaMock.attendee.findMany({ @@ -1532,13 +1516,10 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, }); - const createdBooking = await handleNewBooking(req); - // Ensure that the old booking is cancelled const oldBooking = await prismaMock.booking.findFirst({ where: { @@ -1990,15 +1971,11 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const rescheduledBooking = await handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, }); - req.userId = organizer.id; - - const rescheduledBooking = await handleNewBooking(req); - // Ensure that the booking has been moved expect(rescheduledBooking?.startTime).toEqual(secondBookingStartTime); expect(rescheduledBooking?.endTime).toEqual(secondBookingEndTime); @@ -2190,15 +2167,11 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, + const rescheduledBooking = await handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, }); - req.userId = organizer.id; - - const rescheduledBooking = await handleNewBooking(req); - // Ensure that the booking has been moved expect(rescheduledBooking?.startTime).toEqual(new Date(secondBookingStartTime)); expect(rescheduledBooking?.endTime).toEqual(new Date(secondBookingEndTime)); @@ -2401,15 +2374,13 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - req.userId = organizer.id; - // const rescheduledBooking = await handleNewBooking(req); - await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.NotEnoughAvailableSeats); + await expect(() => + handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, + }) + ).rejects.toThrowError(ErrorCode.NotEnoughAvailableSeats); }); test("When trying to reschedule in a non-available slot, throw an error", async () => { @@ -2547,14 +2518,12 @@ describe("handleSeats", () => { }, }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); - - req.userId = organizer.id; - - await expect(() => handleNewBooking(req)).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); + await expect(() => + handleNewBooking({ + bookingData: mockBookingData, + userId: organizer.id, + }) + ).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }); }); diff --git a/packages/features/ee/organizations/lib/orgDomains.ts b/packages/features/ee/organizations/lib/orgDomains.ts index 3ee1d66a26..562be161df 100644 --- a/packages/features/ee/organizations/lib/orgDomains.ts +++ b/packages/features/ee/organizations/lib/orgDomains.ts @@ -52,6 +52,34 @@ export function getOrgSlug(hostname: string, forcedSlug?: string) { return null; } +export function getOrgDomainConfig({ + hostname, + fallback, + forcedSlug, + isPlatform, +}: { + hostname: string; + fallback?: string | string[]; + forcedSlug?: string; + isPlatform?: boolean; +}) { + if (isPlatform && forcedSlug) { + return { + isValidOrgDomain: true, + currentOrgDomain: forcedSlug, + }; + } + + return getOrgDomainConfigFromHostname({ + hostname, + fallback, + forcedSlug, + }); +} + +/** + * @deprecated Use `getOrgDomainConfig` instead. To be removed in a future release. getOrgDomainConfig is more flexible and can be used without next request. + */ export function orgDomainConfig(req: IncomingMessage | undefined, fallback?: string | string[]) { const forPlatform = isPlatformRequest(req); const forcedSlugHeader = req?.headers?.["x-cal-force-slug"]; diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index 703ea79ed5..2585651421 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -177,7 +177,7 @@ async function handler(req: NextApiRequest) { bookingFields: eventType.bookingFields, }); const reqBody = await getBookingData({ - req, + reqBody: req.body, eventType, schema, });