diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 4a6304ca502926..0b313351d1f2dd 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import { countBy } from "lodash"; +import type { Logger } from "tslog"; import { v4 as uuid } from "uuid"; import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability"; @@ -399,6 +400,17 @@ export function getUsersWithCredentialsConsideringContactOwner({ return contactOwnerAndFixedHosts; } +const getStartTime = ( + startTimeInput: string, + timeZone?: string, + eventType: Exclude>, null> +) => { + const startTimeMin = dayjs.utc().add(eventType.minimumBookingNotice || 1, "minutes"); + const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone); + + return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime; +}; + export const getAvailableSlots = async ( ...args: Parameters ): Promise> => { @@ -444,14 +456,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { - const startTimeMin = dayjs.utc().add(eventType.minimumBookingNotice || 1, "minutes"); - const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone); - - return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime; - }; - - const startTime = getStartTime(startTimeAdjustedForRollingWindowComputation, input.timeZone); + const startTime = getStartTime(startTimeAdjustedForRollingWindowComputation, input.timeZone, eventType); const endTime = input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone); @@ -483,158 +488,50 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise host.user.id === originalRescheduledBooking?.userId || 0 - ); - } - const usersWithCredentials = monitorCallbackSync(getUsersWithCredentialsConsideringContactOwner, { - contactOwnerEmail, - hosts: routedHostsWithContactOwnerAndFixedHosts, - }); - - loggerWithEventDetails.debug("Using users", { - usersWithCredentials: usersWithCredentials.map((user) => user.email), - }); + // If contact skipping, determine if there's availability within two weeks + if (contactOwnerEmail && aggregatedAvailability.length > 0) { + const twoWeeksFromNow = dayjs().add(2, "week"); + const diff = aggregatedAvailability[0].start.diff(twoWeeksFromNow, "day"); - const durationToUse = input.duration || 0; - - const startTimeDate = - input.rescheduleUid && durationToUse - ? startTime.subtract(durationToUse, "minute").toDate() - : startTime.toDate(); - - const endTimeDate = - input.rescheduleUid && durationToUse ? endTime.add(durationToUse, "minute").toDate() : endTime.toDate(); - - const sharedQuery = { - startTime: { lte: endTimeDate }, - endTime: { gte: startTimeDate }, - status: { - in: [BookingStatus.ACCEPTED], - }, - }; - - const allUserIds = usersWithCredentials.map((user) => user.id); - const currentBookingsAllUsers = await monitorCallbackAsync( - getExistingBookings, - startTimeDate, - endTimeDate, - eventType, - sharedQuery, - usersWithCredentials, - allUserIds - ); - - const outOfOfficeDaysAllUsers = await monitorCallbackAsync( - getOOODates, - startTimeDate, - endTimeDate, - allUserIds - ); - - const bookingLimits = parseBookingLimit(eventType?.bookingLimits); - const durationLimits = parseDurationLimit(eventType?.durationLimits); - let busyTimesFromLimitsBookingsAllUsers: Awaited> = []; - - if (eventType && (bookingLimits || durationLimits)) { - busyTimesFromLimitsBookingsAllUsers = await monitorCallbackAsync(getBusyTimesForLimitChecks, { - userIds: allUserIds, - eventTypeId: eventType.id, - startDate: startTime.format(), - endDate: endTime.format(), - rescheduleUid: input.rescheduleUid, - bookingLimits, - durationLimits, - }); - } - - const users = monitorCallbackSync(function enrichUsersWithData() { - return usersWithCredentials.map((currentUser) => { - return { - ...currentUser, - currentBookings: currentBookingsAllUsers - .filter( - (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) - ) - .map((bookings) => { - const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; - return bookingWithoutAttendees; - }), - outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), - }; - }); - }); + if (diff > 0) { + const routedHostsAndFixedHosts = routedHostsWithContactOwnerAndFixedHosts.filter( + (host) => host.email !== contactOwnerEmail + ); - const premappedUsersAvailability = await getUsersAvailability({ - users, - query: { - dateFrom: startTime.format(), - dateTo: endTime.format(), - eventTypeId: eventType.id, - afterEventBuffer: eventType.afterEventBuffer, - beforeEventBuffer: eventType.beforeEventBuffer, - duration: input.duration || 0, - returnDateOverrides: false, - }, - initialData: { - eventType, - currentSeats, - rescheduleUid: input.rescheduleUid, - busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, - }, - }); - /* We get all users working hours and busy slots */ - const allUsersAvailability = premappedUsersAvailability.map( - ( - { busy, dateRanges, oooExcludedDateRanges, currentSeats: _currentSeats, timeZone, datesOutOfOffice }, - index - ) => { - const currentUser = users[index]; - if (!currentSeats && _currentSeats) currentSeats = _currentSeats; - return { - timeZone, - dateRanges, - oooExcludedDateRanges, - busy, - user: currentUser, - datesOutOfOffice, - }; + if (routedHostsAndFixedHosts.length > 0) { + // if the first available slot is more than 2 weeks from now, round robin as normal + ({ aggregatedAvailability, allUsersAvailability, usersWithCredentials } = + await calculateHostsAndAvailabilities({ + input, + eventType, + routedHostsWithContactOwnerAndFixedHosts: routedHostsAndFixedHosts, + contactOwnerEmail, + loggerWithEventDetails, + startTime, + endTime, + currentSeats, + })); + } } - ); - - const availabilityCheckProps = { - eventLength: input.duration || eventType.length, - currentSeats, - }; - - const aggregatedAvailability = monitorCallbackSync( - getAggregatedAvailability, - allUsersAvailability, - eventType.schedulingType - ); + } const isTeamEvent = eventType.schedulingType === SchedulingType.COLLECTIVE || @@ -1098,3 +995,174 @@ export function getAllDatesWithBookabilityStatus(availableDates: string[]) { } return allDates; } + +const calculateHostsAndAvailabilities = async ({ + input, + eventType, + routedHostsWithContactOwnerAndFixedHosts, + contactOwnerEmail, + loggerWithEventDetails, + startTime, + endTime, + currentSeats, +}: { + input: TGetScheduleInputSchema; + eventType: Exclude>, null>; + routedHostsWithContactOwnerAndFixedHosts: Awaited< + ReturnType + >; + contactOwnerEmail?: string | null; + loggerWithEventDetails: Logger; + startTime: ReturnType; + endTime: string; + currentSeats?: CurrentSeats | undefined; +}) => { + if ( + input.rescheduleUid && + eventType.rescheduleWithSameRoundRobinHost && + eventType.schedulingType === SchedulingType.ROUND_ROBIN + ) { + const originalRescheduledBooking = await prisma.booking.findFirst({ + where: { + uid: input.rescheduleUid, + status: { + in: [BookingStatus.ACCEPTED], + }, + }, + select: { + userId: true, + }, + }); + routedHostsWithContactOwnerAndFixedHosts = routedHostsWithContactOwnerAndFixedHosts.filter( + (host) => host.user.id === originalRescheduledBooking?.userId || 0 + ); + } + + const usersWithCredentials = monitorCallbackSync(getUsersWithCredentialsConsideringContactOwner, { + contactOwnerEmail, + hosts: routedHostsWithContactOwnerAndFixedHosts, + }); + + loggerWithEventDetails.debug("Using users", { + usersWithCredentials: usersWithCredentials.map((user) => user.email), + }); + + const durationToUse = input.duration || 0; + + const startTimeDate = + input.rescheduleUid && durationToUse + ? startTime.subtract(durationToUse, "minute").toDate() + : startTime.toDate(); + + const endTimeDate = + input.rescheduleUid && durationToUse ? endTime.add(durationToUse, "minute").toDate() : endTime.toDate(); + + const sharedQuery = { + startTime: { lte: endTimeDate }, + endTime: { gte: startTimeDate }, + status: { + in: [BookingStatus.ACCEPTED], + }, + }; + + const allUserIds = usersWithCredentials.map((user) => user.id); + const currentBookingsAllUsers = await monitorCallbackAsync( + getExistingBookings, + startTimeDate, + endTimeDate, + eventType, + sharedQuery, + usersWithCredentials, + allUserIds + ); + + const outOfOfficeDaysAllUsers = await monitorCallbackAsync( + getOOODates, + startTimeDate, + endTimeDate, + allUserIds + ); + + const bookingLimits = parseBookingLimit(eventType?.bookingLimits); + const durationLimits = parseDurationLimit(eventType?.durationLimits); + let busyTimesFromLimitsBookingsAllUsers: Awaited> = []; + + if (eventType && (bookingLimits || durationLimits)) { + busyTimesFromLimitsBookingsAllUsers = await monitorCallbackAsync(getBusyTimesForLimitChecks, { + userIds: allUserIds, + eventTypeId: eventType.id, + startDate: startTime.format(), + endDate: endTime.format(), + rescheduleUid: input.rescheduleUid, + bookingLimits, + durationLimits, + }); + } + + const users = monitorCallbackSync(function enrichUsersWithData() { + return usersWithCredentials.map((currentUser) => { + return { + ...currentUser, + currentBookings: currentBookingsAllUsers + .filter( + (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) + ) + .map((bookings) => { + const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; + return bookingWithoutAttendees; + }), + outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), + }; + }); + }); + + const premappedUsersAvailability = await getUsersAvailability({ + users, + query: { + dateFrom: startTime.format(), + dateTo: endTime.format(), + eventTypeId: eventType.id, + afterEventBuffer: eventType.afterEventBuffer, + beforeEventBuffer: eventType.beforeEventBuffer, + duration: input.duration || 0, + returnDateOverrides: false, + }, + initialData: { + eventType, + currentSeats, + rescheduleUid: input.rescheduleUid, + busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, + }, + }); + /* We get all users working hours and busy slots */ + const allUsersAvailability = premappedUsersAvailability.map( + ( + { busy, dateRanges, oooExcludedDateRanges, currentSeats: _currentSeats, timeZone, datesOutOfOffice }, + index + ) => { + const currentUser = users[index]; + if (!currentSeats && _currentSeats) currentSeats = _currentSeats; + return { + timeZone, + dateRanges, + oooExcludedDateRanges, + busy, + user: currentUser, + datesOutOfOffice, + }; + } + ); + + const availabilityCheckProps = { + eventLength: input.duration || eventType.length, + currentSeats, + }; + + const aggregatedAvailability = monitorCallbackSync( + getAggregatedAvailability, + allUsersAvailability, + eventType.schedulingType + ); + + return { aggregatedAvailability, allUsersAvailability, usersWithCredentials }; +};