diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts
index f448591a..f6c8f598 100644
--- a/apps/web/pages/api/book/event.ts
+++ b/apps/web/pages/api/book/event.ts
@@ -21,6 +21,7 @@ import { getErrorFromUnknown } from "@lib/errors";
import { getEventName } from "@lib/event";
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
+import { EventBusyDate } from "@lib/integrations/calendar/constants/types";
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
import logger from "@lib/logger";
@@ -408,27 +409,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
- type Booking = Prisma.PromiseReturnType;
- let booking: Booking | null = null;
- try {
- booking = await createBooking();
- evt.uid = booking.uid;
- } catch (_err) {
- const err = getErrorFromUnknown(_err);
- log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
- if (err.code === "P2002") {
- res.status(409).json({ message: "booking.conflict" });
- return;
- }
- res.status(500).end();
- return;
- }
-
let results: EventResult[] = [];
let referencesToCreate: PartialReference[] = [];
let user: User | null = null;
- /** Let's start cheking for availability */
+ /** Let's start checking for availability */
for (const currentUser of users) {
if (!currentUser) {
console.error(`currentUser not found`);
@@ -443,68 +428,94 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const credentials = currentUser.credentials;
+ const calendarBusyTimes: EventBusyDate[] = await prisma.booking
+ .findMany({
+ where: {
+ userId: currentUser.id,
+ eventTypeId: eventTypeId,
+ },
+ })
+ .then((bookings) => bookings.map((booking) => ({ end: booking.endTime, start: booking.startTime })));
+
if (credentials) {
- const calendarBusyTimes = await getBusyCalendarTimes(
- credentials,
- reqBody.start,
- reqBody.end,
- selectedCalendars
+ await getBusyCalendarTimes(credentials, reqBody.start, reqBody.end, selectedCalendars).then(
+ (busyTimes) => calendarBusyTimes.push(...busyTimes)
);
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
calendarBusyTimes.push(...videoBusyTimes);
- console.log("calendarBusyTimes==>>>", calendarBusyTimes);
-
- const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
- start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
- end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
- }));
-
- let isAvailableToBeBooked = true;
- try {
- isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
- } catch {
- log.debug({
- message: "Unable set isAvailableToBeBooked. Using true. ",
- });
- }
-
- if (!isAvailableToBeBooked) {
- const error = {
- errorCode: "BookingUserUnAvailable",
- message: `${currentUser.name} is unavailable at this time.`,
- };
-
- log.debug(`Booking ${currentUser.name} failed`, error);
- }
-
- let timeOutOfBounds = false;
-
- try {
- timeOutOfBounds = isOutOfBounds(reqBody.start, {
- periodType: eventType.periodType,
- periodDays: eventType.periodDays,
- periodEndDate: eventType.periodEndDate,
- periodStartDate: eventType.periodStartDate,
- periodCountCalendarDays: eventType.periodCountCalendarDays,
- timeZone: currentUser.timeZone,
- });
- } catch {
- log.debug({
- message: "Unable set timeOutOfBounds. Using false. ",
- });
- }
-
- if (timeOutOfBounds) {
- const error = {
- errorCode: "BookingUserUnAvailable",
- message: `${currentUser.name} is unavailable at this time.`,
- };
-
- log.debug(`Booking ${currentUser.name} failed`, error);
- res.status(400).json(error);
- }
}
+
+ console.log("calendarBusyTimes==>>>", calendarBusyTimes);
+
+ const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
+ start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
+ end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
+ }));
+
+ let isAvailableToBeBooked = true;
+ try {
+ isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
+ } catch {
+ log.debug({
+ message: "Unable set isAvailableToBeBooked. Using true. ",
+ });
+ }
+
+ if (!isAvailableToBeBooked) {
+ const error = {
+ errorCode: "BookingUserUnAvailable",
+ message: `${currentUser.name} is unavailable at this time.`,
+ };
+
+ log.debug(`Booking ${currentUser.name} failed`, error);
+ res.status(409).json(error);
+ return;
+ }
+
+ let timeOutOfBounds = false;
+
+ try {
+ timeOutOfBounds = isOutOfBounds(reqBody.start, {
+ periodType: eventType.periodType,
+ periodDays: eventType.periodDays,
+ periodEndDate: eventType.periodEndDate,
+ periodStartDate: eventType.periodStartDate,
+ periodCountCalendarDays: eventType.periodCountCalendarDays,
+ timeZone: currentUser.timeZone,
+ });
+ } catch {
+ log.debug({
+ message: "Unable set timeOutOfBounds. Using false. ",
+ });
+ }
+
+ if (timeOutOfBounds) {
+ const error = {
+ errorCode: "BookingUserUnAvailable",
+ message: `${currentUser.name} is unavailable at this time.`,
+ };
+
+ log.debug(`Booking ${currentUser.name} failed`, error);
+ res.status(400).json(error);
+ return;
+ }
+ }
+
+ type Booking = Prisma.PromiseReturnType;
+ let booking: Booking | null = null;
+ try {
+ booking = await createBooking();
+ evt.uid = booking.uid;
+ } catch (_err) {
+ const err = getErrorFromUnknown(_err);
+ log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
+ if (err.code === "P2002") {
+ res.status(409).json({ message: "booking.conflict" });
+ return;
+ }
+ res.status(500).end();
+ return;
}
if (!user) throw Error("Can't continue, user not found.");
diff --git a/apps/web/playwright/booking-pages.test.ts b/apps/web/playwright/booking-pages.test.ts
index 0e365ef6..f9efb754 100644
--- a/apps/web/playwright/booking-pages.test.ts
+++ b/apps/web/playwright/booking-pages.test.ts
@@ -1,16 +1,80 @@
import { expect, test } from "@playwright/test";
+import prisma from "@lib/prisma";
+
import { todo } from "./lib/testUtils";
+const deleteBookingsByEmail = async (email: string) =>
+ prisma.booking.deleteMany({
+ where: {
+ user: {
+ email,
+ },
+ },
+ });
+
test.describe("free user", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/free");
});
+
+ test.afterEach(async () => {
+ // delete test bookings
+ await deleteBookingsByEmail("free@example.com");
+ });
+
test("only one visible event", async ({ page }) => {
await expect(page.locator(`[href="/free/30min"]`)).toBeVisible();
await expect(page.locator(`[href="/free/60min"]`)).not.toBeVisible();
});
+ test("cannot book same slot multiple times", async ({ page }) => {
+ // Click first event type
+ await page.click('[data-testid="event-type-link"]');
+ // Click [data-testid="incrementMonth"]
+ await page.click('[data-testid="incrementMonth"]');
+ // Click [data-testid="day"]
+ await page.click('[data-testid="day"][data-disabled="false"]');
+ // Click [data-testid="time"]
+ await page.click('[data-testid="time"]');
+
+ // Navigate to book page
+ await page.waitForNavigation({
+ url(url) {
+ return url.pathname.endsWith("/book");
+ },
+ });
+
+ // save booking url
+ const bookingUrl: string = page.url();
+
+ const bookTimeSlot = async () => {
+ // --- fill form
+ await page.fill('[name="name"]', "Test Testson");
+ await page.fill('[name="email"]', "test@example.com");
+ await page.press('[name="email"]', "Enter");
+ };
+
+ // book same time spot twice
+ await bookTimeSlot();
+
+ // Make sure we're navigated to the success page
+ await page.waitForNavigation({
+ url(url) {
+ return url.pathname.endsWith("/success");
+ },
+ });
+
+ // return to same time spot booking page
+ await page.goto(bookingUrl);
+
+ // book same time spot again
+ await bookTimeSlot();
+
+ // check for error message
+ await expect(page.locator("[data-testid=booking-fail]")).toBeVisible();
+ });
+
todo("`/free/30min` is bookable");
todo("`/free/60min` is not bookable");
@@ -21,6 +85,11 @@ test.describe("pro user", () => {
await page.goto("/pro");
});
+ test.afterEach(async () => {
+ // delete test bookings
+ await deleteBookingsByEmail("pro@example.com");
+ });
+
test("pro user's page has at least 2 visible events", async ({ page }) => {
const $eventTypes = await page.$$("[data-testid=event-types] > *");
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
diff --git a/yarn.lock b/yarn.lock
index ab67685c..5f6d55bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1012,7 +1012,12 @@
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.3.tgz#f77c6bb5cb4a614a5d730fb880cab502d48abf37"
integrity sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==
-"@heroicons/react@^1.0.4", "@heroicons/react@^1.0.5":
+"@heroicons/react@^1.0.4":
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324"
+ integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==
+
+"@heroicons/react@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.5.tgz#2fe4df9d33eb6ce6d5178a0f862e97b61c01e27d"
integrity sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==
@@ -2590,9 +2595,9 @@
integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg==
"@stripe/stripe-js@^1.17.1":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.23.0.tgz#62eed14e83c63c3e8c27f14f6b1e6feb8496c867"
- integrity sha512-+7w4rVs71Fk8/8uzyzQB5GotHSH9mjOjxM3EYDq/3MR3I2ewELHtvWVMOqfS/9WSKCaKv7h7eFLsMZGpK5jApQ==
+ version "1.24.0"
+ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.24.0.tgz#d23977f364565981f8ab30b1b540e367f72abc5c"
+ integrity sha512-8CEILOpzoRhGwvgcf6y+BlPyEq1ZqxAv3gsX7LvokFYvbcyH72GRcHQMGXuZS3s7HqfYQuTSFrvZNL/qdkgA9Q==
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
@@ -5072,11 +5077,16 @@ dayjs-business-time@^1.0.4:
dependencies:
dayjs "^1.10.4"
-dayjs@^1.10.4, dayjs@^1.10.6, dayjs@^1.10.7:
+dayjs@^1.10.4, dayjs@^1.10.6:
version "1.10.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
+dayjs@^1.10.7:
+ version "1.10.8"
+ resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
+ integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
+
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -11520,9 +11530,9 @@ postcss@8.4.5:
source-map-js "^1.0.1"
postcss@^8.1.6, postcss@^8.3.5, postcss@^8.3.6:
- version "8.4.7"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.7.tgz#f99862069ec4541de386bf57f5660a6c7a0875a8"
- integrity sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==
+ version "8.4.8"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032"
+ integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==
dependencies:
nanoid "^3.3.1"
picocolors "^1.0.0"
@@ -15400,9 +15410,9 @@ zod@^3.8.2:
integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==
zod@^3.9.5:
- version "3.12.0"
- resolved "https://registry.yarnpkg.com/zod/-/zod-3.12.0.tgz#84ba9f6bdb7835e2483982d5f52cfffcb6a00346"
- integrity sha512-w+mmntgEL4hDDL5NLFdN6Fq2DSzxfmlSoJqiYE1/CApO8EkOCxvJvRYEVf8Vr/lRs3i6gqoiyFM6KRcWqqdBzQ==
+ version "3.13.4"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.13.4.tgz#5d6fe03ef4824a637d7ef50b5441cf6ab3acede0"
+ integrity sha512-LZRucWt4j/ru5azOkJxCfpR87IyFDn8h2UODdqvXzZLb3K7bb9chUrUIGTy3BPsr8XnbQYfQ5Md5Hu2OYIo1mg==
zwitch@^1.0.0:
version "1.0.5"