Skip to content

Commit

Permalink
refactor: Refactor seats logic (#12905)
Browse files Browse the repository at this point in the history
* Refactor createBooking

* Type fix

* Abstract handleSeats

* Create Invitee type

* Create OrganizerUser type

* Abstract addVideoCallDataToEvt

* Abstract createLoggerWithEventDetails

* Abstract `handleAppStatus` from handler

* Create ReqAppsStatus type

* Move `deleteMeeting` and `getCalendar`

* Set parameters for `handleSeats`

* Typescript refactor

* Change function params from req

* Type fix

* Move handleSeats

* Abstract lastAttendeeDeleteBooking

* Create function for rescheduling seated events

* Fix imports on reschedule seats function

* Fix imports

* Import handleSeats function

* Fix rescheduleUid type

* Refactor owner reschedule to new time slot

* Refactor combine two booking times together

* Reschedule as an attendee

* Refactor createNewSeat

* Remove old handleSeats

* Remove lastAttendeeDeleteBooking from handleNewBooking

* Test for new attendee right params are passed

* Unit test params for reschedule

* Typo fix

* Clean up

* Create new seat test

* Test when attendee already signs up for booking

* Type fix

* Test reschedule move attendee to existing booking

* On reschedule create new booking

* Test on last attendee cancel booking

* Owner reschedule to new time slot

* Owner rescheduling, merge two bookings together

* Test: when merging more than available seats, then fail

* Test: fail when event is full

* Remove duplicate E2E tests

* Clean up

* Rename `addVideoCallDataToEvt` to `addVideoCallDataToEvent`

* Refactor `calcAppsStatus`

* Assign `evt` to resutl of `addVideoCallDataToEvent`

* Use prisma.transaction when moving attendees

* Clean create seat call

* Use ErrorCode enum

* Use attendeeRescheduledSeatedBooking function

* Await function

* Prevent double triggering of workflows

* Use inviteeToAdd in createNewSeat

* Remove unused error code

* Remove old handleSeats file

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

---------

Co-authored-by: Morgan <[email protected]>
Co-authored-by: Peer Richelsen <[email protected]>
Co-authored-by: Erik <[email protected]>
  • Loading branch information
4 people authored Jan 15, 2024
1 parent 5c6e104 commit a0f1ceb
Show file tree
Hide file tree
Showing 16 changed files with 2,917 additions and 1,788 deletions.
297 changes: 3 additions & 294 deletions apps/web/playwright/booking-seats.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { expect } from "@playwright/test";
import { uuid } from "short-uuid";
import { v4 as uuidv4 } from "uuid";

import { randomString } from "@calcom/lib/random";
Expand All @@ -8,7 +7,6 @@ import { BookingStatus } from "@calcom/prisma/enums";

import { test } from "./lib/fixtures";
import {
bookTimeSlot,
createNewSeatedEventType,
selectFirstAvailableTimeSlotNextMonth,
createUserWithSeatedEventAndAttendees,
Expand All @@ -29,75 +27,8 @@ test.describe("Booking with Seats", () => {
await expect(page.locator(`text=Event type updated successfully`)).toBeVisible();
});

test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
const slug = "my-2-seated-event";
const user = await users.create({
name: "Seated event user",
eventTypes: [
{
title: "My 2-seated event",
slug,
length: 60,
seatsPerTimeSlot: 2,
seatsShowAttendees: true,
},
],
});
await page.goto(`/${user.username}/${slug}`);

let bookingUrl = "";

await test.step("Attendee #1 can book a seated event time slot", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
await test.step("Attendee #2 can book the same seated event time slot", async () => {
await page.goto(`/${user.username}/${slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);

await page.waitForURL(/bookingUid/);
bookingUrl = page.url();
await bookTimeSlot(page, { email: "[email protected]", name: "Jane Doe" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
await test.step("Attendee #3 cannot click on the same seated event time slot", async () => {
await page.goto(`/${user.username}/${slug}`);

await page.click('[data-testid="incrementMonth"]');

// TODO: Find out why the first day is always booked on tests
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await expect(page.locator('[data-testid="time"][data-disabled="true"]')).toBeVisible();
});
await test.step("Attendee #3 cannot book the same seated event time slot accessing via url", async () => {
await page.goto(bookingUrl);

await bookTimeSlot(page, { email: "[email protected]", name: "Rick" });
await expect(page.locator("[data-testid=success-page]")).toBeHidden();
});

await test.step("User owner should have only 1 booking with 3 attendees", async () => {
// Make sure user owner has only 1 booking with 3 attendees
const bookings = await prisma.booking.findMany({
where: { eventTypeId: user.eventTypes.find((e) => e.slug === slug)?.id },
select: {
id: true,
attendees: {
select: {
id: true,
},
},
},
});

expect(bookings).toHaveLength(1);
expect(bookings[0].attendees).toHaveLength(2);
});
});

test(`Attendees can cancel a seated event time slot`, async ({ page, users, bookings }) => {
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
test(`Prevent attendees from cancel when having invalid URL params`, async ({ page, users, bookings }) => {
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "[email protected]", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "[email protected]", timeZone: "Europe/Berlin" },
{ name: "John Third", email: "[email protected]", timeZone: "Europe/Berlin" },
Expand All @@ -120,30 +51,6 @@ test.describe("Booking with Seats", () => {
data: bookingSeats,
});

await test.step("Attendee #1 should be able to cancel their booking", async () => {
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${bookingSeats[0].referenceUid}`);

await page.locator('[data-testid="cancel"]').click();
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
await page.locator('[data-testid="confirm_cancel"]').click();
await page.waitForLoadState("networkidle");

await expect(page).toHaveURL(/\/booking\/.*/);

const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
await expect(cancelledHeadline).toBeVisible();

// Old booking should still exist, with one less attendee
const updatedBooking = await prisma.booking.findFirst({
where: { id: bookingSeats[0].bookingId },
include: { attendees: true },
});

const attendeeIds = updatedBooking?.attendees.map(({ id }) => id);
expect(attendeeIds).toHaveLength(2);
expect(attendeeIds).not.toContain(bookingAttendees[0].id);
});

await test.step("Attendee #2 shouldn't be able to cancel booking using only booking/uid", async () => {
await page.goto(`/booking/${booking.uid}`);

Expand All @@ -156,29 +63,6 @@ test.describe("Booking with Seats", () => {
// expect cancel button to don't be in the page
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
});

await test.step("All attendees cancelling should delete the booking for the user", async () => {
// The remaining 2 attendees cancel
for (let i = 1; i < bookingSeats.length; i++) {
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${bookingSeats[i].referenceUid}`);

await page.locator('[data-testid="cancel"]').click();
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
await page.locator('[data-testid="confirm_cancel"]').click();

await expect(page).toHaveURL(/\/booking\/.*/);

const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
await expect(cancelledHeadline).toBeVisible();
}

// Should expect old booking to be cancelled
const updatedBooking = await prisma.booking.findFirst({
where: { id: bookingSeats[0].bookingId },
});
expect(updatedBooking).not.toBeNull();
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
});
});

test("Owner shouldn't be able to cancel booking without login in", async ({ page, bookings, users }) => {
Expand Down Expand Up @@ -224,181 +108,6 @@ test.describe("Booking with Seats", () => {
});

test.describe("Reschedule for booking with seats", () => {
test("Should reschedule booking with seats", async ({ page, users, bookings }) => {
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: `first+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: `second+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
{ name: "John Third", email: `third+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },
]);
const bookingAttendees = await prisma.attendee.findMany({
where: { bookingId: booking.id },
select: {
id: true,
email: true,
},
});

const bookingSeats = [
{ bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() },
{ bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() },
{ bookingId: booking.id, attendeeId: bookingAttendees[2].id, referenceUid: uuidv4() },
];

await prisma.bookingSeat.createMany({
data: bookingSeats,
});

const references = await prisma.bookingSeat.findMany({
where: { bookingId: booking.id },
});

await page.goto(`/reschedule/${references[2].referenceUid}`);

await selectFirstAvailableTimeSlotNextMonth(page);

// expect input to be filled with attendee number 3 data
const thirdAttendeeElement = await page.locator("input[name=name]");
const attendeeName = await thirdAttendeeElement.inputValue();
expect(attendeeName).toBe("John Third");

await page.locator('[data-testid="confirm-reschedule-button"]').click();

// should wait for URL but that path starts with booking/
await page.waitForURL(/\/booking\/.*/);

await expect(page).toHaveURL(/\/booking\/.*/);

// Should expect new booking to be created for John Third
const newBooking = await prisma.booking.findFirst({
where: {
attendees: {
some: { email: bookingAttendees[2].email },
},
},
include: { seatsReferences: true, attendees: true },
});
expect(newBooking?.status).toBe(BookingStatus.PENDING);
expect(newBooking?.attendees.length).toBe(1);
expect(newBooking?.attendees[0].name).toBe("John Third");
expect(newBooking?.seatsReferences.length).toBe(1);

// Should expect old booking to be accepted with two attendees
const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
include: { seatsReferences: true, attendees: true },
});

expect(oldBooking?.status).toBe(BookingStatus.ACCEPTED);
expect(oldBooking?.attendees.length).toBe(2);
expect(oldBooking?.seatsReferences.length).toBe(2);
});

test("Should reschedule booking with seats and if everyone rescheduled it should be deleted", async ({
page,
users,
bookings,
}) => {
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "[email protected]", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "[email protected]", timeZone: "Europe/Berlin" },
]);

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

