Advanced date override handling and timezone compatibility improvements
calcom/cal.com#8330
Golden recall: 50%
Extra findings: 5
Golden Comments (1/2 found)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | Incorrect end time calculation using slotStartTime instead of slotEndTime |
MEDIUM | ✓ Found | "end is computed from slotStartTime instead of slotEndTime (copy-paste error), making start and end identical. The right-hand bound test (end > workingHour.endTime) thus compares the slot start against the range end, so a slot that begins before but extends past the range end is not rejected. Currently masked by getSlots never emitting overflowing slots, this is still a latent defect" (packages/trpc/server/routers/viewer/slots.ts L142) |
| #2 | Using === for dayjs object comparison will always return false as it compares object references, not values. Use .isSame() method instead: dayjs(date.start).add(utcOffset, 'minutes').isSame(dayjs(date.end).add(utcOffset, minutes)) |
MEDIUM | ✗ Not found | The review critiques slots.ts at length — the short-circuit before the busy check (L133), the inverted workingHours check (L143), the end-time copy-paste (L142) — and lib/slots.ts (DST offset L219, optional timeZone L212), but no candidate mentions the comparison by reference: start === end on two dayjs objects compares object references and always returns false, where .isSame() is required. See packages/trpc/server/routers/viewer/slots.ts:114 |
Supplementary Findings (5 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
Slots inside a date override short-circuit before the busy check: when dateOverrideExist is true, return true exits before busy.every(...) runs — a fixed host with an existing booking overlapping their own override is still offered as available, a double-booking hazard for round-robin events |
slots.ts:133 | HIGH | ✓ Real — double-booking hazard; the most severe finding of the review |
The workingHours check uses find and marks the slot unavailable if it violates ANY range, instead of requiring it to be inside at least one range — hosts with split working hours lose valid slots; regression affects all personal/fixed/loose hosts, not only override days |
slots.ts:143 | HIGH | ✓ Real — inverted check removes valid slots for split-schedule hosts |
The date-override timezone shift computes offset once from offsets at override.start only — wrong if the window spans a DST transition; hour() * 60 + minute() also silently wraps a window crossing midnight (e.g. 23:30 + 60 min → 00:30) |
lib/slots.ts:219 | MEDIUM | ✓ Real — DST and midnight rollover in the shifted coordinate space |
The added test only exercises a personal event (single user, no ROUND_ROBIN, hosts: []) — the PR's primary scenario (round-robin with fixed host + loose host and a date override) is not covered |
getSchedule.test.ts:739 | MEDIUM | ✓ Real — the headline scenario of the PR is untested |
override.timeZone is accessed unconditionally while TimeRange.timeZone is optional in the public GetSlots type — any future caller without it would call .tz(undefined) and misbehave |
slots.ts:212 | LOW | ✓ Borderline: defensive nit; the only current caller always passes timeZone |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| 🌥️ Tech | 🌥️ Feat |
|---|---|
| 2 high, 3 medium, 1 low | 1 issue, 1 mitigated |
The MR correctly threads timeZone into date overrides and adds a router-side guard to exclude fixed hosts' slots outside their override window, and the new tests cover a timezone-shift case. However, the new availability logic in the router contains three defects — a short-circuit that skips the busy check (double-booking risk), an inverted workingHours check that removes valid slots, and an end-time copy-paste error — and the test suite does not cover the headline round-robin fixed-host scenario. These issues should be addressed before merge.
These might need a close look
- 🔴 packages/trpc/server/routers/viewer/slots.ts (L133)
Slots inside a date override short-circuit before the busy check: whendateOverrideExistis true,return trueexits beforebusy.every(...)runs, so a fixed host with an existing calendar/Cal booking overlapping their own override is still offered as available — a double-booking hazard for round-robin events. A date override should only override working hours, not suppress the busy-time check. - 🔴 packages/trpc/server/routers/viewer/slots.ts (L143)
The workingHours check usesfindand marks the slot unavailable if it violates ANY range, instead of requiring it to be inside at least one range. For hosts with split working hours on the same day (e.g. 09:00–13:00 and 14:00–18:00), a slot in the second range is wrongly removed. This regression affects all personal/fixed/loose hosts, not only override days.
Worth checking
- 🟡 packages/trpc/server/routers/viewer/slots.ts (L142)
endis computed fromslotStartTimeinstead ofslotEndTime(copy-paste error), makingstartandendidentical. The right-hand bound test (end > workingHour.endTime) thus compares the slot start against the range end, so a slot that begins before but extends past the range end is not rejected. Currently masked bygetSlotsnever emitting overflowing slots, this is still a latent defect. - 🟡 packages/lib/slots.ts (L219)
The date-override timezone shift computesoffsetonce from offsets atoverride.startonly, which is wrong if the override window spans a DST transition in either zone. Alsohour() * 60 + minute()rolls over after 24:00, silently wrapping a window that crosses midnight in the shifted coordinate space (e.g. 23:30 + 60 min → 00:30), producing an empty/truncated/mis-placed window. Consider clamping/segmenting across midnight rather than mod-wrapping. - 🟡 apps/web/test/lib/getSchedule.test.ts (L739)
The added test only exercises a personal event (single user, noROUND_ROBIN,hosts: []). The PR's primary scenario — a round-robin event with a fixed host + loose host, a date override on the fixed host, and an assertion that slots where the fixed host is unavailable are removed — is not covered. Please add aROUND_ROBINtest withisFixedhost and date override.
Small things (take or leave)
- 🔵 packages/lib/slots.ts (L212)
override.timeZoneis accessed unconditionally whileTimeRange.timeZoneis optional in the publicGetSlotstype. Today the only caller passingdateOverridesalways suppliestimeZone, but any future caller without it would call.tz(undefined)and misbehave. Either maketimeZonerequired on the override type or guard the access. Relatedly, the router'scheckIfIsAvailablesignature typesdateOverridesas{ start: Date; end: Date }[], omittinguserId/timeZoneactually present — align the type withTimeRange.
✅ Feature-level checklist
Not there yet
- ❌ Date overrides of fixed hosts are taken into account for round-robin events, preventing round-robin events being booked where the fixed hosts are unavailable.
The newcheckIfIsAvailableguard short-circuits on override slots before the busy check, so a fixed host with an existing booking overlapping their override can still be offered — a double-booking hazard. The workingHours check is also inverted and the end time is computed from slotStartTime, which can wrongly remove or fail to reject slots. The test suite does not exercise the round-robin fixed-host scenario.
Partially covered
- ⚠️ Date overrides adjust to different timezones (available slots on the booking page change according to the invitee's timezone).
The implementation threadstimeZoneand applies an offset, and a Dhaka +6:00 test is covered. However, the offset is computed only at override start, which may be incorrect across DST transitions, and past-midnight rollover is not handled. Alsooverride.timeZoneis accessed unconditionally while the type is optional.
FriendlyReviewer found 1 of the 2 golden comments of this PR — recall 50%. Golden #1 (the
slotStartTime/slotEndTimecopy-paste) is found atslots.tsL142 with the full mechanics:startandendare identical, the right-hand bound test compares the slot start against the range end, and the defect is currently masked bygetSlotsnever emitting overflowing slots. Golden #2 (comparing two dayjs objects with===) is missed.The miss says a lot about where the review's attention went. It critiques the same file at length — the short-circuit before the busy check (L133), the inverted workingHours check (L143), the timezone-shift math (lib/slots.ts L219), the optional
timeZoneaccess (L212) — but no candidate examines the semantics of the equality comparison:start === endon two dayjs objects compares references and always returns false, where.isSame()is needed. The two high findings it did produce are genuine and arguably more severe than either golden: the L133 short-circuit can double-book a fixed host, and the L143 invertedfind-based check removes valid slots for any host with split working hours — a regression affecting all hosts, not just override days.The most useful supplementary item is the test-coverage finding (getSchedule.test.ts L739): the PR's headline scenario — a round-robin event with a fixed host and a date override — is never exercised by the new test, which only covers a personal event. That is exactly the behavior the review's own high findings put at risk. Overall the review is strong on control flow and arithmetic but blind to value-comparison semantics, which is precisely where golden #2 sits.