feat: Round robin, when attendee books same timeslot return original booking (#21455)
This commit is contained in:
@@ -333,7 +333,7 @@ test.describe("Bookings", () => {
|
||||
// Second booking - should get a different host
|
||||
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await bookTimeSlot(page, { email: "attendee1@test.com" });
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
const secondHost = await page.getByTestId("booking-host-name").textContent();
|
||||
expect(secondHost).not.toBeNull();
|
||||
@@ -342,7 +342,7 @@ test.describe("Bookings", () => {
|
||||
// Third booking - should get a different host
|
||||
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await bookTimeSlot(page, { email: "attendee2@test.com" });
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
const thirdHost = await page.getByTestId("booking-host-name").textContent();
|
||||
expect(thirdHost).not.toBeNull();
|
||||
@@ -352,7 +352,7 @@ test.describe("Bookings", () => {
|
||||
// Fourth booking - should get a different host
|
||||
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await bookTimeSlot(page, { email: "attendee3@test.com" });
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
const fourthHost = await page.getByTestId("booking-host-name").textContent();
|
||||
expect(fourthHost).not.toBeNull();
|
||||
|
||||
@@ -66,6 +66,7 @@ import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFre
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getLuckyUser } from "@calcom/lib/server/getLuckyUser";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -110,7 +111,6 @@ import type { IEventTypePaymentCredentialType, Invitee, IsFixedAwareUser } from
|
||||
import { validateBookingTimeIsNotOutOfBounds } from "./handleNewBooking/validateBookingTimeIsNotOutOfBounds";
|
||||
import { validateEventLength } from "./handleNewBooking/validateEventLength";
|
||||
import handleSeats from "./handleSeats/handleSeats";
|
||||
import { getBookingRequest } from "./requiresConfirmation/getBookingRequest";
|
||||
|
||||
const translator = short();
|
||||
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
@@ -489,20 +489,24 @@ async function handler(
|
||||
bookerEmail,
|
||||
});
|
||||
|
||||
// If a request already exists for the timeslot, return that request
|
||||
if (!isConfirmedByDefault && !userReschedulingIsOwner) {
|
||||
const requestedBooking = await getBookingRequest({
|
||||
// For unconfirmed bookings or round robin bookings with the same attendee and timeslot, return the original booking
|
||||
if (
|
||||
(!isConfirmedByDefault && !userReschedulingIsOwner) ||
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||
) {
|
||||
const existingBooking = await BookingRepository.getValidBookingFromEventTypeForAttendee({
|
||||
eventTypeId,
|
||||
bookerEmail,
|
||||
bookerPhoneNumber,
|
||||
startTime: new Date(dayjs(reqBody.start).utc().format()),
|
||||
filterForUnconfirmed: !isConfirmedByDefault,
|
||||
});
|
||||
|
||||
if (requestedBooking) {
|
||||
if (existingBooking) {
|
||||
const bookingResponse = {
|
||||
...requestedBooking,
|
||||
...existingBooking,
|
||||
user: {
|
||||
...requestedBooking.user,
|
||||
...existingBooking.user,
|
||||
email: null,
|
||||
},
|
||||
paymentRequired: false,
|
||||
|
||||
@@ -58,7 +58,7 @@ import { WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { resetTestEmails } from "@calcom/lib/testEmails";
|
||||
import { CreationSource, WatchlistSeverity, WatchlistType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
@@ -3280,6 +3280,126 @@ describe("handleNewBooking", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Returning original booking", () => {
|
||||
test("Return the original booking if a request is made twice by the same attendee when a booking requires confirmation", async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
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],
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
requiresConfirmation: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const response = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
const secondResponse = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expect(secondResponse.id).toEqual(response.id);
|
||||
});
|
||||
test("Return the original booking if request is made by the same attendee for a round robin event", async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
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],
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
requiresConfirmation: true,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const response = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
const secondResponse = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expect(secondResponse.id).toEqual(response.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Blocked users", () => {
|
||||
test("user is locked", async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
|
||||
@@ -779,4 +779,37 @@ export class BookingRepository {
|
||||
...managedBookings,
|
||||
];
|
||||
}
|
||||
|
||||
static async getValidBookingFromEventTypeForAttendee({
|
||||
eventTypeId,
|
||||
bookerEmail,
|
||||
bookerPhoneNumber,
|
||||
startTime,
|
||||
filterForUnconfirmed,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
bookerEmail?: string;
|
||||
bookerPhoneNumber?: string;
|
||||
startTime: Date;
|
||||
filterForUnconfirmed?: boolean;
|
||||
}) {
|
||||
return await prisma.booking.findFirst({
|
||||
where: {
|
||||
eventTypeId,
|
||||
attendees: {
|
||||
some: {
|
||||
email: bookerEmail,
|
||||
phoneNumber: bookerPhoneNumber,
|
||||
},
|
||||
},
|
||||
startTime,
|
||||
status: filterForUnconfirmed ? BookingStatus.PENDING : BookingStatus.ACCEPTED,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
references: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user