Repository navigation
Date override fixes - #8330
Date override fixes#8330
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
| } | ||
| ); | ||
|
|
||
| const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule( |
There was a problem hiding this comment.
Here attendee has a different timezone than the organizer. This test would have failed before and fixes #8329
| const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); | ||
| const offset = inviteeUtcOffset - organizerUtcOffset; | ||
|
|
||
| return { |
There was a problem hiding this comment.
the times of activeOverrides show the date override in the organizer's availability timezone, we return the overrides in the time of the timezone set on the booking page
| //check if date override for slot exists | ||
| let dateOverrideExist = false; | ||
|
|
||
| if ( |
There was a problem hiding this comment.
If a date override exists on the day of the slot and the slot starts before that or ends after that date override it will return false (not available)
|
@hariombalhara I think you created the getSchedule tests, right? It uses |
📦 Next.js Bundle Analysis for @calcom/webThis analysis was generated by the Next.js Bundle Analysis action. 🤖 Two Pages Changed SizeThe following pages changed size from the code in this PR compared to its base branch:
DetailsOnly the gzipped size is provided here based on an expert tip. First Load is the size of the global bundle plus the bundle for the individual page. If a user were to show up to your website and land on a given page, the first load size represents the amount of javascript that user would need to download. If Any third party scripts you have added directly to your app using the The "Budget %" column shows what percentage of your performance budget the First Load total takes up. For example, if your budget was 100kb, and a given page's first load size was 10kb, it would be 10% of your budget. You can also see how much this has increased or decreased compared to the base branch of your PR. If this percentage has increased by 20% or more, there will be a red status indicator applied, indicating that special attention should be given to this. If you see "+/- <0.01%" it means that there was a change in bundle size, but it is a trivial enough amount that it can be ignored. |
Current Playwright Test Results Summary✅ 67 Passing - Run may still be in progress, this comment will be updated as current testing workflow or job completes... (Last updated on 04/18/2023 03:14:28pm UTC) Run DetailsRunning Workflow PR Update on Github Actions Commit: ee38fd2 Started: 04/18/2023 03:08:13pm UTC
|
| Test Case | Last 7 days Failures | Last 7 days Flakes |
|---|---|---|
|
Manage Booking Questions For User EventType Do a booking with a user added question and verify a few thing in b/w
Retry 1 • Initial Attempt |
1.22% (3)3 / 246 runsfailed over last 7 days |
3.25% (8)8 / 246 runsflaked over last 7 days |
📄 apps/web/playwright/embed-code-generator.e2e.ts • 1 Flake
Test Case Results
| Test Case | Last 7 days Failures | Last 7 days Flakes |
|---|---|---|
|
Embed Code Generator Tests Event Type Edit Page open Embed Dialog for the Event Type
Retry 1 • Initial Attempt |
3.16% (8)8 / 253 runsfailed over last 7 days |
22.92% (58)58 / 253 runsflaked over last 7 days |
|
Tests for the fix of #8207 are still missing. I am working on it |
| const organizerUtcOffset = dayjs(override.start.toString()).tz(organizerTimeZone).utcOffset(); | ||
| const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); |
There was a problem hiding this comment.
NIT
| const organizerUtcOffset = dayjs(override.start.toString()).tz(organizerTimeZone).utcOffset(); | |
| const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); | |
| const organizerUtcOffset = dayjs(override.start).tz(organizerTimeZone).utcOffset(); | |
| const inviteeUtcOffset = dayjs(override.start).tz(timeZone).utcOffset(); |
Not tested but do we need this to be toString? We seem to just use override.start/end everywhere
There was a problem hiding this comment.
toString and non-toString are basically identical. for Dayjs purposes I'd probably do, but it's probably all the same:
Dayjs.utc(override.start.toISOString()).tz(...
sean-brydon
left a comment
There was a problem hiding this comment.
Nice work <3 UTC gets complicated i can see this one being tricky
| time: slot.time, | ||
| ...schedule, | ||
| ...availabilityCheckProps, | ||
| organizerTimeZone: schedule.timeZone, |
There was a problem hiding this comment.
@alex we want to have the schedule's timezone here
📦 Next.js Bundle Analysis for @calcom/webThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
This reverts commit 3ef3284.
mfeuerstein
left a comment
There was a problem hiding this comment.
PR Review — approved
Reviewed 4 files. 0 high-severity issues found. Verdict: approved.
packages/types/schedule.d.ts (low)
- Reviewed packages/types/schedule.d.ts — looks good
packages/trpc/server/routers/viewer/slots.ts (low)
- Reviewed packages/trpc/server/routers/viewer/slots.ts — looks good
apps/web/test/lib/getSchedule.test.ts (low)
- Reviewed apps/web/test/lib/getSchedule.test.ts — looks good
packages/lib/slots.ts (low)
- Reviewed packages/lib/slots.ts — looks good
ron-x5labs
left a comment
There was a problem hiding this comment.
Code Review: Fix date overrides for fixed hosts (round-robin) and timezone adjustment
Problem
Date overrides of fixed hosts were ignored for round-robin events, allowing bookings when fixed hosts were unavailable (#8207). Separately, date overrides were always rendered in the organizer's UTC time regardless of the attendee's timezone (#8329/#8273).
Solution Reviewed
Two changes: (1) checkIfIsAvailable (packages/trpc/server/routers/viewer/slots.ts) now receives dateOverrides, workingHours, and organizerTimeZone, and rejects slots outside a fixed host's date-override window (or outside working hours when no override applies that day); getSchedule threads per-host timeZone into each checkIfIsAvailable call. (2) getSlots (packages/lib/slots.ts) now offsets each override's start/end by inviteeUtcOffset - organizerUtcOffset (using the per-host override.timeZone) instead of reading raw UTC hours. TimeRange.timeZone is now optional on the type.
Summary
The approach is sound and the common single-override / moderate-timezone case works, but the new checkIfIsAvailable block contains a couple of clear code defects (a dead === branch and an end computed from the wrong variable) and an edge case where large timezone gaps silently drop an override. The primary #8207 fix also has no test coverage — the added test only exercises the timezone fix on a single-user personal event.
Files Reviewed
packages/trpc/server/routers/viewer/slots.ts— deeply reviewedpackages/lib/slots.ts— deeply reviewedpackages/types/schedule.d.ts— lightly reviewed (optional field addition, backward compatible)apps/web/test/lib/getSchedule.test.ts— deeply reviewed
Verification
- Type check / tests — skipped: the review worktree has no installed
node_modulesand a fullyarn installis too expensive for a review pass. Findings are based on source inspection and trace-through of the timezone math.
Verdict
Request changes before merge — the logic is correct for the targeted case, but the dead === branch, the end = slotStartTime bug, and the lack of any test for the primary round-robin fixed-host fix should be addressed.
| slotStartTime.format("YYYY MM DD") | ||
| ) { | ||
| dateOverrideExist = true; | ||
| if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { |
There was a problem hiding this comment.
🟡 This === compares two freshly-constructed dayjs objects by reference. dayjs does not implement value equality for ===, so two distinct instances are never equal — this condition is always false and the intended zero-length-override (start === end) branch is dead code. Use .isSame(other, "minute") (or compare the underlying Date instants) if the zero-length case needs handling.
| workingHours.find((workingHour) => { | ||
| if (workingHour.days.includes(slotStartTime.day())) { | ||
| const start = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||
| const end = slotStartTime.hour() * 60 + slotStartTime.minute(); |
There was a problem hiding this comment.
🟡 end is computed from slotStartTime — identical to start on the line above — so it ignores eventLength/slotEndTime. end > workingHour.endTime therefore tests the slot start against the working-hour end, and a slot that starts within working hours but extends past workingHour.endTime is not rejected here. This should be slotEndTime.hour() * 60 + slotEndTime.minute(). Impact is currently masked because buildSlots pre-bounds generated slots to the working window, but the local check is wrong and will mis-fire if slots reach this function by another path.
| let dateOverrideExist = false; | ||
|
|
||
| if ( | ||
| dateOverrides.find((date) => { |
There was a problem hiding this comment.
🟡 Multiple date overrides on the same day for one host are mishandled. dateOverrides.find does not stop once a covering override is found: a slot that is inside override A (callback returns nothing for A) is then evaluated against override B on the same day, and B's "slot is outside this window" branch (lines 117-121 / 123) returns true, so find returns truthy and the valid slot is marked unavailable (return false). Only the single-override-per-day case works. The loop should conclude "available" as soon as any override on the matching day fully contains the slot.
| const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; | ||
|
|
||
| if ( | ||
| dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") === |
There was a problem hiding this comment.
🟡 dayjs(date.start) parses the Date in the server's local timezone (no .utc()), then .format("YYYY MM DD") is compared against slotStartTime.format(...) where slotStartTime = time.utc() (UTC). On a non-UTC host this can produce an off-by-one-day match near midnight and mis-classify whether a slot falls on an override day, silently dropping the override. The isBefore/isAfter checks below use absolute instants and are unaffected. Use dayjs.utc(date.start) so the date derivation is server-independent. (Production is typically UTC, so impact is latent.)
|
|
||
| return { | ||
| userIds: override.userId ? [override.userId] : [], | ||
| startTime: |
There was a problem hiding this comment.
🟡 The offset conversion can wrap past midnight for large timezone gaps. dayjs(override.start).utc().add(offset, "minute").hour() re-extracts the hour (mod 24), so when the shifted window straddles midnight in the invitee frame, startTime ends up greater than endTime (e.g. a 09:00-17:00 organizer override with a +13h offset becomes 22:00-06:00 → startTime 1320, endTime 360). buildSlots then drops the whole override via if (start >= end) continue, so the override day silently shows zero availability for far-apart tz pairs. Consider handling the overflow the way getWorkingHours does (split into prev/next day using MINUTES_IN_DAY) rather than a bare hour()*60+minute() extraction.
| } | ||
| ); | ||
|
|
||
| const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule( |
There was a problem hiding this comment.
🟡 This new assertion only covers the timezone-adjustment fix (#8329): it re-queries the existing single-user personal event (no schedulingType, hosts: []) with a +6:00 invitee timezone. It does not cover the primary #8207 fix — date overrides of fixed hosts in round-robin events. The only ROUND_ROBIN test in the suite uses hosts: [] (line ~1109), so fixedHosts = userAvailability.filter(a => a.user.isFixed) is empty and fixedHosts.every(checkIfIsAvailable(...)) is a vacuous no-op; the new checkIfIsAvailable date-override/working-hours block (slots.ts lines 102-151) is never reached on the round-robin path. Please add a test with a round-robin event where a fixed host has a date override that restricts slots while a loose host remains available.
ron-x5labs
left a comment
There was a problem hiding this comment.
Code Review: Fix date overrides for fixed hosts (round-robin) and timezone adjustment
Problem
Date overrides of fixed hosts were ignored for round-robin/collective events (#8207), and date overrides were always rendered in the organizer's UTC time regardless of the attendee's timezone (#8329/#8273).
Solution Reviewed
Two changes: (1) checkIfIsAvailable (packages/trpc/server/routers/viewer/slots.ts) now receives dateOverrides, workingHours, and organizerTimeZone and rejects slots outside a fixed host's date-override window (or outside working hours when no override applies that day); getSchedule threads per-host timeZone into each call. (2) getSlots (packages/lib/slots.ts) converts override start/end from the organizer's offset to the invitee's offset so slots land at the correct UTC instant, and TimeRange gains an optional timeZone.
Summary
The timezone-conversion in getSlots is correct and the new single-host test guards it. However, the new date-override/working-hours validation added to checkIfIsAvailable has several logic defects that, for the PR's core multi-host scenario (collective/round-robin with per-host overrides), both over-reject and under-reject slots. The working-hours fallback is also broken for split-shift schedules. Recommend changes before merge.
Files Reviewed
packages/trpc/server/routers/viewer/slots.ts— deeply reviewed (high risk: availability logic)packages/lib/slots.ts— deeply reviewed (medium risk: slot generation)apps/web/test/lib/getSchedule.test.ts— deeply reviewed (test coverage)packages/types/schedule.d.ts— lightly reviewed (type addition)
Verification
- Test suite: skipped — repository dependencies are not installed in this review environment (
node_modulesabsent), soTZ=UTC yarn testcould not be executed. Findings are grounded by readingcheckIfIsAvailable,getSlots,getWorkingHours(packages/lib/availability.ts), andgetUserAvailability(packages/core/getUserAvailability.ts) in the PR worktree. - dayjs reference-equality behavior confirmed against dayjs semantics (each
.add()returns a new wrapper instance).
Issues Found
See inline comments. Summary:
- Blocking: date-override block rejects all slots with 2+ same-day overrides and accepts partial-overlap slots; working-hours
endcomputed fromslotStartTimeinstead ofslotEndTime;workingHours.findrejects every slot on split-shift days. - Non-blocking:
===between two dayjs instances is always false (dead zero-length guard); override day-match compares a shifteddate.startagainst the slot's raw UTC date, mis-detecting overrides whose window crosses UTC midnight. - Test coverage: no test exercises the fixed-host round-robin/collective date-override case (#8207); the timezone assertion only varies the invitee tz by 30 minutes (single host, no cross-day-boundary case).
Verdict
Recommend changes before merge — the override/working-hours validation in checkIfIsAvailable is incorrect for the multi-host and split-shift cases this PR targets; the slot-generation timezone fix itself is sound.
| let dateOverrideExist = false; | ||
|
|
||
| if ( | ||
| dateOverrides.find((date) => { |
There was a problem hiding this comment.
🔴 Blocking — multiple same-day overrides reject all slots, and partial overlaps are accepted.
dateOverrides.find(cb) returns the first override whose callback returns truthy, and the callback returns truthy when the slot is outside that override (slotEndTime <= o.start or slotStartTime > o.end). With two same-day overrides (e.g. host A 10:00–11:00 and host B 14:00–15:00, which is exactly the collective/round-robin case this PR targets since dateOverrides is the flatMap across all hosts), a slot fully inside A returns undefined for A but true for B → find returns B → return false. Every slot is outside at least one same-day override, so a fixed host with 2+ same-day overrides (own or other hosts') gets zero availability that day.
Separately, the boundary checks only reject slots entirely-before or entirely-after an override, so a slot that partially overlaps (e.g. 09:30–10:30 vs override 10:00–11:00, generated from the aggregate window of another host) makes the callback return undefined, dateOverrideExist is true, and line 133 returns true — booking a slot that extends outside the fixed host's override, the exact thing this PR prevents.
Fix: collect same-day overrides, then require containment in at least one:
const slotLocalDate = organizerTimeZone ? time.tz(organizerTimeZone).format("YYYY MM DD") : slotStartTime.format("YYYY MM DD");
const sameDay = dateOverrides.filter((d) =>
(organizerTimeZone ? dayjs.tz(d.start, organizerTimeZone).format("YYYY MM DD") : dayjs(d.start).format("YYYY MM DD")) === slotLocalDate
);
if (sameDay.length) {
const contained = sameDay.some((d) => {
const oStart = organizerTimeZone ? dayjs.tz(d.start, organizerTimeZone) : dayjs(d.start);
const oEnd = organizerTimeZone ? dayjs.tz(d.end, organizerTimeZone) : dayjs(d.end);
return !slotStartTime.isBefore(oStart) && !slotEndTime.isAfter(oEnd);
});
if (!contained) return false;
return busy.every(/* ...existing busy check... */);
}| workingHours.find((workingHour) => { | ||
| if (workingHour.days.includes(slotStartTime.day())) { | ||
| const start = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||
| const end = slotStartTime.hour() * 60 + slotStartTime.minute(); |
There was a problem hiding this comment.
🔴 Blocking — end is computed from slotStartTime, not slotEndTime.
Line 141 sets start = slotStartTime.hour()*60 + slotStartTime.minute() and line 142 sets end to the same expression, so end === start. The guard end > workingHour.endTime therefore degenerates to start > workingHour.endTime, which only catches slots that start after the block end. A slot that starts before workingHour.endTime but whose end (start + eventLength) exceeds it passes the filter and is reported available — e.g. working hours 09:00–17:00, 60-min event, slot 16:30 → start=990, end=990 (should be 1050), 990 > 1020 is false → 16:30–17:30 offered though it overruns 17:00. Because checkIfIsAvailable runs per fixed host with that host's own workingHours while slots are generated from the aggregate working hours, a slot that fits the aggregate but overruns this host's narrower block-end is wrongly offered.
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotEndTime.hour() * 60 + slotEndTime.minute();| } | ||
|
|
||
| //if no date override for slot exists check if it is within normal work hours | ||
| if ( |
There was a problem hiding this comment.
🔴 Blocking — workingHours.find rejects every slot on split-shift days.
The callback returns true (→ find truthy → slot unavailable) when the slot is out of a block. For a split shift with two same-day blocks (09:00–12:00 and 13:00–17:00, both days:[1]), a slot inside block 1 makes block 1 return undefined, then block 2 returns true because start < workingHour.startTime (600 < 780). find returns the first truthy → the slot is declared outside working hours. Since the blocks are disjoint, every slot is outside at least one block, so all slots on a multi-block day are filtered out. getWorkingHours (packages/lib/availability.ts) explicitly emits multiple WorkingHours entries per day for overflow/cross-midnight schedules, so this is reachable.
Invert to "available if any block contains the slot":
const inSomeBlock = workingHours.some((wh) => {
if (!wh.days.includes(slotStartTime.day())) return false;
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotEndTime.hour() * 60 + slotEndTime.minute();
return start >= wh.startTime && end <= wh.endTime;
});
if (!inSomeBlock) {
return false;
}| slotStartTime.format("YYYY MM DD") | ||
| ) { | ||
| dateOverrideExist = true; | ||
| if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { |
There was a problem hiding this comment.
🟡 === compares object identity. dayjs(date.start).add(...) and dayjs(date.end).add(...) each allocate a new Dayjs wrapper, so this condition is always false and the return true on line 115 is unreachable dead code — the intended zero-length-override short-circuit never fires.
if (dayjs(date.start).add(utcOffset, "minutes").isSame(dayjs(date.end).add(utcOffset, "minutes"))) {
return true;
}|
|
||
| if ( | ||
| dateOverrides.find((date) => { | ||
| const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; |
There was a problem hiding this comment.
🟡 The override day-match compares a shifted date.start against the slot's raw UTC date, which mis-detects overrides whose window crosses UTC midnight.
date.start/date.end are storage artifacts built as dayjs.utc(override.date).hour(h).minute(m) (packages/core/getUserAvailability.ts:221), so their UTC wall-clock equals the organizer-local clock. utcOffset = dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 then subtracts the organizer offset, yielding the real UTC instant of the override start, and its UTC date is compared to slotStartTime.format("YYYY MM DD") (the slot's UTC date). For an organizer in +5:30 with an override 05:00–07:00 IST on date D, the real start is 23:30 UTC on D-1, so A = D-1; a valid slot at 06:00 IST = 00:30 UTC on D has B = D; A != B → override not detected → the slot falls through to the working-hours check and is rejected (06:00 is outside 09:30–18:00). Compare organizer-local dates on both sides instead:
const overrideLocalDate = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).format("YYYY MM DD") : dayjs(date.start).format("YYYY MM DD");
const slotLocalDate = organizerTimeZone ? time.tz(organizerTimeZone).format("YYYY MM DD") : slotStartTime.format("YYYY MM DD");
if (overrideLocalDate === slotLocalDate) { ... }| } | ||
| ); | ||
|
|
||
| const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule( |
There was a problem hiding this comment.
💡 The added test is a single-user, non-team event (hosts: [], no schedulingType), so it never enters the eventType.hosts.map(({ isFixed, user }) => …) branch in getSchedule and never exercises getAggregateWorkingHours(..., ROUND_ROBIN) with date overrides — i.e. the PR's primary stated fix (#8207, "date overrides of fixed hosts are taken into account for round-robin events") is not covered. The only ROUND_ROBIN test in this file uses IstWorkHours (no override). The timezone sub-assertion (lines 788–804) does guard the override.timeZone offset fix, but only with a 30-minute invitee-tz delta and a single host, so cross-day-boundary selection of activeOverrides is untested.
Suggest adding a team scenario: schedulingType: 'ROUND_ROBIN', two hosts (one fixed with IstWorkHoursWithDateOverride(date), one loose with different hours), asserting the fixed host's override restricts the shared slot set and the loose host still contributes slots the override removed; mirror for 'COLLECTIVE'. Also add an assertion pair with a large tz delta (e.g. organizer +5:30, invitee -08:00) where the invitee day boundary moves relative to the override date.
ron-x5labs
left a comment
There was a problem hiding this comment.
Code Review: Fix date overrides for fixed hosts (round-robin) and timezone adjustment
Problem
Date overrides of fixed hosts were ignored for round-robin events (#8207), and date overrides were always rendered in the organizer's UTC time regardless of the attendee's timezone (#8329/#8273).
Solution Reviewed
Two changes: (1) checkIfIsAvailable (packages/trpc/server/routers/viewer/slots.ts) now receives dateOverrides, workingHours, and organizerTimeZone, and validates slots against a fixed host's date-override window and working hours; getSchedule threads per-host timeZone into each call. (2) getSlots (packages/lib/slots.ts) converts override start/end from the organizer's offset to the invitee's offset. TimeRange gains an optional timeZone field.
Summary
The timezone-conversion in getSlots is correct and the new single-host test guards it. However, the new date-override/working-hours validation in checkIfIsAvailable has several logic defects: a return true that bypasses the busy check (enabling double-booking), a copy-paste bug computing end from the wrong variable, and a workingHours.find pattern that rejects all slots on split-shift days. The primary #8207 fix also lacks test coverage.
Files Reviewed
packages/trpc/server/routers/viewer/slots.ts— deeply reviewed (high risk: availability logic)packages/lib/slots.ts— deeply reviewed (medium risk: slot generation)packages/types/schedule.d.ts— lightly reviewed (optional field, backward compatible)apps/web/test/lib/getSchedule.test.ts— deeply reviewed (test coverage)
Verification
- Type check / tests — skipped: no
node_modulesin the review worktree; a fullyarn installis too expensive for a review pass. Findings are grounded by source inspection and trace-through ofcheckIfIsAvailable,getSlots,buildSlots,getWorkingHours,getAggregateWorkingHours, andgetUserAvailability.
Issues Found
See inline comments. Summary:
- 🔴 Blocking:
return trueat line 134 bypasses the busy check during override windows (double-booking);endcomputed fromslotStartTimeinstead ofslotEndTime(line 142);workingHours.findoutside-semantics rejects all slots on multi-block/split-shift days (line 139). - 🟡 Non-blocking:
===between dayjs instances is always false (line 114, dead code);dayjs(date.start)without.utc()parses in server-local tz (line 110); offset conversion wraps past midnight for large tz gaps (lib/slots.ts:219). - 💡 Suggestion: the added test doesn't exercise the primary #8207 fix (round-robin fixed host with date override).
Verdict
Recommend changes before merge — the override/working-hours validation in checkIfIsAvailable is incorrect for the multi-block and busy-conflict cases this PR targets; the slot-generation timezone fix itself is sound.
| } | ||
|
|
||
| if (dateOverrideExist) { | ||
| return true; |
There was a problem hiding this comment.
🔴 When a fixed host has a date override on a day, slots within the override window set dateOverrideExist = true and hit return true here — before the busy.every(...) check at line 153. The slot is marked available even if the host has a conflicting booking during the override, enabling double-booking. Fall through to the busy check instead of returning early:
if (dateOverrideExist) {
// Override replaces working hours, but still check for conflicting bookings
return busy.every((busyTime) => { /* ...existing busy check... */ });
}| workingHours.find((workingHour) => { | ||
| if (workingHour.days.includes(slotStartTime.day())) { | ||
| const start = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||
| const end = slotStartTime.hour() * 60 + slotStartTime.minute(); |
There was a problem hiding this comment.
🔴 end is computed from slotStartTime — identical to start on the line above — so it ignores eventLength/slotEndTime. The guard end > workingHour.endTime only catches slots that start after the block end; a slot that starts within working hours but extends past workingHour.endTime passes (e.g. working hours end 17:00, 60-min event, slot 16:30 → end=990 should be 1050, 990 > 1020 is false → offered though it overruns). Use slotEndTime:
const end = slotEndTime.hour() * 60 + slotEndTime.minute();|
|
||
| //if no date override for slot exists check if it is within normal work hours | ||
| if ( | ||
| workingHours.find((workingHour) => { |
There was a problem hiding this comment.
🔴 workingHours.find returns the first block the slot is outside, so a slot inside block 1 is rejected by block 2 on split-shift days. getWorkingHours (packages/lib/availability.ts) explicitly emits multiple WorkingHours entries for the same day for cross-midnight schedules. Since the blocks are disjoint, every slot is outside at least one block → all slots on multi-block days are filtered out. Use some with inside-semantics:
const inSomeBlock = workingHours.some((wh) => {
if (!wh.days.includes(slotStartTime.day())) return false;
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
const end = slotEndTime.hour() * 60 + slotEndTime.minute();
return start >= wh.startTime && end <= wh.endTime;
});
if (!inSomeBlock) return false;| slotStartTime.format("YYYY MM DD") | ||
| ) { | ||
| dateOverrideExist = true; | ||
| if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { |
There was a problem hiding this comment.
🟡 === compares object identity. dayjs(date.start).add(...) and dayjs(date.end).add(...) each allocate a new Dayjs wrapper, so this is always false — the zero-length-override short-circuit is dead code. Use .isSame(..., "minute") if the zero-length case needs handling.
| const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; | ||
|
|
||
| if ( | ||
| dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") === |
There was a problem hiding this comment.
🟡 dayjs(date.start) parses the Date in the server's local timezone (no .utc()), then the formatted date is compared against slotStartTime which is UTC. On a non-UTC server this can produce an off-by-one-day match near midnight and mis-detect whether a slot falls on an override day. Use dayjs.utc(date.start) so the date derivation is server-independent (production is typically UTC, so impact is latent).
| return { | ||
| userIds: override.userId ? [override.userId] : [], | ||
| startTime: | ||
| dayjs(override.start).utc().add(offset, "minute").hour() * 60 + |
There was a problem hiding this comment.
🟡 The offset conversion can wrap past midnight for large timezone gaps. dayjs(override.start).utc().add(offset, "minute").hour() re-extracts the hour (mod 24), so when the shifted window straddles midnight in the invitee frame, startTime ends up greater than endTime and buildSlots drops the whole override via if (start >= end) continue — the override day shows zero availability for far-apart tz pairs. Consider handling the overflow the way getWorkingHours does (split into prev/next day using MINUTES_IN_DAY).
| } | ||
| ); | ||
|
|
||
| const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule( |
There was a problem hiding this comment.
💡 This test is a single-user personal event (hosts: [], no schedulingType), so fixedHosts = userAvailability.filter(a => a.user.isFixed) is empty and the new checkIfIsAvailable date-override/working-hours block is never reached on the round-robin path. The PR's primary fix (#8207 — date overrides of fixed hosts in round-robin events) is not covered. Suggest adding a team scenario: schedulingType: 'ROUND_ROBIN', one fixed host with a date override and one loose host with different hours, asserting the fixed host's override restricts the shared slot set.

What does this PR do?
This PR fixes that date overrides of fixed hosts are taken into account for round-robin events. This prevents round-robin events being booked where the fixed hosts are unavailable.
This PR also fixes that date overrides didn't adjust to different timezones. If the organizer has a date override from 10-11 UTC it always showed the slots from 10-11 open no matter what timezone the attendee has.
Fixes #8207
Fixes #8329
Fixes: #8273
Environment: Staging(main branch) / Production
Type of change
How should this be tested?
Date overrides for fixed hosts (round-robin):
Date override timezones issue: