From 5b50a469b23e7c846c78cec77f8f15b4e2e5b561 Mon Sep 17 00:00:00 2001 From: Mehul Date: Fri, 16 Feb 2024 23:18:57 +0530 Subject: [PATCH] fix: double bookings on seated event (#13406) * fix: double bookings on seated event * test: correcting handleSeats.test.ts fix: seat rescheduling * Add test for booking w/o bookingUid in req * Add back `bookingUid` * add 'reqBookingUid' parameter * Add `reqBookingUid` * Fix --------- Co-authored-by: Keith Williams Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Joe Au-Yeung --- .../features/bookings/lib/handleNewBooking.ts | 2 +- .../lib/handleSeats/create/createNewSeat.ts | 3 +- .../bookings/lib/handleSeats/handleSeats.ts | 10 +- .../lib/handleSeats/test/handleSeats.test.ts | 133 +++++++++++++++++- 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index f717182c6d..d6b0199343 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1511,7 +1511,7 @@ async function handler( }; // For seats, if the booking already exists then we want to add the new attendee to the existing booking - if (eventType.seatsPerTimeSlot && (reqBody.bookingUid || rescheduleUid)) { + if (eventType.seatsPerTimeSlot) { const newBooking = await handleSeats({ rescheduleUid, reqBookingUid: reqBody.bookingUid, diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index be6ad6da05..214d7752a6 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -28,7 +28,6 @@ const createNewSeat = async ( attendeeLanguage, invitee, eventType, - reqBookingUid, additionalNotes, noEmail, paymentAppData, @@ -68,7 +67,7 @@ const createNewSeat = async ( await prisma.booking.update({ where: { - uid: reqBookingUid, + uid: seatedBooking.uid, }, include: { attendees: true, diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index 7fbde526e0..be8dffff78 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -41,9 +41,10 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { { uid: rescheduleUid || reqBookingUid, }, + { eventTypeId: eventType.id, - startTime: evt.startTime, + startTime: new Date(evt.startTime), }, ], status: BookingStatus.ACCEPTED, @@ -63,10 +64,15 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { }, }); - if (!seatedBooking) { + if (!seatedBooking && rescheduleUid) { throw new HttpError({ statusCode: 404, message: ErrorCode.BookingNotFound }); } + // We might be trying to create a new booking + if (!seatedBooking) { + return; + } + // See if attendee is already signed up for timeslot if ( seatedBooking.attendees.find((attendee) => attendee.email === invitee[0].email) && diff --git a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts index 9b3cee49d7..7c09a4061f 100644 --- a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts +++ b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts @@ -90,7 +90,7 @@ describe("handleSeats", () => { await handleNewBooking(req); - expect(spy).toHaveBeenCalledTimes(0); + expect(spy).toHaveBeenCalledTimes(1); }); test("handleSeats is called when a new attendee is added", async () => { @@ -194,7 +194,6 @@ describe("handleSeats", () => { expect(handleSeatsCall).toEqual( expect.objectContaining({ bookerEmail: booker.email, - reqBookingUid: bookingUid, reqBodyUser: reqBookingUser, tAttendees: expect.any(Function), additionalNotes: expect.anything(), @@ -338,7 +337,6 @@ describe("handleSeats", () => { expect.objectContaining({ rescheduleUid: bookingUid, bookerEmail: booker.email, - reqBookingUid: bookingUid, reqBodyUser: reqBookingUser, tAttendees: expect.any(Function), additionalNotes: expect.anything(), @@ -511,6 +509,135 @@ describe("handleSeats", () => { ); }); + // Testing in case of a wave of people book a time slot at the same time + test("Attendee should be added to existing seated event when bookingUid is not present", async () => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + + const booker = getBooker({ + email: "seat2@example.com", + name: "Seat 2", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }); + + const bookingId = 1; + const bookingUid = "abc123"; + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const bookingStartTime = `${plus1DateString}T04:00:00Z`; + const bookingEndTime = `${plus1DateString}T04:30:00Z`; + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slug: "seated-event", + slotInterval: 30, + length: 30, + users: [ + { + id: 101, + }, + ], + seatsPerTimeSlot: 3, + seatsShowAttendees: false, + }, + ], + bookings: [ + { + id: bookingId, + uid: bookingUid, + eventTypeId: 1, + status: BookingStatus.ACCEPTED, + startTime: bookingStartTime, + endTime: bookingEndTime, + metadata: { + videoCallUrl: "https://existing-daily-video-call-url.example.com", + }, + references: [ + { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + credentialId: null, + }, + ], + attendees: [ + getMockBookingAttendee({ + id: 1, + name: "Seat 1", + email: "seat1@test.com", + locale: "en", + + timeZone: "America/Toronto", + bookingSeat: { + referenceUid: "booking-seat-1", + data: {}, + }, + }), + ], + }, + ], + organizer, + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + const reqBookingUser = "seatedAttendee"; + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + user: reqBookingUser, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + await handleNewBooking(req); + + const newAttendee = await prismaMock.attendee.findFirst({ + where: { + email: booker.email, + bookingId: bookingId, + }, + include: { + bookingSeat: true, + }, + }); + + // Check for the existence of the new attendee w/ booking seat + expect(newAttendee?.bookingSeat).toEqual( + expect.objectContaining({ + referenceUid: expect.any(String), + data: expect.any(Object), + bookingId: bookingId, + }) + ); + }); + test("If attendee is already a part of the booking then throw an error", async () => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;