Skip to content

[benchmark][TypeScript] Advanced date override handling and timezone compatibility improvements - #10

Open
hsander wants to merge 1 commit into
benchmark/cal_dot_com/pr-8330-basefrom
benchmark/cal_dot_com/pr-8330
Open

hsander wants to merge 1 commit into
benchmark/cal_dot_com/pr-8330-basefrom
benchmark/cal_dot_com/pr-8330

Conversation

@hsander

@hsander hsander commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Benchmark fixture for cal_dot_com PR #8330.\n\nGolden comments: benchmark/golden_comments/cal_dot_com.json — PR 8330.

if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
return true;
const slotEndTime = time.add(eventLength, "minutes").utc();
const slotStartTime = time.utc();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Сравнение dayjs-объектов через === вместо .isSame() — проверка нулевого переопределения никогда не срабатывает

В slots.ts:30 два независимо созданных экземпляра dayjs сравниваются оператором ===, который проверяет ссылочную идентичность, а не временное равенство. Выражение dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes") всегда равно false, из-за чего ветка раннего возврата для переопределений нулевой длительности становится недостижимой. Следует использовать .isSame() с соответствующей единицей измерения или сравнивать Unix-таймстампы через .valueOf().

}
if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) {
return true;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Проверка конца рабочих часов использует slotStartTime вместо slotEndTime

В slots.ts:55 обе переменные start и end вычисляются из slotStartTime, из-за чего они идентичны. Проверка должна определять, начинается ли слот до начала рабочего дня ИЛИ заканчивается ли он после конца рабочего дня. Из-за ошибки слот, который начинается в рабочее время, но выходит за его конец, ошибочно проходит проверку и помечается как доступный. Переменная end должна вычисляться из slotEndTime.

...userSchedule,
...availabilityCheckProps,
organizerTimeZone: userSchedule.timeZone,
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Дублирование ошибки с slotStartTime вместо slotEndTime в новом guard'е рабочих часов

В slots.ts:137 в новом блоке проверки рабочих часов обе переменные start и end вычисляются из slotStartTime: const start = slotStartTime.hour() * 60 + slotStartTime.minute() и const end = slotStartTime.hour() * 60 + slotStartTime.minute(). Значение end должно вычисляться из slotEndTime (уже рассчитанного выше как time.add(eventLength, 'minutes').utc()). Это та же ошибка, что и в slots.ts:55, но в другом месте — вероятно, результат копирования кода.

eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone;

for (
let currentCheckedTime = startTime;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 Сравнение dayjs через === в slots.ts:110 — мёртвый код для нулевых переопределений

В slots.ts:110 проверка dayjs(date.start).add(utcOffset, 'minutes') === dayjs(date.end).add(utcOffset, 'minutes') сравнивает два различных экземпляра dayjs строгим равенством, что всегда даёт false. Ветка становится недостижимым кодом. Это тот же класс ошибки, что и в slots.ts:30. Заменить на .isSame() или сравнение .valueOf().

);
}
time: slot.time,
...schedule,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 organizerTimeZone может быть undefined при отсутствии пользователя в userAvailability

В slots.ts:120 вызов userAvailability.find() может вернуть undefined, если ни один пользователь не соответствует slotUserId. В этом случае organizerTimeZone становится undefined, и последующий вызов dayjs.tz(date.start, organizerTimeZone) в checkIfIsAvailable получит неопределённую таймзону. В зависимости от поведения плагина timezone это может вызвать исключение или тихо произвести неверный расчёт смещения (fallback на 0). Следует явно обрабатывать случай отсутствия пользователя.

userId: availability.user.id,
timeZone: availability.timeZone,
...override,
}))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 Флаг dateOverrideExist мутируется как побочный эффект внутри Array.prototype.find

В slots.ts:99 dateOverrides.find(...) используется в первую очередь ради побочного эффекта — установки внешнего флага dateOverrideExist = true. Колбэк find возвращает true в ветках 'вне переопределения' (что заставляет внешний if вернуть false), и неявно возвращает undefined в остальных случаях. Такая логика трудна для понимания и сопровождения: семантика find смешивается с управлением потоком через побочные эффекты. Рекомендуется разделить поиск и проверку условий, либо использовать for...of с явным break.

