2022-06-15 20:54:31 +00:00
import { SchedulingType } from "@prisma/client" ;
import { z } from "zod" ;
2022-08-22 23:53:51 +00:00
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours" ;
2022-06-15 20:54:31 +00:00
import type { CurrentSeats } from "@calcom/core/getUserAvailability" ;
import { getUserAvailability } from "@calcom/core/getUserAvailability" ;
2022-07-11 23:35:50 +00:00
import dayjs , { Dayjs } from "@calcom/dayjs" ;
2022-08-12 19:29:29 +00:00
import { getDefaultEvent } from "@calcom/lib/defaultEvents" ;
2022-08-22 23:53:51 +00:00
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds" ;
2022-07-07 15:26:22 +00:00
import logger from "@calcom/lib/logger" ;
2022-08-12 18:18:13 +00:00
import { performance } from "@calcom/lib/server/perfObserver" ;
2022-08-22 23:53:51 +00:00
import getTimeSlots from "@calcom/lib/slots" ;
2022-07-22 17:27:06 +00:00
import prisma , { availabilityUserSelect } from "@calcom/prisma" ;
2022-10-12 05:29:04 +00:00
import { EventBusyDate } from "@calcom/types/Calendar" ;
2022-06-27 21:01:46 +00:00
import { TimeRange } from "@calcom/types/schedule" ;
2022-06-15 20:54:31 +00:00
import { TRPCError } from "@trpc/server" ;
2022-07-22 17:27:06 +00:00
import { createRouter } from "../../createRouter" ;
2022-06-15 20:54:31 +00:00
const getScheduleSchema = z
. object ( {
// startTime ISOString
2022-07-02 16:13:39 +00:00
startTime : z.string ( ) ,
2022-06-15 20:54:31 +00:00
// endTime ISOString
2022-07-02 16:13:39 +00:00
endTime : z.string ( ) ,
2022-06-15 20:54:31 +00:00
// Event type ID
2022-08-12 19:29:29 +00:00
eventTypeId : z.number ( ) . int ( ) . optional ( ) ,
// Event type slug
eventTypeSlug : z.string ( ) ,
2022-06-27 21:01:46 +00:00
// invitee timezone
timeZone : z.string ( ) . optional ( ) ,
2022-06-15 20:54:31 +00:00
// or list of users (for dynamic events)
usernameList : z.array ( z . string ( ) ) . optional ( ) ,
2022-07-07 15:26:22 +00:00
debug : z.boolean ( ) . optional ( ) ,
2022-06-15 20:54:31 +00:00
} )
. refine (
( data ) = > ! ! data . eventTypeId || ! ! data . usernameList ,
"Either usernameList or eventTypeId should be filled in."
) ;
export type Slot = {
time : string ;
attendees? : number ;
bookingUid? : string ;
users? : string [ ] ;
} ;
2022-08-22 23:53:51 +00:00
const checkIfIsAvailable = ( {
2022-06-15 20:54:31 +00:00
time ,
busy ,
eventLength ,
beforeBufferTime ,
currentSeats ,
} : {
time : Dayjs ;
2022-10-12 05:29:04 +00:00
busy : ( TimeRange | { start : string ; end : string } | EventBusyDate ) [ ] ;
2022-06-15 20:54:31 +00:00
eventLength : number ;
beforeBufferTime : number ;
currentSeats? : CurrentSeats ;
2022-08-22 23:53:51 +00:00
} ) : boolean = > {
2022-06-15 20:54:31 +00:00
if ( currentSeats ? . some ( ( booking ) = > booking . startTime . toISOString ( ) === time . toISOString ( ) ) ) {
return true ;
}
2022-06-30 00:33:19 +00:00
const slotEndTime = time . add ( eventLength , "minutes" ) . utc ( ) ;
2022-08-25 16:57:15 +00:00
const slotStartTime = time . utc ( ) ;
2022-06-30 00:33:19 +00:00
return busy . every ( ( busyTime ) = > {
2022-08-25 16:57:15 +00:00
const startTime = dayjs . utc ( busyTime . start ) . subtract ( beforeBufferTime , "minutes" ) . utc ( ) ;
2022-06-30 00:33:19 +00:00
const endTime = dayjs . utc ( busyTime . end ) ;
2022-06-15 20:54:31 +00:00
2022-07-11 11:00:08 +00:00
if ( endTime . isBefore ( slotStartTime ) || startTime . isAfter ( slotEndTime ) ) {
return true ;
}
if ( slotStartTime . isBetween ( startTime , endTime , null , "[)" ) ) {
return false ;
} else if ( slotEndTime . isBetween ( startTime , endTime , null , "(]" ) ) {
return false ;
}
2022-06-15 20:54:31 +00:00
// Check if start times are the same
2022-06-30 00:33:19 +00:00
if ( time . utc ( ) . isBetween ( startTime , endTime , null , "[)" ) ) {
2022-06-15 20:54:31 +00:00
return false ;
}
// Check if slot end time is between start and end time
else if ( slotEndTime . isBetween ( startTime , endTime ) ) {
return false ;
}
// Check if startTime is between slot
else if ( startTime . isBetween ( time , slotEndTime ) ) {
return false ;
}
2022-06-27 21:01:46 +00:00
2022-06-15 20:54:31 +00:00
return true ;
} ) ;
} ;
2022-08-22 23:53:51 +00:00
/** This should be called getAvailableSlots */
2022-06-15 20:54:31 +00:00
export const slotsRouter = createRouter ( ) . query ( "getSchedule" , {
input : getScheduleSchema ,
async resolve ( { input , ctx } ) {
2022-07-21 16:44:23 +00:00
return await getSchedule ( input , ctx ) ;
} ,
} ) ;
2022-08-22 23:53:51 +00:00
async function getEventType ( ctx : { prisma : typeof prisma } , input : z.infer < typeof getScheduleSchema > ) {
return ctx . prisma . eventType . findUnique ( {
2022-07-21 16:44:23 +00:00
where : {
id : input.eventTypeId ,
} ,
select : {
id : true ,
minimumBookingNotice : true ,
length : true ,
seatsPerTimeSlot : true ,
timeZone : true ,
slotInterval : true ,
beforeEventBuffer : true ,
afterEventBuffer : true ,
2022-10-12 05:29:04 +00:00
bookingLimits : true ,
2022-07-21 16:44:23 +00:00
schedulingType : true ,
periodType : true ,
periodStartDate : true ,
periodEndDate : true ,
periodCountCalendarDays : true ,
periodDays : true ,
schedule : {
select : {
availability : true ,
timeZone : true ,
2022-06-15 20:54:31 +00:00
} ,
2022-07-21 16:44:23 +00:00
} ,
availability : {
select : {
startTime : true ,
endTime : true ,
days : true ,
2022-06-15 20:54:31 +00:00
} ,
2022-07-21 16:44:23 +00:00
} ,
users : {
select : {
. . . availabilityUserSelect ,
2022-06-15 20:54:31 +00:00
} ,
} ,
2022-07-21 16:44:23 +00:00
} ,
} ) ;
2022-08-22 23:53:51 +00:00
}
2022-08-12 19:29:29 +00:00
2022-08-22 23:53:51 +00:00
async function getDynamicEventType ( ctx : { prisma : typeof prisma } , input : z.infer < typeof getScheduleSchema > ) {
2022-08-12 19:29:29 +00:00
// For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic
const dynamicEventType = getDefaultEvent ( input . eventTypeSlug ) ;
2022-08-22 23:53:51 +00:00
const users = await ctx . prisma . user . findMany ( {
where : {
username : {
in : input . usernameList ,
2022-08-12 19:29:29 +00:00
} ,
2022-08-22 23:53:51 +00:00
} ,
select : {
allowDynamicBooking : true ,
. . . availabilityUserSelect ,
} ,
} ) ;
const isDynamicAllowed = ! users . some ( ( user ) = > ! user . allowDynamicBooking ) ;
if ( ! isDynamicAllowed ) {
throw new TRPCError ( {
message : "Some of the users in this group do not allow dynamic booking" ,
code : "UNAUTHORIZED" ,
2022-08-12 19:29:29 +00:00
} ) ;
}
2022-08-22 23:53:51 +00:00
return Object . assign ( { } , dynamicEventType , {
users ,
} ) ;
}
function getRegularOrDynamicEventType (
ctx : { prisma : typeof prisma } ,
input : z.infer < typeof getScheduleSchema >
) {
const isDynamicBooking = ! input . eventTypeId ;
return isDynamicBooking ? getDynamicEventType ( ctx , input ) : getEventType ( ctx , input ) ;
}
2022-08-12 19:29:29 +00:00
2022-08-22 23:53:51 +00:00
/** This should be called getAvailableSlots */
export async function getSchedule ( input : z.infer < typeof getScheduleSchema > , ctx : { prisma : typeof prisma } ) {
if ( input . debug === true ) {
logger . setSettings ( { minLevel : "debug" } ) ;
}
if ( process . env . INTEGRATION_TEST_MODE === "true" ) {
logger . setSettings ( { minLevel : "silly" } ) ;
}
const startPrismaEventTypeGet = performance . now ( ) ;
const eventType = await getRegularOrDynamicEventType ( ctx , input ) ;
2022-07-21 16:44:23 +00:00
const endPrismaEventTypeGet = performance . now ( ) ;
logger . debug (
` Prisma eventType get took ${ endPrismaEventTypeGet - startPrismaEventTypeGet } ms for event: ${
input . eventTypeId
} `
) ;
if ( ! eventType ) {
throw new TRPCError ( { code : "NOT_FOUND" } ) ;
}
2022-06-15 20:54:31 +00:00
2022-07-21 16:44:23 +00:00
const startTime =
input . timeZone === "Etc/GMT"
? dayjs . utc ( input . startTime )
: dayjs ( input . startTime ) . utc ( ) . tz ( input . timeZone ) ;
const endTime =
input . timeZone === "Etc/GMT" ? dayjs . utc ( input . endTime ) : dayjs ( input . endTime ) . utc ( ) . tz ( input . timeZone ) ;
2022-06-15 20:54:31 +00:00
2022-07-21 16:44:23 +00:00
if ( ! startTime . isValid ( ) || ! endTime . isValid ( ) ) {
throw new TRPCError ( { message : "Invalid time range given." , code : "BAD_REQUEST" } ) ;
}
let currentSeats : CurrentSeats | undefined = undefined ;
2022-06-27 21:01:46 +00:00
2022-08-22 23:53:51 +00:00
/* We get all users working hours and busy slots */
const usersWorkingHoursAndBusySlots = await Promise . all (
2022-07-21 16:44:23 +00:00
eventType . users . map ( async ( currentUser ) = > {
const {
busy ,
workingHours ,
currentSeats : _currentSeats ,
2022-08-22 23:53:51 +00:00
timeZone ,
2022-07-21 16:44:23 +00:00
} = await getUserAvailability (
{
userId : currentUser.id ,
2022-08-12 19:29:29 +00:00
username : currentUser.username || "" ,
2022-07-21 16:44:23 +00:00
dateFrom : startTime.format ( ) ,
dateTo : endTime.format ( ) ,
eventTypeId : input.eventTypeId ,
afterEventBuffer : eventType.afterEventBuffer ,
} ,
{ user : currentUser , eventType , currentSeats }
) ;
if ( ! currentSeats && _currentSeats ) currentSeats = _currentSeats ;
2022-06-15 20:54:31 +00:00
2022-07-21 16:44:23 +00:00
return {
2022-08-22 23:53:51 +00:00
timeZone ,
2022-06-15 20:54:31 +00:00
workingHours ,
2022-07-21 16:44:23 +00:00
busy ,
} ;
} )
) ;
2022-08-22 23:53:51 +00:00
const workingHours = getAggregateWorkingHours ( usersWorkingHoursAndBusySlots , eventType . schedulingType ) ;
const computedAvailableSlots : Record < string , Slot [ ] > = { } ;
2022-07-21 16:44:23 +00:00
const availabilityCheckProps = {
eventLength : eventType.length ,
beforeBufferTime : eventType.beforeEventBuffer ,
currentSeats ,
} ;
2022-08-22 23:53:51 +00:00
const isTimeWithinBounds = ( _time : Parameters < typeof isTimeOutOfBounds > [ 0 ] ) = >
! isTimeOutOfBounds ( _time , {
2022-07-21 16:44:23 +00:00
periodType : eventType.periodType ,
periodStartDate : eventType.periodStartDate ,
periodEndDate : eventType.periodEndDate ,
periodCountCalendarDays : eventType.periodCountCalendarDays ,
periodDays : eventType.periodDays ,
} ) ;
2022-06-15 20:54:31 +00:00
2022-08-22 23:53:51 +00:00
let currentCheckedTime = startTime ;
2022-07-21 16:44:23 +00:00
let getSlotsTime = 0 ;
let checkForAvailabilityTime = 0 ;
let getSlotsCount = 0 ;
let checkForAvailabilityCount = 0 ;
2022-08-08 20:17:33 +00:00
2022-07-21 16:44:23 +00:00
do {
const startGetSlots = performance . now ( ) ;
// get slots retrieves the available times for a given day
2022-08-22 23:53:51 +00:00
const timeSlots = getTimeSlots ( {
inviteeDate : currentCheckedTime ,
2022-07-21 16:44:23 +00:00
eventLength : eventType.length ,
workingHours ,
minimumBookingNotice : eventType.minimumBookingNotice ,
frequency : eventType.slotInterval || eventType . length ,
} ) ;
2022-08-08 20:17:33 +00:00
2022-07-21 16:44:23 +00:00
const endGetSlots = performance . now ( ) ;
getSlotsTime += endGetSlots - startGetSlots ;
getSlotsCount ++ ;
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
const filterStrategy =
! eventType . schedulingType || eventType . schedulingType === SchedulingType . COLLECTIVE
? ( "every" as const )
: ( "some" as const ) ;
2022-07-07 15:26:22 +00:00
2022-08-22 23:53:51 +00:00
const availableTimeSlots = timeSlots . filter ( isTimeWithinBounds ) . filter ( ( time ) = >
usersWorkingHoursAndBusySlots [ filterStrategy ] ( ( schedule ) = > {
2022-07-21 16:44:23 +00:00
const startCheckForAvailability = performance . now ( ) ;
2022-08-22 23:53:51 +00:00
const isAvailable = checkIfIsAvailable ( { time , . . . schedule , . . . availabilityCheckProps } ) ;
2022-07-21 16:44:23 +00:00
const endCheckForAvailability = performance . now ( ) ;
checkForAvailabilityCount ++ ;
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability ;
2022-08-22 23:53:51 +00:00
return isAvailable ;
2022-07-21 16:44:23 +00:00
} )
2022-07-07 15:26:22 +00:00
) ;
2022-08-22 23:53:51 +00:00
computedAvailableSlots [ currentCheckedTime . format ( "YYYY-MM-DD" ) ] = availableTimeSlots . map ( ( time ) = > ( {
2022-07-21 16:44:23 +00:00
time : time.toISOString ( ) ,
users : eventType.users.map ( ( user ) = > user . username || "" ) ,
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
. . . ( currentSeats ? . some ( ( booking ) = > booking . startTime . toISOString ( ) === time . toISOString ( ) ) && {
attendees :
currentSeats [
currentSeats . findIndex ( ( booking ) = > booking . startTime . toISOString ( ) === time . toISOString ( ) )
] . _count . attendees ,
bookingUid :
currentSeats [
currentSeats . findIndex ( ( booking ) = > booking . startTime . toISOString ( ) === time . toISOString ( ) )
] . uid ,
} ) ,
} ) ) ;
2022-08-22 23:53:51 +00:00
currentCheckedTime = currentCheckedTime . add ( 1 , "day" ) ;
} while ( currentCheckedTime . isBefore ( endTime ) ) ;
2022-07-21 16:44:23 +00:00
logger . debug ( ` getSlots took ${ getSlotsTime } ms and executed ${ getSlotsCount } times ` ) ;
logger . debug (
` checkForAvailability took ${ checkForAvailabilityTime } ms and executed ${ checkForAvailabilityCount } times `
) ;
2022-08-22 23:53:51 +00:00
logger . silly ( ` Available slots: ${ JSON . stringify ( computedAvailableSlots ) } ` ) ;
2022-07-27 19:12:42 +00:00
2022-07-21 16:44:23 +00:00
return {
2022-08-22 23:53:51 +00:00
slots : computedAvailableSlots ,
2022-07-21 16:44:23 +00:00
} ;
}