-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: date-algorithm-enhanced #358
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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // slot is outside of working hours | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+137
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. Bug: Lines 141-142 both use 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) {This means a slot that starts within working hours but extends past 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.3.13)[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.
Critical: Reference-equality comparison of dayjs objects on line 114 is always
false.dayjs(...) === dayjs(...)compares two distinct object references — this will never betrue. Use.isSame()to compare dayjs values:Additionally, the broader
find()-based logic here is incorrect when multiple date overrides exist for the same day (e.g., two users with different override windows, or a user with morning + afternoon override blocks). Thefind()will match any override that the slot falls outside of, even if the slot is valid inside a different override. This causes valid slots to be rejected.For example, with overrides A (10am–12pm) and B (2pm–4pm), a slot at 11am would be incorrectly rejected because it falls outside override B, even though it's inside override A.
Consider restructuring: if any override matches this day, the slot should be accepted if it falls within at least one of the matching overrides (i.e., use
some()to check containment, notfind()to check exclusion).📝 Committable suggestion
🧰 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)
🤖 Prompt for AI Agents