@hsander

hsander commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔵 Одинаковое смещённое значение dayjs пересчитывается четыре раза на каждое переопределение

В lib/slots.ts:42 внутри колбэка activeOverrides.flatMap выражение dayjs(override.start).utc().add(offset, 'minute') конструируется дважды (для .hour() и .minute()), и dayjs(override.end).utc().add(offset, 'minute') — также дважды. Каждое конструирование создаёт новый объект dayjs и повторно применяет смещение. Это не критично для производительности, но делает код избыточным и подверженным ошибкам при копировании. Рекомендуется вынести значения в локальные переменные.

endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
const overrides = activeOverrides.flatMap((override) => {
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 Риск DST: смещение вычисляется один раз из override.start и применяется к override.end

В lib/slots.ts:5 смещение UTC вычисляется на основе override.start для организатора и приглашённого. Если переопределение охватывает момент перехода на летнее/зимнее время, смещение на момент override.end может отличаться от смещения на момент override.start. Применение одного смещения к обеим границам может сдвинуть конец окна, сделав переопределение короче или длиннее ожидаемого. Следует вычислять смещение отдельно для каждой границы.

@hsander

hsander commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔵 Парсинг Date.toString() для получения смещения таймзоны — хрупкий и зависит от локали

В lib/slots.ts:43 dayjs(override.start.toString()).tz(override.timeZone).utcOffset() преобразует Date в строку через toString() (например, 'Mon Jul 01 2026 18:30:00 GMT+0000 (Coordinated Universal Time)') и затем повторно парсит её dayjs. Формат toString() зависит от реализации и локали, а в некоторых движках включает название таймзоны в скобках, что делает парсинг ненадёжным. Вместо этого следует передавать объект Date напрямую в dayjs или использовать dayjs.unix(override.start.getTime() / 1000).

@hsander

hsander commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔵 organizerTimeZone может быть undefined в ветке loose-host — тихий fallback на смещение 0

В slots.ts:219 const userSchedule = userAvailability.find(...) может вернуть undefined, после чего organizerTimeZone: userSchedule?.timeZone становится undefined. Внутри checkIfIsAvailable расчёт смещения fallback'ит на 0 (organizerTimeZone ? dayjs.tz(...) : 0), что тихо обрабатывает переопределение как UTC. Это может привести к некорректной доступности слотов без видимой ошибки. Следует либо логировать предупреждение, либо явно отклонять слот при отсутствии таймзоны организатора.

@hsander

hsander commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔍 Автоматическое ревью кода

Краткий итог

Синтез ревью: 11 уникальных групп замечаний по 2 исходн. выполнениям; 10 для публикации (критических: 0, высоких: 3, средних: 4, низких: 3, мелких: 0).

Замечания

🟠 Важно

  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:30] Сравнение dayjs-объектов через === вместо .isSame() — проверка нулевого переопределения никогда не срабатывает — В slots.ts:30 два независимо созданных экземпляра dayjs сравниваются оператором ===, который проверяет ссылочную идентичность, а не временное равенство. Выражение dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes") всегда равно false, из-за чего ветка раннего возврата для переопределений нулевой длительности становится недостижимой. Следует использовать .isSame() с соответствующей единицей измерения или сравнивать Unix-таймстампы через .valueOf().
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:55] Проверка конца рабочих часов использует slotStartTime вместо slotEndTime — В slots.ts:55 обе переменные start и end вычисляются из slotStartTime, из-за чего они идентичны. Проверка должна определять, начинается ли слот до начала рабочего дня ИЛИ заканчивается ли он после конца рабочего дня. Из-за ошибки слот, который начинается в рабочее время, но выходит за его конец, ошибочно проходит проверку и помечается как доступный. Переменная end должна вычисляться из slotEndTime.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:137] Дублирование ошибки с slotStartTime вместо slotEndTime в новом guard'е рабочих часов — В slots.ts:137 в новом блоке проверки рабочих часов обе переменные start и end вычисляются из slotStartTime: const start = slotStartTime.hour() * 60 + slotStartTime.minute() и const end = slotStartTime.hour() * 60 + slotStartTime.minute(). Значение end должно вычисляться из slotEndTime (уже рассчитанного выше как time.add(eventLength, 'minutes').utc()). Это та же ошибка, что и в slots.ts:55, но в другом месте — вероятно, результат копирования кода.

