From afe1f5b72f6a5d6de5f7f6a473ccabad13d815b5 Mon Sep 17 00:00:00 2001 From: alannnc Date: Fri, 21 Apr 2023 06:49:53 -0700 Subject: [PATCH] Fix/seats-cancel-links (#8394) * Fix cancel links * Use searchParams API to build cancelLink * Fix cancel showSeats * Added test for owner cancel attendee list --------- Co-authored-by: Alex van Andel --- apps/web/pages/booking/[uid].tsx | 55 +++++-- apps/web/playwright/booking-seats.e2e.ts | 140 ++++++++++++++++++ .../features/bookings/lib/handleNewBooking.ts | 1 + packages/lib/CalEventParser.ts | 14 +- 4 files changed, 194 insertions(+), 16 deletions(-) diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 4bf32fa92b..ca02ce8376 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -891,16 +891,31 @@ const getEventTypesFromDB = async (id: number) => { }; }; -const handleSeatsEventTypeOnBooking = ( +const handleSeatsEventTypeOnBooking = async ( eventType: { seatsPerTimeSlot?: number | null; seatsShowAttendees: boolean | null; [x: string | number | symbol]: unknown; }, bookingInfo: Partial< - Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }> + Prisma.BookingGetPayload<{ + include: { + attendees: { select: { name: true; email: true } }; + seatsReferences: { select: { referenceUid: true } }; + user: { + select: { + id: true; + name: true; + email: true; + username: true; + timeZone: true; + }; + }; + }; + }> >, - email: string + seatReferenceUid?: string, + userId?: number ) => { if (eventType?.seatsPerTimeSlot !== null) { // @TODO: right now bookings with seats doesn't save every description that its entered by every user @@ -908,12 +923,34 @@ const handleSeatsEventTypeOnBooking = ( } else { return; } + // @TODO: If handling teams, we need to do more check ups for this. + if (bookingInfo?.user?.id === userId) { + return; + } + if (!eventType.seatsShowAttendees) { - const attendee = bookingInfo?.attendees?.find((a) => { - return a.email === email; + const seatAttendee = await prisma.bookingSeat.findFirst({ + where: { + referenceUid: seatReferenceUid, + }, + include: { + attendee: { + select: { + name: true, + email: true, + }, + }, + }, }); - bookingInfo["attendees"] = attendee ? [attendee] : []; + if (seatAttendee) { + const attendee = bookingInfo?.attendees?.find((a) => { + return a.email === seatAttendee.attendee?.email; + }); + bookingInfo["attendees"] = attendee ? [attendee] : []; + } else { + bookingInfo["attendees"] = []; + } } return bookingInfo; }; @@ -931,7 +968,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const parsedQuery = querySchema.safeParse(context.query); if (!parsedQuery.success) return { notFound: true }; - const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data; + const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data; const bookingInfoRaw = await prisma.booking.findFirst({ where: { @@ -1037,8 +1074,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { slug: eventType.team?.slug || eventType.users[0]?.username || null, }; - if (bookingInfo !== null && email && eventType.seatsPerTimeSlot) { - handleSeatsEventTypeOnBooking(eventType, bookingInfo, email); + if (bookingInfo !== null && eventType.seatsPerTimeSlot) { + await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id); } const payment = await prisma.payment.findFirst({ diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index 17d83f2809..35619f9409 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -313,5 +313,145 @@ test.describe("Booking with Seats", () => { // Validate that the number of seats its 10 expect(await page.locator("text=9 / 10 Seats available").count()).toEqual(0); }); + + test("Should cancel with seats but event should be still accesible and with one less attendee/seat", async ({ + page, + users, + bookings, + }) => { + const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ + { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, + { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, + ]); + await user.login(); + + const bookingAttendees = await prisma.attendee.findMany({ + where: { bookingId: booking.id }, + select: { + id: true, + }, + }); + + const bookingSeats = [ + { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, + { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, + ]; + + await prisma.bookingSeat.createMany({ + data: bookingSeats, + }); + + // Now we cancel the booking as the first attendee + // booking/${bookingUid}?cancel=true&allRemainingBookings=false&seatReferenceUid={bookingSeat.referenceUid} + await page.goto( + `/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}` + ); + + await page.locator('[data-testid="cancel"]').click(); + + await page.waitForLoadState("networkidle"); + + await expect(page).toHaveURL(/.*booking/); + + await page.goto( + `/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}` + ); + + // Page should not be 404 + await page.locator('[data-testid="cancel"]').click(); + + await page.waitForLoadState("networkidle"); + + await expect(page).toHaveURL(/.*booking/); + }); + + test("Should book with seats and hide attendees info from showAttendees true", async ({ + page, + users, + bookings, + }) => { + const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ + { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, + { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, + ]); + await user.login(); + const bookingWithEventType = await prisma.booking.findFirst({ + where: { uid: booking.uid }, + select: { + id: true, + eventTypeId: true, + }, + }); + + await prisma.eventType.update({ + data: { + seatsShowAttendees: false, + }, + where: { + id: bookingWithEventType?.eventTypeId || -1, + }, + }); + + const bookingAttendees = await prisma.attendee.findMany({ + where: { bookingId: booking.id }, + select: { + id: true, + }, + }); + + const bookingSeats = [ + { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, + { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, + ]; + + await prisma.bookingSeat.createMany({ + data: bookingSeats, + }); + + // Go to cancel page and see that attendees are listed and myself as I'm owner of the booking + await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`); + + const foundFirstAttendeeAsOwner = await page.locator('p[data-testid="attendee-first+seats@cal.com"]'); + await expect(foundFirstAttendeeAsOwner).toHaveCount(1); + const foundSecondAttendeeAsOwner = await page.locator('p[data-testid="attendee-second+seats@cal.com"]'); + await expect(foundSecondAttendeeAsOwner).toHaveCount(1); + await page.pause(); + await page.goto("auth/logout"); + + // Now we cancel the booking as the first attendee + // booking/${bookingUid}?cancel=true&allRemainingBookings=false&seatReferenceUid={bookingSeat.referenceUid} + await page.goto( + `/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[0].referenceUid}` + ); + + // No attendees should be displayed only the one that it's cancelling + const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]'); + + await expect(notFoundSecondAttendee).toHaveCount(0); + const foundFirstAttendee = await page.locator('p[data-testid="attendee-first+seats@cal.com"]'); + await expect(foundFirstAttendee).toHaveCount(1); + + await prisma.eventType.update({ + data: { + seatsShowAttendees: true, + }, + where: { + id: bookingWithEventType?.eventTypeId || -1, + }, + }); + + await page.goto( + `/booking/${booking.uid}?cancel=true&allRemainingBookings=false&seatReferenceUid=${bookingSeats[1].referenceUid}` + ); + + // Now attendees should be displayed + const foundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]'); + + await expect(foundSecondAttendee).toHaveCount(1); + const foundFirstAttendeeAgain = await page + .locator('p[data-testid="attendee-first+seats@cal.com"]') + .first(); + await expect(foundFirstAttendeeAgain).toHaveCount(1); + }); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 6449e4f6b5..0e463e6e69 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1443,6 +1443,7 @@ async function handler( * deep cloning evt to avoid this */ const copyEvent = cloneDeep(evt); + copyEvent.uid = booking.uid; await sendScheduledSeatsEmails(copyEvent, invitee[0], newSeat, !!eventType.seatsShowAttendees); const credentials = await refreshCredentials(organizerUser.credentials); diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts index cce5f9f283..0da136346b 100644 --- a/packages/lib/CalEventParser.ts +++ b/packages/lib/CalEventParser.ts @@ -150,7 +150,7 @@ export const getUid = (calEvent: CalendarEvent): string => { }; const getSeatReferenceId = (calEvent: CalendarEvent): string => { - return calEvent.attendeeSeatId ? `seatReferenceUid=${calEvent.attendeeSeatId}` : ""; + return calEvent.attendeeSeatId ? calEvent.attendeeSeatId : ""; }; export const getManageLink = (calEvent: CalendarEvent) => { @@ -161,12 +161,12 @@ ${WEBAPP_URL + "/booking/" + getUid(calEvent) + "?changes=true"} }; export const getCancelLink = (calEvent: CalendarEvent): string => { - return ( - WEBAPP_URL + - `/booking/${getUid( - calEvent - )}?cancel=true&allRemainingBookings=${!!calEvent.recurringEvent}&${getSeatReferenceId}` - ); + const cancelLink = new URL(WEBAPP_URL + `/booking/${getUid(calEvent)}`); + cancelLink.searchParams.append("cancel", "true"); + cancelLink.searchParams.append("allRemainingBookings", String(!!calEvent.recurringEvent)); + const seatReferenceUid = getSeatReferenceId(calEvent); + if (seatReferenceUid) cancelLink.searchParams.append("seatReferenceUid", seatReferenceUid); + return cancelLink.toString(); }; export const getRescheduleLink = (calEvent: CalendarEvent): string => {