* add eslint config * migrate autoLock to features * migrate teamService to features * migrate userCreationService * migrate insights services to features * migrate ProfileRepository * update imports * migrate filter segmen tests * migrate filter segment repository * migrate getBusyTimes * migrate getLocaleFromRequest * refactor csvUtils * make filename clearer * migrate getLuckyUser integration test * migrate autoLock test to features * wip * refactors * migrate useBookerUrl * migrate more * wip * Migrate eventTypeRepository * membership repository * update imports * update imports * migrate * move organization repository * update imports * update imports * wip * wip * wip * wip * wip * wip * wip * wip * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix tests * fix type checks * fix * fix * migrate * update imports * fix tests * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix
285 lines
8.5 KiB
TypeScript
285 lines
8.5 KiB
TypeScript
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import type { EventType } from "@calcom/features/availability/lib/getUserAvailability";
|
|
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
|
import { getCheckBookingLimitsService } from "@calcom/features/di/containers/BookingLimits";
|
|
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
|
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
|
|
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
|
import LimitManager from "@calcom/lib/intervalLimits/limitManager";
|
|
import { isBookingWithinPeriod } from "@calcom/lib/intervalLimits/utils";
|
|
import { getPeriodStartDatesBetween } from "@calcom/lib/intervalLimits/utils/getPeriodStartDatesBetween";
|
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
|
import { performance } from "@calcom/lib/server/perfObserver";
|
|
import prisma from "@calcom/prisma";
|
|
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
|
|
|
const _getBusyTimesFromLimits = async (
|
|
bookingLimits: IntervalLimit | null,
|
|
durationLimits: IntervalLimit | null,
|
|
dateFrom: Dayjs,
|
|
dateTo: Dayjs,
|
|
duration: number | undefined,
|
|
eventType: NonNullable<EventType>,
|
|
bookings: EventBusyDetails[],
|
|
timeZone: string,
|
|
rescheduleUid?: string
|
|
) => {
|
|
performance.mark("limitsStart");
|
|
|
|
// shared amongst limiters to prevent processing known busy periods
|
|
const limitManager = new LimitManager();
|
|
|
|
// run this first, as counting bookings should always run faster..
|
|
if (bookingLimits) {
|
|
performance.mark("bookingLimitsStart");
|
|
await getBusyTimesFromBookingLimits({
|
|
bookings,
|
|
bookingLimits,
|
|
dateFrom,
|
|
dateTo,
|
|
eventTypeId: eventType.id,
|
|
limitManager,
|
|
rescheduleUid,
|
|
timeZone,
|
|
});
|
|
performance.mark("bookingLimitsEnd");
|
|
performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd");
|
|
}
|
|
|
|
// ..than adding up durations (especially for the whole year)
|
|
if (durationLimits) {
|
|
performance.mark("durationLimitsStart");
|
|
await getBusyTimesFromDurationLimits(
|
|
bookings,
|
|
durationLimits,
|
|
dateFrom,
|
|
dateTo,
|
|
duration,
|
|
eventType,
|
|
limitManager,
|
|
timeZone,
|
|
rescheduleUid
|
|
);
|
|
performance.mark("durationLimitsEnd");
|
|
performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd");
|
|
}
|
|
|
|
performance.mark("limitsEnd");
|
|
performance.measure(`checking all limits took $1'`, "limitsStart", "limitsEnd");
|
|
|
|
return limitManager.getBusyTimes();
|
|
};
|
|
|
|
const _getBusyTimesFromBookingLimits = async (params: {
|
|
bookings: EventBusyDetails[];
|
|
bookingLimits: IntervalLimit;
|
|
dateFrom: Dayjs;
|
|
dateTo: Dayjs;
|
|
limitManager: LimitManager;
|
|
rescheduleUid?: string;
|
|
eventTypeId?: number;
|
|
teamId?: number;
|
|
user?: { id: number; email: string };
|
|
includeManagedEvents?: boolean;
|
|
timeZone?: string | null;
|
|
}) => {
|
|
const {
|
|
bookings,
|
|
bookingLimits,
|
|
dateFrom,
|
|
dateTo,
|
|
limitManager,
|
|
eventTypeId,
|
|
teamId,
|
|
user,
|
|
rescheduleUid,
|
|
includeManagedEvents = false,
|
|
timeZone,
|
|
} = params;
|
|
|
|
for (const key of descendingLimitKeys) {
|
|
const limit = bookingLimits?.[key];
|
|
if (!limit) continue;
|
|
|
|
const unit = intervalLimitKeyToUnit(key);
|
|
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit);
|
|
|
|
for (const periodStart of periodStartDates) {
|
|
if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
|
|
|
|
// special handling of yearly limits to improve performance
|
|
if (unit === "year") {
|
|
try {
|
|
const checkBookingLimitsService = getCheckBookingLimitsService();
|
|
await checkBookingLimitsService.checkBookingLimit({
|
|
eventStartDate: periodStart.toDate(),
|
|
limitingNumber: limit,
|
|
eventId: eventTypeId,
|
|
key,
|
|
teamId,
|
|
user,
|
|
rescheduleUid,
|
|
includeManagedEvents,
|
|
timeZone,
|
|
});
|
|
} catch (_) {
|
|
limitManager.addBusyTime(periodStart, unit);
|
|
if (periodStartDates.every((start) => limitManager.isAlreadyBusy(start, unit))) {
|
|
return;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const periodEnd = periodStart.endOf(unit);
|
|
let totalBookings = 0;
|
|
|
|
for (const booking of bookings) {
|
|
// consider booking part of period independent of end date
|
|
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone || "UTC")) {
|
|
continue;
|
|
}
|
|
totalBookings++;
|
|
if (totalBookings >= limit) {
|
|
limitManager.addBusyTime(periodStart, unit);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const _getBusyTimesFromDurationLimits = async (
|
|
bookings: EventBusyDetails[],
|
|
durationLimits: IntervalLimit,
|
|
dateFrom: Dayjs,
|
|
dateTo: Dayjs,
|
|
duration: number | undefined,
|
|
eventType: NonNullable<EventType>,
|
|
limitManager: LimitManager,
|
|
timeZone: string,
|
|
rescheduleUid?: string
|
|
) => {
|
|
for (const key of descendingLimitKeys) {
|
|
const limit = durationLimits?.[key];
|
|
if (!limit) continue;
|
|
|
|
const unit = intervalLimitKeyToUnit(key);
|
|
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit);
|
|
|
|
for (const periodStart of periodStartDates) {
|
|
if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
|
|
|
|
const selectedDuration = (duration || eventType.length) ?? 0;
|
|
|
|
if (selectedDuration > limit) {
|
|
limitManager.addBusyTime(periodStart, unit);
|
|
continue;
|
|
}
|
|
|
|
// special handling of yearly limits to improve performance
|
|
if (unit === "year") {
|
|
const bookingRepo = new BookingRepository(prisma);
|
|
const totalYearlyDuration = await bookingRepo.getTotalBookingDuration({
|
|
eventId: eventType.id,
|
|
startDate: periodStart.toDate(),
|
|
endDate: periodStart.endOf(unit).toDate(),
|
|
rescheduleUid,
|
|
});
|
|
if (totalYearlyDuration + selectedDuration > limit) {
|
|
limitManager.addBusyTime(periodStart, unit);
|
|
if (periodStartDates.every((start) => limitManager.isAlreadyBusy(start, unit))) {
|
|
return;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const periodEnd = periodStart.endOf(unit);
|
|
let totalDuration = selectedDuration;
|
|
|
|
for (const booking of bookings) {
|
|
// consider booking part of period independent of end date
|
|
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone || "UTC")) {
|
|
continue;
|
|
}
|
|
totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute");
|
|
if (totalDuration > limit) {
|
|
limitManager.addBusyTime(periodStart, unit);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const getBusyTimesFromDurationLimits = withReporting(
|
|
_getBusyTimesFromDurationLimits,
|
|
"getBusyTimesFromDurationLimits"
|
|
);
|
|
|
|
const _getBusyTimesFromTeamLimits = async (
|
|
user: { id: number; email: string },
|
|
bookingLimits: IntervalLimit,
|
|
dateFrom: Dayjs,
|
|
dateTo: Dayjs,
|
|
teamId: number,
|
|
includeManagedEvents: boolean,
|
|
timeZone: string,
|
|
rescheduleUid?: string
|
|
) => {
|
|
const busyTimesService = getBusyTimesService();
|
|
const { limitDateFrom, limitDateTo } = busyTimesService.getStartEndDateforLimitCheck(
|
|
dateFrom.toISOString(),
|
|
dateTo.toISOString(),
|
|
bookingLimits
|
|
);
|
|
|
|
const bookingRepo = new BookingRepository(prisma);
|
|
const bookings = await bookingRepo.getAllAcceptedTeamBookingsOfUser({
|
|
user,
|
|
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 limitManager = new LimitManager();
|
|
|
|
await getBusyTimesFromBookingLimits({
|
|
bookings: busyTimes,
|
|
bookingLimits,
|
|
dateFrom,
|
|
dateTo,
|
|
limitManager,
|
|
rescheduleUid,
|
|
teamId,
|
|
user,
|
|
includeManagedEvents,
|
|
timeZone,
|
|
});
|
|
|
|
return limitManager.getBusyTimes();
|
|
};
|
|
|
|
export const getBusyTimesFromLimits = withReporting(_getBusyTimesFromLimits, "getBusyTimesFromLimits");
|
|
|
|
export const getBusyTimesFromBookingLimits = withReporting(
|
|
_getBusyTimesFromBookingLimits,
|
|
"getBusyTimesFromBookingLimits"
|
|
);
|
|
|
|
export const getBusyTimesFromTeamLimits = withReporting(
|
|
_getBusyTimesFromTeamLimits,
|
|
"getBusyTimesFromTeamLimits"
|
|
);
|