💡 Дополнительно

  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:110] Сравнение dayjs через === в slots.ts:110 — мёртвый код для нулевых переопределений — В slots.ts:110 проверка dayjs(date.start).add(utcOffset, 'minutes') === dayjs(date.end).add(utcOffset, 'minutes') сравнивает два различных экземпляра dayjs строгим равенством, что всегда даёт false. Ветка становится недостижимым кодом. Это тот же класс ошибки, что и в slots.ts:30. Заменить на .isSame() или сравнение .valueOf().
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:120] organizerTimeZone может быть undefined при отсутствии пользователя в userAvailability — В slots.ts:120 вызов userAvailability.find() может вернуть undefined, если ни один пользователь не соответствует slotUserId. В этом случае organizerTimeZone становится undefined, и последующий вызов dayjs.tz(date.start, organizerTimeZone) в checkIfIsAvailable получит неопределённую таймзону. В зависимости от поведения плагина timezone это может вызвать исключение или тихо произвести неверный расчёт смещения (fallback на 0). Следует явно обрабатывать случай отсутствия пользователя.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:99] Флаг dateOverrideExist мутируется как побочный эффект внутри Array.prototype.find — В slots.ts:99 dateOverrides.find(...) используется в первую очередь ради побочного эффекта — установки внешнего флага dateOverrideExist = true. Колбэк find возвращает true в ветках 'вне переопределения' (что заставляет внешний if вернуть false), и неявно возвращает undefined в остальных случаях. Такая логика трудна для понимания и сопровождения: семантика find смешивается с управлением потоком через побочные эффекты. Рекомендуется разделить поиск и проверку условий, либо использовать for...of с явным break.
  • [src/typescript/packages/lib/slots.ts:5] Риск DST: смещение вычисляется один раз из override.start и применяется к override.end — В lib/slots.ts:5 смещение UTC вычисляется на основе override.start для организатора и приглашённого. Если переопределение охватывает момент перехода на летнее/зимнее время, смещение на момент override.end может отличаться от смещения на момент override.start. Применение одного смещения к обеим границам может сдвинуть конец окна, сделав переопределение короче или длиннее ожидаемого. Следует вычислять смещение отдельно для каждой границы.
  • [src/typescript/packages/lib/slots.ts:42] Одинаковое смещённое значение dayjs пересчитывается четыре раза на каждое переопределение — В lib/slots.ts:42 внутри колбэка activeOverrides.flatMap выражение dayjs(override.start).utc().add(offset, 'minute') конструируется дважды (для .hour() и .minute()), и dayjs(override.end).utc().add(offset, 'minute') — также дважды. Каждое конструирование создаёт новый объект dayjs и повторно применяет смещение. Это не критично для производительности, но делает код избыточным и подверженным ошибкам при копировании. Рекомендуется вынести значения в локальные переменные.
  • [src/typescript/packages/lib/slots.ts:43] Парсинг Date.toString() для получения смещения таймзоны — хрупкий и зависит от локали — В lib/slots.ts:43 dayjs(override.start.toString()).tz(override.timeZone).utcOffset() преобразует Date в строку через toString() (например, 'Mon Jul 01 2026 18:30:00 GMT+0000 (Coordinated Universal Time)') и затем повторно парсит её dayjs. Формат toString() зависит от реализации и локали, а в некоторых движках включает название таймзоны в скобках, что делает парсинг ненадёжным. Вместо этого следует передавать объект Date напрямую в dayjs или использовать dayjs.unix(override.start.getTime() / 1000).
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:219] organizerTimeZone может быть undefined в ветке loose-host — тихий fallback на смещение 0 — В slots.ts:219 const userSchedule = userAvailability.find(...) может вернуть undefined, после чего organizerTimeZone: userSchedule?.timeZone становится undefined. Внутри checkIfIsAvailable расчёт смещения fallback'ит на 0 (organizerTimeZone ? dayjs.tz(...) : 0), что тихо обрабатывает переопределение как UTC. Это может привести к некорректной доступности слотов без видимой ошибки. Следует либо логировать предупреждение, либо явно отклонять слот при отсутствии таймзоны организатора.