const references = await prisma.bookingSeat.findMany({
where: { bookingId: booking.id },
});

await page.goto(`/reschedule/${references[0].referenceUid}`);

await selectFirstAvailableTimeSlotNextMonth(page);

await page.locator('[data-testid="confirm-reschedule-button"]').click();

await page.waitForURL(/\/booking\/.*/);

await page.goto(`/reschedule/${references[1].referenceUid}`);

await selectFirstAvailableTimeSlotNextMonth(page);

await page.locator('[data-testid="confirm-reschedule-button"]').click();

// Using waitForUrl here fails the assertion `expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);` probably because waitForUrl is considered complete before waitForNavigation and till that time the booking is not cancelled
await page.waitForURL(/\/booking\/.*/);

// Should expect old booking to be cancelled
const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
include: {
seatsReferences: true,
attendees: true,
eventType: {
include: { users: true, hosts: true },
},
},
});

expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
});

test("Should cancel with seats and have no attendees and cancelled", async ({ page, users, bookings }) => {
const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "[email protected]", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "[email protected]", timeZone: "Europe/Berlin" },
]);
await user.apiLogin();

const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
include: { seatsReferences: true, attendees: true },
});

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 organizer
await page.goto(`/booking/${booking.uid}?cancel=true`);

await page.locator('[data-testid="confirm_cancel"]').click();

