+5![mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)






![Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)

![devin-ai-integration[bot]](/assets/img/avatar_default.png)
devin-ai-integration[bot]
GitHub
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
morgan@cal.com <morgan@cal.com>
Anik Dhabal Babu
Benny Joo
Sahitya Chandra
Carina Wollendorfer
CarinaWolli
mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Ayush Kumar
Syed Ali Shahbaz
Lauris Skraucis
supalarry
emrysal
ebeb008f9b
* refactor: convert findQualifiedHostsWithDelegationCredentials to service class with DI - Create QualifiedHostsService class following UserAvailabilityService pattern - Add IQualifiedHostsService interface with prisma and bookingRepo dependencies - Create DI module and container for qualified hosts service - Update filterHostsBySameRoundRobinHost to accept prisma as parameter - Update all usage sites to use the new service: - loadAndValidateUsers.ts - slots/util.ts - test mocks in _post.test.ts - Maintain backward compatibility with original function export - Fix type issues in team properties (rrResetInterval, rrTimestampBasis) Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: update filterHostsBySameRoundRobinHost test to include prisma parameter - Add missing prisma parameter to all test function calls - Resolves unit test failure caused by function signature change Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: resolve type issues in FilterHostsService - Import PrismaClient type instead of using unknown - Fix type compatibility for BookingRepository constructor - Update test mocks to use proper BookingRepository type - Ensure all DI dependencies are properly typed Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor: rename DI files to CamelCase and update imports - Rename all files in packages/lib/di from kebab-case to CamelCase - Update 22 external files with import statements to use new file names - Update internal DI module files with corrected imports - Maintain consistency with TypeScript naming conventions Co-Authored-By: morgan@cal.com <morgan@cal.com> * chore: bump platform libs * chore: bump platform libs * fix: remove obsolete vitest mock after service class refactoring - Remove obsolete mock for old function module - Keep correct mock for new DI container - Resolves CI unit test failures Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: correct import path for calAIPhone zod-utils module Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: Booker active booking limit can't be switched off (#23005) * refactor: Get rid of `getServerSideProps` for /getting-started pages (#23003) * refactor * fix type check * fix: Remove Reporting page within Routing Forms (#22990) * fix error in handleNewBooking (#23011) Co-authored-by: CarinaWolli <wollencarina@gmail.com> * Documentation edits made through Mintlify web editor (#23007) Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com> * fix: Contact support button position changed from absolute to fixed (#23002) * feat: Add private links to API (#22943) * --init * address change requests * adding further changes * address feedback * further changes * further clean-up * clean up * fix module import and others * add guards * remove unnecessary comments * remove unnecessary comments * cleanup * sort coderabbig suggestions * improve check * chore: bump platform libraries --------- Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> * chore: release v5.5.15 * chore: bump platform libs * chore: bump platform libs --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: morgan@cal.com <morgan@cal.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Sahitya Chandra <sahityajb@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com> Co-authored-by: Ayush Kumar <kumarayushkumar@protonmail.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: supalarry <laurisskraucis@gmail.com> Co-authored-by: emrysal <me@alexvanandel.com>
286 lines
8.4 KiB
TypeScript
286 lines
8.4 KiB
TypeScript
import type { Dayjs } from "@calcom/dayjs";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/BookingLimits";
|
|
import { getBusyTimesService } from "@calcom/lib/di/containers/BusyTimes";
|
|
import type { EventType } from "@calcom/lib/getUserAvailability";
|
|
import { getPeriodStartDatesBetween } from "@calcom/lib/intervalLimits/utils/getPeriodStartDatesBetween";
|
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
|
import { performance } from "@calcom/lib/server/perfObserver";
|
|
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
|
import prisma from "@calcom/prisma";
|
|
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
|
|
|
import { descendingLimitKeys, intervalLimitKeyToUnit } from "../intervalLimit";
|
|
import type { IntervalLimit } from "../intervalLimitSchema";
|
|
import LimitManager from "../limitManager";
|
|
import { isBookingWithinPeriod } from "../utils";
|
|
|
|
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"
|
|
);
|