refactor: convert getBusyTimes to service class with dependency injection (#22949)
* refactor: convert getBusyTimes to service class with dependency injection - Create BusyTimesService following UserAvailabilityService pattern - Add DI tokens, module, and container setup for BusyTimesService - Update all usage locations to use service instead of direct function calls - Maintain existing function signatures for backward compatibility - Add legacy exports to ensure smooth transition - Update type references in trpc util to use service prototype - Fix linting issues by making legacy exports async with proper imports - All tests pass and type checking succeeds Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor: busy time service * fix: api v2 eslint plugins version mismatch * chore: bump platform libs * chore: bump platform libs * fix: missing di busyTimesModule in slots service --------- 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: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
morgan@cal.com
Morgan
parent
3ea73e75d6
commit
29314cde75
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.288",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.291",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
@@ -98,6 +98,8 @@
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6",
|
||||
"@typescript-eslint/parser": "^6",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
|
||||
@@ -8,12 +8,13 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
@@ -31,7 +32,8 @@ import { UserAvailabilityService } from "@/lib/services/user-availability.servic
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
UserAvailabilityService
|
||||
UserAvailabilityService,
|
||||
BusyTimesService,
|
||||
],
|
||||
exports: [AvailableSlotsService],
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.rep
|
||||
import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
@@ -48,6 +49,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
eventTypeRepository,
|
||||
redisService
|
||||
),
|
||||
busyTimesService: new BusyTimesService(bookingRepository),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { BusyTimesService as BaseBusyTimesService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class BusyTimesService extends BaseBusyTimesService {
|
||||
constructor(bookingRepository: PrismaBookingRepository) {
|
||||
super({
|
||||
bookingRepo: bookingRepository,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ 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 { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes";
|
||||
import { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
|
||||
import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
|
||||
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";
|
||||
@@ -62,7 +62,7 @@ const _ensureAvailableUsers = async (
|
||||
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 userAvailabilityService = getUserAvailabilityService();
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
|
||||
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
|
||||
@@ -74,9 +74,12 @@ const _ensureAvailableUsers = async (
|
||||
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
||||
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
||||
|
||||
const busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> =
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimesFromLimitsBookingsAllUsers: Awaited<
|
||||
ReturnType<typeof busyTimesService.getBusyTimesForLimitChecks>
|
||||
> =
|
||||
eventType && (bookingLimits || durationLimits)
|
||||
? await getBusyTimesForLimitChecks({
|
||||
? await busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: eventType.users.map((u) => u.id),
|
||||
eventTypeId: eventType.id,
|
||||
startDate: startDateTimeUtc.format(),
|
||||
|
||||
@@ -7,17 +7,18 @@ import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/s
|
||||
|
||||
import { availableSlotsModule } from "../modules/available-slots";
|
||||
import { bookingRepositoryModule } from "../modules/booking";
|
||||
import { busyTimesModule } from "../modules/busy-times";
|
||||
import { cacheModule } from "../modules/cache";
|
||||
import { checkBookingLimitsModule } from "../modules/check-booking-limits";
|
||||
import { eventTypeRepositoryModule } from "../modules/eventType";
|
||||
import { featuresRepositoryModule } from "../modules/features";
|
||||
import { getUserAvailabilityModule } from "../modules/get-user-availability";
|
||||
import { oooRepositoryModule } from "../modules/ooo";
|
||||
import { routingFormResponseRepositoryModule } from "../modules/routingFormResponse";
|
||||
import { scheduleRepositoryModule } from "../modules/schedule";
|
||||
import { selectedSlotsRepositoryModule } from "../modules/selectedSlots";
|
||||
import { teamRepositoryModule } from "../modules/team";
|
||||
import { userRepositoryModule } from "../modules/user";
|
||||
import { getUserAvailabilityModule } from "../modules/get-user-availability";
|
||||
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||
@@ -34,7 +35,9 @@ container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
|
||||
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
|
||||
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
|
||||
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
|
||||
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule)
|
||||
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
|
||||
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
||||
|
||||
export function getAvailableSlotsService() {
|
||||
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createContainer } from "@evyweb/ioctopus";
|
||||
|
||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||
|
||||
import type { BusyTimesService } from "../../getBusyTimes";
|
||||
import { bookingRepositoryModule } from "../modules/booking";
|
||||
import { busyTimesModule } from "../modules/busy-times";
|
||||
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
||||
|
||||
export function getBusyTimesService() {
|
||||
return container.get<BusyTimesService>(DI_TOKENS.BUSY_TIMES_SERVICE);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { createContainer } from "@evyweb/ioctopus";
|
||||
|
||||
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||
|
||||
import { getUserAvailabilityModule } from "../modules/get-user-availability";
|
||||
import type { UserAvailabilityService } from "../../getUserAvailability";
|
||||
import { bookingRepositoryModule } from "../modules/booking";
|
||||
|
||||
import { busyTimesModule } from "../modules/busy-times";
|
||||
import { eventTypeRepositoryModule } from "../modules/eventType";
|
||||
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||
|
||||
import { getUserAvailabilityModule } from "../modules/get-user-availability";
|
||||
import { oooRepositoryModule } from "../modules/ooo";
|
||||
import { UserAvailabilityService } from "../../getUserAvailability";
|
||||
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||
@@ -18,9 +17,9 @@ container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
||||
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
|
||||
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
||||
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||
|
||||
|
||||
export function getUserAvailabilityService() {
|
||||
return container.get<UserAvailabilityService>(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE);
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
|
||||
redisClient: DI_TOKENS.REDIS_CLIENT,
|
||||
cacheService: DI_TOKENS.CACHE_SERVICE,
|
||||
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
|
||||
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE
|
||||
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE,
|
||||
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
|
||||
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { IBusyTimesService } from "../../getBusyTimes";
|
||||
import { BusyTimesService } from "../../getBusyTimes";
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
|
||||
export const busyTimesModule = createModule();
|
||||
busyTimesModule.bind(DI_TOKENS.BUSY_TIMES_SERVICE).toClass(BusyTimesService, {
|
||||
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
|
||||
} satisfies Record<keyof IBusyTimesService, symbol>);
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { IUserAvailabilityService } from "../../getUserAvailability";
|
||||
import { UserAvailabilityService } from "../../getUserAvailability";
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
import { IUserAvailabilityService, UserAvailabilityService } from "../../getUserAvailability";
|
||||
|
||||
export const getUserAvailabilityModule = createModule();
|
||||
getUserAvailabilityModule.bind(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE).toClass(UserAvailabilityService, {
|
||||
|
||||
@@ -35,4 +35,6 @@ export const DI_TOKENS = {
|
||||
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE: Symbol("CheckBookingAndDurationLimitsServiceModule"),
|
||||
GET_USER_AVAILABILITY_SERVICE: Symbol("GetUserAvailabilityService"),
|
||||
GET_USER_AVAILABILITY_SERVICE_MODULE: Symbol("GetUserAvailabilityModule"),
|
||||
BUSY_TIMES_SERVICE: Symbol("BusyTimesService"),
|
||||
BUSY_TIMES_SERVICE_MODULE: Symbol("BusyTimesServiceModule"),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { getBusyTimes } from "./getBusyTimes";
|
||||
import { getBusyTimesService } from "./di/containers/busy-times";
|
||||
|
||||
const startOfTomorrow = dayjs().add(1, "day").startOf("day");
|
||||
const tomorrowDate = startOfTomorrow.format("YYYY-MM-DD");
|
||||
@@ -58,7 +58,8 @@ const mockBookings = ({
|
||||
|
||||
describe("getBusyTimes", () => {
|
||||
it("blocks a regular time slot", async () => {
|
||||
const busyTimes = await getBusyTimes({
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimes({
|
||||
credentials: [],
|
||||
userId: 1,
|
||||
userEmail: "exampleuser1@example.com",
|
||||
@@ -85,7 +86,8 @@ describe("getBusyTimes", () => {
|
||||
]);
|
||||
});
|
||||
it("should block before and after buffer times", async () => {
|
||||
const busyTimes = await getBusyTimes({
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimes({
|
||||
credentials: [],
|
||||
userId: 1,
|
||||
userEmail: "exampleuser1@example.com",
|
||||
@@ -106,7 +108,8 @@ describe("getBusyTimes", () => {
|
||||
]);
|
||||
});
|
||||
it("should have busy times only if seated with remaining seats when buffers exist", async () => {
|
||||
const busyTimes = await getBusyTimes({
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const busyTimes = await busyTimesService.getBusyTimes({
|
||||
credentials: [],
|
||||
userId: 1,
|
||||
eventTypeId: 1,
|
||||
|
||||
+338
-330
@@ -11,6 +11,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -18,367 +19,374 @@ import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes";
|
||||
import { BookingRepository } from "./server/repository/booking";
|
||||
|
||||
const _getBusyTimes = async (params: {
|
||||
credentials: CredentialForCalendarService[];
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
username: string;
|
||||
eventTypeId?: number;
|
||||
startTime: string;
|
||||
beforeEventBuffer?: number;
|
||||
afterEventBuffer?: number;
|
||||
endTime: string;
|
||||
selectedCalendars: SelectedCalendar[];
|
||||
seatedEvent?: boolean;
|
||||
rescheduleUid?: string | null;
|
||||
duration?: number | null;
|
||||
currentBookings?:
|
||||
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
|
||||
eventType: Pick<
|
||||
EventType,
|
||||
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
|
||||
> | null;
|
||||
_count?: {
|
||||
seatsReferences: number;
|
||||
};
|
||||
})[]
|
||||
| null;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
credentials,
|
||||
userId,
|
||||
userEmail,
|
||||
username,
|
||||
eventTypeId,
|
||||
startTime,
|
||||
endTime,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
seatedEvent,
|
||||
rescheduleUid,
|
||||
duration,
|
||||
bypassBusyCalendarTimes = false,
|
||||
shouldServeCache,
|
||||
} = params;
|
||||
export interface IBusyTimesService {
|
||||
bookingRepo: BookingRepository;
|
||||
}
|
||||
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
userId,
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
export class BusyTimesService {
|
||||
constructor(public readonly dependencies: IBusyTimesService) {}
|
||||
|
||||
/**
|
||||
* A user is considered busy within a given time period if there
|
||||
* is a booking they own OR attend.
|
||||
*
|
||||
* Performs a query for all bookings where:
|
||||
* - The given booking is owned by this user, or..
|
||||
* - The current user has a different booking at this time he/she attends
|
||||
*
|
||||
* See further discussion within this GH issue:
|
||||
* https://github.com/calcom/cal.com/issues/6374
|
||||
*
|
||||
* NOTE: Changes here will likely require changes to some mocking
|
||||
* logic within getSchedule.test.ts:addBookings
|
||||
*/
|
||||
performance.mark("prismaBookingGetStart");
|
||||
|
||||
const startTimeDate =
|
||||
rescheduleUid && duration ? dayjs(startTime).subtract(duration, "minute").toDate() : new Date(startTime);
|
||||
const endTimeDate =
|
||||
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
|
||||
|
||||
// to also get bookings that are outside of start and end time, but the buffer falls within the start and end time
|
||||
const definedBufferTimes = getDefinedBufferTimes();
|
||||
const maxBuffer = definedBufferTimes[definedBufferTimes.length - 1];
|
||||
const startTimeAdjustedWithMaxBuffer = dayjs(startTimeDate).subtract(maxBuffer, "minute").toDate();
|
||||
const endTimeAdjustedWithMaxBuffer = dayjs(endTimeDate).add(maxBuffer, "minute").toDate();
|
||||
|
||||
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
|
||||
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
|
||||
// This function is called from multiple places but we aren't refactoring all of them at this moment
|
||||
// to avoid potential side effects.
|
||||
let bookings = params.currentBookings;
|
||||
|
||||
if (!bookings) {
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
|
||||
userIdAndEmailMap: new Map([[userId, userEmail]]),
|
||||
eventTypeId,
|
||||
startDate: startTimeAdjustedWithMaxBuffer,
|
||||
endDate: endTimeAdjustedWithMaxBuffer,
|
||||
seatedEvent,
|
||||
});
|
||||
}
|
||||
|
||||
const bookingSeatCountMap: { [x: string]: number } = {};
|
||||
const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => {
|
||||
const { id, startTime, endTime, eventType, title, ...rest } = booking;
|
||||
|
||||
const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0);
|
||||
const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0);
|
||||
|
||||
if (rest._count?.seatsReferences) {
|
||||
const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`;
|
||||
bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0;
|
||||
bookingSeatCountMap[bookedAt]++;
|
||||
// Seat references on the current event are non-blocking until the event is fully booked.
|
||||
if (
|
||||
// there are still seats available.
|
||||
bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) &&
|
||||
// and this is the seated event, other event types should be blocked.
|
||||
eventTypeId === eventType?.id
|
||||
) {
|
||||
// then we ONLY add the before/after buffer times as busy times.
|
||||
if (minutesToBlockBeforeEvent) {
|
||||
aggregate.push({
|
||||
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
|
||||
end: dayjs(startTime).toDate(), // The event starts after the buffer
|
||||
});
|
||||
}
|
||||
if (minutesToBlockAfterEvent) {
|
||||
aggregate.push({
|
||||
start: dayjs(endTime).toDate(), // The event ends before the buffer
|
||||
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
|
||||
});
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
// if it does get blocked at this point; we remove the bookingSeatCountMap entry
|
||||
// doing this allows using the map later to remove the ranges from calendar busy times.
|
||||
delete bookingSeatCountMap[bookedAt];
|
||||
}
|
||||
// rescheduling the same booking to the same time should be possible. Why?
|
||||
if (rest.uid === rescheduleUid) {
|
||||
return aggregate;
|
||||
}
|
||||
aggregate.push({
|
||||
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
|
||||
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
|
||||
title,
|
||||
source: `eventType-${eventType?.id}-booking-${id}`,
|
||||
});
|
||||
return aggregate;
|
||||
}, []);
|
||||
|
||||
logger.debug(
|
||||
`Busy Time from Cal Bookings ${JSON.stringify({
|
||||
busyTimes,
|
||||
bookings: bookings?.map((booking) => getPiiFreeBooking(booking)),
|
||||
numCredentials: credentials?.length,
|
||||
})}`
|
||||
);
|
||||
performance.mark("prismaBookingGetEnd");
|
||||
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
|
||||
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
|
||||
const startConnectedCalendarsGet = performance.now();
|
||||
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
async _getBusyTimes(params: {
|
||||
credentials: CredentialForCalendarService[];
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
username: string;
|
||||
eventTypeId?: number;
|
||||
startTime: string;
|
||||
beforeEventBuffer?: number;
|
||||
afterEventBuffer?: number;
|
||||
endTime: string;
|
||||
selectedCalendars: SelectedCalendar[];
|
||||
seatedEvent?: boolean;
|
||||
rescheduleUid?: string | null;
|
||||
duration?: number | null;
|
||||
currentBookings?:
|
||||
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
|
||||
eventType: Pick<
|
||||
EventType,
|
||||
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
|
||||
> | null;
|
||||
_count?: {
|
||||
seatsReferences: number;
|
||||
};
|
||||
})[]
|
||||
| null;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
userId,
|
||||
userEmail,
|
||||
username,
|
||||
eventTypeId,
|
||||
startTime,
|
||||
endTime,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
seatedEvent,
|
||||
rescheduleUid,
|
||||
duration,
|
||||
bypassBusyCalendarTimes = false,
|
||||
shouldServeCache,
|
||||
} = params;
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new Error(
|
||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||
(calendar) => calendar.id
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||
} ms for user ${username}`,
|
||||
JSON.stringify({
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
userId,
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
calendarBusyTimes,
|
||||
})
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
|
||||
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
||||
const [start, end] = key.split("<>");
|
||||
return {
|
||||
start: dayjs(start),
|
||||
end: dayjs(end),
|
||||
};
|
||||
});
|
||||
/**
|
||||
* A user is considered busy within a given time period if there
|
||||
* is a booking they own OR attend.
|
||||
*
|
||||
* Performs a query for all bookings where:
|
||||
* - The given booking is owned by this user, or..
|
||||
* - The current user has a different booking at this time he/she attends
|
||||
*
|
||||
* See further discussion within this GH issue:
|
||||
* https://github.com/calcom/cal.com/issues/6374
|
||||
*
|
||||
* NOTE: Changes here will likely require changes to some mocking
|
||||
* logic within getSchedule.test.ts:addBookings
|
||||
*/
|
||||
performance.mark("prismaBookingGetStart");
|
||||
|
||||
if (rescheduleUid) {
|
||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||
// calendar busy time from original rescheduled booking should not be blocked
|
||||
if (originalRescheduleBooking) {
|
||||
openSeatsDateRanges.push({
|
||||
start: dayjs(originalRescheduleBooking.startTime),
|
||||
end: dayjs(originalRescheduleBooking.endTime),
|
||||
});
|
||||
}
|
||||
const startTimeDate =
|
||||
rescheduleUid && duration
|
||||
? dayjs(startTime).subtract(duration, "minute").toDate()
|
||||
: new Date(startTime);
|
||||
const endTimeDate =
|
||||
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
|
||||
|
||||
// to also get bookings that are outside of start and end time, but the buffer falls within the start and end time
|
||||
const definedBufferTimes = getDefinedBufferTimes();
|
||||
const maxBuffer = definedBufferTimes[definedBufferTimes.length - 1];
|
||||
const startTimeAdjustedWithMaxBuffer = dayjs(startTimeDate).subtract(maxBuffer, "minute").toDate();
|
||||
const endTimeAdjustedWithMaxBuffer = dayjs(endTimeDate).add(maxBuffer, "minute").toDate();
|
||||
|
||||
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
|
||||
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
|
||||
// This function is called from multiple places but we aren't refactoring all of them at this moment
|
||||
// to avoid potential side effects.
|
||||
let bookings = params.currentBookings;
|
||||
|
||||
if (!bookings) {
|
||||
const bookingRepo = this.dependencies.bookingRepo;
|
||||
bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
|
||||
userIdAndEmailMap: new Map([[userId, userEmail]]),
|
||||
eventTypeId,
|
||||
startDate: startTimeAdjustedWithMaxBuffer,
|
||||
endDate: endTimeAdjustedWithMaxBuffer,
|
||||
seatedEvent,
|
||||
});
|
||||
}
|
||||
|
||||
const result = subtract(
|
||||
calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
end: dayjs(value.end),
|
||||
start: dayjs(value.start),
|
||||
})),
|
||||
openSeatsDateRanges
|
||||
);
|
||||
const bookingSeatCountMap: { [x: string]: number } = {};
|
||||
const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => {
|
||||
const { id, startTime, endTime, eventType, title, ...rest } = booking;
|
||||
|
||||
busyTimes.push(
|
||||
...result.map((busyTime) => ({
|
||||
...busyTime,
|
||||
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
||||
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
||||
}))
|
||||
);
|
||||
const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0);
|
||||
const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0);
|
||||
|
||||
/*
|
||||
if (rest._count?.seatsReferences) {
|
||||
const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`;
|
||||
bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0;
|
||||
bookingSeatCountMap[bookedAt]++;
|
||||
// Seat references on the current event are non-blocking until the event is fully booked.
|
||||
if (
|
||||
// there are still seats available.
|
||||
bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) &&
|
||||
// and this is the seated event, other event types should be blocked.
|
||||
eventTypeId === eventType?.id
|
||||
) {
|
||||
// then we ONLY add the before/after buffer times as busy times.
|
||||
if (minutesToBlockBeforeEvent) {
|
||||
aggregate.push({
|
||||
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
|
||||
end: dayjs(startTime).toDate(), // The event starts after the buffer
|
||||
});
|
||||
}
|
||||
if (minutesToBlockAfterEvent) {
|
||||
aggregate.push({
|
||||
start: dayjs(endTime).toDate(), // The event ends before the buffer
|
||||
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
|
||||
});
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
// if it does get blocked at this point; we remove the bookingSeatCountMap entry
|
||||
// doing this allows using the map later to remove the ranges from calendar busy times.
|
||||
delete bookingSeatCountMap[bookedAt];
|
||||
}
|
||||
// rescheduling the same booking to the same time should be possible. Why?
|
||||
if (rest.uid === rescheduleUid) {
|
||||
return aggregate;
|
||||
}
|
||||
aggregate.push({
|
||||
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
|
||||
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
|
||||
title,
|
||||
source: `eventType-${eventType?.id}-booking-${id}`,
|
||||
});
|
||||
return aggregate;
|
||||
}, []);
|
||||
|
||||
logger.debug(
|
||||
`Busy Time from Cal Bookings ${JSON.stringify({
|
||||
busyTimes,
|
||||
bookings: bookings?.map((booking) => getPiiFreeBooking(booking)),
|
||||
numCredentials: credentials?.length,
|
||||
})}`
|
||||
);
|
||||
performance.mark("prismaBookingGetEnd");
|
||||
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
|
||||
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
|
||||
const startConnectedCalendarsGet = performance.now();
|
||||
|
||||
const calendarBusyTimesQuery = await getBusyCalendarTimes(
|
||||
credentials,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedCalendars,
|
||||
shouldServeCache
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new Error(
|
||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||
(calendar) => calendar.id
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||
} ms for user ${username}`,
|
||||
JSON.stringify({
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
calendarBusyTimes,
|
||||
})
|
||||
);
|
||||
|
||||
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
||||
const [start, end] = key.split("<>");
|
||||
return {
|
||||
start: dayjs(start),
|
||||
end: dayjs(end),
|
||||
};
|
||||
});
|
||||
|
||||
if (rescheduleUid) {
|
||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||
// calendar busy time from original rescheduled booking should not be blocked
|
||||
if (originalRescheduleBooking) {
|
||||
openSeatsDateRanges.push({
|
||||
start: dayjs(originalRescheduleBooking.startTime),
|
||||
end: dayjs(originalRescheduleBooking.endTime),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = subtract(
|
||||
calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
end: dayjs(value.end),
|
||||
start: dayjs(value.start),
|
||||
})),
|
||||
openSeatsDateRanges
|
||||
);
|
||||
|
||||
busyTimes.push(
|
||||
...result.map((busyTime) => ({
|
||||
...busyTime,
|
||||
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
||||
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
||||
}))
|
||||
);
|
||||
|
||||
/*
|
||||
// TODO: Disabled until we can filter Zoom events by date. Also this is adding too much latency.
|
||||
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
||||
console.log("videoBusyTimes", videoBusyTimes);
|
||||
busyTimes.push(...videoBusyTimes);
|
||||
*/
|
||||
} else {
|
||||
logger.warn(`No credentials found for user ${userId}`, {
|
||||
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
|
||||
});
|
||||
}
|
||||
logger.debug(
|
||||
"getBusyTimes:",
|
||||
JSON.stringify({
|
||||
allBusyTimes: busyTimes,
|
||||
})
|
||||
);
|
||||
return busyTimes;
|
||||
};
|
||||
|
||||
export const getBusyTimes = withReporting(_getBusyTimes, "getBusyTimes");
|
||||
|
||||
export function getStartEndDateforLimitCheck(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
bookingLimits?: IntervalLimit | null,
|
||||
durationLimits?: IntervalLimit | null
|
||||
) {
|
||||
const startTimeAsDayJs = stringToDayjs(startDate);
|
||||
const endTimeAsDayJs = stringToDayjs(endDate);
|
||||
|
||||
let limitDateFrom = stringToDayjs(startDate);
|
||||
let limitDateTo = stringToDayjs(endDate);
|
||||
|
||||
// expand date ranges by absolute minimum required to apply limits
|
||||
// (yearly limits are handled separately for performance)
|
||||
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
|
||||
if (bookingLimits?.[key] || durationLimits?.[key]) {
|
||||
const unit = intervalLimitKeyToUnit(key);
|
||||
limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
|
||||
limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
|
||||
} else {
|
||||
logger.warn(`No credentials found for user ${userId}`, {
|
||||
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { limitDateFrom, limitDateTo };
|
||||
}
|
||||
|
||||
export async function getBusyTimesForLimitChecks(params: {
|
||||
userIds: number[];
|
||||
eventTypeId: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
rescheduleUid?: string | null;
|
||||
bookingLimits?: IntervalLimit | null;
|
||||
durationLimits?: IntervalLimit | null;
|
||||
}) {
|
||||
const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
|
||||
|
||||
performance.mark("getBusyTimesForLimitChecksStart");
|
||||
|
||||
let busyTimes: EventBusyDetails[] = [];
|
||||
|
||||
if (!bookingLimits && !durationLimits) {
|
||||
logger.debug(
|
||||
"getBusyTimes:",
|
||||
JSON.stringify({
|
||||
allBusyTimes: busyTimes,
|
||||
})
|
||||
);
|
||||
return busyTimes;
|
||||
}
|
||||
|
||||
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
|
||||
startDate,
|
||||
endDate,
|
||||
bookingLimits,
|
||||
durationLimits
|
||||
);
|
||||
getBusyTimes = withReporting(this._getBusyTimes.bind(this), "getBusyTimes");
|
||||
|
||||
logger.silly(
|
||||
`Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
getStartEndDateforLimitCheck(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
bookingLimits?: IntervalLimit | null,
|
||||
durationLimits?: IntervalLimit | null
|
||||
) {
|
||||
const startTimeAsDayJs = stringToDayjs(startDate);
|
||||
const endTimeAsDayJs = stringToDayjs(endDate);
|
||||
|
||||
const where: Prisma.BookingWhereInput = {
|
||||
userId: {
|
||||
in: userIds,
|
||||
},
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
// FIXME: bookings that overlap on one side will never be counted
|
||||
startTime: {
|
||||
gte: limitDateFrom.toDate(),
|
||||
},
|
||||
endTime: {
|
||||
lte: limitDateTo.toDate(),
|
||||
},
|
||||
};
|
||||
let limitDateFrom = stringToDayjs(startDate);
|
||||
let limitDateTo = stringToDayjs(endDate);
|
||||
|
||||
if (rescheduleUid) {
|
||||
where.NOT = {
|
||||
uid: rescheduleUid,
|
||||
};
|
||||
// expand date ranges by absolute minimum required to apply limits
|
||||
// (yearly limits are handled separately for performance)
|
||||
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
|
||||
if (bookingLimits?.[key] || durationLimits?.[key]) {
|
||||
const unit = intervalLimitKeyToUnit(key);
|
||||
limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
|
||||
limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
|
||||
}
|
||||
}
|
||||
|
||||
return { limitDateFrom, limitDateTo };
|
||||
}
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
async getBusyTimesForLimitChecks(params: {
|
||||
userIds: number[];
|
||||
eventTypeId: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
rescheduleUid?: string | null;
|
||||
bookingLimits?: IntervalLimit | null;
|
||||
durationLimits?: IntervalLimit | null;
|
||||
}) {
|
||||
const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
|
||||
|
||||
performance.mark("getBusyTimesForLimitChecksStart");
|
||||
|
||||
let busyTimes: EventBusyDetails[] = [];
|
||||
|
||||
if (!bookingLimits && !durationLimits) {
|
||||
return busyTimes;
|
||||
}
|
||||
|
||||
const { limitDateFrom, limitDateTo } = this.getStartEndDateforLimitCheck(
|
||||
startDate,
|
||||
endDate,
|
||||
bookingLimits,
|
||||
durationLimits
|
||||
);
|
||||
|
||||
logger.silly(
|
||||
`Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
|
||||
const where: Prisma.BookingWhereInput = {
|
||||
userId: {
|
||||
in: userIds,
|
||||
},
|
||||
title: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
// FIXME: bookings that overlap on one side will never be counted
|
||||
startTime: {
|
||||
gte: limitDateFrom.toDate(),
|
||||
},
|
||||
endTime: {
|
||||
lte: limitDateTo.toDate(),
|
||||
},
|
||||
};
|
||||
|
||||
busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
|
||||
start: dayjs(startTime).toDate(),
|
||||
end: dayjs(endTime).toDate(),
|
||||
title,
|
||||
source: `eventType-${eventType?.id}-booking-${id}`,
|
||||
userId,
|
||||
}));
|
||||
if (rescheduleUid) {
|
||||
where.NOT = {
|
||||
uid: rescheduleUid,
|
||||
};
|
||||
}
|
||||
|
||||
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
|
||||
performance.mark("getBusyTimesForLimitChecksEnd");
|
||||
performance.measure(
|
||||
`prisma booking get for limits took $1'`,
|
||||
"getBusyTimesForLimitChecksStart",
|
||||
"getBusyTimesForLimitChecksEnd"
|
||||
);
|
||||
return busyTimes;
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
title: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
|
||||
start: dayjs(startTime).toDate(),
|
||||
end: dayjs(endTime).toDate(),
|
||||
title,
|
||||
source: `eventType-${eventType?.id}-booking-${id}`,
|
||||
userId,
|
||||
}));
|
||||
|
||||
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
|
||||
performance.mark("getBusyTimesForLimitChecksEnd");
|
||||
performance.measure(
|
||||
`prisma booking get for limits took $1'`,
|
||||
"getBusyTimesForLimitChecksStart",
|
||||
"getBusyTimesForLimitChecksEnd"
|
||||
);
|
||||
return busyTimes;
|
||||
}
|
||||
}
|
||||
|
||||
export default withReporting(_getBusyTimes, "getBusyTimes");
|
||||
|
||||
@@ -36,7 +36,7 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { getBusyTimes } from "./getBusyTimes";
|
||||
import { getBusyTimesService } from "./di/containers/busy-times";
|
||||
import { getPeriodStartDatesBetween as getPeriodStartDatesBetweenUtil } from "./intervalLimits/utils/getPeriodStartDatesBetween";
|
||||
import { withReporting } from "./sentryWrapper";
|
||||
|
||||
@@ -440,7 +440,8 @@ export class UserAvailabilityService {
|
||||
|
||||
let busyTimes = [];
|
||||
try {
|
||||
busyTimes = await getBusyTimes({
|
||||
const busyTimesService = getBusyTimesService();
|
||||
busyTimes = await busyTimesService.getBusyTimes({
|
||||
credentials: user.credentials,
|
||||
startTime: getBusyTimesStart,
|
||||
endTime: getBusyTimesEnd,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/booking-limits";
|
||||
import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
|
||||
import { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
|
||||
import type { EventType } from "@calcom/lib/getUserAvailability";
|
||||
import { getPeriodStartDatesBetween } from "@calcom/lib/intervalLimits/utils/getPeriodStartDatesBetween";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
@@ -229,7 +229,8 @@ const _getBusyTimesFromTeamLimits = async (
|
||||
timeZone: string,
|
||||
rescheduleUid?: string
|
||||
) => {
|
||||
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
|
||||
const busyTimesService = getBusyTimesService();
|
||||
const { limitDateFrom, limitDateTo } = busyTimesService.getStartEndDateforLimitCheck(
|
||||
dateFrom.toISOString(),
|
||||
dateTo.toISOString(),
|
||||
bookingLimits
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
|
||||
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
|
||||
export { AvailableSlotsService };
|
||||
|
||||
export { BusyTimesService };
|
||||
|
||||
@@ -15,8 +15,9 @@ import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import type { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
|
||||
import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability";
|
||||
import { getBusyTimesForLimitChecks, getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
|
||||
import type { BusyTimesService } from "@calcom/lib/getBusyTimes";
|
||||
import type {
|
||||
CurrentSeats,
|
||||
EventType,
|
||||
@@ -103,6 +104,7 @@ export interface IAvailableSlotsService {
|
||||
cacheService: CacheService;
|
||||
checkBookingLimitsService: CheckBookingLimitsService;
|
||||
userAvailabilityService: UserAvailabilityService;
|
||||
busyTimesService: BusyTimesService;
|
||||
redisClient: IRedisService;
|
||||
}
|
||||
|
||||
@@ -373,13 +375,13 @@ export class AvailableSlotsService {
|
||||
return userBusyTimesMap;
|
||||
}
|
||||
|
||||
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
|
||||
const { limitDateFrom, limitDateTo } = this.dependencies.busyTimesService.getStartEndDateforLimitCheck(
|
||||
dateFrom.toISOString(),
|
||||
dateTo.toISOString(),
|
||||
bookingLimits || durationLimits
|
||||
);
|
||||
|
||||
const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({
|
||||
const busyTimesFromLimitsBookings = await this.dependencies.busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: users.map((user) => user.id),
|
||||
eventTypeId: eventType.id,
|
||||
startDate: limitDateFrom.format(),
|
||||
@@ -566,7 +568,7 @@ export class AvailableSlotsService {
|
||||
timeZone: string,
|
||||
rescheduleUid?: string
|
||||
) {
|
||||
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
|
||||
const { limitDateFrom, limitDateTo } = this.dependencies.busyTimesService.getStartEndDateforLimitCheck(
|
||||
dateFrom.toISOString(),
|
||||
dateTo.toISOString(),
|
||||
bookingLimits
|
||||
@@ -797,19 +799,21 @@ export class AvailableSlotsService {
|
||||
? parseDurationLimit(eventType?.durationLimits)
|
||||
: null;
|
||||
|
||||
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
|
||||
let busyTimesFromLimitsBookingsAllUsers: Awaited<
|
||||
ReturnType<typeof getBusyTimesService.prototype.getBusyTimesForLimitChecks>
|
||||
> = [];
|
||||
|
||||
if (eventType && (bookingLimits || durationLimits)) {
|
||||
// TODO: DI getBusyTimesForLimitChecks
|
||||
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
|
||||
userIds: allUserIds,
|
||||
eventTypeId: eventType.id,
|
||||
startDate: startTime.format(),
|
||||
endDate: endTime.format(),
|
||||
rescheduleUid: input.rescheduleUid,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
});
|
||||
busyTimesFromLimitsBookingsAllUsers =
|
||||
await this.dependencies.busyTimesService.getBusyTimesForLimitChecks({
|
||||
userIds: allUserIds,
|
||||
eventTypeId: eventType.id,
|
||||
startDate: startTime.format(),
|
||||
endDate: endTime.format(),
|
||||
rescheduleUid: input.rescheduleUid,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
});
|
||||
}
|
||||
|
||||
let busyTimesFromLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
|
||||
@@ -868,7 +872,6 @@ export class AvailableSlotsService {
|
||||
const enrichUsersWithData = withReporting(_enrichUsersWithData.bind(this), "enrichUsersWithData");
|
||||
const users = enrichUsersWithData();
|
||||
|
||||
// TODO: DI getUsersAvailability
|
||||
const premappedUsersAvailability = await this.dependencies.userAvailabilityService.getUsersAvailability({
|
||||
users,
|
||||
query: {
|
||||
|
||||
@@ -2500,7 +2500,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.288"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.291"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -2531,6 +2531,8 @@ __metadata:
|
||||
"@types/luxon": ^3.3.7
|
||||
"@types/passport-jwt": ^3.0.13
|
||||
"@types/supertest": ^2.0.12
|
||||
"@typescript-eslint/eslint-plugin": ^6
|
||||
"@typescript-eslint/parser": ^6
|
||||
body-parser: ^1.20.2
|
||||
bull: ^4.12.4
|
||||
class-transformer: ^0.5.1
|
||||
@@ -3556,13 +3558,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.288":
|
||||
version: 0.0.288
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.288"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.291":
|
||||
version: 0.0.291
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.291"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 9f44ba9e258a94c9b8bb5388c400696f6e6acd20dbf73865d62739b1aad212626bb181d88b38b30d08659c8b9183d27aba94bdd52b36e6c6c3cc7594d1af29d7
|
||||
checksum: 09cb5db584698acccd4447b4007df2fe2d66352c6ba823ef8c354fd3458e59414a9502f6950438e1260df71613b95f1e5a14bb54e2c7ffe6cc1dafe8b643fb9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5715,7 +5717,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/regexpp@npm:^4.10.0":
|
||||
"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.5.1":
|
||||
version: 4.12.1
|
||||
resolution: "@eslint-community/regexpp@npm:4.12.1"
|
||||
checksum: 0d628680e204bc316d545b4993d3658427ca404ae646ce541fcc65306b8c712c340e5e573e30fb9f85f4855c0c5f6dca9868931f2fcced06417fbe1a0c6cd2d6
|
||||
@@ -17629,6 +17631,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-schema@npm:^7.0.12":
|
||||
version: 7.0.15
|
||||
resolution: "@types/json-schema@npm:7.0.15"
|
||||
checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json5@npm:^0.0.29":
|
||||
version: 0.0.29
|
||||
resolution: "@types/json5@npm:0.0.29"
|
||||
@@ -18156,6 +18165,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/semver@npm:^7.5.0":
|
||||
version: 7.7.0
|
||||
resolution: "@types/semver@npm:7.7.0"
|
||||
checksum: d488eaeddb23879a0a8a759bed667e1a76cb0dd4d23e3255538e24c189db387357953ca9e7a3bda2bb7f95e84cac8fe0db4fbe6b3456e893043337732d1d23cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/send@npm:*":
|
||||
version: 0.17.4
|
||||
resolution: "@types/send@npm:0.17.4"
|
||||
@@ -18462,21 +18478,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0":
|
||||
"@typescript-eslint/eslint-plugin@npm:^6":
|
||||
version: 6.21.0
|
||||
resolution: "@typescript-eslint/parser@npm:6.21.0"
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": ^4.5.1
|
||||
"@typescript-eslint/scope-manager": 6.21.0
|
||||
"@typescript-eslint/types": 6.21.0
|
||||
"@typescript-eslint/typescript-estree": 6.21.0
|
||||
"@typescript-eslint/type-utils": 6.21.0
|
||||
"@typescript-eslint/utils": 6.21.0
|
||||
"@typescript-eslint/visitor-keys": 6.21.0
|
||||
debug: ^4.3.4
|
||||
graphemer: ^1.4.0
|
||||
ignore: ^5.2.4
|
||||
natural-compare: ^1.4.0
|
||||
semver: ^7.5.4
|
||||
ts-api-utils: ^1.0.1
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68
|
||||
checksum: 5ef2c502255e643e98051e87eb682c2a257e87afd8ec3b9f6274277615e1c2caf3131b352244cfb1987b8b2c415645eeacb9113fa841fc4c9b2ac46e8aed6efd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18496,6 +18519,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0, @typescript-eslint/parser@npm:^6":
|
||||
version: 6.21.0
|
||||
resolution: "@typescript-eslint/parser@npm:6.21.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": 6.21.0
|
||||
"@typescript-eslint/types": 6.21.0
|
||||
"@typescript-eslint/typescript-estree": 6.21.0
|
||||
"@typescript-eslint/visitor-keys": 6.21.0
|
||||
debug: ^4.3.4
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^5.52.0":
|
||||
version: 5.52.0
|
||||
resolution: "@typescript-eslint/parser@npm:5.52.0"
|
||||
@@ -18560,6 +18601,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:6.21.0":
|
||||
version: 6.21.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:6.21.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree": 6.21.0
|
||||
"@typescript-eslint/utils": 6.21.0
|
||||
debug: ^4.3.4
|
||||
ts-api-utils: ^1.0.1
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 77025473f4d80acf1fafcce99c5c283e557686a61861febeba9c9913331f8a41e930bf5cd8b7a54db502a57b6eb8ea6d155cbd4f41349ed00e3d7aeb1f477ddc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.28.0":
|
||||
version: 8.28.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.28.0"
|
||||
@@ -18694,6 +18752,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:6.21.0":
|
||||
version: 6.21.0
|
||||
resolution: "@typescript-eslint/utils@npm:6.21.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": ^4.4.0
|
||||
"@types/json-schema": ^7.0.12
|
||||
"@types/semver": ^7.5.0
|
||||
"@typescript-eslint/scope-manager": 6.21.0
|
||||
"@typescript-eslint/types": 6.21.0
|
||||
"@typescript-eslint/typescript-estree": 6.21.0
|
||||
semver: ^7.5.4
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
checksum: b129b3a4aebec8468259f4589985cb59ea808afbfdb9c54f02fad11e17d185e2bf72bb332f7c36ec3c09b31f18fc41368678b076323e6e019d06f74ee93f7bf2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.28.0":
|
||||
version: 8.28.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.28.0"
|
||||
@@ -29699,7 +29774,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore@npm:^5.3.1":
|
||||
"ignore@npm:^5.2.4, ignore@npm:^5.3.1":
|
||||
version: 5.3.2
|
||||
resolution: "ignore@npm:5.3.2"
|
||||
checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be
|
||||
|
||||
Reference in New Issue
Block a user