await expect(page).toHaveURL(/\/booking\/.*/);

// Should expect old booking to be cancelled
const updatedBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
include: { seatsReferences: true, attendees: true },
});

expect(oldBooking?.startTime).not.toBe(updatedBooking?.startTime);
});

test("If rescheduled/cancelled booking with seats it should display the correct number of seats", async ({
page,
users,
Expand Down Expand Up @@ -457,7 +166,7 @@ test.describe("Reschedule for booking with seats", () => {
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 ({
test("Should cancel with seats but event should be still accessible and with one less attendee/seat", async ({
page,
users,
bookings,
Expand Down
5 changes: 5 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,11 @@
"uprade_to_create_instant_bookings": "Upgrade to Enterprise and let guests join an instant call that attendees can jump straight into. This is only for team event types",
"dont_want_to_wait": "Don't want to wait?",
"meeting_started": "Meeting Started",
"booking_not_found_error": "Could not find booking",
"booking_seats_full_error": "Booking seats are full",
"missing_payment_credential_error": "Missing payment credentials",
"missing_payment_app_id_error": "Missing payment app id",
"not_enough_available_seats_error": "Booking does not have enough available seats",
"user_redirect_title": "{{username}} is currently away for a brief period of time.",
"user_redirect_description": "In the meantime, {{profile.username}} will be in charge of all the new scheduled meetings on behalf of {{username}}.",
"out_of_office": "Out of office",
Expand Down
Loading

0 comments on commit a0f1ceb

Please sign in to comment.