Автоматическое ревью от Review Engine v2.0

busy: EventBusyDate[];
eventLength: number;
dateOverrides?: {
start: Date;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Сравнение dayjs-объектов через === всегда возвращает false (строка 20)

Каждый вызов dayjs(...) создаёт новый экземпляр объекта, поэтому === сравнивает ссылки и всегда возвращает false. Данная ветка фактически является мёртвым кодом. Предположительно, намерение состояло в проверке, совпадают ли моменты начала и окончания переопределения, для чего требуется .isSame().

slotStartTime.format("YYYY MM DD")
) {
dateOverrideExist = true;
if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Сравнение dayjs-объектов через === (сравнение по ссылке) всегда даёт false

Выражение if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) сравнивает два только что созданных экземпляра dayjs по ссылке. Каждый вызов dayjs(...) создаёт новый объект, поэтому === всегда возвращает false, и ветка обработки нулевых/мгновенных переопределений дат становится мёртвым кодом. Окружающая логика доступности полагается на эту ветку для обработки переопределений с совпадающим началом и концом. Следует использовать .isSame().

const slotStartTime = time.utc();

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Логика рабочих часов помечает слот недоступным, если он не попадает хотя бы в один из интервалов

Колбэк find возвращает truthy, если слот выходит за пределы конкретного рабочего часа. Если find возвращает truthy для любого рабочего часа, слот помечается недоступным. Это означает, что слот должен попадать во ВСЕ записи рабочих часов одновременно. При наличии нескольких интервалов в один день (например, утренний блок 9–12 и дневной блок 13–17) слот в 10:00 будет некорректно признан недоступным, так как он не попадает в дневной блок. Логику следует изменить: слот доступен, если он попадает хотя бы в один рабочий интервал.

const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;

