* refactor: replace shouldServeCache with mode parameter for calendar cache control Replace the boolean shouldServeCache parameter with a new CalendarFetchMode type that can have values 'slots', 'overlay', and 'booking'. This provides better control over when to serve cache vs relay on calendar providers. - 'slots' mode: Check feature flags and use cache when available (for getting actual calendar availability) - 'overlay' mode: Don't use cache (for overlay calendar availability) - 'booking' mode: Don't use cache (for booking confirmation) - undefined: Same as 'slots' for backwards compatibility The cache decision logic is now centralized in getCalendar.ts based on the mode parameter. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: update CalendarService.test.ts to use mode parameter Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: use shared GetAvailabilityParams type across all calendar services - Import GetAvailabilityParams and GetAvailabilityWithTimeZonesParams from @calcom/types/Calendar - Replace inline type definitions with shared types in all calendar service implementations - Update BaseCalendarService, CalendarCacheWrapper, and CalendarTelemetryWrapper - Ensures consistent typing and follows DRY principles Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add missing IntegrationCalendar import and update mock getAvailability signature - Add IntegrationCalendar back to sendgrid CalendarService imports - Update bookingScenario mock getAvailability to use typed params object - Add listCalendars method to mock Calendar object Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: update test files to use typed params object for getAvailability methods - Update CalendarCacheWrapper.test.ts to call getAvailability/getAvailabilityWithTimeZones with params object - Update getCalendarsEvents.test.ts toHaveBeenCalledWith assertions to expect params object - All 32 tests now pass Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: consolidate to single GetAvailabilityParams type for both getAvailability methods - Remove GetAvailabilityWithTimeZonesParams, use GetAvailabilityParams for both methods - Add mode parameter to getAvailabilityWithTimeZones calls - Update wrapper classes to pass mode through to underlying calendar - Update test files to include mode in getAvailabilityWithTimeZones calls and assertions Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: use EventBusyDate with optional timeZone for getAvailabilityWithTimeZones - Add optional timeZone field to EventBusyDate type - Update getAvailabilityWithTimeZones return type to use EventBusyDate[] - Update Google Calendar service and wrapper classes to use the new type Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: type errors and remove unused calendar watching methods - Fix type errors in CalendarCacheWrapper.ts (convert null to undefined for timeZone) - Fix type errors in getCalendarsEvents.ts (ensure timeZone is always present) - Remove unused watchCalendar/unwatchCalendar from Calendar interface - Remove unused startWatchingCalendarsInGoogle/stopWatchingCalendarsInGoogle from GoogleCalendarService - Remove unused imports (uuid, uniqueBy, GOOGLE_WEBHOOK_URL, ONE_MONTH_IN_MS) Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: remove watchCalendar/unwatchCalendar from CalendarTelemetryWrapper Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: remove verbose JSDoc param comments from wrapper classes Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: make mode parameter required in getCalendar Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add required mode parameter to all getCalendar callers Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: provide default mode value in getBusyCalendarTimes Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to remaining getCalendar callers Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to vital and wipemycalother reschedule Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add mode parameter to credential-sync API endpoint Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: add 'none' mode to CalendarFetchMode and use as default Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * test: add mode parameter to getCalendarsEvents test calls Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * Update packages/app-store/delegationCredential.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
266 lines
9.2 KiB
TypeScript
266 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 { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
|
import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability";
|
|
import { buildDateRanges } from "@calcom/features/schedules/lib/date-ranges";
|
|
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 { CalendarFetchMode } from "@calcom/types/Calendar";
|
|
|
|
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>,
|
|
mode?: CalendarFetchMode
|
|
// 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,
|
|
mode,
|
|
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");
|