refactor: move isRestrictionScheduleEnabled into AvailableSlotsService with DI (#23015)

* refactor: move isRestrictionScheduleEnabled into AvailableSlotsService with DI

- Remove external isRestrictionScheduleEnabled function call from AvailableSlotsService
- Add IFeaturesRepository to IAvailableSlotsService interface dependencies
- Implement checkRestrictionScheduleEnabled private method within AvailableSlotsService
- Update DI module to inject featuresRepo dependency
- Update API v2 service to pass featuresRepository to base service
- Resolves TODO comment about implementing DI for isRestrictionScheduleEnabled

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: bump platform libs

* fix: api v2 di for qualifiedHostsService

* chore: bump platform libs

* fix unit test

---------

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:
authored and GitHub committed 2025-08-12 13:18:21 +03:00
1 parent 01a0d43943
commit 211b958c58
13 files changed
+90 -96

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.298",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -11,6 +11,8 @@ 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 { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisService } from "@/modules/redis/redis.service";
@@ -34,6 +36,8 @@ import { Module } from "@nestjs/common";
AvailableSlotsService,
UserAvailabilityService,
BusyTimesService,
FilterHostsService,
QualifiedHostsService,
],
exports: [AvailableSlotsService],
})
@@ -10,6 +10,7 @@ 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 { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
@@ -29,7 +30,12 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
eventTypeRepository: PrismaEventTypeRepository,
userRepository: PrismaUserRepository,
redisService: RedisService,
featuresRepository: PrismaFeaturesRepository
featuresRepository: PrismaFeaturesRepository,
qualifiedHostsService: QualifiedHostsService,
checkBookingLimitsService: CheckBookingLimitsService,
cacheService: CacheService,
userAvailabilityService: UserAvailabilityService,
busyTimesService: BusyTimesService
) {
super({
oooRepo: oooRepoDependency,
@@ -41,15 +47,12 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
eventTypeRepo: eventTypeRepository,
userRepo: userRepository,
redisClient: redisService,
checkBookingLimitsService: new CheckBookingLimitsService(bookingRepository),
cacheService: new CacheService(featuresRepository),
userAvailabilityService: new UserAvailabilityService(
oooRepoDependency,
bookingRepository,
eventTypeRepository,
redisService
),
busyTimesService: new BusyTimesService(bookingRepository),
checkBookingLimitsService,
cacheService,
userAvailabilityService,
busyTimesService,
qualifiedHostsService,
featuresRepo: featuresRepository,
});
}
}
@@ -0,0 +1,13 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { Injectable } from "@nestjs/common";
import { FilterHostsService as BaseFilterHostsService } from "@calcom/platform-libraries/slots";
@Injectable()
export class FilterHostsService extends BaseFilterHostsService {
constructor(bookingRepository: PrismaBookingRepository) {
super({
bookingRepo: bookingRepository,
});
}
}
@@ -0,0 +1,15 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
import { Injectable } from "@nestjs/common";
import { QualifiedHostsService as BaseQualifiedHostsService } from "@calcom/platform-libraries/slots";
@Injectable()
export class QualifiedHostsService extends BaseQualifiedHostsService {
constructor(bookingRepository: PrismaBookingRepository, filterHostsService: FilterHostsService) {
super({
bookingRepo: bookingRepository,
filterHostsService,
});
}
}
@@ -5,8 +5,8 @@ import type { Mock } from "vitest";
import { RRResetInterval, SchedulingType } from "@calcom/prisma/enums";
import { getQualifiedHostsService } from "../di/containers/QualifiedHosts";
import { filterHostsByLeadThreshold } from "./filterHostsByLeadThreshold";
import { findQualifiedHostsWithDelegationCredentials } from "./findQualifiedHostsWithDelegationCredentials";
import * as getRoutedUsers from "./getRoutedUsers";
// Mock the filterHostsByLeadThreshold function
@@ -21,6 +21,7 @@ afterEach(() => {
(filterHostsByLeadThreshold as Mock).mockClear();
});
const qualifiedHostsService = getQualifiedHostsService();
describe("findQualifiedHostsWithDelegationCredentials", async () => {
it("should return qualified hosts based on mock of filterHostsByLeadThreshold", async () => {
const hosts = [
@@ -92,7 +93,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
};
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [],
rescheduleUid: null,
@@ -144,7 +145,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
};
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [],
rescheduleUid: null,
@@ -231,7 +232,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
};
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [],
rescheduleUid: null,
@@ -320,7 +321,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
};
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [1],
rescheduleUid: null,
@@ -398,7 +399,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
};
prismaMock.booking.findFirst.mockResolvedValue({ userId: 2 });
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [],
rescheduleUid: "recheduleUid",
@@ -478,7 +479,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
.mockImplementation(async () => [hosts[0]]);
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [0, 1, 2],
rescheduleUid: null,
@@ -572,7 +573,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
]);
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [2, 3],
rescheduleUid: null,
@@ -672,7 +673,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
(filterHostsByLeadThreshold as Mock).mockResolvedValue(rrHostsAfterFairness);
// Call the function under test
const result = await findQualifiedHostsWithDelegationCredentials({
const result = await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
routedTeamMemberIds: [2, 3],
rescheduleUid: null,
@@ -6,7 +6,6 @@ import {
import type { EventType } from "@calcom/lib/bookings/getRoutedUsers";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import type { PrismaClient } from "@calcom/prisma";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
@@ -15,7 +14,6 @@ import type { RoutingFormResponse } from "../server/getLuckyUser";
import { filterHostsByLeadThreshold } from "./filterHostsByLeadThreshold";
export interface IQualifiedHostsService {
prisma: PrismaClient;
bookingRepo: BookingRepository;
filterHostsService: FilterHostsService;
}
@@ -227,65 +225,3 @@ export class QualifiedHostsService {
"findQualifiedHostsWithDelegationCredentials"
);
}
const _findQualifiedHostsWithDelegationCredentials = async <
T extends {
email: string;
id: number;
credentials: CredentialPayload[];
userLevelSelectedCalendars: SelectedCalendar[];
} & Record<string, unknown>
>({
eventType,
rescheduleUid,
routedTeamMemberIds,
contactOwnerEmail,
routingFormResponse,
}: {
eventType: {
id: number;
schedulingType: SchedulingType | null;
hosts?: Host<T>[];
users: T[];
maxLeadThreshold?: number | null;
team?: {
parentId?: number | null;
rrResetInterval: string | null;
rrTimestampBasis: string | null;
} | null;
isRRWeightsEnabled: boolean;
rescheduleWithSameRoundRobinHost: boolean;
teamId?: number;
includeNoShowInRRCalculation: boolean;
} & EventType;
rescheduleUid: string | null;
routedTeamMemberIds: number[];
contactOwnerEmail: string | null;
routingFormResponse: RoutingFormResponse | null;
}) => {
const { prisma } = await import("@calcom/prisma");
const { BookingRepository } = await import("@calcom/lib/server/repository/booking");
const { FilterHostsService } = await import("@calcom/lib/bookings/filterHostsBySameRoundRobinHost");
const bookingRepo = new BookingRepository(prisma);
const filterHostsService = new FilterHostsService({ bookingRepo });
const service = new QualifiedHostsService({
prisma,
bookingRepo,
filterHostsService,
});
return service.findQualifiedHostsWithDelegationCredentials({
eventType,
rescheduleUid,
routedTeamMemberIds,
contactOwnerEmail,
routingFormResponse,
});
};
export const findQualifiedHostsWithDelegationCredentials = withReporting(
_findQualifiedHostsWithDelegationCredentials,
"findQualifiedHostsWithDelegationCredentials"
);
@@ -12,8 +12,10 @@ import { cacheModule } from "../modules/Cache";
import { checkBookingLimitsModule } from "../modules/CheckBookingLimits";
import { eventTypeRepositoryModule } from "../modules/EventType";
import { featuresRepositoryModule } from "../modules/Features";
import { filterHostsModule } from "../modules/FilterHosts";
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
import { oooRepositoryModule } from "../modules/Ooo";
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
import { scheduleRepositoryModule } from "../modules/Schedule";
import { selectedSlotsRepositoryModule } from "../modules/SelectedSlots";
@@ -37,6 +39,9 @@ container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimits
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
container.load(DI_TOKENS.FILTER_HOSTS_SERVICE_MODULE, filterHostsModule);
container.load(DI_TOKENS.QUALIFIED_HOSTS_SERVICE_MODULE, qualifiedHostsModule);
export function getAvailableSlotsService() {
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
@@ -20,4 +20,6 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE,
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
} satisfies Record<keyof IAvailableSlotsService, symbol>);
@@ -6,7 +6,6 @@ import { DI_TOKENS } from "../tokens";
export const qualifiedHostsModule = createModule();
qualifiedHostsModule.bind(DI_TOKENS.QUALIFIED_HOSTS_SERVICE).toClass(QualifiedHostsService, {
prisma: DI_TOKENS.PRISMA_CLIENT,
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
filterHostsService: DI_TOKENS.FILTER_HOSTS_SERVICE,
} satisfies Record<keyof IQualifiedHostsService, symbol>);
+6
View File
@@ -1,6 +1,12 @@
import { FilterHostsService } from "@calcom/lib/bookings/filterHostsBySameRoundRobinHost";
import { QualifiedHostsService } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
export { AvailableSlotsService };
export { BusyTimesService };
export { QualifiedHostsService };
export { FilterHostsService };
@@ -8,14 +8,15 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { IRedisService } from "@calcom/features/redis/IRedisService";
import type { QualifiedHostsService } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
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/BusyTimes";
import { getQualifiedHostsService } from "@calcom/lib/di/containers/QualifiedHosts";
import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability";
import type { BusyTimesService } from "@calcom/lib/getBusyTimes";
import type {
@@ -40,7 +41,6 @@ import {
BookingDateInPastError,
} from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { isRestrictionScheduleEnabled } from "@calcom/lib/restrictionSchedule";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { ISelectedSlotRepository } from "@calcom/lib/server/repository/ISelectedSlotRepository";
@@ -106,6 +106,8 @@ export interface IAvailableSlotsService {
userAvailabilityService: UserAvailabilityService;
busyTimesService: BusyTimesService;
redisClient: IRedisService;
featuresRepo: FeaturesRepository;
qualifiedHostsService: QualifiedHostsService;
}
function withSlotsCache(
@@ -924,6 +926,14 @@ export class AvailableSlotsService {
};
}
private async checkRestrictionScheduleEnabled(teamId?: number): Promise<boolean> {
if (!teamId) {
return false;
}
return await this.dependencies.featuresRepo.checkIfTeamHasFeature(teamId, "restriction-schedule");
}
private async _getRegularOrDynamicEventType(
input: TGetScheduleInputSchema,
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
@@ -1027,9 +1037,8 @@ export class AvailableSlotsService {
});
}
const qualifiedHostsService = getQualifiedHostsService();
const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } =
await qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
await this.dependencies.qualifiedHostsService.findQualifiedHostsWithDelegationCredentials({
eventType,
rescheduleUid: input.rescheduleUid ?? null,
routedTeamMemberIds,
@@ -1143,8 +1152,9 @@ export class AvailableSlotsService {
let availableTimeSlots: typeof timeSlots = [];
const bookerClientUid = ctx?.req?.cookies?.uid;
// TODO: DI isRestrictionScheduleEnabled
const isRestrictionScheduleFeatureEnabled = await isRestrictionScheduleEnabled(eventType.team?.id);
const isRestrictionScheduleFeatureEnabled = await this.checkRestrictionScheduleEnabled(
eventType.team?.id
);
if (eventType.restrictionScheduleId && isRestrictionScheduleFeatureEnabled) {
const restrictionSchedule = await this.dependencies.scheduleRepo.findScheduleByIdForBuildDateRanges({
scheduleId: eventType.restrictionScheduleId,
+5 -5
View File
@@ -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.298"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.300"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
@@ -3559,13 +3559,13 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.298":
version: 0.0.298
resolution: "@calcom/platform-libraries@npm:0.0.298"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.300":
version: 0.0.300
resolution: "@calcom/platform-libraries@npm:0.0.300"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 63401c90d15ce05a17905f68eb36e655c6368b430ee8c28df371c2b91d5033cab846e96406e28d2c716e8e361eefaf96caf2c3a26fa63bc239ec97632b0b7613
checksum: bff9f08a0586169f2d5a85167f0a0e0e57ca93b637ca858a72b5d3d943ceef37d652efa3d2e6f7561548e4dfe3aabb5c0c6e0a8d38f68d3d21d5e6993e56ad7d
languageName: node
linkType: hard