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


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




![devin-ai-integration[bot]](/assets/img/avatar_default.png)
devin-ai-integration[bot]
GitHub
morgan@cal.com <morgan@cal.com>
morgan@cal.com <morgan@cal.com>
CarinaWolli
mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Lauris Skraucis
supalarry
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
mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Ayush Kumar
Syed Ali Shahbaz
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>
265 lines
9.2 KiB
TypeScript
265 lines
9.2 KiB
TypeScript
import type { Logger } from "tslog";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
|
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
|
import { getBusyTimesService } from "@calcom/lib/di/containers/BusyTimes";
|
|
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
|
|
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
|
|
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
|
import prisma from "@calcom/prisma";
|
|
|
|
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
|
import type { BookingType } from "./originalRescheduledBookingUtils";
|
|
import type { IsFixedAwareUser } from "./types";
|
|
|
|
type DateRange = {
|
|
start: Dayjs;
|
|
end: Dayjs;
|
|
};
|
|
|
|
const getDateTimeInUtc = (timeInput: string, timeZone?: string) => {
|
|
return timeZone === "Etc/GMT" ? dayjs.utc(timeInput) : dayjs(timeInput).tz(timeZone).utc();
|
|
};
|
|
|
|
const getOriginalBookingDuration = (originalBooking?: BookingType) => {
|
|
return originalBooking
|
|
? dayjs(originalBooking.endTime).diff(dayjs(originalBooking.startTime), "minutes")
|
|
: undefined;
|
|
};
|
|
|
|
const hasDateRangeForBooking = (
|
|
dateRanges: DateRange[],
|
|
startDateTimeUtc: dayjs.Dayjs,
|
|
endDateTimeUtc: dayjs.Dayjs
|
|
) => {
|
|
let dateRangeForBooking = false;
|
|
|
|
for (const dateRange of dateRanges) {
|
|
if (
|
|
(startDateTimeUtc.isAfter(dateRange.start) || startDateTimeUtc.isSame(dateRange.start)) &&
|
|
(endDateTimeUtc.isBefore(dateRange.end) || endDateTimeUtc.isSame(dateRange.end))
|
|
) {
|
|
dateRangeForBooking = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return dateRangeForBooking;
|
|
};
|
|
|
|
const _ensureAvailableUsers = async (
|
|
eventType: Omit<getEventTypeResponse, "users"> & {
|
|
users: IsFixedAwareUser[];
|
|
},
|
|
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
|
loggerWithEventDetails: Logger<unknown>,
|
|
shouldServeCache?: boolean
|
|
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
|
|
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
|
|
const userAvailabilityService = getUserAvailabilityService();
|
|
const availableUsers: IsFixedAwareUser[] = [];
|
|
|
|
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
|
|
const endDateTimeUtc = getDateTimeInUtc(input.dateTo, input.timeZone);
|
|
|
|
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
|
const originalBookingDuration = getOriginalBookingDuration(input.originalRescheduledBooking);
|
|
|
|
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
|
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
|
|
|
const busyTimesService = getBusyTimesService();
|
|
const busyTimesFromLimitsBookingsAllUsers: Awaited<
|
|
ReturnType<typeof busyTimesService.getBusyTimesForLimitChecks>
|
|
> =
|
|
eventType && (bookingLimits || durationLimits)
|
|
? await busyTimesService.getBusyTimesForLimitChecks({
|
|
userIds: eventType.users.map((u) => u.id),
|
|
eventTypeId: eventType.id,
|
|
startDate: startDateTimeUtc.format(),
|
|
endDate: endDateTimeUtc.format(),
|
|
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
|
bookingLimits,
|
|
durationLimits,
|
|
})
|
|
: [];
|
|
|
|
const usersAvailability = await userAvailabilityService.getUsersAvailability({
|
|
users: eventType.users,
|
|
query: {
|
|
...input,
|
|
eventTypeId: eventType.id,
|
|
duration: originalBookingDuration,
|
|
returnDateOverrides: false,
|
|
dateFrom: startDateTimeUtc.format(),
|
|
dateTo: endDateTimeUtc.format(),
|
|
beforeEventBuffer: eventType.beforeEventBuffer,
|
|
afterEventBuffer: eventType.afterEventBuffer,
|
|
bypassBusyCalendarTimes: false,
|
|
shouldServeCache,
|
|
withSource: true,
|
|
},
|
|
initialData: {
|
|
eventType,
|
|
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
|
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers,
|
|
},
|
|
});
|
|
|
|
const piiFreeInputDataForLogging = safeStringify({
|
|
startDateTimeUtc,
|
|
endDateTimeUtc,
|
|
...{
|
|
...input,
|
|
originalRescheduledBooking: input.originalRescheduledBooking
|
|
? {
|
|
...input.originalRescheduledBooking,
|
|
user: input.originalRescheduledBooking?.user
|
|
? getPiiFreeUser(input.originalRescheduledBooking.user)
|
|
: null,
|
|
}
|
|
: undefined,
|
|
},
|
|
});
|
|
|
|
if (eventType.restrictionScheduleId) {
|
|
try {
|
|
const restrictionSchedule = await prisma.schedule.findUnique({
|
|
where: { id: eventType.restrictionScheduleId },
|
|
select: {
|
|
id: true,
|
|
timeZone: true,
|
|
userId: true,
|
|
availability: {
|
|
select: {
|
|
days: true,
|
|
startTime: true,
|
|
endTime: true,
|
|
date: true,
|
|
},
|
|
},
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
defaultScheduleId: true,
|
|
travelSchedules: {
|
|
select: {
|
|
id: true,
|
|
timeZone: true,
|
|
startDate: true,
|
|
endDate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!restrictionSchedule) {
|
|
loggerWithEventDetails.error(`Restriction schedule ${eventType.restrictionScheduleId} not found`);
|
|
throw new Error(ErrorCode.RestrictionScheduleNotFound);
|
|
}
|
|
|
|
const restrictionTimezone = eventType.useBookerTimezone
|
|
? input.timeZone
|
|
: restrictionSchedule.timeZone!;
|
|
|
|
if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) {
|
|
loggerWithEventDetails.error(
|
|
`No timezone is set for the restriction schedule and useBookerTimezone is false`
|
|
);
|
|
throw new Error(ErrorCode.BookingNotAllowedByRestrictionSchedule);
|
|
}
|
|
|
|
const restrictionAvailability = restrictionSchedule.availability.map((rule) => ({
|
|
days: rule.days,
|
|
startTime: rule.startTime,
|
|
endTime: rule.endTime,
|
|
date: rule.date,
|
|
}));
|
|
|
|
const isDefaultSchedule = restrictionSchedule.user.defaultScheduleId === restrictionSchedule.id;
|
|
const travelSchedules =
|
|
isDefaultSchedule && !eventType.useBookerTimezone
|
|
? restrictionSchedule.user.travelSchedules.map((schedule) => ({
|
|
startDate: dayjs(schedule.startDate),
|
|
endDate: schedule.endDate ? dayjs(schedule.endDate) : undefined,
|
|
timeZone: schedule.timeZone,
|
|
}))
|
|
: [];
|
|
|
|
const { dateRanges: restrictionRanges } = buildDateRanges({
|
|
availability: restrictionAvailability,
|
|
timeZone: restrictionTimezone,
|
|
dateFrom: startDateTimeUtc,
|
|
dateTo: endDateTimeUtc,
|
|
travelSchedules,
|
|
});
|
|
|
|
if (!hasDateRangeForBooking(restrictionRanges, startDateTimeUtc, endDateTimeUtc)) {
|
|
loggerWithEventDetails.error(
|
|
`Booking outside restriction schedule availability.`,
|
|
piiFreeInputDataForLogging
|
|
);
|
|
throw new Error(ErrorCode.BookingNotAllowedByRestrictionSchedule);
|
|
}
|
|
} catch (error) {
|
|
loggerWithEventDetails.error(`Error checking restriction schedule.`, piiFreeInputDataForLogging);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
usersAvailability.forEach((userAvailability, index) => {
|
|
const { oooExcludedDateRanges: dateRanges, busy: bufferedBusyTimes } = userAvailability;
|
|
const user = eventType.users[index];
|
|
|
|
loggerWithEventDetails.debug(
|
|
"calendarBusyTimes==>>>",
|
|
JSON.stringify({ bufferedBusyTimes, dateRanges, isRecurringEvent: eventType.recurringEvent })
|
|
);
|
|
|
|
if (!dateRanges.length) {
|
|
loggerWithEventDetails.error(
|
|
`User ${user.id} does not have availability at this time.`,
|
|
piiFreeInputDataForLogging
|
|
);
|
|
return;
|
|
}
|
|
|
|
//check if event time is within the date range
|
|
if (!hasDateRangeForBooking(dateRanges, startDateTimeUtc, endDateTimeUtc)) {
|
|
loggerWithEventDetails.error(`No date range for booking.`, piiFreeInputDataForLogging);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const foundConflict = checkForConflicts({
|
|
busy: bufferedBusyTimes,
|
|
time: startDateTimeUtc,
|
|
eventLength: duration,
|
|
});
|
|
if (!foundConflict) {
|
|
availableUsers.push({ ...user, availabilityData: userAvailability });
|
|
}
|
|
} catch (error) {
|
|
loggerWithEventDetails.error("Unable set isAvailableToBeBooked. Using true. ", error);
|
|
}
|
|
});
|
|
|
|
if (availableUsers.length === 0) {
|
|
loggerWithEventDetails.error(`No available users found.`, piiFreeInputDataForLogging);
|
|
throw new Error(ErrorCode.NoAvailableUsersFound);
|
|
}
|
|
|
|
// make sure TypeScript understands availableUsers is at least one.
|
|
return availableUsers.length === 1 ? [availableUsers[0]] : [availableUsers[0], ...availableUsers.slice(1)];
|
|
};
|
|
|
|
export const ensureAvailableUsers = withReporting(_ensureAvailableUsers, "ensureAvailableUsers");
|