Skip to content
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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

测试用例中新增了对不同时区(+6:00)的 getSchedule 调用,但注释说明‘it should return the same as this is the utc time’存在误导性。实际逻辑中,getSchedule 的行为可能受时区影响,若未正确处理 UTC 时间转换,可能导致返回结果不一致。该注释暗示行为应相同,但未验证函数是否真正忽略时区差异,存在潜在逻辑错误风险。

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 788

💬 详细说明:

  • 测试用例中新增了对不同时区(+6:00)的 getSchedule 调用,但注释说明‘it should return the same as this is the utc time’存在误导性。实际逻辑中,getSchedule 的行为可能受时区影响,若未正确处理 UTC 时间转换,可能导致返回结果不一致。该注释暗示行为应相同,但未验证函数是否真正忽略时区差异,存在潜在逻辑错误风险。

📝 问题代码:

const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule(

💡 修复建议:

移除误导性注释,或在代码中明确确保 getSchedule 在输入为 UTC 时间且 timeZone 不同时仍能正确处理时间边界。建议添加断言验证时区转换后的结果一致性,或重构逻辑以保证跨时区调用行为一致。

✅ 修复示例:



🔗 参考链接

{
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 +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 AI 代码审查发现问题

📋 问题概述

发现 2 个邻近问题(第 219–222 行)

📍 问题详情

🔴 问题 1 | 严重程度: HIGH | 行号: 219

💬 详细说明:

  • 每次处理覆盖时间时都会重复执行相同的日期转换操作,增加不必要的计算开销,特别是在大量覆盖数据情况下会影响性能;若 offset 超出合理范围,可能引发时间计算异常,导致系统行为不可控。

📝 问题代码:

dayjs(override.start).utc().add(offset, "minute").hour() * 60 + dayjs(override.start).utc().add(offset, "minute").minute()

💡 修复建议:

提取公共表达式,避免重复计算;同时添加对 offset 的有效性校验,确保其在合理范围内(例如 ±1440 分钟)。建议缓存 dayjs(override.start).utc().add(offset, "minute") 的结果,然后分别获取小时和分钟。

✅ 修复示例:

const adjustedStart = dayjs(override.start).utc().add(offset, "minute");
      return {
        userIds: override.userId ? [override.userId] : [],
        startTime: adjustedStart.hour() * 60 + adjustedStart.minute(),
        endTime: dayjs(override.end).utc().add(offset, "minute").hour() * 60 + dayjs(override.end).utc().add(offset, "minute").minute(),
      };
🔴 问题 2 | 严重程度: HIGH | 行号: 222

💬 详细说明:

  • 每次处理覆盖时间时都会重复执行相同的日期转换操作,增加不必要的计算开销,特别是在大量覆盖数据情况下会影响性能;同时存在因 offset 超出合理范围导致时间计算错误的风险。

📝 问题代码:

dayjs(override.end).utc().add(offset, "minute").hour() * 60 + dayjs(override.end).utc().add(offset, "minute").minute()

💡 修复建议:

提取公共表达式,避免重复计算;同时添加对 offset 的有效性校验,确保其在合理范围内(例如 ±1440 分钟)。

✅ 修复示例:

const adjustedEnd = dayjs(override.end).utc().add(offset, "minute");
      return {
        userIds: override.userId ? [override.userId] : [],
        startTime: adjustedStart.hour() * 60 + adjustedStart.minute(),
        endTime: adjustedEnd.hour() * 60 + adjustedEnd.minute(),
      };

🔗 参考链接

dayjs(override.end).utc().add(offset, "minute").minute(),
};
});

// 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 = [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

发现 2 个邻近问题(第 79–80 行)

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 79

💬 详细说明:

  • 函数 checkIfIsAvailable 的参数 dateOverrides 被默认初始化为空数组,但其类型定义为可选(?),且在调用方传入时未明确处理空值。这可能导致后续逻辑误判:当实际无日期覆盖时,仍会进入 dateOverrides.find 判断分支,造成不必要的计算和潜在逻辑错误。

📝 问题代码:

dateOverrides = []

💡 修复建议:

将 dateOverrides 的默认值改为 null,并在函数内部显式判断是否为 null 或空数组,避免对空数组的无效遍历。同时更新函数签名以反映该参数可能为 null。

✅ 修复示例:

dateOverrides = null
🟡 问题 2 | 严重程度: MEDIUM | 行号: 80

💬 详细说明:

  • workingHours 参数被默认初始化为空数组,但其类型为 WorkingHours[],且在函数中用于判断工作时间范围。若用户未设置任何工作时间,空数组会导致后续 find 检查始终返回 false,从而错误地认为所有时间段都处于非工作时间,导致可用性判断失效。

📝 问题代码:

workingHours = []

💡 修复建议:

将 workingHours 默认值设为 null,并在函数内增加对 null/undefined 的检查,确保不因空数组而误判。同时更新函数签名以体现该参数可为 null。

✅ 修复示例:

workingHours = null

🔗 参考链接

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;
}

//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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

在 workingHours 检查逻辑中,start 和 end 变量被设置为相同的值(都基于 slotStartTime),这会导致工作时间检查逻辑错误

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 142

💬 详细说明:

  • 工作时间检查逻辑错误,可能导致错误的时间槽可用性判断,影响日程安排功能的准确性

📝 问题代码:

const start = slotStartTime.hour() * 60 + slotStartTime.minute();
        const end = slotStartTime.hour() * 60 + slotStartTime.minute();

💡 修复建议:

正确设置 start 和 end 时间,start 应基于 slotStartTime,end 应基于 slotEndTime

✅ 修复示例:

const start = slotStartTime.hour() * 60 + slotStartTime.minute();
        const end = slotEndTime.hour() * 60 + slotEndTime.minute();

🔗 参考链接

if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 AI 代码审查发现问题

📋 问题概述

在 getTimeSlots 调用中传递 organizerTimeZone,但该参数未经过校验或默认值处理。若输入的 timeZone 为非法值(如 'invalid/timezone'),可能导致 dayjs.tz() 内部异常或返回错误时间,进而影响整个日程生成逻辑,存在潜在崩溃风险。

📍 问题详情

🔴 问题 1 | 严重程度: HIGH | 行号: 456

💬 详细说明:

  • 在 getTimeSlots 调用中传递 organizerTimeZone,但该参数未经过校验或默认值处理。若输入的 timeZone 为非法值(如 'invalid/timezone'),可能导致 dayjs.tz() 内部异常或返回错误时间,进而影响整个日程生成逻辑,存在潜在崩溃风险。

📝 问题代码:

organizerTimeZone

💡 修复建议:

在调用 getTimeSlots 前,对 organizerTimeZone 进行有效性验证,若无效则使用默认值(如 'UTC')或抛出错误,防止不可控的时间解析行为。

✅ 修复示例:

organizerTimeZone: organizerTimeZone && dayjs.tz.isValid(organizerTimeZone) ? organizerTimeZone : 'UTC'

🔗 参考链接

})
);
}
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

在 checkIfIsAvailable 中,organizerTimeZone 被重复从 schedule.timeZone 传入,但该值已在上层调用中通过 organizerTimeZone 参数统一传递。此重复赋值冗余且易引发维护歧义,若未来修改调度逻辑,可能造成不一致。

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 492

💬 详细说明:

  • 在 checkIfIsAvailable 中,organizerTimeZone 被重复从 schedule.timeZone 传入,但该值已在上层调用中通过 organizerTimeZone 参数统一传递。此重复赋值冗余且易引发维护歧义,若未来修改调度逻辑,可能造成不一致。

📝 问题代码:

organizerTimeZone: schedule.timeZone

💡 修复建议:

移除该行中的 organizerTimeZone 重传,直接使用外部传入的 organizerTimeZone 参数,保持上下文一致性。

✅ 修复示例:

organizerTimeZone

🔗 参考链接

});
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

在 looseHostAvailability 处理逻辑中,organizerTimeZone 从 userSchedule.timeZone 重新传入,但该值与外部传入的 organizerTimeZone 语义相同。重复赋值造成代码冗余,且若两者不一致,将破坏时间一致性,引入潜在 bug。

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 517

💬 详细说明:

  • 在 looseHostAvailability 处理逻辑中,organizerTimeZone 从 userSchedule.timeZone 重新传入,但该值与外部传入的 organizerTimeZone 语义相同。重复赋值造成代码冗余,且若两者不一致,将破坏时间一致性,引入潜在 bug。

📝 问题代码:

organizerTimeZone: userSchedule.timeZone

💡 修复建议:

统一使用外部传入的 organizerTimeZone,移除此处的重复赋值,确保时间上下文一致。

✅ 修复示例:

organizerTimeZone

🔗 参考链接

});
});
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

在 selectedSlots 处理逻辑中,organizerTimeZone 从 userSchedule.timeZone 传入,但该值应与全局 organizerTimeZone 保持一致。若 userSchedule.timeZone 为空或不一致,将导致时间偏移错误,影响可用性判断。

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 585

💬 详细说明:

  • 在 selectedSlots 处理逻辑中,organizerTimeZone 从 userSchedule.timeZone 传入,但该值应与全局 organizerTimeZone 保持一致。若 userSchedule.timeZone 为空或不一致,将导致时间偏移错误,影响可用性判断。

📝 问题代码:

organizerTimeZone: userSchedule?.timeZone

💡 修复建议:

统一使用外部传入的 organizerTimeZone,避免局部变量干扰,确保时间逻辑一致性。

✅ 修复示例:

organizerTimeZone

🔗 参考链接

});
});
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