if (
dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") ===

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Проверка рабочих часов использует slotStartTime для вычисления как начала, так и окончания слота

Переменная end вычисляется из slotStartTime вместо slotEndTime. Из-за этого условие end > workingHour.endTime сравнивает только время начала слота с концом рабочего часа. Слоты, которые начинаются в пределах рабочих часов, но выходят за их конец (например, 60-минутный слот, начинающийся в 16:30 при окончании рабочих часов в 17:00), некорректно проходят фильтр и помечаются как доступные. Переменная end должна вычисляться из slotEndTime.

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
Owner Author

Choose a reason for hiding this comment

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

🟠 Фильтр рабочих часов использует время начала слота для обеих границ

В новом фильтре рабочих часов const start = slotStartTime.hour() * 60 + slotStartTime.minute(); и const end = slotStartTime.hour() * 60 + slotStartTime.minute(); вычисляются из одного и того же slotStartTime. Переменная end должна вычисляться из slotEndTime (начало слота + eventLength). Поскольку end отражает время начала слота, условие end > workingHour.endTime срабатывает только тогда, когда начало слота уже за пределами рабочего часа, пропуская слоты, которые выходят за конец рабочего времени.

endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
const overrides = activeOverrides.flatMap((override) => {
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 override.timeZone опционален, но передаётся без проверки в dayjs .tz()

Вызов dayjs(override.start.toString()).tz(override.timeZone) использует override.timeZone, тип которого timeZone?: string (опциональное поле в schedule.d.ts). Когда timeZone равно undefined (например, доступность организатора без указанного часового пояса), dayjs(...).tz(undefined) молча откатывается к локальному времени сервера, и organizerUtcOffset вычисляется относительно локального пояса сервера, а не UTC. Это может привести к некорректным результатам доступности. Следует добавить проверку на undefined и значение по умолчанию (например, 'UTC').


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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 Повторные вычисления dayjs(date...).add(utcOffset,'minutes') избыточны и подвержены ошибкам

Выражения dayjs(date.start).add(utcOffset, "minutes") и dayjs(date.end).add(utcOffset, "minutes") пересоздаются 5+ раз в одном колбэке (строки 40, 44, 44, 48, 49, 53, 53). Каждый вызов выделяет новый объект dayjs, что усложняет чтение логики и повышает риск рассинхронизации — одна ошибка в выборе базового значения молча изменит поведение. Рекомендуется вычислить значения один раз и переиспользовать.

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

if (

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 dateOverrides.find с побочным эффектом через флаг dateOverrideExist некорректно классифицирует слоты при нескольких переопределениях в один день

Проверка переопределений использует dateOverrides.find(...). Внутри колбэка, когда день слота совпадает с переопределением, но слот находится ВНУТРИ окна переопределения, dateOverrideExist устанавливается в true, а колбэк возвращает undefined (falsy), и find переходит к следующему переопределению. Если последующее переопределение в тот же день находит слот вне своего окна, find возвращает этот элемент, и внешний if выполняет ветку недоступности, даже если слот уже был корректно обработан первым переопределением. Это приводит к ошибочной маркировке доступных слотов как недоступных.

});
});
return slot;
return false;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 organizerTimeZone может быть undefined, если userSchedule не найден

Если userAvailability.find(...) возвращает undefined (пользователь слота не найден в списке доступности), userSchedule?.timeZone вычисляется как undefined. Это значение передаётся как organizerTimeZone, из-за чего checkIfIsAvailable устанавливает utcOffset в 0, пропуская корректировку часового пояса для переопределений дат. Это может привести к некорректным результатам доступности для слотов с не привязанным организатором. Следует явно обрабатывать случай отсутствующего расписания.

