Skip to content

Commit 896f8db

Browse files
fix: align booking limit timezone between availability and validation (calcom#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 721e0bd. * 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>
1 parent f1db27c commit 896f8db

2 files changed

Lines changed: 101 additions & 2 deletions

File tree

apps/web/test/lib/getSchedule.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2735,6 +2735,103 @@ describe("getSchedule", () => {
27352735
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
27362736
});
27372737

2738+
test("booking limits use schedule timezone when set, falling back to user timezone", async () => {
2739+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
2740+
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
2741+
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
2742+
2743+
const scenarioData = {
2744+
eventTypes: [
2745+
{
2746+
id: 1,
2747+
length: 60,
2748+
beforeEventBuffer: 0,
2749+
afterEventBuffer: 0,
2750+
bookingLimits: {
2751+
PER_DAY: 1,
2752+
},
2753+
schedule: {
2754+
id: 1,
2755+
name: "Schedule with timezone",
2756+
availability: [
2757+
{
2758+
userId: null,
2759+
eventTypeId: null,
2760+
days: [0, 1, 2, 3, 4, 5, 6],
2761+
startTime: new Date("1970-01-01T00:00:00.000Z"),
2762+
endTime: new Date("1970-01-01T23:59:59.999Z"),
2763+
date: null,
2764+
},
2765+
],
2766+
timeZone: Timezones["+5:30"],
2767+
},
2768+
users: [
2769+
{
2770+
id: 101,
2771+
},
2772+
],
2773+
},
2774+
],
2775+
users: [
2776+
{
2777+
...TestData.users.example,
2778+
id: 101,
2779+
schedules: [
2780+
{
2781+
id: 2,
2782+
name: "User schedule",
2783+
availability: [
2784+
{
2785+
userId: null,
2786+
eventTypeId: null,
2787+
days: [0, 1, 2, 3, 4, 5, 6],
2788+
startTime: new Date("1970-01-01T00:00:00.000Z"),
2789+
endTime: new Date("1970-01-01T23:59:59.999Z"),
2790+
date: null,
2791+
},
2792+
],
2793+
timeZone: Timezones["+6:00"],
2794+
},
2795+
],
2796+
},
2797+
],
2798+
bookings: [
2799+
{
2800+
userId: 101,
2801+
eventTypeId: 1,
2802+
startTime: `${plus2DateString}T08:00:00.000Z`,
2803+
endTime: `${plus2DateString}T09:00:00.000Z`,
2804+
status: "ACCEPTED" as BookingStatus,
2805+
},
2806+
],
2807+
};
2808+
2809+
await createBookingScenario(scenarioData);
2810+
2811+
const availability = await availableSlotsService.getAvailableSlots({
2812+
input: {
2813+
eventTypeId: 1,
2814+
eventTypeSlug: "",
2815+
startTime: `${plus1DateString}T00:00:00.000Z`,
2816+
endTime: `${plus3DateString}T23:59:59.999Z`,
2817+
timeZone: Timezones["+5:30"],
2818+
isTeamEvent: false,
2819+
orgSlug: null,
2820+
},
2821+
});
2822+
2823+
const availableSlotsInScheduleTz: dayjs.Dayjs[] = [];
2824+
for (const date in availability.slots) {
2825+
availability.slots[date].forEach((timeObj) => {
2826+
availableSlotsInScheduleTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
2827+
});
2828+
}
2829+
2830+
expect(availableSlotsInScheduleTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(
2831+
0
2832+
);
2833+
});
2834+
27382835
test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => {
27392836
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
27402837
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });

packages/trpc/server/routers/viewer/slots/util.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ export class AvailableSlotsService {
823823
let busyTimesFromLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
824824
if (eventType && (bookingLimits || durationLimits)) {
825825
const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email }));
826+
const eventTimeZone = eventType.schedule?.timeZone ?? usersWithCredentials[0]?.timeZone ?? "UTC";
826827
busyTimesFromLimitsMap = await this.getBusyTimesFromLimitsForUsers(
827828
usersForLimits,
828829
bookingLimits,
@@ -831,7 +832,7 @@ export class AvailableSlotsService {
831832
endTime,
832833
typeof input.duration === "number" ? input.duration : undefined,
833834
eventType,
834-
usersWithCredentials[0]?.timeZone || "UTC",
835+
eventTimeZone,
835836
input.rescheduleUid || undefined
836837
);
837838
}
@@ -845,14 +846,15 @@ export class AvailableSlotsService {
845846
let teamBookingLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
846847
if (teamForBookingLimits && teamBookingLimits) {
847848
const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email }));
849+
const eventTimeZone = eventType.schedule?.timeZone ?? usersWithCredentials[0]?.timeZone ?? "UTC";
848850
teamBookingLimitsMap = await this.getBusyTimesFromTeamLimitsForUsers(
849851
usersForTeamLimits,
850852
teamBookingLimits,
851853
startTime,
852854
endTime,
853855
teamForBookingLimits.id,
854856
teamForBookingLimits.includeManagedEventsInLimits,
855-
usersWithCredentials[0]?.timeZone || "UTC",
857+
eventTimeZone,
856858
input.rescheduleUid || undefined
857859
);
858860
}

0 commit comments

Comments
 (0)