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 <iamamrit27@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
This commit is contained in:
sean-brydon
2025-04-02 17:57:58 -04:00
committed by GitHub
co-authored by amrit Morgan Morgan Vernay supalarry Lauris Skraucis
parent 4db3c592e5
commit 417a612aa3
27 changed files with 1301 additions and 785 deletions
+14 -4
View File
@@ -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) {
+1 -1
View File
@@ -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": "*",
@@ -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) {
@@ -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");
}
@@ -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): {
+7 -2
View File
@@ -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;
}
+26 -3
View File
@@ -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;
}
@@ -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;
@@ -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<string, any>;
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 = {
@@ -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<T extends z.ZodType>({
req,
reqBody,
eventType,
schema,
}: {
req: NextApiRequest;
reqBody: Record<string, any>;
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<T extends z.ZodType>({
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<T extends z.ZodType>({
responses,
});
return {
...reqBody,
...parsedBody,
name: responses.name,
email: responses.email,
attendeePhoneNumber: responses.attendeePhoneNumber,
@@ -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");
});
});
},
@@ -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,
});
@@ -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);
@@ -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
);
@@ -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,
@@ -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,
@@ -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,
@@ -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
);
@@ -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");
});
});
@@ -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 };
}
});
@@ -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: {
@@ -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);
});
});
@@ -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",
@@ -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<BookingResponse[]> => {
const data: RecurringBookingCreateBody[] = req.body;
export type PlatformParams = {
platformClientId?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
platformRescheduleUrl?: string;
platformBookingLocation?: string;
};
export type BookingHandlerInput = {
bookingData: Record<string, any>[];
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<BookingResponse[]> => {
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<typeof handleNewBooking> =
handleNewBooking(recurringEventReq);
const promiseEachRecurringBooking: ReturnType<typeof handleNewBooking> = handleNewBooking({
hostname: input.hostname || "",
forcedSlug: input.forcedSlug as string | undefined,
bookingData: recurringEventData,
...handleBookingMeta,
});
const eachRecurringBooking = await promiseEachRecurringBooking;
@@ -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);
});
});
@@ -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"];
@@ -177,7 +177,7 @@ async function handler(req: NextApiRequest) {
bookingFields: eventType.bookingFields,
});
const reqBody = await getBookingData({
req,
reqBody: req.body,
eventType,
schema,
});