Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/web/test/lib/getSchedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,24 @@ describe("getSchedule", () => {
dateString: plus2DateString,
}
);

const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+6:00"],
},
ctx
);
// it should return the same as this is the utc time
expect(scheduleForEventOnADayWithDateOverrideDifferentTimezone).toHaveTimeSlots(
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
{
dateString: plus2DateString,
}
);
});

test("that a user is considered busy when there's a booking they host", async () => {
Expand Down
21 changes: 16 additions & 5 deletions packages/lib/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,22 @@ const getSlots = ({
});

if (!!activeOverrides.length) {
const overrides = activeOverrides.flatMap((override) => ({
userIds: override.userId ? [override.userId] : [],
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
const overrides = activeOverrides.flatMap((override) => {
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
const offset = inviteeUtcOffset - organizerUtcOffset;

return {
userIds: override.userId ? [override.userId] : [],
startTime:
dayjs(override.start).utc().add(offset, "minute").hour() * 60 +
dayjs(override.start).utc().add(offset, "minute").minute(),
endTime:
dayjs(override.end).utc().add(offset, "minute").hour() * 60 +
dayjs(override.end).utc().add(offset, "minute").minute(),
};
});
Comment on lines +211 to +225

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for dateOverrides and where they are populated/created
rg -n "dateOverrides|activeOverrides" packages/lib/slots.ts -B 3 -A 3 | head -150

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1123


🏁 Script executed:

#!/bin/bash
# Search for override type definition or interface
rg -n "override.*timeZone|interface.*override|type.*override" packages/lib/ -i | head -50

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 440


🏁 Script executed:

#!/bin/bash
# Find where activeOverrides is assigned in slots.ts
rg -n "activeOverrides\s*=" packages/lib/slots.ts -B 2 -A 2

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 365


🏁 Script executed:

#!/bin/bash
# Search for where dateOverrides or override objects are created
rg -n "dateOverrides\s*=|new.*Override|Override\s*\{" packages/lib/ -A 5 | head -100

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 450


🏁 Script executed:

#!/bin/bash
# Find and examine the TimeRange type definition
find . -path "*/types/schedule*" -type f | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Search for TimeRange type definition
rg -n "type TimeRange|interface TimeRange" --type ts | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 202


🏁 Script executed:

#!/bin/bash
# Read the TimeRange type definition from both files
echo "=== packages/types/schedule.d.ts ===" && cat -n packages/types/schedule.d.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 820


🏁 Script executed:

#!/bin/bash
# Also check the other schedule.ts file
echo "=== apps/web/lib/types/schedule.ts ===" && cat -n apps/web/lib/types/schedule.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 728


🏁 Script executed:

#!/bin/bash
# Check which @calcom package/alias is being used in the project
find . -name "tsconfig.json" -o -name "tsconfig.*.json" | xargs grep -l "calcom" | head -5

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Check the actual import source for @calcom/types
cat -n tsconfig.json 2>/dev/null || cat -n apps/web/tsconfig.json 2>/dev/null || cat -n packages/tsconfig.json 2>/dev/null | head -50

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 854


🏁 Script executed:

#!/bin/bash
# Look for path mappings or package.json setup
rg -n '"@calcom' tsconfig.json -A 1 | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 140


🏁 Script executed:

#!/bin/bash
# Let's verify what happens when dayjs().tz() is called with undefined
# Create a test to check dayjs behavior
node << 'EOF'
const dayjs = require('dayjs');
require('dayjs/plugin/utc');
require('dayjs/plugin/timezone');

dayjs.extend(require('dayjs/plugin/utc'));
dayjs.extend(require('dayjs/plugin/timezone'));

console.log("Testing dayjs().tz(undefined):");
const result = dayjs('2024-01-15').tz(undefined);
console.log("Result:", result.toString());
console.log("UTC Offset:", result.utcOffset());

console.log("\nTesting dayjs().tz() with no argument:");
const result2 = dayjs('2024-01-15').tz();
console.log("Result:", result2.toString());
console.log("UTC Offset:", result2.utcOffset());
EOF

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 922


🏁 Script executed:

#!/bin/bash
# Check the exact usage context around line 212 - see if there's any validation before the filter
sed -n '200,230p' packages/lib/slots.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1382


🏁 Script executed:

#!/bin/bash
# Search for how timeZone property is used elsewhere in the codebase
rg -n "\.timeZone" packages/lib/slots.ts -B 2 -A 2

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 453


🏁 Script executed:

#!/bin/bash
# Check if there's any validation or fallback for timeZone in the codebase
rg -n "override\.timeZone|dateOverride.*timeZone" packages/lib/ -B 3 -A 3

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 617


🏁 Script executed:

#!/bin/bash
# Check how dayjs().tz() is used elsewhere in the codebase to understand expected behavior
rg -n "\.tz\(" packages/lib/slots.ts -B 1 -A 1 | head -30

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1001


Add validation or fallback for override.timeZone which is optional.

The timeZone property in TimeRange (the type for date overrides) is optional, so override.timeZone can be undefined. Passing undefined to dayjs().tz() on line 212 will not correctly handle the timezone conversion. Add a fallback to organizerTimeZone when timeZone is missing:

const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone || organizerTimeZone).utcOffset();
🤖 Prompt for AI Agents
In @packages/lib/slots.ts around lines 211 - 225, The code uses
override.timeZone which is optional and may be undefined when computing
organizerUtcOffset in the activeOverrides flatMap; update the computation to
fall back to organizerTimeZone when override.timeZone is missing (i.e., use
override.timeZone || organizerTimeZone) and apply the same fallback wherever
override.timeZone is passed into dayjs(...).tz() for calculating
inviteeUtcOffset/offset so timezone conversions are safe; ensure you reference
the activeOverrides flatMap, organizerUtcOffset, inviteeUtcOffset, and offset
calculations when making the change.


// unset all working hours that relate to this user availability override
overrides.forEach((override) => {
let i = -1;
Expand Down
81 changes: 76 additions & 5 deletions packages/trpc/server/routers/viewer/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type prisma from "@calcom/prisma";
import { availabilityUserSelect } from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { WorkingHours } from "@calcom/types/schedule";

import { TRPCError } from "@trpc/server";

Expand Down Expand Up @@ -75,12 +76,21 @@ const checkIfIsAvailable = ({
time,
busy,
eventLength,
dateOverrides = [],
workingHours = [],
currentSeats,
organizerTimeZone,
}: {
time: Dayjs;
busy: EventBusyDate[];
eventLength: number;
dateOverrides?: {
start: Date;
end: Date;
}[];
workingHours?: WorkingHours[];
currentSeats?: CurrentSeats;
organizerTimeZone?: string;
}): boolean => {
if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
return true;
Expand All @@ -89,6 +99,57 @@ const checkIfIsAvailable = ({
const slotEndTime = time.add(eventLength, "minutes").utc();
const slotStartTime = time.utc();

//check if date override for slot exists
let dateOverrideExist = false;

if (
dateOverrides.find((date) => {
const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;

if (
dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") ===
slotStartTime.format("YYYY MM DD")
) {
dateOverrideExist = true;
if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
return true;
}
if (
slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) ||
slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes"))
) {
return true;
}
if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) {
return true;
}
}
})
) {
// slot is not within the date override
return false;
}

if (dateOverrideExist) {
return true;
}
Comment on lines +102 to +135

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: Dayjs object comparison uses === instead of .isSame().

Line 114 compares two Dayjs objects using ===, which checks reference equality rather than value equality. This will always be false unless the objects are the same instance.

Additionally, the find() callback has inconsistent return behavior — it sometimes returns true and sometimes returns nothing (implicitly undefined), relying on side effects via dateOverrideExist. This makes the logic harder to follow and maintain.

🐛 Proposed fix for the Dayjs comparison
-        if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
+        if (dayjs(date.start).add(utcOffset, "minutes").isSame(dayjs(date.end).add(utcOffset, "minutes"))) {
           return true;
         }
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/slots.ts around lines 102 - 135, The
find() callback in the dateOverrides check miscompares Dayjs objects with ===
and mixes returns with side effects via dateOverrideExist; fix by replacing the
=== comparison with dayjs(...).isSame(...) for value equality, compute
adjustedStart and adjustedEnd once inside the dateOverrides.find callback (using
organizerTimeZone adjustments), and ensure the callback always returns a boolean
(return true when the slot is outside the override:
adjustedStart.isSame(adjustedEnd) OR slotEndTime.isSameOrBefore(adjustedStart)
OR slotStartTime.isAfter(adjustedEnd); otherwise return false). Remove or stop
relying on the side-effect variable dateOverrideExist—use the boolean result of
dateOverrides.find (or a dedicated some()/every() call) to decide the subsequent
returns instead of setting dateOverrideExist inside the callback.


//if no date override for slot exists check if it is within normal work hours
if (
workingHours.find((workingHour) => {
if (workingHour.days.includes(slotStartTime.day())) {
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotStartTime.hour() * 60 + slotStartTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}
Comment on lines +137 to +151

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: end variable incorrectly uses slotStartTime instead of slotEndTime.

Line 142 computes end using slotStartTime, making it identical to start. This defeats the purpose of checking if the slot's end time exceeds working hours. Slots that extend past workingHour.endTime will incorrectly be considered valid.

🐛 Proposed fix
   if (
     workingHours.find((workingHour) => {
       if (workingHour.days.includes(slotStartTime.day())) {
         const start = slotStartTime.hour() * 60 + slotStartTime.minute();
-        const end = slotStartTime.hour() * 60 + slotStartTime.minute();
+        const end = slotEndTime.hour() * 60 + slotEndTime.minute();
         if (start < workingHour.startTime || end > workingHour.endTime) {
           return true;
         }
       }
     })
   ) {
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/slots.ts around lines 137 - 151, The
callback that checks workingHours incorrectly computes end using slotStartTime,
making start and end identical; update the calculation of end to use slotEndTime
(e.g., compute end = slotEndTime.hour() * 60 + slotEndTime.minute()) inside the
workingHours.find callback and ensure the predicate returns a boolean (true when
start < workingHour.startTime || end > workingHour.endTime) so slots that extend
past workingHour.endTime are correctly flagged.


return busy.every((busyTime) => {
const startTime = dayjs.utc(busyTime.start).utc();
const endTime = dayjs.utc(busyTime.end);
Expand All @@ -115,7 +176,6 @@ const checkIfIsAvailable = ({
else if (startTime.isBetween(time, slotEndTime)) {
return false;
}

return true;
});
};
Expand Down Expand Up @@ -348,7 +408,11 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
);
// flattens availability of multiple users
const dateOverrides = userAvailability.flatMap((availability) =>
availability.dateOverrides.map((override) => ({ userId: availability.user.id, ...override }))
availability.dateOverrides.map((override) => ({
userId: availability.user.id,
timeZone: availability.timeZone,
...override,
}))
);
const workingHours = getAggregateWorkingHours(userAvailability, eventType.schedulingType);
const availabilityCheckProps = {
Expand All @@ -372,6 +436,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:

const timeSlots: ReturnType<typeof getTimeSlots> = [];

const organizerTimeZone =
eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone;

for (
let currentCheckedTime = startTime;
currentCheckedTime.isBefore(endTime);
Expand All @@ -386,8 +453,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
dateOverrides,
minimumBookingNotice: eventType.minimumBookingNotice,
frequency: eventType.slotInterval || input.duration || eventType.length,
organizerTimeZone:
eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone,
organizerTimeZone,
})
);
}
Expand Down Expand Up @@ -423,13 +489,15 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
time: slot.time,
...schedule,
...availabilityCheckProps,
organizerTimeZone: schedule.timeZone,
});
const endCheckForAvailability = performance.now();
checkForAvailabilityCount++;
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
return isAvailable;
});
});

// what else are you going to call it?
const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed);
if (looseHostAvailability.length > 0) {
Expand All @@ -446,6 +514,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
time: slot.time,
...userSchedule,
...availabilityCheckProps,
organizerTimeZone: userSchedule.timeZone,
});
});
return slot;
Expand Down Expand Up @@ -507,17 +576,19 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
return false;
}

const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId);

return checkIfIsAvailable({
time: slot.time,
busy,
...availabilityCheckProps,
organizerTimeZone: userSchedule?.timeZone,
});
});
return slot;
})
.filter((slot) => !!slot.userIds?.length);
}

availableTimeSlots = availableTimeSlots.filter((slot) => isTimeWithinBounds(slot.time));

const computedAvailableSlots = availableTimeSlots.reduce(
Expand Down
1 change: 1 addition & 0 deletions packages/types/schedule.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type TimeRange = {
userId?: number | null;
start: Date;
end: Date;
timeZone?: string;
};

export type Schedule = TimeRange[][];
Expand Down