diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/ee/calendars/services/calendars.service.ts index 50d88e272209e0..c57608c903b48a 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/ee/calendars/services/calendars.service.ts @@ -83,32 +83,31 @@ export class CalendarsService { calendarsToLoad, userId ); - try { - const calendarBusyTimes = await getBusyCalendarTimes( - this.buildNonDelegationCredentials(credentials), - dateFrom, - dateTo, - composedSelectedCalendars - ); - const calendarBusyTimesConverted = calendarBusyTimes.map( - (busyTime: EventBusyDate & { timeZone?: string }) => { - const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone); - const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone); - const busyTimeStartDate = busyTimeStart.toJSDate(); - const busyTimeEndDate = busyTimeEnd.toJSDate(); - return { - ...busyTime, - start: busyTimeStartDate, - end: busyTimeEndDate, - }; - } - ); - return calendarBusyTimesConverted; - } catch (error) { + const calendarBusyTimesQuery = await getBusyCalendarTimes( + this.buildNonDelegationCredentials(credentials), + dateFrom, + dateTo, + composedSelectedCalendars + ); + if (!calendarBusyTimesQuery.success) { throw new InternalServerErrorException( "Unable to fetch connected calendars events. Please try again later." ); } + const calendarBusyTimesConverted = calendarBusyTimesQuery.data.map( + (busyTime: EventBusyDate & { timeZone?: string }) => { + const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone); + const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone); + const busyTimeStartDate = busyTimeStart.toJSDate(); + const busyTimeEndDate = busyTimeEnd.toJSDate(); + return { + ...busyTime, + start: busyTimeStartDate, + end: busyTimeEndDate, + }; + } + ); + return calendarBusyTimesConverted; } async getUniqCalendarCredentials(calendarsToLoad: Calendar[], userId: User["id"]) { diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index a26d26e72ac99d..b69192904224a2 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1281,12 +1281,15 @@ describe("getSchedule", () => { const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([ - { - start: `${plus3DateString}T04:00:00.000Z`, - end: `${plus3DateString}T05:59:59.000Z`, - }, - ]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ + success: true, + data: [ + { + start: `${plus3DateString}T04:00:00.000Z`, + end: `${plus3DateString}T05:59:59.000Z`, + }, + ], + }); const scenarioData = { eventTypes: [ @@ -1347,12 +1350,15 @@ describe("getSchedule", () => { const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([ - { - start: `${plus3DateString}T04:00:00.000Z`, - end: `${plus3DateString}T05:59:59.000Z`, - }, - ]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ + success: true, + data: [ + { + start: `${plus3DateString}T04:00:00.000Z`, + end: `${plus3DateString}T05:59:59.000Z`, + }, + ], + }); const scenarioData = { eventTypes: [ @@ -1421,7 +1427,7 @@ describe("getSchedule", () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); const scenarioData = { eventTypes: [ diff --git a/packages/lib/CalendarManager.ts b/packages/lib/CalendarManager.ts index 581c8bafa3e08c..8d29874a3a4555 100644 --- a/packages/lib/CalendarManager.ts +++ b/packages/lib/CalendarManager.ts @@ -245,21 +245,11 @@ export const getBusyCalendarTimes = async ( } // const months = getMonths(dateFrom, dateTo); + // Subtract 11 hours from the start date to avoid problems in UTC- time zones. + const startDate = dayjs(dateFrom).subtract(11, "hours").format(); + // Add 14 hours from the start date to avoid problems in UTC+ time zones. + const endDate = dayjs(dateTo).add(14, "hours").format(); try { - // Subtract 11 hours from the start date to avoid problems in UTC- time zones. - const startDate = dayjs(dateFrom).subtract(11, "hours").format(); - // Add 14 hours from the start date to avoid problems in UTC+ time zones. - const endDate = dayjs(dateTo).add(14, "hours").format(); - - log.debug( - "getBusyCalendarTimes manipulated dates", - safeStringify({ - newStartDate: startDate, - newEndDate: endDate, - oldStartDate: dateFrom, - oldEndDate: dateTo, - }) - ); if (includeTimeZone) { results = await getCalendarsEventsWithTimezones( deduplicatedCredentials, @@ -281,8 +271,9 @@ export const getBusyCalendarTimes = async ( selectedCalendarIds: selectedCalendars.map((calendar) => calendar.externalId), error: safeStringify(e), }); + return { success: false, data: [{ start: startDate, end: endDate, source: "error-placeholder" }] }; } - return results.reduce((acc, availability) => acc.concat(availability), []); + return { success: true, data: results.reduce((acc, availability) => acc.concat(availability), []) }; }; export const createEvent = async ( diff --git a/packages/lib/getBusyTimes.ts b/packages/lib/getBusyTimes.ts index 90c09d2c76278f..ee8131027dbb43 100644 --- a/packages/lib/getBusyTimes.ts +++ b/packages/lib/getBusyTimes.ts @@ -179,13 +179,24 @@ const _getBusyTimes = async (params: { performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd"); if (credentials?.length > 0 && !bypassBusyCalendarTimes) { const startConnectedCalendarsGet = performance.now(); - const calendarBusyTimes = await getBusyCalendarTimes( + + const calendarBusyTimesQuery = await getBusyCalendarTimes( credentials, startTime, endTime, selectedCalendars, shouldServeCache ); + + if (!calendarBusyTimesQuery.success) { + throw new Error( + `Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map( + (calendar) => calendar.id + )}` + ); + } + + const calendarBusyTimes = calendarBusyTimesQuery.data; const endConnectedCalendarsGet = performance.now(); logger.debug( `Connected Calendars get took ${ diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index 5654bc76854b68..393bdc480709cb 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -411,24 +411,39 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA ? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id }) : user.userLevelSelectedCalendars; - const busyTimes = await getBusyTimes({ - credentials: user.credentials, - startTime: getBusyTimesStart, - endTime: getBusyTimesEnd, - eventTypeId, - userId: user.id, - userEmail: user.email, - username: `${user.username}`, - beforeEventBuffer, - afterEventBuffer, - selectedCalendars, - seatedEvent: !!eventType?.seatsPerTimeSlot, - rescheduleUid: initialData?.rescheduleUid || null, - duration, - currentBookings: initialData?.currentBookings, - bypassBusyCalendarTimes, - shouldServeCache, - }); + let busyTimes = []; + try { + busyTimes = await getBusyTimes({ + credentials: user.credentials, + startTime: getBusyTimesStart, + endTime: getBusyTimesEnd, + eventTypeId, + userId: user.id, + userEmail: user.email, + username: `${user.username}`, + beforeEventBuffer, + afterEventBuffer, + selectedCalendars, + seatedEvent: !!eventType?.seatsPerTimeSlot, + rescheduleUid: initialData?.rescheduleUid || null, + duration, + currentBookings: initialData?.currentBookings, + bypassBusyCalendarTimes, + shouldServeCache, + }); + } catch (error) { + log.error(`Error fetching busy times for user ${username}:`, error); + return { + busy: [], + timeZone, + dateRanges: [], + oooExcludedDateRanges: [], + workingHours: [], + dateOverrides: [], + currentSeats: [], + datesOutOfOffice: undefined, + }; + } const detailedBusyTimes: EventBusyDetails[] = [ ...busyTimes.map((a) => ({ diff --git a/packages/lib/server/getLuckyUser.test.ts b/packages/lib/server/getLuckyUser.test.ts index bcff1771528cf9..7d14f6debdb8bd 100644 --- a/packages/lib/server/getLuckyUser.test.ts +++ b/packages/lib/server/getLuckyUser.test.ts @@ -55,7 +55,7 @@ it("can find lucky user with maximize availability", async () => { }), ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); // TODO: we may be able to use native prisma generics somehow? @@ -107,7 +107,7 @@ it("can find lucky user with maximize availability and priority ranking", async }), ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); // TODO: we may be able to use native prisma generics somehow? @@ -289,7 +289,7 @@ describe("maximize availability and weights", () => { }), ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); prismaMock.user.findMany.mockResolvedValue(users); prismaMock.host.findMany.mockResolvedValue([]); @@ -392,7 +392,7 @@ describe("maximize availability and weights", () => { }), ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); prismaMock.user.findMany.mockResolvedValue(users); prismaMock.host.findMany.mockResolvedValue([]); @@ -500,7 +500,7 @@ describe("maximize availability and weights", () => { }), ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); prismaMock.user.findMany.mockResolvedValue(users); prismaMock.host.findMany.mockResolvedValue([]); @@ -600,7 +600,7 @@ describe("maximize availability and weights", () => { }, ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([ { @@ -707,13 +707,16 @@ describe("maximize availability and weights", () => { ]; CalendarManagerMock.getBusyCalendarTimes - .mockResolvedValueOnce([ - { - start: dayjs().utc().startOf("month").toDate(), - end: dayjs().utc().startOf("month").add(3, "day").toDate(), - timeZone: "UTC", - }, - ]) + .mockResolvedValueOnce({ + success: true, + data: [ + { + start: dayjs().utc().startOf("month").toDate(), + end: dayjs().utc().startOf("month").add(3, "day").toDate(), + timeZone: "UTC", + }, + ], + }) .mockResolvedValue([]); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); @@ -817,7 +820,7 @@ describe("maximize availability and weights", () => { }, ]; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); // TODO: we may be able to use native prisma generics somehow? @@ -1279,7 +1282,7 @@ describe("attribute weights and virtual queues", () => { chosenRouteId: routeId, }; - CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); + CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] }); prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]); prismaMock.user.findMany.mockResolvedValue(users); diff --git a/packages/lib/server/getLuckyUser.ts b/packages/lib/server/getLuckyUser.ts index 25354d539916e1..8ad0b7f4399b7f 100644 --- a/packages/lib/server/getLuckyUser.ts +++ b/packages/lib/server/getLuckyUser.ts @@ -456,7 +456,7 @@ async function getCalendarBusyTimesOfInterval( rrTimestampBasis: RRTimestampBasis, meetingStartTime?: Date ): Promise<{ userId: number; busyTimes: (EventBusyDate & { timeZone?: string })[] }[]> { - return Promise.all( + const usersBusyTimesQuery = await Promise.all( usersWithCredentials.map((user) => getBusyCalendarTimes( user.credentials, @@ -465,12 +465,19 @@ async function getCalendarBusyTimesOfInterval( user.userLevelSelectedCalendars, true, true - ).then((busyTimes) => ({ - userId: user.id, - busyTimes, - })) + ) ) ); + + return usersBusyTimesQuery.reduce((usersBusyTime, userBusyTimeQuery, index) => { + if (userBusyTimeQuery.success) { + usersBusyTime.push({ + userId: usersWithCredentials[index].id, + busyTimes: userBusyTimeQuery.data, + }); + } + return usersBusyTime; + }, [] as { userId: number; busyTimes: Awaited>["data"] }[]); } async function getBookingsOfInterval({ diff --git a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts index 8fd01a29bde6e7..253c390e5520ac 100644 --- a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts @@ -84,13 +84,22 @@ export const calendarOverlayHandler = async ({ ctx, input }: ListOptions) => { }); // get all clanedar services - const calendarBusyTimes = await getBusyCalendarTimes( + const calendarBusyTimesQuery = await getBusyCalendarTimes( credentials, dateFrom, dateTo, composedSelectedCalendars ); + if (!calendarBusyTimesQuery.success) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch busy calendar times", + }); + } + + const calendarBusyTimes = calendarBusyTimesQuery.data; + // Convert to users timezone const userTimeZone = input.loggedInUsersTz;