From 896f8dbd3dc4f5df049621b3450b030b942d57f4 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:13:42 +0530 Subject: [PATCH] fix: align booking limit timezone between availability and validation (#24846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: align booking limit timezone between availability and validation - Use eventType.schedule?.timeZone for booking limits in availability calculation - Previously used user's timezone causing day boundary mismatch - Add unit tests to verify timezone alignment - Fixes issue where slots remain available after reaching booking limit Co-Authored-By: anik@cal.com * Rename variable limitsTz to eventTimeZone * update * fix: correct timezone fallback to include user timezone without forcing UTC - Changed fallback chain to: schedule → event → user → undefined - Treats empty strings as missing timezones - Does not force UTC when all timezones are missing - Aligns with validation behavior when timezone is undefined - Fixes getSchedule.test.ts failures Co-Authored-By: anik@cal.com * Revert "fix: correct timezone fallback to include user timezone without forcing UTC" This reverts commit 721e0bdfd7b08004e68df6bdf5b97b9bd6cf3d5c. * test: remove complex unit tests with 'as any' casts - Removed Booking Limits Timezone Alignment tests that required heavy mocking - These tests relied on spying private methods and had many 'as any' casts - Timezone alignment is already covered by integration tests in getSchedule.test.ts - Kept BookingDateInPastError test which has no type issues Co-Authored-By: anik@cal.com * address review --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/test/lib/getSchedule.test.ts | 97 +++++++++++++++++++ .../trpc/server/routers/viewer/slots/util.ts | 6 +- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 9229c8bbe5..d6c8ebcb7f 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -2735,6 +2735,103 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit }); + test("booking limits use schedule timezone when set, falling back to user timezone", 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, + }, + schedule: { + id: 1, + name: "Schedule with timezone", + 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"], + }, + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [ + { + id: 2, + name: "User schedule", + 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["+6:00"], + }, + ], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + startTime: `${plus2DateString}T08:00:00.000Z`, + endTime: `${plus2DateString}T09:00:00.000Z`, + status: "ACCEPTED" as BookingStatus, + }, + ], + }; + + await createBookingScenario(scenarioData); + + const availability = await availableSlotsService.getAvailableSlots({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T00:00:00.000Z`, + endTime: `${plus3DateString}T23:59:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + orgSlug: null, + }, + }); + + const availableSlotsInScheduleTz: dayjs.Dayjs[] = []; + for (const date in availability.slots) { + availability.slots[date].forEach((timeObj) => { + availableSlotsInScheduleTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"])); + }); + } + + expect(availableSlotsInScheduleTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe( + 0 + ); + }); + test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index f77035a9c0..8d688bdc83 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -823,6 +823,7 @@ export class AvailableSlotsService { let busyTimesFromLimitsMap: Map | undefined = undefined; if (eventType && (bookingLimits || durationLimits)) { const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + const eventTimeZone = eventType.schedule?.timeZone ?? usersWithCredentials[0]?.timeZone ?? "UTC"; busyTimesFromLimitsMap = await this.getBusyTimesFromLimitsForUsers( usersForLimits, bookingLimits, @@ -831,7 +832,7 @@ export class AvailableSlotsService { endTime, typeof input.duration === "number" ? input.duration : undefined, eventType, - usersWithCredentials[0]?.timeZone || "UTC", + eventTimeZone, input.rescheduleUid || undefined ); } @@ -845,6 +846,7 @@ export class AvailableSlotsService { let teamBookingLimitsMap: Map | undefined = undefined; if (teamForBookingLimits && teamBookingLimits) { const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + const eventTimeZone = eventType.schedule?.timeZone ?? usersWithCredentials[0]?.timeZone ?? "UTC"; teamBookingLimitsMap = await this.getBusyTimesFromTeamLimitsForUsers( usersForTeamLimits, teamBookingLimits, @@ -852,7 +854,7 @@ export class AvailableSlotsService { endTime, teamForBookingLimits.id, teamForBookingLimits.includeManagedEventsInLimits, - usersWithCredentials[0]?.timeZone || "UTC", + eventTimeZone, input.rescheduleUid || undefined ); }