-
Notifications
You must be signed in to change notification settings - Fork 0
Advanced date override handling and timezone compatibility improvements #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: date-algorithm-base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -208,11 +208,22 @@ const getSlots = ({ | |||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!!activeOverrides.length) { | ||||||||||||||||||
| const overrides = activeOverrides.flatMap((override) => ({ | ||||||||||||||||||
| userIds: override.userId ? [override.userId] : [], | ||||||||||||||||||
| startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(), | ||||||||||||||||||
| endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(), | ||||||||||||||||||
| })); | ||||||||||||||||||
| const overrides = activeOverrides.flatMap((override) => { | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||
| const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset(); | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — DST Sensitivity (confidence: 100%) The UTC offset is computed using only Evidence:
Agent: architecture |
||||||||||||||||||
| const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code patterns (confidence: 100%) The UTC offset is calculated only once from Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -210,15 +210,20 @@ const getSlots = ({
if (!!activeOverrides.length) {
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;
+ // Compute offsets independently for start and end to handle DST transitions:
+ // if a DST boundary falls within the override, the UTC offset may differ between
+ // the two endpoints, so we must not reuse the start offset for the end calculation.
+ const organizerStartOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
+ const inviteeStartOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
+ const startOffset = inviteeStartOffset - organizerStartOffset;
+
+ const organizerEndOffset = dayjs(override.end.toString()).tz(override.timeZone).utcOffset();
+ const inviteeEndOffset = dayjs(override.end.toString()).tz(timeZone).utcOffset();
+ const endOffset = inviteeEndOffset - organizerEndOffset;
return {
userIds: override.userId ? [override.userId] : [],
startTime:
- dayjs(override.start).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.start).utc().add(offset, "minute").minute(),
+ dayjs(override.start).utc().add(startOffset, "minute").hour() * 60 +
+ dayjs(override.start).utc().add(startOffset, "minute").minute(),
endTime:
- dayjs(override.end).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.end).utc().add(offset, "minute").minute(),
+ dayjs(override.end).utc().add(endOffset, "minute").hour() * 60 +
+ dayjs(override.end).utc().add(endOffset, "minute").minute(),
};
});🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||
| const offset = inviteeUtcOffset - organizerUtcOffset; | ||||||||||||||||||
|
|
||||||||||||||||||
| return { | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code patterns (confidence: 100%) Adding the offset delta and converting to minute-of-day does not use modular arithmetic. If the offset delta is large enough to shift a time past midnight, the minute-of-day calculation will wrap incorrectly (e.g., hour() will become negative or exceed 23). This is especially problematic for midnight-spanning overrides or large UTC offset differences. Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -210,16 +210,22 @@ const getSlots = ({
if (!!activeOverrides.length) {
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 adjustedStart = dayjs(override.start).utc().add(offset, "minute");
+ const adjustedEnd = dayjs(override.end).utc().add(offset, "minute");
+
+ // Use modular arithmetic to correctly handle times that cross midnight after
+ // the UTC offset is applied (e.g., 23:00 UTC + 120min offset = 01:00 next day).
+ // diff from startOf('day') gives true minute-of-day; % (24*60) keeps it in [0, 1440).
+ const startTime = Math.abs(adjustedStart.diff(adjustedStart.startOf("day"), "minute")) % (24 * 60);
+ const endTime = Math.abs(adjustedEnd.diff(adjustedEnd.startOf("day"), "minute")) % (24 * 60);
+
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,
+ endTime,
};
});🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||
| userIds: override.userId ? [override.userId] : [], | ||||||||||||||||||
| startTime: | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Sign Convention Inconsistency (confidence: 100%) UTC offset calculation is inconsistent between checkIfIsAvailable and getSlots. In checkIfIsAvailable (slots.ts line 109), offset is negated with Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||
| 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 + | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Edge cases (confidence: 100%) The offset calculation in Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Edge cases (confidence: 100%) When the offset delta shifts override start/end past midnight, the minute-of-day calculation ( Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Midnight Wrapping / Negative Minutes (confidence: 100%) The offset delta is applied to the override times and then Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -208,22 +208,27 @@ const getSlots = ({
});
if (!!activeOverrides.length) {
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;
+ // Compute offsets independently at start and end instants to correctly handle
+ // DST transitions that may occur within the override window (e.g., a full-day
+ // override on a clock-change day). Using override.start for both offsets would
+ // apply the wrong UTC offset to endTime after a DST boundary is crossed.
+ const startOrganizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
+ const startInviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
+ const startOffset = startInviteeUtcOffset - startOrganizerUtcOffset;
+
+ const endOrganizerUtcOffset = dayjs(override.end.toString()).tz(override.timeZone).utcOffset();
+ const endInviteeUtcOffset = dayjs(override.end.toString()).tz(timeZone).utcOffset();
+ const endOffset = endInviteeUtcOffset - endOrganizerUtcOffset;
return {
userIds: override.userId ? [override.userId] : [],
startTime:
- dayjs(override.start).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.start).utc().add(offset, "minute").minute(),
+ dayjs(override.start).utc().add(startOffset, "minute").hour() * 60 +
+ dayjs(override.start).utc().add(startOffset, "minute").minute(),
endTime:
- dayjs(override.end).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.end).utc().add(offset, "minute").minute(),
+ dayjs(override.end).utc().add(endOffset, "minute").hour() * 60 +
+ dayjs(override.end).utc().add(endOffset, "minute").minute(),
};
});
// unset all working hours that relate to this user availability override
overrides.forEach((override) => {
let i = -1;🤖 Grapple PR auto-fix • major • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -208,22 +208,30 @@ const getSlots = ({
});
if (!!activeOverrides.length) {
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 adjustedStart = dayjs(override.start).utc().add(offset, "minute");
+ const adjustedEnd = dayjs(override.end).utc().add(offset, "minute");
+
+ // Use startOfInviteeDay as the reference point so that times crossing
+ // midnight are represented as minutes since day-start (possibly > 1439
+ // for next-day overflow, or negative for previous-day underflow).
+ // This avoids the wrap-around bug where hour()*60+minute() gives a small
+ // value (e.g. 30) when the time has crossed past midnight, making
+ // endTime < startTime and producing zero slots.
+ const startTime = adjustedStart.diff(startOfInviteeDay, "minute");
+ const endTime = adjustedEnd.diff(startOfInviteeDay, "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,
+ endTime,
};
});
// unset all working hours that relate to this user availability override
overrides.forEach((override) => {
let i = -1;🤖 Grapple PR auto-fix • major • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -208,22 +208,36 @@ const getSlots = ({
});
if (!!activeOverrides.length) {
- 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(),
- };
- });
-
+ const overrides = activeOverrides.flatMap((override) => {
+ // Convert the override's absolute UTC timestamps into the invitee's local
+ // timezone so that hour/minute extraction reflects the correct calendar day
+ // for the invitee. Using .tz(timeZone) instead of manual offset arithmetic
+ // correctly handles DST transitions and midnight-wrapping: if a 23:00 UTC
+ // override becomes 01:00 in the invitee's zone, the minute-of-day value
+ // (60) would be less than a startTime computed on the same day, producing
+ // an invalid range. We detect and discard such out-of-day overrides below.
+ const startInInviteeTz = dayjs(override.start).tz(timeZone);
+ const endInInviteeTz = dayjs(override.end).tz(timeZone);
+
+ const startTime = startInInviteeTz.hour() * 60 + startInInviteeTz.minute();
+ const endTime = endInInviteeTz.hour() * 60 + endInInviteeTz.minute();
+
+ // Guard: if the timezone conversion pushed the override entirely past
+ // midnight (end wraps to a smaller minute-of-day than start, or the
+ // converted date no longer falls on the invitee's requested day), the
+ // downstream slot generator would produce an empty or inverted range.
+ // Return an empty array so flatMap drops this override silently.
+ if (endTime <= startTime) {
+ return [];
+ }
+
+ return [
+ {
+ userIds: override.userId ? [override.userId] : [],
+ startTime,
+ endTime,
+ },
+ ];
+ });
+
// unset all working hours that relate to this user availability override
overrides.forEach((override) => {
let i = -1;🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||
| dayjs(override.end).utc().add(offset, "minute").minute(), | ||||||||||||||||||
| }; | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Logic Bug — DST-Sensitive Offset Using Only Override Start (confidence: 100%) The offset calculation in Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Logic Bug — Midnight-Spanning Override Wraps Without Modular Arithmetic (confidence: 100%) When the UTC offset delta causes an override's end time to cross midnight, the minute-of-day calculation using Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -210,18 +210,22 @@ const getSlots = ({
if (!!activeOverrides.length) {
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;
+ // Compute offsets independently at each boundary to handle DST transitions
+ // that may occur within the override window (e.g., a 3-hour override spanning
+ // a clock change where utcOffset() differs by 60 minutes at start vs. end).
+ const startOrganizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
+ const startInviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
+ const startOffset = startInviteeUtcOffset - startOrganizerUtcOffset;
+
+ const endOrganizerUtcOffset = dayjs(override.end.toString()).tz(override.timeZone).utcOffset();
+ const endInviteeUtcOffset = dayjs(override.end.toString()).tz(timeZone).utcOffset();
+ const endOffset = endInviteeUtcOffset - endOrganizerUtcOffset;
return {
userIds: override.userId ? [override.userId] : [],
startTime:
- dayjs(override.start).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.start).utc().add(offset, "minute").minute(),
+ dayjs(override.start).utc().add(startOffset, "minute").hour() * 60 +
+ dayjs(override.start).utc().add(startOffset, "minute").minute(),
endTime:
- dayjs(override.end).utc().add(offset, "minute").hour() * 60 +
- dayjs(override.end).utc().add(offset, "minute").minute(),
+ dayjs(override.end).utc().add(endOffset, "minute").hour() * 60 +
+ dayjs(override.end).utc().add(endOffset, "minute").minute(),
};
});🤖 Grapple PR auto-fix • major • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -208,22 +208,32 @@ const getSlots = ({
});
if (!!activeOverrides.length) {
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 adjustedStart = dayjs(override.start).utc().add(offset, "minute");
+ const adjustedEnd = dayjs(override.end).utc().add(offset, "minute");
+
+ // If the UTC offset adjustment pushes the time past midnight, .hour() * 60 + .minute()
+ // wraps back to a small value (e.g., 01:00 → 60 instead of 1500). We detect day overflow
+ // by comparing the adjusted date's day-of-year to the original UTC date's day-of-year and
+ // add 1440 minutes per day crossed so that endTime > startTime always holds.
+ const startDayDelta =
+ adjustedStart.diff(dayjs(override.start).utc().startOf("day"), "day");
+ const endDayDelta =
+ adjustedEnd.diff(dayjs(override.end).utc().startOf("day"), "day");
+
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() +
+ startDayDelta * 1440,
+ endTime:
+ adjustedEnd.hour() * 60 +
+ adjustedEnd.minute() +
+ endDayDelta * 1440,
};
});🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||
| // unset all working hours that relate to this user availability override | ||||||||||||||||||
| overrides.forEach((override) => { | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Unnecessary work (confidence: 100%) For each active override, dayjs(override.start.toString()).tz(override.timeZone) and dayjs(override.start.toString()).tz(timeZone) are called to compute utcOffset, and then dayjs(override.start).utc().add(offset, 'minute') and dayjs(override.end).utc().add(offset, 'minute') are each called twice (once for .hour() and once for .minute()). This is 6 dayjs constructions and tz parses per override, all of which could be collapsed to 2–3. Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/lib/slots.ts
+++ b/packages/lib/slots.ts
@@ -210,16 +210,14 @@ const getSlots = ({
if (!!activeOverrides.length) {
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 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(),
};
});🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||
| let i = -1; | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ import type prisma from "@calcom/prisma"; | |||||||||||||||||||||||||||||||||||
| import { availabilityUserSelect } from "@calcom/prisma"; | ||||||||||||||||||||||||||||||||||||
| import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; | ||||||||||||||||||||||||||||||||||||
| import type { EventBusyDate } from "@calcom/types/Calendar"; | ||||||||||||||||||||||||||||||||||||
| import type { WorkingHours } from "@calcom/types/schedule"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import { TRPCError } from "@trpc/server"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -75,12 +76,21 @@ 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())) { | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — API Contract Change - Function Signature (confidence: 99%) The Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -75,7 +75,7 @@ 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())) {
- 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")) {
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) {
+ // slot is within a date override window — check seats and busy times
+ if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
+ return true;
+ }
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();
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}
+ // After override/working-hours checks pass, a seated booking is always available
+ if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
+ return true;
+ }
+
return busy.every((busyTime) => {🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
|
|
@@ -89,6 +99,57 @@ const checkIfIsAvailable = ({ | |||||||||||||||||||||||||||||||||||
| const slotEndTime = time.add(eventLength, "minutes").utc(); | ||||||||||||||||||||||||||||||||||||
| const slotStartTime = time.utc(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| //check if date override for slot exists | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — Naming Conventions (confidence: 76%) Variable name 'dateOverrideExist' uses singular 'Exist' but should follow camelCase conventions with consistent tense Evidence:
Agent: style |
||||||||||||||||||||||||||||||||||||
| let dateOverrideExist = false; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Inconsistent UTC Offset Application (confidence: 100%) In Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Sign Inversion Inconsistency (confidence: 100%) In checkIfIsAvailable, the utcOffset is computed as Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business logic correctness (confidence: 100%) The UTC offset sign is inverted via Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Edge cases (confidence: 100%) When Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 100% |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| dateOverrides.find((date) => { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug — Incorrect UTC Offset Sign Convention (confidence: 100%) The UTC offset is computed as Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Logic Bug — Missing organizerTimeZone Fallback in checkIfIsAvailable (confidence: 100%) When Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 100% |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code Organization (confidence: 95%) Day-off date override detection uses reference equality (===) between two different Dayjs objects, which will always be false Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 95% |
||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") === | ||||||||||||||||||||||||||||||||||||
| slotStartTime.format("YYYY MM DD") | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Reference Equality Bug (confidence: 100%) Day-off detection uses Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Dayjs Reference Equality Bug (confidence: 100%) Using Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business logic correctness (confidence: 100%) Zero-duration date override check uses Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug - Reference Equality (confidence: 100%) Zero-duration date override check uses Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug - Reference Equality on Objects (confidence: 100%) Dayjs objects are compared using Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| dateOverrideExist = true; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code patterns (confidence: 88%) Object reference comparison used for Dayjs instances will always return false. The condition Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 88% |
||||||||||||||||||||||||||||||||||||
| if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Unnecessary work (confidence: 100%) The utcOffset is recomputed via dayjs.tz() on every call to checkIfIsAvailable for every override in the dateOverrides array. Since checkIfIsAvailable is called O(slots) times per day and dateOverrides is constant across those calls, the tz computation is repeated thousands of times with identical inputs. Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) || | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Boundary Condition (confidence: 100%) The date override boundary check marks a slot as outside the override when Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Boundary Condition - Slot Ending at Override Start (confidence: 100%) The condition Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes")) | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug — Reference Equality on Dayjs Objects (confidence: 100%) Zero-duration date override detection uses JavaScript reference equality ( Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Multiple Date Overrides (confidence: 100%) Using Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Unnecessary work (confidence: 100%) Dayjs object reference equality (===) is used to compare two distinct Dayjs instances. This condition can never be true, so the 'empty override window' early-return branch (return true inside the find, causing the outer if to return false) is permanently dead code. Any date override with start === end intended to mean 'available all day' will be misidentified as a bounded window and slots will be incorrectly marked unavailable. Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Incorrect Availability Logic (confidence: 100%) When Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Inverted Date Override Logic (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| // slot is not within the date override | ||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Day-of-Week Timezone Mismatch (confidence: 93%) The working hours check uses Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -137,7 +137,10 @@ const checkIfIsAvailable = ({
//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();
+ // Convert to organizer's local time for day-of-week comparison to avoid
+ // UTC midnight boundary mismatch (slotStartTime is UTC, but working hours
+ // days[] are defined in the organizer's local timezone).
+ const slotStartLocal = organizerTimeZone
+ ? slotStartTime.tz(organizerTimeZone)
+ : slotStartTime;
+ if (workingHour.days.includes(slotStartLocal.day())) {
+ const start = slotStartLocal.hour() * 60 + slotStartLocal.minute();
+ const end = slotStartLocal.hour() * 60 + slotStartLocal.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Business logic correctness (confidence: 100%) The working-hours day-of-week check uses Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -136,10 +136,14 @@ const checkIfIsAvailable = ({
//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();
+ // Convert UTC slotStartTime to organizer's local time before checking day-of-week
+ // and time-of-day, since workingHours are defined in the organizer's timezone.
+ const localSlotStartTime = organizerTimeZone
+ ? slotStartTime.tz(organizerTimeZone)
+ : slotStartTime;
+ if (workingHour.days.includes(localSlotStartTime.day())) {
+ const start = localSlotStartTime.hour() * 60 + localSlotStartTime.minute();
+ const end = localSlotStartTime.hour() * 60 + localSlotStartTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| if (dateOverrideExist) { | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Copy-Paste Bug (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Timezone Day-of-Week Mismatch (confidence: 100%) The working hours check uses Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • major • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Duplicate Variable Bug (confidence: 100%) Both Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Business logic correctness (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug - Duplicate Variable Assignment (confidence: 100%) Both Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| //if no date override for slot exists check if it is within normal work hours | ||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Working Hours Day Check Missing slotEndTime (confidence: 100%) The working hours check only verifies that Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -138,12 +138,30 @@ const checkIfIsAvailable = ({
//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();
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
+ // If the slot spans midnight, only validate the start portion against this working hour
+ const effectiveEnd = slotEndTime.day() !== slotStartTime.day() ? 24 * 60 : end;
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}
+
+ // If the slot spans midnight, also verify the end portion falls within working hours on the next day
+ if (slotEndTime.day() !== slotStartTime.day()) {
+ const endMinutes = slotEndTime.hour() * 60 + slotEndTime.minute();
+ const endDayHasValidWorkingHour = workingHours.some((workingHour) => {
+ if (workingHour.days.includes(slotEndTime.day())) {
+ // The slot end portion runs from midnight (0) to endMinutes
+ return endMinutes <= workingHour.endTime && workingHour.startTime === 0;
+ }
+ return false;
+ });
+ if (!endDayHasValidWorkingHour) {
+ // slot end crosses into a day with no covering working hours
+ return false;
+ }
+ }
+🤖 Grapple PR auto-fix • minor • Review this diff before applying There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug - Variable Reuse (confidence: 100%) The Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| workingHours.find((workingHour) => { | ||||||||||||||||||||||||||||||||||||
| if (workingHour.days.includes(slotStartTime.day())) { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Missing Working Hours Check for Non-Override Days (confidence: 100%) The working hours check only rejects a slot if Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -137,14 +137,17 @@ const checkIfIsAvailable = ({
//if no date override for slot exists check if it is within normal work hours
+ // A slot with no matching working hour entry means the organizer doesn't work that day at all.
+ const workingHoursForDay = workingHours.filter((workingHour) =>
+ workingHour.days.includes(slotStartTime.day())
+ );
+
+ // No working hour entry covers this day — organizer is not available
+ if (workingHoursForDay.length === 0) {
+ return false;
+ }
+
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;
- }
- }
- })
+ workingHoursForDay.every((workingHour) => {
+ const start = slotStartTime.hour() * 60 + slotStartTime.minute();
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
+ // Return true (slot is "outside") if it falls outside this entry's bounds
+ return start < workingHour.startTime || end > workingHour.endTime;
+ })
) {
// slot is outside of working hours
return false;
}🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| const start = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic error (confidence: 100%) The Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Logic Bug — Day-of-Week from UTC Object (confidence: 100%) The working-hours check uses Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -137,11 +137,19 @@ const checkIfIsAvailable = ({
//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();
+ // Convert the UTC slot time to the organizer's local timezone so that
+ // day-of-week and time-of-day comparisons match how working hours were
+ // configured. Without this, a UTC+10 organizer's 23:00 UTC Monday slot
+ // would be checked against Monday's hours instead of Tuesday's.
+ const slotInOrganizerTz = organizerTimeZone
+ ? slotStartTime.tz(organizerTimeZone)
+ : slotStartTime;
+ if (workingHour.days.includes(slotInOrganizerTz.day())) {
+ const start = slotInOrganizerTz.hour() * 60 + slotInOrganizerTz.minute();
+ const end = slotInOrganizerTz.hour() * 60 + slotInOrganizerTz.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| const end = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug - Variable Assignment (confidence: 100%) In the working hours validation block, the Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| if (start < workingHour.startTime || end > workingHour.endTime) { | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Logic Bug — Wrong Variable for End-Time Boundary Check (confidence: 100%) In the Evidence:
Agent: security There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Code patterns (confidence: 100%) The working-hours check uses Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -137,11 +137,13 @@ const checkIfIsAvailable = ({
//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();
+ // Convert to organizer's local time before comparing day-of-week and time-of-day,
+ // so that e.g. a UTC Monday slot that falls on Tuesday in UTC+8 is validated correctly.
+ const localSlotStartTime = organizerTimeZone
+ ? slotStartTime.tz(organizerTimeZone)
+ : slotStartTime;
+ if (workingHour.days.includes(localSlotStartTime.day())) {
+ const start = localSlotStartTime.hour() * 60 + localSlotStartTime.minute();
+ const end = localSlotStartTime.hour() * 60 + localSlotStartTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Unnecessary work (confidence: 100%) In the working hours check block, both Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • critical • confidence: 100% |
||||||||||||||||||||||||||||||||||||
| // slot is outside of working hours | ||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return busy.every((busyTime) => { | ||||||||||||||||||||||||||||||||||||
| const startTime = dayjs.utc(busyTime.start).utc(); | ||||||||||||||||||||||||||||||||||||
| const endTime = dayjs.utc(busyTime.end); | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 CRITICAL — Algorithmic complexity (confidence: 100%) The dateOverrides and workingHours arrays are searched with .find() inside checkIfIsAvailable, which is called inside a nested loop (per-day × per-slot × per-user). With large availability windows this is O(days × slots × users × overrides), but more critically, dayjs.tz() is called multiple times per override per invocation — each call is expensive and the result is never cached. With 30 days × 48 slots × 5 users × N overrides this becomes a significant performance regression. Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -99,50 +99,56 @@ const checkIfIsAvailable = ({
const slotEndTime = time.add(eventLength, "minutes").utc();
const slotStartTime = time.utc();
+ // Pre-compute the slot's date string once, outside any loop
+ const slotStartDay = slotStartTime.format("YYYY MM DD");
+
//check if date override for slot exists
let dateOverrideExist = false;
+ // Pre-compute adjusted override boundaries once per override (not per-comparison),
+ // avoiding repeated dayjs.tz() + dayjs().add() calls inside the find() predicate.
if (
dateOverrides.find((date) => {
- const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;
-
+ // dayjs.tz() is expensive — compute utcOffset exactly once per override entry
+ const utcOffset = organizerTimeZone
+ ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1
+ : 0;
+
+ // Compute adjusted start/end once per override, not 3-4× per predicate call
+ const adjustedStart = dayjs(date.start).add(utcOffset, "minutes");
+ const adjustedEnd = dayjs(date.end).add(utcOffset, "minutes");
+
if (
- dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") ===
- slotStartTime.format("YYYY MM DD")
+ adjustedStart.format("YYYY MM DD") === slotStartDay
) {
dateOverrideExist = true;
- if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
+ // All-day blocked override: start === end means fully blocked
+ if (adjustedStart.isSame(adjustedEnd)) {
return true;
}
if (
- slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) ||
- slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes"))
+ slotEndTime.isBefore(adjustedStart) ||
+ slotEndTime.isSame(adjustedStart)
) {
return true;
}
- if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) {
+ if (slotStartTime.isAfter(adjustedEnd)) {
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();
+ // Bug fix: end should use slotEndTime, not slotStartTime (was a copy-paste error)
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}
})
) {
// slot is outside of working hours
return false;
}🤖 Grapple PR auto-fix • critical • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
|
|
@@ -115,7 +176,6 @@ const checkIfIsAvailable = ({ | |||||||||||||||||||||||||||||||||||
| else if (startTime.isBetween(time, slotEndTime)) { | ||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -348,7 +408,11 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| // flattens availability of multiple users | ||||||||||||||||||||||||||||||||||||
| const dateOverrides = userAvailability.flatMap((availability) => | ||||||||||||||||||||||||||||||||||||
| availability.dateOverrides.map((override) => ({ userId: availability.user.id, ...override })) | ||||||||||||||||||||||||||||||||||||
| availability.dateOverrides.map((override) => ({ | ||||||||||||||||||||||||||||||||||||
| userId: availability.user.id, | ||||||||||||||||||||||||||||||||||||
| timeZone: availability.timeZone, | ||||||||||||||||||||||||||||||||||||
| ...override, | ||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| const workingHours = getAggregateWorkingHours(userAvailability, eventType.schedulingType); | ||||||||||||||||||||||||||||||||||||
| const availabilityCheckProps = { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -372,6 +436,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const timeSlots: ReturnType<typeof getTimeSlots> = []; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const organizerTimeZone = | ||||||||||||||||||||||||||||||||||||
| eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| for ( | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Unnecessary work (confidence: 99%) organizerTimeZone is derived from eventType.timeZone, eventType?.schedule?.timeZone, or userAvailability?.[0]?.timeZone once correctly before the loop (good), but at lines 489 and 514 it is overridden per-slot with Evidence:
Agent: performance There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -489,6 +489,12 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
// what else are you going to call it?
const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed);
if (looseHostAvailability.length > 0) {
+ // Build a Map<userId, schedule> once — userAvailability is constant across all slot
+ // iterations, so repeated Array.find() inside the loop would be O(N×U). Using a Map
+ // makes each per-slot lookup O(1).
+ const userAvailabilityMap = new Map(
+ userAvailability.map((schedule) => [schedule.user.id, schedule])
+ );
+
availableTimeSlots = availableTimeSlots
.map((slot) => {
slot.userIds = slot.userIds?.filter((slotUserId) => {
@@ -576,7 +582,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
return false;
}
- const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId);
+ const userSchedule = userAvailabilityMap.get(slotUserId);
return checkIfIsAvailable({
time: slot.time,🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| let currentCheckedTime = startTime; | ||||||||||||||||||||||||||||||||||||
| currentCheckedTime.isBefore(endTime); | ||||||||||||||||||||||||||||||||||||
|
|
@@ -386,8 +453,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
| dateOverrides, | ||||||||||||||||||||||||||||||||||||
| minimumBookingNotice: eventType.minimumBookingNotice, | ||||||||||||||||||||||||||||||||||||
| frequency: eventType.slotInterval || input.duration || eventType.length, | ||||||||||||||||||||||||||||||||||||
| organizerTimeZone: | ||||||||||||||||||||||||||||||||||||
| eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone, | ||||||||||||||||||||||||||||||||||||
| organizerTimeZone, | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -423,13 +489,15 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
| time: slot.time, | ||||||||||||||||||||||||||||||||||||
| ...schedule, | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 INFO — Code Organization (confidence: 81%) Inconsistent organizerTimeZone parameter passing: passing schedule.timeZone to checkIfIsAvailable which expects organizerTimeZone Evidence:
Agent: style |
||||||||||||||||||||||||||||||||||||
| ...availabilityCheckProps, | ||||||||||||||||||||||||||||||||||||
| organizerTimeZone: schedule.timeZone, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| const endCheckForAvailability = performance.now(); | ||||||||||||||||||||||||||||||||||||
| checkForAvailabilityCount++; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Missing Propagation of New Parameters (confidence: 100%) The Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -485,7 +485,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
const isAvailable = checkIfIsAvailable({
time: slot.time,
...schedule,
...availabilityCheckProps,
+ dateOverrides: schedule.dateOverrides ?? dateOverrides,
+ workingHours: schedule.workingHours ?? workingHours,
organizerTimeZone: schedule.timeZone,
});
const endCheckForAvailability = performance.now();
@@ -510,7 +512,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
checkIfIsAvailable({
time: slot.time,
...userSchedule,
...availabilityCheckProps,
+ dateOverrides: userSchedule.dateOverrides ?? dateOverrides,
+ workingHours: userSchedule.workingHours ?? workingHours,
organizerTimeZone: userSchedule.timeZone,
});🤖 Grapple PR auto-fix • minor • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability; | ||||||||||||||||||||||||||||||||||||
| return isAvailable; | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR — Data flow (confidence: 94%) At the call site on line 489, Evidence:
Agent: logic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡
--- a/packages/trpc/server/routers/viewer/slots.ts
+++ b/packages/trpc/server/routers/viewer/slots.ts
@@ -486,9 +486,11 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
const isAvailable = checkIfIsAvailable({
time: slot.time,
- ...schedule,
- ...availabilityCheckProps,
+ busy: schedule.busy,
+ dateOverrides: schedule.dateOverrides,
+ workingHours: schedule.workingHours,
+ ...availabilityCheckProps,
organizerTimeZone: schedule.timeZone,
});
const endCheckForAvailability = performance.now();
@@ -511,8 +513,10 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
return checkIfIsAvailable({
time: slot.time,
- ...userSchedule,
- ...availabilityCheckProps,
+ busy: userSchedule.busy,
+ dateOverrides: userSchedule.dateOverrides,
+ workingHours: userSchedule.workingHours,
+ ...availabilityCheckProps,
organizerTimeZone: userSchedule.timeZone,
});🤖 Grapple PR auto-fix • major • Review this diff before applying |
||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // what else are you going to call it? | ||||||||||||||||||||||||||||||||||||
| const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed); | ||||||||||||||||||||||||||||||||||||
| if (looseHostAvailability.length > 0) { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -446,6 +514,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
| time: slot.time, | ||||||||||||||||||||||||||||||||||||
| ...userSchedule, | ||||||||||||||||||||||||||||||||||||
| ...availabilityCheckProps, | ||||||||||||||||||||||||||||||||||||
| organizerTimeZone: userSchedule.timeZone, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| return slot; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -507,17 +576,19 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return checkIfIsAvailable({ | ||||||||||||||||||||||||||||||||||||
| time: slot.time, | ||||||||||||||||||||||||||||||||||||
| busy, | ||||||||||||||||||||||||||||||||||||
| ...availabilityCheckProps, | ||||||||||||||||||||||||||||||||||||
| organizerTimeZone: userSchedule?.timeZone, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| return slot; | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| .filter((slot) => !!slot.userIds?.length); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| availableTimeSlots = availableTimeSlots.filter((slot) => isTimeWithinBounds(slot.time)); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const computedAvailableSlots = availableTimeSlots.reduce( | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ export type TimeRange = { | |||||||||||||||||||||||||||||||||||||||||||||
| userId?: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||
| start: Date; | ||||||||||||||||||||||||||||||||||||||||||||||
| end: Date; | ||||||||||||||||||||||||||||||||||||||||||||||
| timeZone?: string; | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Type Safety - Module Boundary (confidence: 98%) Adding Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Type Definition - Missing Documentation (confidence: 88%) The new optional Evidence:
Agent: style There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 98% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 88% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MINOR — Missing Type Propagation (confidence: 98%) The Evidence:
Agent: architecture There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅
Suggested change
🤖 Grapple PR auto-fix • minor • confidence: 98% |
||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export type Schedule = TimeRange[][]; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 MAJOR — Undefined Timezone Handling (confidence: 100%)
When
override.timeZoneis undefined (the timeZone field on TimeRange is optional),dayjs(override.start.toString()).tz(undefined)will produce unexpected behavior. Dayjstz(undefined)may fall back to the local timezone of the server rather than UTC, leading to incorrect offset calculations.Evidence:
timeZone?: string) in schedule.d.tsdayjs(override.start.toString()).tz(override.timeZone)— no fallback for undefinedavailability.dateOverrides.map(override => ({ ...override, timeZone: availability.timeZone }))— if availability.timeZone is undefined, this propagates undefinedtimeZone?: string— it's optionalavailability.timeZone, which could be undefined if a user's availability record lacks a timezonedayjs(override.start.toString()).tz(override.timeZone)— no null/undefined guarddayjs(override.start.toString()).tz(timeZone)uses the invitee timeZone which should be present, but override.timeZone has no guaranteeAgent: logic