Skip to content

Conversation

@hussam789
Copy link

@hussam789 hussam789 commented Oct 30, 2025

User description

PR #6


PR Type

Bug fix


Description

  • Fix date override handling for different timezones

  • Ensure slots respect organizer timezone when checking availability

  • Add timezone offset calculations for date override comparisons

  • Validate slots within working hours and date overrides correctly


Diagram Walkthrough

flowchart LR
  A["Date Override"] -->|"Apply timezone offset"| B["Calculate UTC offset"]
  B -->|"Adjust slot times"| C["Check availability"]
  C -->|"Validate against overrides"| D["Return available slots"]
  E["Working Hours"] -->|"Validate slot"| C
Loading

File Walkthrough

Relevant files
Tests
getSchedule.test.ts
Add timezone-aware date override test                                       

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

  • Add test case for date override with different timezone (+6:00)
  • Verify slots are correctly returned when timezone differs from
    organizer
  • Test validates UTC time consistency across timezones
+18/-0   
Bug fix
slots.ts
Apply timezone offset to date overrides                                   

packages/lib/slots.ts

  • Calculate organizer and invitee UTC offsets for date overrides
  • Apply timezone offset to override start/end times
  • Convert override times to minutes accounting for timezone differences
  • Ensure slot availability respects timezone-adjusted overrides
+16/-5   
slots.ts
Enhance availability check with timezone-aware overrides 

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

  • Add dateOverrides, workingHours, and organizerTimeZone parameters to
    checkIfIsAvailable
  • Implement date override existence check with timezone offset
    calculation
  • Validate slots are within date override time ranges
  • Check slots against working hours if no date override exists
  • Pass organizer timezone to availability checks for fixed and loose
    hosts
  • Include timezone information when mapping date overrides
+76/-5   
Enhancement
schedule.d.ts
Add timezone field to TimeRange type                                         

packages/types/schedule.d.ts

  • Add optional timeZone field to TimeRange type
  • Enable timezone tracking for date overrides and availability ranges
+1/-0     

* 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 <[email protected]>
@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Faulty time comparison

Description: Using strict equality to compare Dayjs objects (dayjs(date.start).add(...) ===
dayjs(date.end).add(...)) will always be false, potentially bypassing intended override
edge-case logic and leading to incorrect availability checks.
slots.ts [106-131]

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


 ... (clipped 5 lines)
Working hours bypass

Description: Working-hours validation computes both start and end minutes from slotStartTime (end uses
the same minute), which can fail to detect slots exceeding working hours and may allow
booking outside allowed times.
slots.ts [139-151]

Referred Code
  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;
}
Timezone parsing risk

Description: Timezone offset is derived from override.start stringified via toString() before tz/utc
conversions, which can misinterpret timezones across environments and cause incorrect
override application.
slots.ts [211-224]

Referred Code
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(),
  };
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing edge handling: The date override and working hours checks lack null/undefined guards and rely on implicit
truthy finds without error handling or clear return paths for edge cases (e.g., invalid
timezone, equal start/end).

Referred Code
}): boolean => {
  if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
    return true;
  }

  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")) {


 ... (clipped 37 lines)
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No auditing: New availability decision logic was added without any audit logging for critical
scheduling decisions, but the codebase context may handle auditing elsewhere.

Referred Code
}): boolean => {
  if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
    return true;
  }

  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")) {


 ... (clipped 37 lines)
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Ambiguous naming: Variable name 'dateOverrideExist' flags existence but is used alongside a find()
that returns early, which could be clearer with a more expressive structure or naming.

Referred Code
//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;
      }


 ... (clipped 13 lines)
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated inputs: New logic uses 'organizerTimeZone' and date fields without explicit validation
or sanitization in this diff, which could pose risks if external inputs are malformed.

Referred Code
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())) {


 ... (clipped 6 lines)
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Refactor timezone logic for simplicity

The suggestion is to refactor the timezone logic by converting all date/time
information to UTC at the beginning of the process. This change would simplify
the code by removing the need for manual offset calculations and redundant
validation checks.

Examples:

packages/lib/slots.ts [211-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(),

 ... (clipped 5 lines)
packages/trpc/server/routers/viewer/slots.ts [105-131]
  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")) {

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

// in packages/lib/slots.ts
const overrides = activeOverrides.flatMap((override) => {
  const organizerUtcOffset = dayjs(override.start).tz(override.timeZone).utcOffset();
  const inviteeUtcOffset = dayjs(override.start).tz(timeZone).utcOffset();
  const offset = inviteeUtcOffset - organizerUtcOffset;

  return {
    startTime: dayjs(override.start).utc().add(offset, "minute").hour() * 60 + ...,
    endTime: dayjs(override.end).utc().add(offset, "minute").hour() * 60 + ...,
  };
});

// in packages/trpc/server/routers/viewer/slots.ts
function checkIfIsAvailable({ time, dateOverrides, organizerTimeZone }) {
  if (dateOverrides.find(date => {
    const utcOffset = dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1;
    // ... complex comparisons using the offset ...
  })) {
    return false; // Slot is not within the date override
  }
  // ... more logic
}

After:

// In packages/trpc/server/routers/viewer/slots.ts
// 1. Normalize all override times to UTC immediately after fetching.
const dateOverrides = userAvailability.flatMap((availability) =>
  availability.dateOverrides.map((override) => ({
    // Convert to true UTC Dayjs objects here, once.
    start: dayjs.tz(override.start, availability.timeZone),
    end: dayjs.tz(override.end, availability.timeZone),
  }))
);

// 2. Simplify checks by comparing UTC times directly.
function checkIfIsAvailable({ time, dateOverrides }) {
  const applicableOverride = dateOverrides.find(override => time.isSame(override.start, 'day'));
  if (applicableOverride) {
    // Direct comparison of UTC-normalized times.
    return time.isBetween(applicableOverride.start, applicableOverride.end, null, '[]');
  }
  // ... check working hours
}

// 3. Remove manual offset calculations from packages/lib/slots.ts
// The logic becomes much simpler as times are already normalized.
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that the PR's approach to timezone handling is overly complex and spread across multiple files, using manual offset calculations. Refactoring to normalize all times to UTC early would significantly simplify the logic, improve maintainability, and reduce the risk of future timezone-related bugs.

High
Possible issue
Fix incorrect slot end time calculation

Fix a bug in the working hours check by correctly calculating the slot's end
time in minutes. The current code incorrectly uses the slot's start time for
both start and end comparisons.

packages/trpc/server/routers/viewer/slots.ts [138-151]

 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) {
+      const startInMinutes = slotStartTime.hour() * 60 + slotStartTime.minute();
+      const endInMinutes = slotEndTime.hour() * 60 + slotEndTime.minute();
+      if (startInMinutes < workingHour.startTime || endInMinutes > workingHour.endTime) {
         return true;
       }
     }
   })
 ) {
   // slot is outside of working hours
   return false;
 }
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug where the slot's end time is not checked against working hours, which could lead to booking slots that extend beyond the user's availability. This is a critical fix for scheduling correctness.

High
Simplify timezone conversion for overrides

Simplify the date override timezone conversion logic by using
dayjs(date).tz(timeZone) to directly convert the UTC override times to the
invitee's timezone, instead of manually calculating and applying timezone
offsets.

packages/lib/slots.ts [211-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;
+  const inviteeStart = dayjs(override.start).tz(timeZone);
+  const inviteeEnd = dayjs(override.end).tz(timeZone);
 
   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: inviteeStart.hour() * 60 + inviteeStart.minute(),
+    endTime: inviteeEnd.hour() * 60 + inviteeEnd.minute(),
   };
 });
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the manual timezone offset calculation is overly complex and proposes a much simpler, more readable, and less error-prone implementation using dayjs's built-in timezone conversion, which is a significant improvement.

Medium
Simplify date override availability check

Refactor the date override check to use dayjs's timezone-aware comparison
methods instead of manual offset calculations. This simplifies the logic for
verifying if a time slot falls within an override period.

packages/trpc/server/routers/viewer/slots.ts [105-131]

-if (
-  dateOverrides.find((date) => {
-    const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;
+const activeOverride = dateOverrides.find((override) =>
+  dayjs(override.start).tz(organizerTimeZone).isSame(slotStartTime, "day")
+);
 
-    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 (activeOverride) {
+  dateOverrideExist = true;
+  const overrideStart = dayjs(activeOverride.start).tz(organizerTimeZone);
+  const overrideEnd = dayjs(activeOverride.end).tz(organizerTimeZone);
+
+  // An override of 0 duration means the entire day is unavailable.
+  if (overrideStart.isSame(overrideEnd)) {
+    return false;
+  }
+
+  // Check if the slot is outside the override's time range.
+  if (slotStartTime.isBefore(overrideStart) || slotEndTime.isAfter(overrideEnd)) {
+    return false; // Slot is not within the date override
+  }
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion replaces a complex and potentially buggy manual timezone offset calculation with a clearer, more robust approach using dayjs for timezone-aware date comparisons, significantly improving code readability and reliability.

Medium
  • More

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants