Files
calendar/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts
T
+8
devin-ai-integration[bot]GitHubmorgan@cal.com <morgan@cal.com>morgan@cal.com <morgan@cal.com>CarinaWollimintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>Lauris SkraucissupalarryDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>morgan@cal.com <morgan@cal.com>Anik Dhabal BabuBenny JooSahitya ChandraCarina Wollendorfermintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>Ayush KumarSyed Ali Shahbazemrysal
ebeb008f9b refactor: convert findQualifiedHostsWithDelegationCredentials to service class with DI (#22974)
* 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>
2025-08-11 12:24:15 +00:00

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");