Skip to content

Automated Test: date-algorithm-enhanced #374

Closed

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved timezone handling for scheduling by correctly computing override time boundaries across different organizer and invitee timezones.
    • Enhanced availability checks to properly respect working hours and date overrides in timezone-aware scenarios.
  • Tests

    • Added test coverage for timezone behavior in schedule queries.

* fix date override for fixed round robin + time zone in date override

* check if slot is within working hours of fixed hosts

* add test for date override in different time zone

* fix date overrides for not fixed hosts (round robin)

* code clean up

* fix added test

* use the correct timezone of user for date overrides

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request enhances timezone handling across the scheduling system by introducing timezone-aware boundary recalculation for date overrides, propagating timezone information through the slot generation pipeline, extending the TimeRange type to include optional timezone metadata, and adding corresponding test coverage.

Changes

Cohort / File(s) Summary
Type Definitions
packages/types/schedule.d.ts
Added optional timeZone property to TimeRange type to carry timezone information alongside existing userId, start, and end fields.
Core Slot Calculation
packages/lib/slots.ts
Recomputes date override time boundaries using organizer and invitee timezone offsets via dayjs, applying combined offset adjustments to derive timezone-aware startTime and endTime values instead of simple UTC extraction.
Scheduling Router
packages/trpc/server/routers/viewer/slots.ts
Introduces timezone-aware availability checks by deriving organizerTimeZone from event type configuration, threading it through checkIfIsAvailable calls, extending date override handling to include userId and timeZone fields, and adjusting per-slot filtering logic to respect timezone context across both fixed-host and dynamic-attendee scenarios.
Test Coverage
apps/web/test/lib/getSchedule.test.ts
Adds new test scenario validating getSchedule behavior when using a different timezone offset (Timezones["+6:00"]) with UTC-aligned event times, asserting that results remain consistently in UTC despite timezone variations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop through timezones, left and right,
Offsets aligned from dawn to night,
Boundaries recalculate with care,
UTC slots floating everywhere!
DST no more shall cause us fright, 🕐✨

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: date-algorithm-enhanced' is vague and generic, using non-descriptive terms that don't clearly convey the main changes in the pull request. Consider using a more descriptive title that highlights the primary change, such as 'Add timezone-aware date overrides and working hours support' or 'Enhance scheduling algorithm with timezone-aware availability checks'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch date-algorithm-enhanced

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/lib/slots.ts`:
- Around line 211-225: activeOverrides handling can call
dayjs(...).tz(override.timeZone) when override.timeZone is undefined, producing
wrong offsets; change the computation in the block that builds overrides (the
activeOverrides flatMap) to first fall back to the organizer timezone (or skip
offset adjustment) when override.timeZone is missing before calling dayjs.tz,
and replace flatMap with map since each override returns a single object; update
references in this block (override.timeZone,
organizerUtcOffset/inviteeUtcOffset/offset, and the activeOverrides flatMap →
map) so offset calculations only run when a valid timeZone exists or use
organizer timezone as the fallback.

In `@packages/trpc/server/routers/viewer/slots.ts`:
- Around line 114-116: The condition in slots.ts is comparing two Dayjs
instances by reference causing it to always be false; replace the strict
equality check between dayjs(date.start).add(utcOffset, "minutes") and
dayjs(date.end).add(utcOffset, "minutes") with a value comparison using the
Dayjs method .isSame(), i.e. call .isSame(...) on one of those Dayjs objects
(optionally with a unit like "minute" if needed) and keep the existing return
true when they match.
- Around line 141-143: The boundary check wrongly computes end from
slotStartTime causing start and end to be identical; update the calculation that
sets end so it uses slotEndTime (i.e., replace the second use of
slotStartTime.hour()/minute() with slotEndTime.hour()/minute()) in the block
that defines start and end (look for the variables start, end in the slots.ts
logic that compares to workingHour.startTime and workingHour.endTime) so slots
that extend past working hours are correctly filtered.
- Around line 102-151: Replace the two misused find() calls with some() and
remove the mutable dateOverrideExist side-effect: use dateOverrides.some(...) to
first check whether any override falls on the same day as slotStartTime
(respecting organizerTimeZone), then separately check if any matching override
contains or excludes the slot times and return accordingly; similarly replace
the workingHours.find(...) with workingHours.some(...), compute start =
slotStartTime.hour()*60 + slotStartTime.minute() and end = slotEndTime.hour()*60
+ slotEndTime.minute() (fixing the current bug where end is set from
slotStartTime), and return false when some() indicates the slot is outside
working hours. Reference symbols: dateOverrides, organizerTimeZone,
slotStartTime, slotEndTime, dateOverrideExist (remove), workingHours.
🧹 Nitpick comments (1)
packages/lib/slots.ts (1)

218-223: Redundant dayjs instantiation for the same offset-adjusted time.

dayjs(override.start).utc().add(offset, "minute") is computed twice for startTime (and likewise for endTime). Extract to a local variable for clarity and to avoid wasted work.

Proposed refactor
+      const adjustedStart = dayjs(override.start).utc().add(offset, "minute");
+      const adjustedEnd = dayjs(override.end).utc().add(offset, "minute");
       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(),
+        startTime: adjustedStart.hour() * 60 + adjustedStart.minute(),
+        endTime: adjustedEnd.hour() * 60 + adjustedEnd.minute(),
       };

Comment on lines +211 to +225
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(),
};
});

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

override.timeZone may be undefined, causing incorrect offset calculation.

TimeRange.timeZone is optional. If a date override lacks a timeZone value, dayjs(...).tz(undefined) will produce unpredictable results — likely falling back to the local/system timezone rather than the organizer's timezone.

Add a fallback to the organizer timezone (or skip offset adjustment entirely when timeZone is absent):

Proposed fix
     const overrides = activeOverrides.flatMap((override) => {
-      const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
+      const overrideTimeZone = override.timeZone || organizerTimeZone;
+      const organizerUtcOffset = dayjs(override.start.toString()).tz(overrideTimeZone).utcOffset();
       const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
       const offset = inviteeUtcOffset - organizerUtcOffset;

Additionally, flatMap is misleading here since each override produces exactly one object — map is more appropriate.

🤖 Prompt for AI Agents
In `@packages/lib/slots.ts` around lines 211 - 225, activeOverrides handling can
call dayjs(...).tz(override.timeZone) when override.timeZone is undefined,
producing wrong offsets; change the computation in the block that builds
overrides (the activeOverrides flatMap) to first fall back to the organizer
timezone (or skip offset adjustment) when override.timeZone is missing before
calling dayjs.tz, and replace flatMap with map since each override returns a
single object; update references in this block (override.timeZone,
organizerUtcOffset/inviteeUtcOffset/offset, and the activeOverrides flatMap →
map) so offset calculations only run when a valid timeZone exists or use
organizer timezone as the fallback.

Comment on lines +102 to +151
//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();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

find() callbacks should always return a value — use some() or add explicit returns.

Both find() calls (lines 106 and 139) rely on implicit undefined returns for non-matching branches, which the linter flags and makes the intent unclear. Since you're only checking for existence (not using the found element), some() is the right method.

Additionally, the mutable side-effect pattern (dateOverrideExist set inside find()) is fragile. Consider restructuring to separate the "does an override exist for this day" check from the "is the slot within that override" check.

Proposed refactor for the find→some pattern
-  if (
-    dateOverrides.find((date) => {
+  if (
+    dateOverrides.some((date) => {
       const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;
       // ...
+      return false;
     })
   ) {
-  if (
-    workingHours.find((workingHour) => {
+  if (
+    workingHours.some((workingHour) => {
       if (workingHour.days.includes(slotStartTime.day())) {
         // ...
+        return start < workingHour.startTime || end > workingHour.endTime;
       }
+      return false;
     })
   ) {
🧰 Tools
🪛 Biome (2.3.13)

[error] 106-106: This callback passed to find() iterable method should always return a value.

Add missing return statements so that this callback returns a value on all execution paths.

(lint/suspicious/useIterableCallbackReturn)


[error] 139-139: This callback passed to find() iterable method should always return a value.

Add missing return statements so that this callback returns a value on all execution paths.

(lint/suspicious/useIterableCallbackReturn)

🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/slots.ts` around lines 102 - 151, Replace
the two misused find() calls with some() and remove the mutable
dateOverrideExist side-effect: use dateOverrides.some(...) to first check
whether any override falls on the same day as slotStartTime (respecting
organizerTimeZone), then separately check if any matching override contains or
excludes the slot times and return accordingly; similarly replace the
workingHours.find(...) with workingHours.some(...), compute start =
slotStartTime.hour()*60 + slotStartTime.minute() and end = slotEndTime.hour()*60
+ slotEndTime.minute() (fixing the current bug where end is set from
slotStartTime), and return false when some() indicates the slot is outside
working hours. Reference symbols: dateOverrides, organizerTimeZone,
slotStartTime, slotEndTime, dateOverrideExist (remove), workingHours.

