fix: align booking limit timezone between availability and validation (#24846)

* 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 <adhabal2002@gmail.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 <adhabal2002@gmail.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 <adhabal2002@gmail.com>

* address review

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Anik Dhabal Babu
2025-11-04 12:13:42 +05:30
committed by GitHub
co-authored by anik@cal.com Devin AI
parent f1db27c0ad
commit 896f8dbd3d
2 changed files with 101 additions and 2 deletions
+97
View File
@@ -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 });
@@ -823,6 +823,7 @@ export class AvailableSlotsService {
let busyTimesFromLimitsMap: Map<number, EventBusyDetails[]> | 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<number, EventBusyDetails[]> | 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
);
}