return {
userIds: override.userId ? [override.userId] : [],
startTime:
dayjs(override.start).utc().add(offset, "minute").hour() * 60 +

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 dayjs(override.start).utc().add(offset,'minute') вычисляется повторно четыре раза

Выражение dayjs(override.start).utc().add(offset, "minute") вычисляется дважды для startTime.hour() и .minute(), и аналогично для endTime — всего четыре раза. Результат идентичен в каждой паре и может быть вычислен один раз и сохранён в переменную, что улучшит читаемость и производительность.

const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 Новый тест покрывает только корректный путь часового пояса; нет покрытия для багованных веток

Добавленный тест проверяет корректный рендеринг слотов для часового пояса приглашённого +6:00, но не затрагивает новую логику верхней границы рабочих часов (слот, выходящий за конец рабочих часов) и ветку нулевого переопределения дат, обе из которых содержат дефекты. Также файл теста не заканчивается символом новой строки (\ No newline at end of file).

@hsander

hsander commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

🔍 Автоматическое ревью кода

Краткий итог

Синтез ревью: 13 уникальных групп замечаний по 2 исходн. выполнениям; 12 для публикации (критических: 0, высоких: 5, средних: 4, низких: 3, мелких: 0).

Замечания

🟠 Важно

  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:20] Сравнение dayjs-объектов через === всегда возвращает false (строка 20) — Каждый вызов dayjs(...) создаёт новый экземпляр объекта, поэтому === сравнивает ссылки и всегда возвращает false. Данная ветка фактически является мёртвым кодом. Предположительно, намерение состояло в проверке, совпадают ли моменты начала и окончания переопределения, для чего требуется .isSame().
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:44] Сравнение dayjs-объектов через === (сравнение по ссылке) всегда даёт false — Выражение if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) сравнивает два только что созданных экземпляра dayjs по ссылке. Каждый вызов dayjs(...) создаёт новый объект, поэтому === всегда возвращает false, и ветка обработки нулевых/мгновенных переопределений дат становится мёртвым кодом. Окружающая логика доступности полагается на эту ветку для обработки переопределений с совпадающим началом и концом. Следует использовать .isSame().
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:33] Логика рабочих часов помечает слот недоступным, если он не попадает хотя бы в один из интервалов — Колбэк find возвращает truthy, если слот выходит за пределы конкретного рабочего часа. Если find возвращает truthy для любого рабочего часа, слот помечается недоступным. Это означает, что слот должен попадать во ВСЕ записи рабочих часов одновременно. При наличии нескольких интервалов в один день (например, утренний блок 9–12 и дневной блок 13–17) слот в 10:00 будет некорректно признан недоступным, так как он не попадает в дневной блок. Логику следует изменить: слот доступен, если он попадает хотя бы в один рабочий интервал.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:40] Проверка рабочих часов использует slotStartTime для вычисления как начала, так и окончания слота — Переменная end вычисляется из slotStartTime вместо slotEndTime. Из-за этого условие end > workingHour.endTime сравнивает только время начала слота с концом рабочего часа. Слоты, которые начинаются в пределах рабочих часов, но выходят за их конец (например, 60-минутный слот, начинающийся в 16:30 при окончании рабочих часов в 17:00), некорректно проходят фильтр и помечаются как доступные. Переменная end должна вычисляться из slotEndTime.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:72] Фильтр рабочих часов использует время начала слота для обеих границ — В новом фильтре рабочих часов const start = slotStartTime.hour() * 60 + slotStartTime.minute(); и const end = slotStartTime.hour() * 60 + slotStartTime.minute(); вычисляются из одного и того же slotStartTime. Переменная end должна вычисляться из slotEndTime (начало слота + eventLength). Поскольку end отражает время начала слота, условие end > workingHour.endTime срабатывает только тогда, когда начало слота уже за пределами рабочего часа, пропуская слоты, которые выходят за конец рабочего времени.

💡 Дополнительно

  • [src/typescript/packages/lib/slots.ts:5] override.timeZone опционален, но передаётся без проверки в dayjs .tz() — Вызов dayjs(override.start.toString()).tz(override.timeZone) использует override.timeZone, тип которого timeZone?: string (опциональное поле в schedule.d.ts). Когда timeZone равно undefined (например, доступность организатора без указанного часового пояса), dayjs(...).tz(undefined) молча откатывается к локальному времени сервера, и organizerUtcOffset вычисляется относительно локального пояса сервера, а не UTC. Это может привести к некорректным результатам доступности. Следует добавить проверку на undefined и значение по умолчанию (например, 'UTC').
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:37] Повторные вычисления dayjs(date...).add(utcOffset,'minutes') избыточны и подвержены ошибкам — Выражения dayjs(date.start).add(utcOffset, "minutes") и dayjs(date.end).add(utcOffset, "minutes") пересоздаются 5+ раз в одном колбэке (строки 40, 44, 44, 48, 49, 53, 53). Каждый вызов выделяет новый объект dayjs, что усложняет чтение логики и повышает риск рассинхронизации — одна ошибка в выборе базового значения молча изменит поведение. Рекомендуется вычислить значения один раз и переиспользовать.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:35] dateOverrides.find с побочным эффектом через флаг dateOverrideExist некорректно классифицирует слоты при нескольких переопределениях в один день — Проверка переопределений использует dateOverrides.find(...). Внутри колбэка, когда день слота совпадает с переопределением, но слот находится ВНУТРИ окна переопределения, dateOverrideExist устанавливается в true, а колбэк возвращает undefined (falsy), и find переходит к следующему переопределению. Если последующее переопределение в тот же день находит слот вне своего окна, find возвращает этот элемент, и внешний if выполняет ветку недоступности, даже если слот уже был корректно обработан первым переопределением. Это приводит к ошибочной маркировке доступных слотов как недоступных.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:140] organizerTimeZone может быть undefined, если userSchedule не найден — Если userAvailability.find(...) возвращает undefined (пользователь слота не найден в списке доступности), userSchedule?.timeZone вычисляется как undefined. Это значение передаётся как organizerTimeZone, из-за чего checkIfIsAvailable устанавливает utcOffset в 0, пропуская корректировку часового пояса для переопределений дат. Это может привести к некорректным результатам доступности для слотов с не привязанным организатором. Следует явно обрабатывать случай отсутствующего расписания.
  • [src/typescript/packages/lib/slots.ts:12] dayjs(override.start).utc().add(offset,'minute') вычисляется повторно четыре раза — Выражение dayjs(override.start).utc().add(offset, "minute") вычисляется дважды для startTime.hour() и .minute(), и аналогично для endTime — всего четыре раза. Результат идентичен в каждой паре и может быть вычислен один раз и сохранён в переменную, что улучшит читаемость и производительность.
  • [src/typescript/apps/web/test/lib/getSchedule.test.ts:8] Новый тест покрывает только корректный путь часового пояса; нет покрытия для багованных веток — Добавленный тест проверяет корректный рендеринг слотов для часового пояса приглашённого +6:00, но не затрагивает новую логику верхней границы рабочих часов (слот, выходящий за конец рабочих часов) и ветку нулевого переопределения дат, обе из которых содержат дефекты. Также файл теста не заканчивается символом новой строки (\ No newline at end of file).
  • [src/typescript/packages/lib/slots.ts:5] Хрупкое преобразование Date.toString() перед парсингом dayjs — override.start.toString() преобразует Date в строковое представление, зависящее от локали и системного часового пояса, затем повторно парсится dayjs. Это зависит от окружения и излишне, так как dayjs может принимать объекты Date напрямую. В серверных окружениях с не-UTC часовым поясом это может внести тонкие ошибки смещения. Рекомендуется передавать Date напрямую: dayjs(override.start).

