-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: date-algorithm-enhanced #374
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 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||
|
|
@@ -89,6 +99,57 @@ const checkIfIsAvailable = ({ | |||||||||||||
| 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; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+114
to
+116
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. Bug: Dayjs object identity comparison will always be
Use Proposed fix- if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
+ if (dayjs(date.start).add(utcOffset, "minutes").isSame(dayjs(date.end).add(utcOffset, "minutes"))) {
return true;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| if ( | ||||||||||||||
| slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) || | ||||||||||||||
| slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes")) | ||||||||||||||
| ) { | ||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
| if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) { | ||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| }) | ||||||||||||||
| ) { | ||||||||||||||
| // slot is not within the date override | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (dateOverrideExist) { | ||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| //if no date override for slot exists check if it is within normal work hours | ||||||||||||||
| if ( | ||||||||||||||
| workingHours.find((workingHour) => { | ||||||||||||||
| if (workingHour.days.includes(slotStartTime.day())) { | ||||||||||||||
| const start = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||||||||||||||
| const end = slotStartTime.hour() * 60 + slotStartTime.minute(); | ||||||||||||||
| if (start < workingHour.startTime || end > workingHour.endTime) { | ||||||||||||||
|
Comment on lines
+141
to
+143
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. Bug: Both Proposed fix const start = slotStartTime.hour() * 60 + slotStartTime.minute();
- const end = slotStartTime.hour() * 60 + slotStartTime.minute();
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| }) | ||||||||||||||
| ) { | ||||||||||||||
| // slot is outside of working hours | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+102
to
+151
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.
Both Additionally, the mutable side-effect pattern ( Proposed refactor for the find→some pattern- if (
- dateOverrides.find((date) => {
+ if (
+ dateOverrides.some((date) => {
const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;
// ...
+ return false;
})
) {- if (
- workingHours.find((workingHour) => {
+ if (
+ workingHours.some((workingHour) => {
if (workingHour.days.includes(slotStartTime.day())) {
// ...
+ return start < workingHour.startTime || end > workingHour.endTime;
}
+ return false;
})
) {🧰 Tools🪛 Biome (2.3.13)[error] 106-106: This callback passed to find() iterable method should always return a value. Add missing return statements so that this callback returns a value on all execution paths. (lint/suspicious/useIterableCallbackReturn) [error] 139-139: This callback passed to find() iterable method should always return a value. Add missing return statements so that this callback returns a value on all execution paths. (lint/suspicious/useIterableCallbackReturn) 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| return busy.every((busyTime) => { | ||||||||||||||
| const startTime = dayjs.utc(busyTime.start).utc(); | ||||||||||||||
| const endTime = dayjs.utc(busyTime.end); | ||||||||||||||
|
|
@@ -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 ( | ||||||||||||||
| 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, | ||||||||||||||
| ...availabilityCheckProps, | ||||||||||||||
| organizerTimeZone: schedule.timeZone, | ||||||||||||||
| }); | ||||||||||||||
| const endCheckForAvailability = performance.now(); | ||||||||||||||
| checkForAvailabilityCount++; | ||||||||||||||
| checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability; | ||||||||||||||
| return isAvailable; | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| // 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( | ||||||||||||||
|
|
||||||||||||||
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.
override.timeZonemay beundefined, causing incorrect offset calculation.TimeRange.timeZoneis optional. If a date override lacks atimeZonevalue,dayjs(...).tz(undefined)will produce unpredictable results — likely falling back to the local/system timezone rather than the organizer's timezone.Add a fallback to the organizer timezone (or skip offset adjustment entirely when
timeZoneis absent):Proposed fix
const overrides = activeOverrides.flatMap((override) => { - const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset(); + const overrideTimeZone = override.timeZone || organizerTimeZone; + const organizerUtcOffset = dayjs(override.start.toString()).tz(overrideTimeZone).utcOffset(); const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); const offset = inviteeUtcOffset - organizerUtcOffset;Additionally,
flatMapis misleading here since each override produces exactly one object —mapis more appropriate.🤖 Prompt for AI Agents