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:
authored and GitHub committed 2025-04-02 17:57:58 -04:00
1 parent 4db3c592e5
commit 417a612aa3
27 files changed
+1301 -785

No files matched your search

@@ -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,
});