Comment on lines +114 to +116
if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
return true;
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Dayjs object identity comparison will always be false.

dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes") compares two distinct Dayjs object references, not their underlying time values. This condition will never be true, even when start and end are identical.

Use .isSame() for value comparison:

Proposed fix
-        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;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
return true;
}
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 114 - 116, The
condition in slots.ts is comparing two Dayjs instances by reference causing it
to always be false; replace the strict equality check between
dayjs(date.start).add(utcOffset, "minutes") and dayjs(date.end).add(utcOffset,
"minutes") with a value comparison using the Dayjs method .isSame(), i.e. call
.isSame(...) on one of those Dayjs objects (optionally with a unit like "minute"
if needed) and keep the existing return true when they match.

Comment on lines +141 to +143
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotStartTime.hour() * 60 + slotStartTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: end is computed from slotStartTime instead of slotEndTime, making the boundary check ineffective.

Both start and end resolve to the same value (slotStartTime.hour() * 60 + slotStartTime.minute()), so the end > workingHour.endTime check is equivalent to start > workingHour.endTime. Slots that start within working hours but extend past them won't be filtered out.

Proposed fix
         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) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotStartTime.hour() * 60 + slotStartTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotEndTime.hour() * 60 + slotEndTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/slots.ts` around lines 141 - 143, The
boundary check wrongly computes end from slotStartTime causing start and end to
be identical; update the calculation that sets end so it uses slotEndTime (i.e.,
replace the second use of slotStartTime.hour()/minute() with
slotEndTime.hour()/minute()) in the block that defines start and end (look for
the variables start, end in the slots.ts logic that compares to
workingHour.startTime and workingHour.endTime) so slots that extend past working
hours are correctly filtered.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant