diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index f24d1ec1b5..c50ed5feeb 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -182,6 +182,18 @@ export type GetUserAvailabilityInitialData = { reason: Pick | null; })[]; busyTimesFromLimitsBookings: EventBusyDetails[]; + busyTimesFromLimits?: Map; + eventTypeForLimits?: { + id: number; + bookingLimits?: unknown; + durationLimits?: unknown; + } | null; + teamBookingLimits?: Map; + teamForBookingLimits?: { + id: number; + bookingLimits?: unknown; + includeManagedEventsInLimits: boolean; + } | null; }; export type GetAvailabilityUser = NonNullable; @@ -352,40 +364,49 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const bookingLimits = parseBookingLimit(eventType?.bookingLimits); const durationLimits = parseDurationLimit(eventType?.durationLimits); - const busyTimesFromLimits = - eventType && (bookingLimits || durationLimits) - ? await getBusyTimesFromLimits( - bookingLimits, - durationLimits, - dateFrom.tz(timeZone), - dateTo.tz(timeZone), - duration, - eventType, - initialData?.busyTimesFromLimitsBookings ?? [], - timeZone, - initialData?.rescheduleUid ?? undefined - ) - : []; + let busyTimesFromLimits: EventBusyDetails[] = []; + + if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) { + busyTimesFromLimits = initialData.busyTimesFromLimits.get(user.id) || []; + } else if (eventType && (bookingLimits || durationLimits)) { + // Fall back to individual query if not available in initialData + busyTimesFromLimits = await getBusyTimesFromLimits( + bookingLimits, + durationLimits, + dateFrom.tz(timeZone), + dateTo.tz(timeZone), + duration, + eventType, + initialData?.busyTimesFromLimitsBookings ?? [], + timeZone, + initialData?.rescheduleUid ?? undefined + ); + } const teamForBookingLimits = + initialData?.teamForBookingLimits ?? eventType?.team ?? (eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null); const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); - const busyTimesFromTeamLimits = - teamForBookingLimits && teamBookingLimits - ? await getBusyTimesFromTeamLimits( - user, - teamBookingLimits, - dateFrom.tz(timeZone), - dateTo.tz(timeZone), - teamForBookingLimits.id, - teamForBookingLimits.includeManagedEventsInLimits, - timeZone, - initialData?.rescheduleUid ?? undefined - ) - : []; + let busyTimesFromTeamLimits: EventBusyDetails[] = []; + + if (initialData?.teamBookingLimits && teamForBookingLimits) { + busyTimesFromTeamLimits = initialData.teamBookingLimits.get(user.id) || []; + } else if (teamForBookingLimits && teamBookingLimits) { + // Fall back to individual query if not available in initialData + busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits( + user, + teamBookingLimits, + dateFrom.tz(timeZone), + dateTo.tz(timeZone), + teamForBookingLimits.id, + teamForBookingLimits.includeManagedEventsInLimits, + timeZone, + initialData?.rescheduleUid ?? undefined + ); + } // TODO: only query what we need after applying limits (shrink date range) const getBusyTimesStart = dateFrom.toISOString(); @@ -598,10 +619,15 @@ export const getPeriodStartDatesBetween = ( return monitorCallbackSync(_getPeriodStartDatesBetween, ...args); }; -const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => { +const _getPeriodStartDatesBetween = ( + dateFrom: Dayjs, + dateTo: Dayjs, + period: IntervalLimitUnit, + timeZone?: string +) => { const dates = []; - let startDate = dayjs(dateFrom).startOf(period); - const endDate = dayjs(dateTo).endOf(period); + let startDate = timeZone ? dayjs(dateFrom).tz(timeZone).startOf(period) : dayjs(dateFrom).startOf(period); + const endDate = timeZone ? dayjs(dateTo).tz(timeZone).endOf(period) : dayjs(dateTo).endOf(period); while (startDate.isBefore(endDate)) { dates.push(startDate); diff --git a/packages/lib/intervalLimits/limitManager.ts b/packages/lib/intervalLimits/limitManager.ts index bbff888d2b..e659b3c404 100644 --- a/packages/lib/intervalLimits/limitManager.ts +++ b/packages/lib/intervalLimits/limitManager.ts @@ -14,31 +14,32 @@ export default class LimitManager { /** * Creates a busy map key */ - private static createKey(start: Dayjs, unit: IntervalLimitUnit): BusyMapKey { - return `${unit}-${start.startOf(unit).toISOString()}`; + private static createKey(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string): BusyMapKey { + const tzStart = timeZone ? start.tz(timeZone) : start; + return `${unit}-${tzStart.startOf(unit).toISOString()}`; } /** * Checks if already marked busy by ancestors or siblings */ - isAlreadyBusy(start: Dayjs, unit: IntervalLimitUnit) { - if (this.busyMap.has(LimitManager.createKey(start, "year"))) return true; + isAlreadyBusy(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string) { + if (this.busyMap.has(LimitManager.createKey(start, "year", timeZone))) return true; - if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month"))) { + if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month", timeZone))) { return true; } else if ( unit === "week" && // weeks can be part of two months - ((this.busyMap.has(LimitManager.createKey(start, "month")) && - this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month"))) || - this.busyMap.has(LimitManager.createKey(start, "week"))) + ((this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) && + this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month", timeZone))) || + this.busyMap.has(LimitManager.createKey(start, "week", timeZone))) ) { return true; } else if ( unit === "day" && - (this.busyMap.has(LimitManager.createKey(start, "month")) || - this.busyMap.has(LimitManager.createKey(start, "week")) || - this.busyMap.has(LimitManager.createKey(start, "day"))) + (this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) || + this.busyMap.has(LimitManager.createKey(start, "week", timeZone)) || + this.busyMap.has(LimitManager.createKey(start, "day", timeZone))) ) { return true; } else { @@ -49,10 +50,11 @@ export default class LimitManager { /** * Adds a new busy time */ - addBusyTime(start: Dayjs, unit: IntervalLimitUnit) { - this.busyMap.set(`${unit}-${start.toISOString()}`, { - start: start.toISOString(), - end: start.endOf(unit).toISOString(), + addBusyTime(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string) { + const tzStart = timeZone ? start.tz(timeZone) : start; + this.busyMap.set(`${unit}-${tzStart.toISOString()}`, { + start: tzStart.toISOString(), + end: tzStart.endOf(unit).toISOString(), }); } diff --git a/packages/lib/server/repository/booking.ts b/packages/lib/server/repository/booking.ts index b02d77729f..0f6e1fe95c 100644 --- a/packages/lib/server/repository/booking.ts +++ b/packages/lib/server/repository/booking.ts @@ -17,6 +17,22 @@ type TeamBookingsParamsBase = { shouldReturnCount?: boolean; }; +type TeamBookingsMultipleUsersParamsBase = { + users: { id: number; email: string }[]; + teamId: number; + startDate: Date; + endDate: Date; + excludedUid?: string | null; + includeManagedEvents: boolean; + shouldReturnCount?: boolean; +}; + +type TeamBookingsMultipleUsersParamsWithCount = TeamBookingsMultipleUsersParamsBase & { + shouldReturnCount: true; +}; + +type TeamBookingsMultipleUsersParamsWithoutCount = TeamBookingsMultipleUsersParamsBase; + type TeamBookingsParamsWithCount = TeamBookingsParamsBase & { shouldReturnCount: true; }; @@ -666,4 +682,116 @@ export class BookingRepository { }, }); } + + static async getAllAcceptedTeamBookingsOfUsers( + params: TeamBookingsMultipleUsersParamsWithCount + ): Promise; + + static async getAllAcceptedTeamBookingsOfUsers( + params: TeamBookingsMultipleUsersParamsWithoutCount + ): Promise>; + + static async getAllAcceptedTeamBookingsOfUsers(params: TeamBookingsMultipleUsersParamsBase) { + const { users, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } = + params; + + const baseWhere: Prisma.BookingWhereInput = { + status: BookingStatus.ACCEPTED, + startTime: { + gte: startDate, + }, + endTime: { + lte: endDate, + }, + ...(excludedUid && { + uid: { + not: excludedUid, + }, + }), + }; + + const userIds = users.map((user) => user.id); + const userEmails = users.map((user) => user.email); + + const whereCollectiveRoundRobinOwner: Prisma.BookingWhereInput = { + ...baseWhere, + userId: { + in: userIds, + }, + eventType: { + teamId, + }, + }; + + const whereCollectiveRoundRobinBookingsAttendee: Prisma.BookingWhereInput = { + ...baseWhere, + attendees: { + some: { + email: { + in: userEmails, + }, + }, + }, + eventType: { + teamId, + }, + }; + + const whereManagedBookings: Prisma.BookingWhereInput = { + ...baseWhere, + userId: { + in: userIds, + }, + eventType: { + parent: { + teamId, + }, + }, + }; + + if (shouldReturnCount) { + const collectiveRoundRobinBookingsOwner = await prisma.booking.count({ + where: whereCollectiveRoundRobinOwner, + }); + + const collectiveRoundRobinBookingsAttendee = await prisma.booking.count({ + where: whereCollectiveRoundRobinBookingsAttendee, + }); + + let managedBookings = 0; + + if (includeManagedEvents) { + managedBookings = await prisma.booking.count({ + where: whereManagedBookings, + }); + } + + const totalNrOfBooking = + collectiveRoundRobinBookingsOwner + collectiveRoundRobinBookingsAttendee + managedBookings; + + return totalNrOfBooking; + } + + const collectiveRoundRobinBookingsOwner = await prisma.booking.findMany({ + where: whereCollectiveRoundRobinOwner, + }); + + const collectiveRoundRobinBookingsAttendee = await prisma.booking.findMany({ + where: whereCollectiveRoundRobinBookingsAttendee, + }); + + let managedBookings: typeof collectiveRoundRobinBookingsAttendee = []; + + if (includeManagedEvents) { + managedBookings = await prisma.booking.findMany({ + where: whereManagedBookings, + }); + } + + return [ + ...collectiveRoundRobinBookingsOwner, + ...collectiveRoundRobinBookingsAttendee, + ...managedBookings, + ]; + } } diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 356747dbab..285964f2ef 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -15,11 +15,21 @@ import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants"; import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability"; -import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes"; -import type { CurrentSeats, GetAvailabilityUser, IFromUser, IToUser } from "@calcom/lib/getUserAvailability"; -import { getUsersAvailability } from "@calcom/lib/getUserAvailability"; +import { getBusyTimesForLimitChecks, getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes"; +import type { + CurrentSeats, + GetAvailabilityUser, + IFromUser, + IToUser, + EventType, +} from "@calcom/lib/getUserAvailability"; +import { getUsersAvailability, getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability"; +import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit"; +import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits"; +import LimitManager from "@calcom/lib/intervalLimits/limitManager"; +import { checkBookingLimit } from "@calcom/lib/intervalLimits/server/checkBookingLimits"; import { calculatePeriodLimits, isTimeOutOfBounds, @@ -28,6 +38,7 @@ import { import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import monitorCallbackAsync, { monitorCallbackSync } from "@calcom/lib/sentryWrapper"; +import { getTotalBookingDuration } from "@calcom/lib/server/queries"; import { BookingRepository as BookingRepo } from "@calcom/lib/server/repository/booking"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; import { UserRepository, withSelectedCalendars } from "@calcom/lib/server/repository/user"; @@ -36,7 +47,7 @@ import prisma, { availabilityUserSelect } from "@calcom/prisma"; import { PeriodType, Prisma } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import type { EventBusyDate } from "@calcom/types/Calendar"; +import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar"; import type { CredentialPayload, CredentialForCalendarService } from "@calcom/types/Credential"; import { TRPCError } from "@trpc/server"; @@ -834,6 +845,395 @@ export function getAllDatesWithBookabilityStatus(availableDates: string[]) { return allDates; } +/** + * Gets busy times from limits for multiple users + */ +const getBusyTimesFromLimitsForUsers = async ( + users: { id: number; email: string }[], + bookingLimits: IntervalLimit | null, + durationLimits: IntervalLimit | null, + dateFrom: Dayjs, + dateTo: Dayjs, + duration: number | undefined, + eventType: NonNullable, + timeZone: string, + rescheduleUid?: string +) => { + const userBusyTimesMap = new Map(); + + if (!bookingLimits && !durationLimits) { + return userBusyTimesMap; + } + + const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( + dateFrom.toISOString(), + dateTo.toISOString(), + bookingLimits || durationLimits + ); + + const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({ + userIds: users.map((user) => user.id), + eventTypeId: eventType.id, + startDate: limitDateFrom.format(), + endDate: limitDateTo.format(), + rescheduleUid, + bookingLimits, + durationLimits, + }); + + const globalLimitManager = new LimitManager(); + + if (bookingLimits) { + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of busyTimesFromLimitsBookings) { + const bookingStart = dayjs(booking.start).tz(timeZone); + const bookingDay = bookingStart.format("YYYY-MM-DD"); + const periodStartDay = periodStart.format("YYYY-MM-DD"); + const periodEndDay = periodEnd.format("YYYY-MM-DD"); + + if (bookingDay < periodStartDay || bookingDay > periodEndDay) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + + for (const user of users) { + const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id); + const limitManager = new LimitManager(); + + for (const busyTime of globalLimitManager.getBusyTimes()) { + const start = dayjs(busyTime.start); + const end = dayjs(busyTime.end); + + let unit: "year" | "month" | "week" | "day"; + + if (end.diff(start, "year") >= 1) { + unit = "year"; + } else if (end.diff(start, "month") >= 1) { + unit = "month"; + } else if (end.diff(start, "week") >= 1) { + unit = "week"; + } else { + unit = "day"; + } + + limitManager.addBusyTime(start, unit, timeZone); + } + + await monitorCallbackAsync(async () => { + if (bookingLimits) { + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + if (unit === "year") { + try { + await checkBookingLimit({ + eventStartDate: periodStart.toDate(), + limitingNumber: limit, + eventId: eventType.id, + key, + user, + rescheduleUid, + timeZone, + }); + } catch (_) { + limitManager.addBusyTime(periodStart, unit, timeZone); + if ( + periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) + ) { + break; + } + } + continue; + } + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of userBookings) { + const bookingStart = dayjs(booking.start).tz(timeZone); + const bookingDay = bookingStart.format("YYYY-MM-DD"); + const periodStartDay = periodStart.format("YYYY-MM-DD"); + const periodEndDay = periodEnd.format("YYYY-MM-DD"); + + if (bookingDay < periodStartDay || bookingDay > periodEndDay) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + + if (durationLimits) { + for (const key of descendingLimitKeys) { + const limit = durationLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + const selectedDuration = (duration || eventType.length) ?? 0; + + if (selectedDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + continue; + } + + if (unit === "year") { + const totalYearlyDuration = await getTotalBookingDuration({ + eventId: eventType.id, + startDate: periodStart.toDate(), + endDate: periodStart.endOf(unit).toDate(), + rescheduleUid, + }); + if (totalYearlyDuration + selectedDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + if ( + periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) + ) { + break; + } + } + continue; + } + + const periodEnd = periodStart.endOf(unit); + let totalDuration = selectedDuration; + + for (const booking of userBookings) { + const bookingStart = dayjs(booking.start).tz(timeZone); + const bookingDay = bookingStart.format("YYYY-MM-DD"); + const periodStartDay = periodStart.format("YYYY-MM-DD"); + const periodEndDay = periodEnd.format("YYYY-MM-DD"); + + if (bookingDay < periodStartDay || bookingDay > periodEndDay) { + continue; + } + + totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute"); + if (totalDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + }); + + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + } + + return userBusyTimesMap; +}; + +/** + * Gets busy times from team booking limits for multiple users + */ +const getBusyTimesFromTeamLimitsForUsers = async ( + users: { id: number; email: string }[], + bookingLimits: IntervalLimit, + dateFrom: Dayjs, + dateTo: Dayjs, + teamId: number, + includeManagedEvents: boolean, + timeZone: string, + rescheduleUid?: string +) => { + const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( + dateFrom.toISOString(), + dateTo.toISOString(), + bookingLimits + ); + + const bookings = await BookingRepo.getAllAcceptedTeamBookingsOfUsers({ + users, + teamId, + startDate: limitDateFrom.toDate(), + endDate: limitDateTo.toDate(), + excludedUid: rescheduleUid, + includeManagedEvents, + }); + + const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ + start: dayjs(startTime).toDate(), + end: dayjs(endTime).toDate(), + title, + source: `eventType-${eventTypeId}-booking-${id}`, + userId, + })); + + const globalLimitManager = new LimitManager(); + + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of busyTimes) { + const bookingStart = dayjs(booking.start).tz(timeZone); + const bookingDay = bookingStart.format("YYYY-MM-DD"); + const periodStartDay = periodStart.format("YYYY-MM-DD"); + const periodEndDay = periodEnd.format("YYYY-MM-DD"); + + if (bookingDay < periodStartDay || bookingDay > periodEndDay) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + + const userBusyTimesMap = new Map(); + + for (const user of users) { + const userBusyTimes = busyTimes.filter((busyTime) => busyTime.userId === user.id); + const limitManager = new LimitManager(); + + for (const busyTime of globalLimitManager.getBusyTimes()) { + const start = dayjs(busyTime.start); + const end = dayjs(busyTime.end); + + let unit: "year" | "month" | "week" | "day"; + + if (end.diff(start, "year") >= 1) { + unit = "year"; + } else if (end.diff(start, "month") >= 1) { + unit = "month"; + } else if (end.diff(start, "week") >= 1) { + unit = "week"; + } else { + unit = "day"; + } + + limitManager.addBusyTime(start, unit, timeZone); + } + + await monitorCallbackAsync(async () => { + const bookingLimitsParams = { + bookings: userBusyTimes, + bookingLimits, + dateFrom, + dateTo, + limitManager, + rescheduleUid, + teamId, + user, + includeManagedEvents, + timeZone, + }; + + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + if (unit === "year") { + try { + await checkBookingLimit({ + eventStartDate: periodStart.toDate(), + limitingNumber: limit, + key, + teamId, + user, + rescheduleUid, + includeManagedEvents, + timeZone, + }); + } catch (_) { + limitManager.addBusyTime(periodStart, unit, timeZone); + if ( + periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) + ) { + return; + } + } + continue; + } + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of userBusyTimes) { + const bookingStart = dayjs(booking.start).tz(timeZone); + const bookingDay = bookingStart.format("YYYY-MM-DD"); + const periodStartDay = periodStart.format("YYYY-MM-DD"); + const periodEndDay = periodEnd.format("YYYY-MM-DD"); + + if (bookingDay < periodStartDay || bookingDay > periodEndDay) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + }); + + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + } + + return userBusyTimesMap; +}; + const calculateHostsAndAvailabilities = async ({ input, eventType, @@ -905,6 +1305,43 @@ const calculateHostsAndAvailabilities = async ({ }); } + let busyTimesFromLimitsMap: Map | undefined = undefined; + if (eventType && (bookingLimits || durationLimits)) { + const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + busyTimesFromLimitsMap = await getBusyTimesFromLimitsForUsers( + usersForLimits, + bookingLimits, + durationLimits, + startTime, + endTime, + typeof input.duration === "number" ? input.duration : undefined, + eventType, + usersWithCredentials[0]?.timeZone || "UTC", + input.rescheduleUid || undefined + ); + } + + const teamForBookingLimits = + eventType?.team ?? + (eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null); + + const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); + + let teamBookingLimitsMap: Map | undefined = undefined; + if (teamForBookingLimits && teamBookingLimits) { + const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + teamBookingLimitsMap = await getBusyTimesFromTeamLimitsForUsers( + usersForTeamLimits, + teamBookingLimits, + startTime, + endTime, + teamForBookingLimits.id, + teamForBookingLimits.includeManagedEventsInLimits, + usersWithCredentials[0]?.timeZone || "UTC", + input.rescheduleUid || undefined + ); + } + const users = monitorCallbackSync(function enrichUsersWithData() { return usersWithCredentials.map((currentUser) => { return { @@ -940,6 +1377,10 @@ const calculateHostsAndAvailabilities = async ({ currentSeats, rescheduleUid: input.rescheduleUid, busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, + busyTimesFromLimits: busyTimesFromLimitsMap, + eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null, + teamBookingLimits: teamBookingLimitsMap, + teamForBookingLimits: teamForBookingLimits, }, }); /* We get all users working hours and busy slots */