From 7bb42ba57705c2698549da00080bd1add3c0b50e Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:17:05 -0500 Subject: [PATCH] fix: booking limits (#12172) Co-authored-by: CarinaWolli --- apps/web/test/lib/getSchedule.test.ts | 119 ++++++++++++++++++ .../utils/bookingScenario/bookingScenario.ts | 4 + .../features/bookings/lib/handleNewBooking.ts | 12 +- packages/lib/server/checkBookingLimits.ts | 13 +- packages/prisma/zod-utils.ts | 6 +- 5 files changed, 145 insertions(+), 9 deletions(-) diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 77fd0bcb32..e74d338686 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -4,6 +4,7 @@ import prismock from "../../../../tests/libs/__mocks__/prisma"; import { diff } from "jest-diff"; import { describe, expect, vi, beforeEach, afterEach, test } from "vitest"; +import dayjs from "@calcom/dayjs"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; @@ -874,6 +875,124 @@ describe("getSchedule", () => { } ); }); + test("test that booking limit is working correctly if user is all day available", async () => { + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); + const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + beforeEventBuffer: 0, + afterEventBuffer: 0, + bookingLimits: { + PER_DAY: 1, + }, + users: [ + { + id: 101, + }, + ], + }, + { + id: 2, + length: 60, + beforeEventBuffer: 0, + afterEventBuffer: 0, + bookingLimits: { + PER_DAY: 2, + }, + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [ + { + id: 1, + name: "All Day available", + availability: [ + { + userId: null, + eventTypeId: null, + days: [0, 1, 2, 3, 4, 5, 6], + startTime: new Date("1970-01-01T00:00:00.000Z"), + endTime: new Date("1970-01-01T23:59:59.999Z"), + date: null, + }, + ], + timeZone: Timezones["+5:30"], + }, + ], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + startTime: `${plus2DateString}T08:30:00.000Z`, + endTime: `${plus2DateString}T08:29:59.999Z`, + status: "ACCEPTED" as BookingStatus, + }, + { + userId: 101, + eventTypeId: 2, + startTime: `${plus2DateString}T08:30:00.000Z`, + endTime: `${plus2DateString}T08:29:59.999Z`, + status: "ACCEPTED" as BookingStatus, + }, + ], + }; + + await createBookingScenario(scenarioData); + + const thisUserAvailabilityBookingLimitOne = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T00:00:00.000Z`, + endTime: `${plus3DateString}T23:59:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + const thisUserAvailabilityBookingLimitTwo = await getSchedule({ + input: { + eventTypeId: 2, + eventTypeSlug: "", + startTime: `${plus1DateString}T00:00:00.000Z`, + endTime: `${plus3DateString}T23:59:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + let availableSlotsInTz: dayjs.Dayjs[] = []; + for (const date in thisUserAvailabilityBookingLimitOne.slots) { + thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => { + availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"])); + }); + } + + expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit + + availableSlotsInTz = []; + for (const date in thisUserAvailabilityBookingLimitTwo.slots) { + thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => { + availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"])); + }); + } + expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that + }); }); describe("Team Event", () => { diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 2420851ae8..0a3809721c 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -101,6 +101,9 @@ export type InputEventType = { requiresConfirmation?: boolean; destinationCalendar?: Prisma.DestinationCalendarCreateInput; schedule?: InputUser["schedules"][number]; + bookingLimits?: { + PER_DAY?: number; + }; } & Partial>; type WhiteListedBookingProps = { @@ -199,6 +202,7 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser timeZone: null, beforeEventBuffer: 0, afterEventBuffer: 0, + bookingLimits: {}, schedulingType: null, length: 15, //TODO: What is the purpose of periodStartDate and periodEndDate? Test these? diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0684e771d9..1021ebfbfd 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -867,7 +867,12 @@ async function handler( ) { const startAsDate = dayjs(reqBody.start).toDate(); if (eventType.bookingLimits) { - await checkBookingLimits(eventType.bookingLimits as IntervalLimit, startAsDate, eventType.id); + await checkBookingLimits( + eventType.bookingLimits as IntervalLimit, + startAsDate, + eventType.id, + eventType.schedule?.timeZone + ); } if (eventType.durationLimits) { await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id); @@ -915,8 +920,8 @@ async function handler( }), }, { - dateFrom: reqBody.start, - dateTo: reqBody.end, + dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(), + dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(), timeZone: reqBody.timeZone, originalRescheduledBooking, }, @@ -983,7 +988,6 @@ async function handler( const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone; const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common"); - // use host default if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) { const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); diff --git a/packages/lib/server/checkBookingLimits.ts b/packages/lib/server/checkBookingLimits.ts index 0778ad92b2..91c581d146 100644 --- a/packages/lib/server/checkBookingLimits.ts +++ b/packages/lib/server/checkBookingLimits.ts @@ -11,14 +11,15 @@ import { parseBookingLimit } from "../isBookingLimits"; export async function checkBookingLimits( bookingLimits: IntervalLimit, eventStartDate: Date, - eventId: number + eventId: number, + timeZone?: string | null ) { const parsedBookingLimits = parseBookingLimit(bookingLimits); if (!parsedBookingLimits) return false; // not iterating entries to preserve types const limitCalculations = ascendingLimitKeys.map((key) => - checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId }) + checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId, timeZone }) ); try { @@ -33,19 +34,23 @@ export async function checkBookingLimit({ eventId, key, limitingNumber, + timeZone, }: { eventStartDate: Date; eventId: number; key: keyof IntervalLimit; limitingNumber: number | undefined; + timeZone?: string | null; }) { { + const eventDateInOrganizerTz = timeZone ? dayjs(eventStartDate).tz(timeZone) : dayjs(eventStartDate); + if (!limitingNumber) return; const unit = intervalLimitKeyToUnit(key); - const startDate = dayjs(eventStartDate).startOf(unit).toDate(); - const endDate = dayjs(eventStartDate).endOf(unit).toDate(); + const startDate = dayjs(eventDateInOrganizerTz).startOf(unit).toDate(); + const endDate = dayjs(eventDateInOrganizerTz).endOf(unit).toDate(); const bookingsInPeriod = await prisma.booking.count({ where: { diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index a1aebdbeb4..dba23604b7 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -205,7 +205,11 @@ export const stringOrNumber = z.union([ z.number().int(), ]); -export const stringToDayjs = z.string().transform((val) => dayjs(val)); +export const stringToDayjs = z.string().transform((val) => { + const matches = val.match(/([+-]\d{2}:\d{2})$/); + const timezone = matches ? matches[1] : "+00:00"; + return dayjs(val).utcOffset(timezone); +}); export const bookingCreateBodySchema = z.object({ end: z.string().optional(),