Автоматическое ревью от Review Engine v2.0

endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
const overrides = activeOverrides.flatMap((override) => {
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 Хрупкое преобразование Date.toString() перед парсингом dayjs

override.start.toString() преобразует Date в строковое представление, зависящее от локали и системного часового пояса, затем повторно парсится dayjs. Это зависит от окружения и излишне, так как dayjs может принимать объекты Date напрямую. В серверных окружениях с не-UTC часовым поясом это может внести тонкие ошибки смещения. Рекомендуется передавать Date напрямую: dayjs(override.start).

const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;

if (
dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") ===

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟠 Проверка рабочих часов использует slotStartTime для обеих границ вместо slotEndTime для конца

В функции checkIfIsAvailable переменная end вычисляется как slotStartTime.hour() * 60 + slotStartTime.minute() — копия start. Должно использоваться slotEndTime, чтобы проверить, что конец слота находится в пределах рабочих часов. В текущем виде слот, начинающийся в рабочее время, но заканчивающийся после workingHour.endTime, ошибочно проходит проверку и помечается как доступный.

dateOverrides?: {
start: Date;
end: Date;
}[];

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 dayjs-объекты сравниваются через === (сравнение по ссылке) — условие всегда false

Выражение dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes") сравнивает два разных экземпляра dayjs по ссылке. dayjs никогда не возвращает один и тот же объект, поэтому условие всегда ложно. Ветка раннего возврата для пустого переопределения (равные start и end) никогда не срабатывает, и логика проваливается в последующие проверки.

endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
const overrides = activeOverrides.flatMap((override) => {
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 dayjs().tz(override.timeZone) вызывается с возможно undefined значением timeZone

Поле timeZone в типе DateOverride сделано optional (timeZone?: string), а в viewer/slots.ts оно берётся из availability.timeZone, который также может быть undefined. Вызов dayjs(override.start.toString()).tz(override.timeZone) с undefined тихо подставляет часовой пояс по умолчанию dayjs вместо часового пояса организатора, что приводит к неверному расчёту organizerUtcOffset и неправильным диапазонам переопределений.

// flattens availability of multiple users
const dateOverrides = userAvailability.flatMap((availability) =>
availability.dateOverrides.map((override) => ({ userId: availability.user.id, ...override }))
availability.dateOverrides.map((override) => ({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 organizerTimeZone становится undefined при неудачном userAvailability.find(), молча отключая коррекцию смещения

Если userAvailability.find() возвращает undefined (например, ID пользователя слота не найден в данных доступности), organizerTimeZone становится undefined. В checkIfIsAvailable это приводит к utcOffset = 0, и логика сопоставления дат переопределения использует сырую UTC-дату вместо локальной даты организатора. Слоты могут быть ошибочно включены или исключены из доступности.

let dateOverrideExist = false;

if (
dateOverrides.find((date) => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 Проверка доступности переопределений полагается на побочные эффекты внутри callback'а find()

Callback dateOverrides.find(...) мутирует внешний флаг dateOverrideExist и одновременно возвращает true/false, управляя как find, так и последующей веткой if (dateOverrideExist). Смешение побочных эффектов с truthiness-логикой find затрудняет понимание потока управления: двойной смысл возвращаемого значения («вне переопределения» vs «совпадение по дате») неочевиден и легко ломается при рефакторинге.

@hsander

hsander commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

🔍 Автоматическое ревью кода

Краткий итог

Синтез ревью: 10 уникальных групп замечаний по 2 исходн. выполнениям; 5 для публикации (критических: 0, высоких: 1, средних: 3, низких: 1, мелких: 0).

Замечания

🟠 Важно

  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:40] Проверка рабочих часов использует slotStartTime для обеих границ вместо slotEndTime для конца — В функции checkIfIsAvailable переменная end вычисляется как slotStartTime.hour() * 60 + slotStartTime.minute() — копия start. Должно использоваться slotEndTime, чтобы проверить, что конец слота находится в пределах рабочих часов. В текущем виде слот, начинающийся в рабочее время, но заканчивающийся после workingHour.endTime, ошибочно проходит проверку и помечается как доступный.

💡 Дополнительно

  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:22] dayjs-объекты сравниваются через === (сравнение по ссылке) — условие всегда false — Выражение dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes") сравнивает два разных экземпляра dayjs по ссылке. dayjs никогда не возвращает один и тот же объект, поэтому условие всегда ложно. Ветка раннего возврата для пустого переопределения (равные start и end) никогда не срабатывает, и логика проваливается в последующие проверки.
  • [src/typescript/packages/lib/slots.ts:5] dayjs().tz(override.timeZone) вызывается с возможно undefined значением timeZone — Поле timeZone в типе DateOverride сделано optional (timeZone?: string), а в viewer/slots.ts оно берётся из availability.timeZone, который также может быть undefined. Вызов dayjs(override.start.toString()).tz(override.timeZone) с undefined тихо подставляет часовой пояс по умолчанию dayjs вместо часового пояса организатора, что приводит к неверному расчёту organizerUtcOffset и неправильным диапазонам переопределений.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:95] organizerTimeZone становится undefined при неудачном userAvailability.find(), молча отключая коррекцию смещения — Если userAvailability.find() возвращает undefined (например, ID пользователя слота не найден в данных доступности), organizerTimeZone становится undefined. В checkIfIsAvailable это приводит к utcOffset = 0, и логика сопоставления дат переопределения использует сырую UTC-дату вместо локальной даты организатора. Слоты могут быть ошибочно включены или исключены из доступности.
  • [src/typescript/packages/trpc/server/routers/viewer/slots.ts:36] Проверка доступности переопределений полагается на побочные эффекты внутри callback'а find() — Callback dateOverrides.find(...) мутирует внешний флаг dateOverrideExist и одновременно возвращает true/false, управляя как find, так и последующей веткой if (dateOverrideExist). Смешение побочных эффектов с truthiness-логикой find затрудняет понимание потока управления: двойной смысл возвращаемого значения («вне переопределения» vs «совпадение по дате») неочевиден и легко ломается при рефакторинге.

Автоматическое ревью от Review Engine v2.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant