2023-05-24 23:35:44 +00:00
import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager" ;
import prismaMock from "../../../../tests/libs/__mocks__/prisma" ;
2023-01-04 22:40:06 +00:00
import { diff } from "jest-diff" ;
2023-05-24 23:35:44 +00:00
import { describe , expect , vi , beforeEach , afterEach , test } from "vitest" ;
2022-07-21 16:44:23 +00:00
import prisma from "@calcom/prisma" ;
2023-05-02 11:44:05 +00:00
import type { BookingStatus } from "@calcom/prisma/enums" ;
2023-04-25 22:39:47 +00:00
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types" ;
2023-08-07 14:00:01 +00:00
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util" ;
2023-09-06 19:23:53 +00:00
import { getDate , getGoogleCalendarCredential , createBookingScenario } from "../utils/bookingScenario" ;
2022-07-21 16:44:23 +00:00
2022-08-22 23:53:51 +00:00
// TODO: Mock properly
prismaMock . eventType . findUnique . mockResolvedValue ( null ) ;
prismaMock . user . findMany . mockResolvedValue ( [ ] ) ;
2023-05-24 23:35:44 +00:00
vi . mock ( "@calcom/lib/constants" , ( ) = > ( {
2022-08-29 22:33:53 +00:00
IS_PRODUCTION : true ,
2023-05-24 23:35:44 +00:00
WEBAPP_URL : "http://localhost:3000"
2022-08-29 22:33:53 +00:00
} ) ) ;
2022-07-21 16:44:23 +00:00
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers < R > {
toHaveTimeSlots ( expectedSlots : string [ ] , date : { dateString : string } ) : R ;
}
}
}
expect . extend ( {
2023-01-04 22:40:06 +00:00
toHaveTimeSlots (
schedule : { slots : Record < string , Slot [ ] > } ,
expectedSlots : string [ ] ,
{ dateString } : { dateString : string }
) {
if ( ! schedule . slots [ ` ${ dateString } ` ] ) {
return {
pass : false ,
message : ( ) = > ` has no timeslots for ${ dateString } ` ,
} ;
}
if (
! schedule . slots [ ` ${ dateString } ` ]
. map ( ( slot ) = > slot . time )
. every ( ( actualSlotTime , index ) = > {
return ` ${ dateString } T ${ expectedSlots [ index ] } ` === actualSlotTime ;
} )
) {
return {
pass : false ,
message : ( ) = >
` has incorrect timeslots for ${ dateString } . \ n \ r ${ diff (
expectedSlots . map ( ( expectedSlot ) = > ` ${ dateString } T ${ expectedSlot } ` ) ,
schedule . slots [ ` ${ dateString } ` ] . map ( ( slot ) = > slot . time )
) } ` ,
} ;
}
2022-07-21 16:44:23 +00:00
return {
pass : true ,
message : ( ) = > "has correct timeslots " ,
} ;
} ,
} ) ;
2023-01-04 22:40:06 +00:00
const Timezones = {
"+5:30" : "Asia/Kolkata" ,
"+6:00" : "Asia/Dhaka" ,
} ;
2022-07-28 10:37:00 +00:00
2023-01-04 22:40:06 +00:00
const TestData = {
selectedCalendars : {
google : {
integration : "google_calendar" ,
externalId : "john@example.com" ,
} ,
} ,
credentials : {
google : getGoogleCalendarCredential ( ) ,
} ,
schedules : {
IstWorkHours : {
id : 1 ,
name : "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT" ,
availability : [
{
userId : null ,
eventTypeId : null ,
days : [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ,
2023-07-05 16:47:41 +00:00
startTime : new Date ( "1970-01-01T09:30:00.000Z" ) ,
endTime : new Date ( "1970-01-01T18:00:00.000Z" ) ,
2023-01-04 22:40:06 +00:00
date : null ,
} ,
] ,
timeZone : Timezones [ "+5:30" ] ,
} ,
IstWorkHoursWithDateOverride : ( dateString : string ) = > ( {
id : 1 ,
name : "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)" ,
availability : [
{
userId : null ,
eventTypeId : null ,
days : [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ,
2023-07-05 16:47:41 +00:00
startTime : new Date ( "1970-01-01T09:30:00.000Z" ) ,
endTime : new Date ( "1970-01-01T18:00:00.000Z" ) ,
2023-01-04 22:40:06 +00:00
date : null ,
} ,
{
userId : null ,
eventTypeId : null ,
days : [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ,
2023-07-05 16:47:41 +00:00
startTime : new Date ( "1970-01-01T14:00:00.000Z" ) ,
endTime : new Date ( "1970-01-01T18:00:00.000Z" ) ,
2023-01-04 22:40:06 +00:00
date : dateString ,
} ,
] ,
timeZone : Timezones [ "+5:30" ] ,
} ) ,
} ,
users : {
example : {
2023-09-06 19:23:53 +00:00
name : "Example" ,
2023-01-04 22:40:06 +00:00
username : "example" ,
defaultScheduleId : 1 ,
email : "example@example.com" ,
timeZone : Timezones [ "+5:30" ] ,
} ,
} ,
apps : {
googleCalendar : {
slug : "google-calendar" ,
dirName : "whatever" ,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
keys : {
expiry_date : Infinity ,
client_id : "client_id" ,
client_secret : "client_secret" ,
redirect_uris : [ "http://localhost:3000/auth/callback" ] ,
} ,
} ,
} ,
2022-07-21 16:44:23 +00:00
} ;
2023-02-13 12:46:37 +00:00
2022-07-21 16:44:23 +00:00
const cleanup = async ( ) = > {
await prisma . eventType . deleteMany ( ) ;
await prisma . user . deleteMany ( ) ;
await prisma . schedule . deleteMany ( ) ;
await prisma . selectedCalendar . deleteMany ( ) ;
await prisma . credential . deleteMany ( ) ;
await prisma . booking . deleteMany ( ) ;
await prisma . app . deleteMany ( ) ;
} ;
beforeEach ( async ( ) = > {
await cleanup ( ) ;
} ) ;
afterEach ( async ( ) = > {
await cleanup ( ) ;
} ) ;
2023-01-04 22:40:06 +00:00
describe ( "getSchedule" , ( ) = > {
2023-01-11 13:24:02 +00:00
describe ( "Calendar event" , ( ) = > {
test ( "correctly identifies unavailable slots from calendar" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
2023-05-24 23:35:44 +00:00
CalendarManagerMock . getBusyCalendarTimes . mockResolvedValue ( [
{
start : ` ${ plus2DateString } T04:45:00.000Z ` ,
end : ` ${ plus2DateString } T23:00:00.000Z ` ,
} ,
] ) ;
2023-01-11 13:24:02 +00:00
const scenarioData = {
eventTypes : [
{
id : 1 ,
slotInterval : 45 ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-11 13:24:02 +00:00
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ,
] ,
apps : [ TestData . apps . googleCalendar ] ,
} ;
// An event with one accepted booking
createBookingScenario ( scenarioData ) ;
2023-04-25 22:39:47 +00:00
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
} ,
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-11 13:24:02 +00:00
2023-01-11 17:33:34 +00:00
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
2023-01-11 13:24:02 +00:00
expect ( scheduleForDayWithAGoogleCalendarBooking ) . toHaveTimeSlots ( [ ` 04:00:00.000Z ` ] , {
dateString : plus2DateString ,
} ) ;
} ) ;
} ) ;
2022-07-21 16:44:23 +00:00
describe ( "User Event" , ( ) = > {
2023-01-04 22:40:06 +00:00
test ( "correctly identifies unavailable slots from Cal Bookings in different status" , async ( ) = > {
2022-07-21 16:44:23 +00:00
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
const { dateString : plus3DateString } = getDate ( { dateIncrement : 3 } ) ;
// An event with one accepted booking
2023-01-04 22:40:06 +00:00
createBookingScenario ( {
// An event with length 30 minutes, slotInterval 45 minutes, and minimumBookingNotice 1440 minutes (24 hours)
eventTypes : [
{
id : 1 ,
// If `slotInterval` is set, it supersedes `length`
slotInterval : 45 ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-04 22:40:06 +00:00
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
] ,
bookings : [
// That event has one accepted booking from 4:00 to 4:15 in GMT on Day + 3 which is 9:30 to 9:45 in IST
{
eventTypeId : 1 ,
userId : 101 ,
status : "ACCEPTED" ,
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
startTime : ` ${ plus3DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus3DateString } T04:15:00.000Z ` ,
} ,
{
eventTypeId : 1 ,
userId : 101 ,
status : "REJECTED" ,
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
startTime : ` ${ plus2DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T04:15:00.000Z ` ,
} ,
{
eventTypeId : 1 ,
userId : 101 ,
status : "CANCELLED" ,
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
startTime : ` ${ plus2DateString } T05:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T05:15:00.000Z ` ,
} ,
{
eventTypeId : 1 ,
userId : 101 ,
status : "PENDING" ,
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
startTime : ` ${ plus2DateString } T06:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T06:15:00.000Z ` ,
} ,
] ,
2022-07-21 16:44:23 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
// Day Plus 2 is completely free - It only has non accepted bookings
2023-04-25 22:39:47 +00:00
const scheduleOnCompletelyFreeDay = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2022-07-21 16:44:23 +00:00
2023-01-04 22:40:06 +00:00
// getSchedule returns timeslots in GMT
2022-07-21 16:44:23 +00:00
expect ( scheduleOnCompletelyFreeDay ) . toHaveTimeSlots (
[
"04:00:00.000Z" ,
"04:45:00.000Z" ,
"05:30:00.000Z" ,
"06:15:00.000Z" ,
"07:00:00.000Z" ,
"07:45:00.000Z" ,
"08:30:00.000Z" ,
"09:15:00.000Z" ,
"10:00:00.000Z" ,
"10:45:00.000Z" ,
"11:30:00.000Z" ,
] ,
{
dateString : plus2DateString ,
}
) ;
2023-01-04 22:40:06 +00:00
// Day plus 3
2023-04-25 22:39:47 +00:00
const scheduleForDayWithOneBooking = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus2DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus3DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
2022-07-21 16:44:23 +00:00
expect ( scheduleForDayWithOneBooking ) . toHaveTimeSlots (
[
2023-01-04 22:40:06 +00:00
// "04:00:00.000Z", - This slot is unavailable because of the booking from 4:00 to 4:15
2023-07-05 16:47:41 +00:00
` 04:15:00.000Z ` ,
` 05:00:00.000Z ` ,
` 05:45:00.000Z ` ,
` 06:30:00.000Z ` ,
` 07:15:00.000Z ` ,
` 08:00:00.000Z ` ,
` 08:45:00.000Z ` ,
` 09:30:00.000Z ` ,
` 10:15:00.000Z ` ,
` 11:00:00.000Z ` ,
` 11:45:00.000Z ` ,
2022-07-21 16:44:23 +00:00
] ,
{
dateString : plus3DateString ,
}
) ;
} ) ;
2023-01-04 22:40:06 +00:00
test ( "slots are available as per `length`, `slotInterval` of the event" , async ( ) = > {
createBookingScenario ( {
eventTypes : [
2022-07-21 16:44:23 +00:00
{
2023-01-04 22:40:06 +00:00
id : 1 ,
length : 30 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
{
id : 2 ,
length : 30 ,
slotInterval : 120 ,
users : [
2022-07-21 16:44:23 +00:00
{
2023-01-04 22:40:06 +00:00
id : 101 ,
2022-07-21 16:44:23 +00:00
} ,
] ,
} ,
] ,
2023-01-04 22:40:06 +00:00
users : [
2022-07-21 16:44:23 +00:00
{
2023-01-04 22:40:06 +00:00
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
2022-07-21 16:44:23 +00:00
} ,
] ,
} ) ;
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventWith30Length = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-07-18 00:57:34 +00:00
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventWith30Length ) . toHaveTimeSlots (
2022-07-21 16:44:23 +00:00
[
` 04:00:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 04:30:00.000Z ` ,
` 05:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 05:30:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 06:00:00.000Z ` ,
` 06:30:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 07:00:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 07:30:00.000Z ` ,
` 08:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 08:30:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 09:00:00.000Z ` ,
` 09:30:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 10:00:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 10:30:00.000Z ` ,
` 11:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 11:30:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 12:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
] ,
{
2023-01-04 22:40:06 +00:00
dateString : plus2DateString ,
2022-07-21 16:44:23 +00:00
}
) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 2 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
// `slotInterval` takes precedence over `length`
2023-07-18 00:57:34 +00:00
// 4:30 is utc so it is 10:00 in IST
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventWith30minsLengthAndSlotInterval2hrs ) . toHaveTimeSlots (
2023-07-18 00:57:34 +00:00
[ ` 04:30:00.000Z ` , ` 06:30:00.000Z ` , ` 08:30:00.000Z ` , ` 10:30:00.000Z ` , ` 12:30:00.000Z ` ] ,
2023-01-04 22:40:06 +00:00
{
dateString : plus2DateString ,
}
) ;
} ) ;
2022-07-21 16:44:23 +00:00
2023-03-10 22:10:56 +00:00
// FIXME: Fix minimumBookingNotice is respected test
2023-06-06 11:59:57 +00:00
// eslint-disable-next-line playwright/no-skipped-test
2023-01-04 22:40:06 +00:00
test . skip ( "minimumBookingNotice is respected" , async ( ) = > {
2023-05-24 23:35:44 +00:00
vi . useFakeTimers ( ) . setSystemTime (
2023-01-04 22:40:06 +00:00
( ( ) = > {
const today = new Date ( ) ;
// Beginning of the day in current timezone of the system
return new Date ( today . getFullYear ( ) , today . getMonth ( ) , today . getDate ( ) ) ;
} ) ( )
2022-07-21 16:44:23 +00:00
) ;
2023-01-04 22:40:06 +00:00
createBookingScenario ( {
eventTypes : [
{
id : 1 ,
length : 120 ,
minimumBookingNotice : 13 * 60 , // Would take the minimum bookable time to be 18:30UTC+13 = 7:30AM UTC
users : [
{
id : 101 ,
} ,
] ,
} ,
{
id : 2 ,
length : 120 ,
minimumBookingNotice : 10 * 60 , // Would take the minimum bookable time to be 18:30UTC+10 = 4:30AM UTC
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
2022-07-21 16:44:23 +00:00
{
2023-01-04 22:40:06 +00:00
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
2022-07-21 16:44:23 +00:00
} ,
] ,
} ) ;
2023-01-04 22:40:06 +00:00
const { dateString : todayDateString } = getDate ( ) ;
const { dateString : minus1DateString } = getDate ( { dateIncrement : - 1 } ) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventWithBookingNotice13Hrs = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ minus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ todayDateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventWithBookingNotice13Hrs ) . toHaveTimeSlots (
2022-07-21 16:44:23 +00:00
[
2023-01-04 22:40:06 +00:00
/*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ ` 08:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
` 10:00:00.000Z ` ,
2023-01-04 22:40:06 +00:00
` 12:00:00.000Z ` ,
2022-07-21 16:44:23 +00:00
] ,
2023-01-04 22:40:06 +00:00
{
dateString : todayDateString ,
}
) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventWithBookingNotice10Hrs = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 2 ,
eventTypeSlug : "" ,
startTime : ` ${ minus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ todayDateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventWithBookingNotice10Hrs ) . toHaveTimeSlots (
[
/*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/
` 06:00:00.000Z ` ,
` 08:00:00.000Z ` ,
` 10:00:00.000Z ` ,
` 12:00:00.000Z ` ,
] ,
{
dateString : todayDateString ,
}
) ;
2023-05-24 23:35:44 +00:00
vi . useRealTimers ( ) ;
2023-01-04 22:40:06 +00:00
} ) ;
test ( "afterBuffer and beforeBuffer tests - Non Cal Busy Time" , async ( ) = > {
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
const { dateString : plus3DateString } = getDate ( { dateIncrement : 3 } ) ;
2023-05-24 23:35:44 +00:00
CalendarManagerMock . getBusyCalendarTimes . mockResolvedValue ( [
{
start : ` ${ plus3DateString } T04:00:00.000Z ` ,
end : ` ${ plus3DateString } T05:59:59.000Z ` ,
} ,
] ) ;
2023-01-04 22:40:06 +00:00
const scenarioData = {
eventTypes : [
{
id : 1 ,
length : 120 ,
beforeEventBuffer : 120 ,
afterEventBuffer : 120 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ,
] ,
apps : [ TestData . apps . googleCalendar ] ,
} ;
createBookingScenario ( scenarioData ) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventOnADayWithNonCalBooking = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus2DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus3DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventOnADayWithNonCalBooking ) . toHaveTimeSlots (
[
// `04:00:00.000Z`, // - 4 AM is booked
// `06:00:00.000Z`, // - 6 AM is not available because 08:00AM slot has a `beforeEventBuffer`
` 08:00:00.000Z ` , // - 8 AM is available because of availability of 06:00 - 07:59
` 10:00:00.000Z ` ,
` 12:00:00.000Z ` ,
] ,
{
dateString : plus3DateString ,
}
) ;
} ) ;
test ( "afterBuffer and beforeBuffer tests - Cal Busy Time" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
const { dateString : plus3DateString } = getDate ( { dateIncrement : 3 } ) ;
2023-05-24 23:35:44 +00:00
CalendarManagerMock . getBusyCalendarTimes . mockResolvedValue ( [
{
start : ` ${ plus3DateString } T04:00:00.000Z ` ,
end : ` ${ plus3DateString } T05:59:59.000Z ` ,
} ,
] ) ;
2023-01-04 22:40:06 +00:00
const scenarioData = {
eventTypes : [
{
id : 1 ,
length : 120 ,
beforeEventBuffer : 120 ,
afterEventBuffer : 120 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ,
] ,
bookings : [
{
userId : 101 ,
eventTypeId : 1 ,
startTime : ` ${ plus2DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T05:59:59.000Z ` ,
status : "ACCEPTED" as BookingStatus ,
} ,
] ,
apps : [ TestData . apps . googleCalendar ] ,
} ;
createBookingScenario ( scenarioData ) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventOnADayWithCalBooking = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventOnADayWithCalBooking ) . toHaveTimeSlots (
[
// `04:00:00.000Z`, // - 4 AM is booked
// `06:00:00.000Z`, // - 6 AM is not available because of afterBuffer(120 mins) of the existing booking(4-5:59AM slot)
// `08:00:00.000Z`, // - 8 AM is not available because of beforeBuffer(120mins) of possible booking at 08:00
` 10:00:00.000Z ` ,
` 12:00:00.000Z ` ,
] ,
{
dateString : plus2DateString ,
}
) ;
} ) ;
2023-05-17 11:56:55 +00:00
test ( "Start times are offset (offsetStart)" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
2023-05-24 23:35:44 +00:00
CalendarManagerMock . getBusyCalendarTimes . mockResolvedValue ( [ ] ) ;
2023-05-17 11:56:55 +00:00
const scenarioData = {
eventTypes : [
{
id : 1 ,
length : 25 ,
offsetStart : 5 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
credentials : [ getGoogleCalendarCredential ( ) ] ,
selectedCalendars : [ TestData . selectedCalendars . google ] ,
} ,
] ,
apps : [ TestData . apps . googleCalendar ] ,
} ;
createBookingScenario ( scenarioData ) ;
const schedule = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-05-17 11:56:55 +00:00
} ) ;
expect ( schedule ) . toHaveTimeSlots (
[
` 04:05:00.000Z ` ,
` 04:35:00.000Z ` ,
` 05:05:00.000Z ` ,
` 05:35:00.000Z ` ,
` 06:05:00.000Z ` ,
` 06:35:00.000Z ` ,
` 07:05:00.000Z ` ,
` 07:35:00.000Z ` ,
` 08:05:00.000Z ` ,
` 08:35:00.000Z ` ,
` 09:05:00.000Z ` ,
` 09:35:00.000Z ` ,
` 10:05:00.000Z ` ,
` 10:35:00.000Z ` ,
` 11:05:00.000Z ` ,
` 11:35:00.000Z ` ,
` 12:05:00.000Z ` ,
] ,
{
dateString : plus2DateString ,
}
) ;
} ) ;
2023-01-04 22:40:06 +00:00
test ( "Check for Date overrides" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
const scenarioData = {
eventTypes : [
{
id : 1 ,
length : 60 ,
users : [
{
id : 101 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHoursWithDateOverride ( plus2DateString ) ] ,
} ,
] ,
} ;
createBookingScenario ( scenarioData ) ;
2023-04-25 22:39:47 +00:00
const scheduleForEventOnADayWithDateOverride = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForEventOnADayWithDateOverride ) . toHaveTimeSlots (
[ "08:30:00.000Z" , "09:30:00.000Z" , "10:30:00.000Z" , "11:30:00.000Z" ] ,
{
dateString : plus2DateString ,
}
) ;
} ) ;
2023-02-13 12:46:37 +00:00
test ( "that a user is considered busy when there's a booking they host" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
createBookingScenario ( {
eventTypes : [
// A Collective Event Type hosted by this user
{
id : 1 ,
slotInterval : 45 ,
schedulingType : "COLLECTIVE" ,
2023-02-21 02:45:10 +00:00
hosts : [
{
id : 101 ,
} ,
{
id : 102 ,
} ,
] ,
2023-02-13 12:46:37 +00:00
} ,
// A default Event Type which this user owns
{
id : 2 ,
2023-07-05 16:47:41 +00:00
length : 15 ,
2023-02-13 12:46:37 +00:00
slotInterval : 45 ,
users : [ { id : 101 } ] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
2023-02-21 02:45:10 +00:00
{
. . . TestData . users . example ,
id : 102 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
2023-02-13 12:46:37 +00:00
] ,
bookings : [
// Create a booking on our Collective Event Type
{
2023-02-21 02:45:10 +00:00
userId : 101 ,
attendees : [
{
email : "IntegrationTestUser102@example.com" ,
} ,
] ,
2023-02-13 12:46:37 +00:00
eventTypeId : 1 ,
status : "ACCEPTED" ,
startTime : ` ${ plus2DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T04:15:00.000Z ` ,
} ,
] ,
} ) ;
// Requesting this user's availability for their
// individual Event Type
2023-04-25 22:39:47 +00:00
const thisUserAvailability = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 2 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : false ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-02-13 12:46:37 +00:00
expect ( thisUserAvailability ) . toHaveTimeSlots (
[
// `04:00:00.000Z`, // <- This slot should be occupied by the Collective Event
2023-07-05 16:47:41 +00:00
` 04:15:00.000Z ` ,
` 05:00:00.000Z ` ,
` 05:45:00.000Z ` ,
` 06:30:00.000Z ` ,
` 07:15:00.000Z ` ,
` 08:00:00.000Z ` ,
` 08:45:00.000Z ` ,
` 09:30:00.000Z ` ,
` 10:15:00.000Z ` ,
` 11:00:00.000Z ` ,
` 11:45:00.000Z ` ,
2023-02-13 12:46:37 +00:00
] ,
{
dateString : plus2DateString ,
}
) ;
} ) ;
2023-01-04 22:40:06 +00:00
} ) ;
describe ( "Team Event" , ( ) = > {
test ( "correctly identifies unavailable slots from calendar for all users in collective scheduling, considers bookings of users in other events as well" , async ( ) = > {
const { dateString : todayDateString } = getDate ( ) ;
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
createBookingScenario ( {
eventTypes : [
// An event having two users with one accepted booking
{
id : 1 ,
slotInterval : 45 ,
2023-07-05 16:47:41 +00:00
schedulingType : "COLLECTIVE" ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-04 22:40:06 +00:00
users : [
{
id : 101 ,
} ,
{
id : 102 ,
} ,
] ,
} ,
{
id : 2 ,
slotInterval : 45 ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-04 22:40:06 +00:00
users : [
{
id : 102 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
{
. . . TestData . users . example ,
id : 102 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
] ,
bookings : [
{
userId : 101 ,
eventTypeId : 1 ,
status : "ACCEPTED" ,
startTime : ` ${ plus2DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T04:15:00.000Z ` ,
} ,
{
userId : 102 ,
eventTypeId : 2 ,
status : "ACCEPTED" ,
startTime : ` ${ plus2DateString } T05:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T05:45:00.000Z ` ,
} ,
] ,
} ) ;
2023-04-25 22:39:47 +00:00
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ todayDateString } T18:30:00.000Z ` ,
endTime : ` ${ plus1DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : true ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
expect ( scheduleForTeamEventOnADayWithNoBooking ) . toHaveTimeSlots (
[
` 04:00:00.000Z ` ,
` 04:45:00.000Z ` ,
` 05:30:00.000Z ` ,
` 06:15:00.000Z ` ,
` 07:00:00.000Z ` ,
` 07:45:00.000Z ` ,
` 08:30:00.000Z ` ,
` 09:15:00.000Z ` ,
` 10:00:00.000Z ` ,
` 10:45:00.000Z ` ,
` 11:30:00.000Z ` ,
] ,
{
dateString : plus1DateString ,
}
) ;
2023-04-25 22:39:47 +00:00
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : true ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-07-05 16:47:41 +00:00
2023-01-04 22:40:06 +00:00
// A user with blocked time in another event, still affects Team Event availability
// It's a collective availability, so both user 101 and 102 are considered for timeslots
expect ( scheduleForTeamEventOnADayWithOneBookingForEachUser ) . toHaveTimeSlots (
[
//`04:00:00.000Z`, - Blocked with User 101
2023-07-05 16:47:41 +00:00
` 04:15:00.000Z ` ,
fix: better slot starting times
## What does this PR do?
Currently, we start the first slot always at the nearest 15 minutes. This is not ideal as for some duration other slot starting time make more sense. So with this PR the starting times are defined as follow:
- Frequency is exact hours (60, 120, 180, ...), slot start time is a full hour
- Frequency is half hours (30, 90, ...), slot start time is half or full hours (8:00, 8:30, ...)
- Same with 20-minute events (20, 40, ...) and 10-minute events
- Everything else will start at the nearest 15 min slot
It also fixes that slot times are shifted when there is a busy slot with a different duration. Here is a before and after of a 30-min event with a 5-minute busy slot at 1:00 pm
Before:
![Screenshot 2023-07-07 at 13 31 45](https://github.com/calcom/cal.com/assets/30310907/b92d4ff4-49f1-48f4-a973-99266f61d919)
After
![Screenshot 2023-07-07 at 13 34 01](https://github.com/calcom/cal.com/assets/30310907/042c7ef7-8c2a-4cd9-b663-183bc07b5864)
#### 30 Minute events, availability starting at 7:15
Before:
![Screenshot 2023-07-06 at 12 40 00](https://github.com/calcom/cal.com/assets/30310907/752ed978-83cf-4ee9-a38d-b5795df6daec)
After:
![Screenshot 2023-07-06 at 12 40 42](https://github.com/calcom/cal.com/assets/30310907/5d51ec15-5be8-4f3b-b374-46dad35216b8)
## Type of change
- Bug fix (non-breaking change which fixes an issue)
## How should this be tested?
- Check if slot times are shown as described
- Test with different intervals/durations
- Test with busy times
- Test with different availabilities
## Mandatory Tasks
- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-07-10 22:32:26 +00:00
//`05:00:00.000Z`, - Blocked with User 102 in event 2
2023-07-05 16:47:41 +00:00
` 05:45:00.000Z ` ,
` 06:30:00.000Z ` ,
` 07:15:00.000Z ` ,
` 08:00:00.000Z ` ,
` 08:45:00.000Z ` ,
` 09:30:00.000Z ` ,
` 10:15:00.000Z ` ,
` 11:00:00.000Z ` ,
` 11:45:00.000Z ` ,
2023-01-04 22:40:06 +00:00
] ,
{ dateString : plus2DateString }
) ;
} ) ;
test ( "correctly identifies unavailable slots from calendar for all users in Round Robin scheduling, considers bookings of users in other events as well" , async ( ) = > {
const { dateString : plus1DateString } = getDate ( { dateIncrement : 1 } ) ;
const { dateString : plus2DateString } = getDate ( { dateIncrement : 2 } ) ;
const { dateString : plus3DateString } = getDate ( { dateIncrement : 3 } ) ;
createBookingScenario ( {
eventTypes : [
// An event having two users with one accepted booking
{
id : 1 ,
slotInterval : 45 ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-04 22:40:06 +00:00
users : [
{
id : 101 ,
} ,
{
id : 102 ,
} ,
] ,
schedulingType : "ROUND_ROBIN" ,
} ,
{
id : 2 ,
slotInterval : 45 ,
2023-01-11 17:33:34 +00:00
length : 45 ,
2023-01-04 22:40:06 +00:00
users : [
{
id : 102 ,
} ,
] ,
} ,
] ,
users : [
{
. . . TestData . users . example ,
id : 101 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
{
. . . TestData . users . example ,
id : 102 ,
schedules : [ TestData . schedules . IstWorkHours ] ,
} ,
] ,
bookings : [
{
userId : 101 ,
eventTypeId : 1 ,
status : "ACCEPTED" ,
startTime : ` ${ plus2DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus2DateString } T04:15:00.000Z ` ,
} ,
{
userId : 102 ,
eventTypeId : 2 ,
status : "ACCEPTED" ,
startTime : ` ${ plus2DateString } T05:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T05:45:00.000Z ` ,
} ,
{
userId : 101 ,
eventTypeId : 1 ,
status : "ACCEPTED" ,
startTime : ` ${ plus3DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus3DateString } T04:15:00.000Z ` ,
} ,
{
userId : 102 ,
eventTypeId : 2 ,
status : "ACCEPTED" ,
startTime : ` ${ plus3DateString } T04:00:00.000Z ` ,
endTime : ` ${ plus3DateString } T04:15:00.000Z ` ,
} ,
] ,
} ) ;
2023-04-25 22:39:47 +00:00
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus1DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus2DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : true ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
// A user with blocked time in another event, still affects Team Event availability
expect ( scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots ) . toHaveTimeSlots (
[
` 04:00:00.000Z ` , // - Blocked with User 101 but free with User 102. Being RoundRobin it is still bookable
` 04:45:00.000Z ` ,
` 05:30:00.000Z ` , // - Blocked with User 102 but free with User 101. Being RoundRobin it is still bookable
` 06:15:00.000Z ` ,
` 07:00:00.000Z ` ,
` 07:45:00.000Z ` ,
` 08:30:00.000Z ` ,
` 09:15:00.000Z ` ,
` 10:00:00.000Z ` ,
` 10:45:00.000Z ` ,
` 11:30:00.000Z ` ,
] ,
{ dateString : plus2DateString }
) ;
2023-04-25 22:39:47 +00:00
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule ( {
2023-08-24 23:41:06 +00:00
input : {
eventTypeId : 1 ,
eventTypeSlug : "" ,
startTime : ` ${ plus2DateString } T18:30:00.000Z ` ,
endTime : ` ${ plus3DateString } T18:29:59.999Z ` ,
timeZone : Timezones [ "+5:30" ] ,
isTeamEvent : true ,
}
2023-04-25 22:39:47 +00:00
} ) ;
2023-01-04 22:40:06 +00:00
// A user with blocked time in another event, still affects Team Event availability
expect ( scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot ) . toHaveTimeSlots (
[
//`04:00:00.000Z`, // - Blocked with User 101 as well as User 102, so not available in Round Robin
2023-07-05 16:47:41 +00:00
` 04:15:00.000Z ` ,
` 05:00:00.000Z ` ,
` 05:45:00.000Z ` ,
` 06:30:00.000Z ` ,
` 07:15:00.000Z ` ,
` 08:00:00.000Z ` ,
` 08:45:00.000Z ` ,
` 09:30:00.000Z ` ,
` 10:15:00.000Z ` ,
` 11:00:00.000Z ` ,
` 11:45:00.000Z ` ,
2023-01-04 22:40:06 +00:00
] ,
{ dateString : plus3DateString }
2022-07-21 16:44:23 +00:00
) ;
} ) ;
} ) ;
} ) ;
2023-01-04 22:40:06 +00:00