refactor: convert handleNotificationWhenNoSlots to service class with DI (#23055)

* refactor: convert handleNotificationWhenNoSlots to service class with DI

- Convert standalone handleNotificationWhenNoSlots function to NoSlotsNotificationService class
- Add INoSlotsNotificationService interface following existing patterns
- Create NoSlotsNotification DI module and add to container
- Add NO_SLOTS_NOTIFICATION_SERVICE tokens to DI_TOKENS
- Inject service into AvailableSlotsService dependencies
- Update AvailableSlotsService to use injected service instead of direct function call
- Update all test cases to use service class pattern
- Follows existing DI patterns used by other services like BusyTimesService and CheckBookingLimitsService

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

* refactor: move Prisma calls to repositories and create dedicated DI container

- Add findOrganizationSettingsBySlug and findTeamSlugById methods to TeamRepository
- Add findTeamAdminsByTeamId method to MembershipRepository
- Create MembershipRepository DI module and tokens
- Update NoSlotsNotificationService to inject TeamRepository, MembershipRepository, and Redis client
- Create dedicated DI container for NoSlotsNotificationService
- Update AvailableSlots DI container to include MembershipRepository
- Replace direct Prisma and Redis calls with repository methods and injected client
- Update tests to use DI container instead of direct service instantiation
- Fix Redis interface import path

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

* chore: DI noSlotsNotification service

* chore: bump library

---------

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:
devin-ai-integration[bot]
2025-08-15 08:30:04 -03:00
committed by GitHub
co-authored by morgan@cal.com Morgan
parent 0af62da16b
commit 13e2e340ac
19 changed files with 256 additions and 122 deletions
+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.311",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -1,6 +1,7 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
@@ -12,6 +13,7 @@ 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 { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
PrismaTeamRepository,
RedisService,
PrismaFeaturesRepository,
PrismaMembershipRepository,
CheckBookingLimitsService,
CacheService,
AvailableSlotsService,
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
BusyTimesService,
FilterHostsService,
QualifiedHostsService,
NoSlotsNotificationService,
],
exports: [AvailableSlotsService],
})
@@ -0,0 +1,12 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
import { PrismaClient } from "@calcom/prisma";
@Injectable()
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
constructor(private readonly dbWrite: PrismaWriteService) {
super(dbWrite.prisma as unknown as PrismaClient);
}
}
@@ -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 { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
checkBookingLimitsService: CheckBookingLimitsService,
cacheService: CacheService,
userAvailabilityService: UserAvailabilityService,
busyTimesService: BusyTimesService
busyTimesService: BusyTimesService,
noSlotsNotificationService: NoSlotsNotificationService
) {
super({
oooRepo: oooRepoDependency,
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
busyTimesService,
qualifiedHostsService,
featuresRepo: featuresRepository,
noSlotsNotificationService,
});
}
}
@@ -0,0 +1,21 @@
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
@Injectable()
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
constructor(
teamRepository: PrismaTeamRepository,
membershipRepository: PrismaMembershipRepository,
redisService: RedisService
) {
super({
teamRepo: teamRepository,
membershipRepo: membershipRepository,
redisClient: redisService,
});
}
}
@@ -14,6 +14,8 @@ import { eventTypeRepositoryModule } from "../modules/EventType";
import { featuresRepositoryModule } from "../modules/Features";
import { filterHostsModule } from "../modules/FilterHosts";
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
import { membershipRepositoryModule } from "../modules/Membership";
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
import { oooRepositoryModule } from "../modules/Ooo";
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
@@ -29,6 +31,7 @@ container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
container.load(DI_TOKENS.SELECTED_SLOT_REPOSITORY_MODULE, selectedSlotsRepositoryModule);
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
@@ -42,6 +45,7 @@ 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);
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
export function getAvailableSlotsService() {
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
@@ -0,0 +1,21 @@
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 type { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
import { membershipRepositoryModule } from "../modules/Membership";
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
import { teamRepositoryModule } from "../modules/Team";
const container = createContainer();
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
export function getNoSlotsNotificationService() {
return container.get<NoSlotsNotificationService>(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE);
}
@@ -22,4 +22,5 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
noSlotsNotificationService: DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE,
} satisfies Record<keyof IAvailableSlotsService, symbol>);
+9
View File
@@ -0,0 +1,9 @@
import { createModule } from "@evyweb/ioctopus";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
export const membershipRepositoryModule = createModule();
membershipRepositoryModule
.bind(DI_TOKENS.MEMBERSHIP_REPOSITORY)
.toClass(MembershipRepository, [DI_TOKENS.PRISMA_CLIENT]);
@@ -0,0 +1,13 @@
import { createModule } from "@evyweb/ioctopus";
import type { INoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
import { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
import { DI_TOKENS } from "../tokens";
export const noSlotsNotificationModule = createModule();
noSlotsNotificationModule.bind(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE).toClass(NoSlotsNotificationService, {
teamRepo: DI_TOKENS.TEAM_REPOSITORY,
membershipRepo: DI_TOKENS.MEMBERSHIP_REPOSITORY,
redisClient: DI_TOKENS.REDIS_CLIENT,
} satisfies Record<keyof INoSlotsNotificationService, symbol>);
+4
View File
@@ -41,4 +41,8 @@ export const DI_TOKENS = {
QUALIFIED_HOSTS_SERVICE_MODULE: Symbol("QualifiedHostsServiceModule"),
FILTER_HOSTS_SERVICE: Symbol("FilterHostsService"),
FILTER_HOSTS_SERVICE_MODULE: Symbol("FilterHostsServiceModule"),
NO_SLOTS_NOTIFICATION_SERVICE: Symbol("NoSlotsNotificationService"),
NO_SLOTS_NOTIFICATION_SERVICE_MODULE: Symbol("NoSlotsNotificationServiceModule"),
MEMBERSHIP_REPOSITORY: Symbol("MembershipRepository"),
MEMBERSHIP_REPOSITORY_MODULE: Symbol("MembershipRepositoryModule"),
};
@@ -460,4 +460,28 @@ export class MembershipRepository {
select: select ?? { userId: true },
})) as unknown as Promise<MembershipDTOFromSelect<TSelect>[]>;
}
async findTeamAdminsByTeamId({ teamId }: { teamId: number }) {
return await this.prismaClient.membership.findMany({
where: {
team: {
id: teamId,
parentId: {
not: null,
},
},
role: {
in: ["ADMIN", "OWNER"],
},
},
select: {
user: {
select: {
email: true,
locale: true,
},
},
},
});
}
}
+27
View File
@@ -350,4 +350,31 @@ export class TeamRepository {
return team?.parent;
}
async findOrganizationSettingsBySlug({ slug }: { slug: string }) {
return await this.prismaClient.team.findFirst({
where: {
slug,
isOrganization: true,
},
select: {
organizationSettings: {
select: {
adminGetsNoSlotsNotification: true,
},
},
},
});
}
async findTeamSlugById({ id }: { id: number }) {
return await this.prismaClient.team.findUnique({
where: {
id,
},
select: {
slug: true,
},
});
}
}
@@ -7,3 +7,4 @@ export { PrismaSelectedSlotRepository } from "@calcom/lib/server/repository/Pris
export { TeamRepository as PrismaTeamRepository } from "@calcom/lib/server/repository/team";
export { UserRepository as PrismaUserRepository } from "@calcom/lib/server/repository/user";
export { FeaturesRepository as PrismaFeaturesRepository } from "@calcom/features/flags/features.repository";
export { MembershipRepository as PrismaMembershipRepository } from "@calcom/lib/server/repository/membership";
+2
View File
@@ -1,6 +1,7 @@
import { FilterHostsService } from "@calcom/lib/bookings/filterHostsBySameRoundRobinHost";
import { QualifiedHostsService } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
import { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
export { AvailableSlotsService };
@@ -10,3 +11,4 @@ export { BusyTimesService };
export { QualifiedHostsService };
export { FilterHostsService };
export { NoSlotsNotificationService };
@@ -6,8 +6,7 @@ import { vi, describe, it, beforeAll, afterAll, expect, beforeEach, afterEach }
import dayjs from "@calcom/dayjs";
import * as CalcomEmails from "@calcom/emails";
import { RedisService } from "@calcom/features/redis/RedisService";
import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
import { getNoSlotsNotificationService } from "@calcom/lib/di/containers/NoSlotsNotification";
vi.mock("@calcom/features/redis/RedisService", () => {
const mockedRedis = vi.fn();
@@ -84,14 +83,16 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
};
// Call the function with teamId
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
// Mock length to be one then recall to trigger email
mocked.lrange.mockResolvedValueOnce([""]);
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
const service2 = getNoSlotsNotificationService();
await service2.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).toHaveBeenCalled();
});
it("Should not send a notification if the org has them disabled", async () => {
@@ -113,7 +114,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
isValidOrgDomain: true,
};
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
});
@@ -153,7 +155,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
mocked.lrange.mockResolvedValueOnce([""]); // This will trigger email sending
// Call with specific teamId
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123, // specific teamId
@@ -205,7 +208,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
mocked.lrange.mockResolvedValueOnce([""]);
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
// teamId intentionally omitted
@@ -233,7 +237,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
mocked.lrange.mockResolvedValueOnce([""]);
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123,
@@ -263,7 +268,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
isValidOrgDomain: true,
};
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123,
@@ -318,7 +324,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
mocked.lrange.mockResolvedValueOnce([""]);
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123,
@@ -375,7 +382,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
// First notification cycle - simulate having one previous occurrence
mocked.lrange.mockResolvedValueOnce([""]); // One previous occurrence
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123,
@@ -391,7 +399,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
// This better simulates the real Redis state after the first notification
mocked.lrange.mockResolvedValueOnce(["", ""]); // Two occurrences now
await handleNotificationWhenNoSlots({
const service2 = getNoSlotsNotificationService();
await service2.handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId: 123,
@@ -436,7 +445,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
// First event type notification
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for first event
await handleNotificationWhenNoSlots({
const service = getNoSlotsNotificationService();
await service.handleNotificationWhenNoSlots({
eventDetails: { ...baseEventDetails, eventSlug: "event1" },
orgDetails,
teamId: 123,
@@ -451,7 +461,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
// For the second event type, also simulate one previous occurrence
// This needs to be a separate mock since it's a different key in Redis
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for second event
await handleNotificationWhenNoSlots({
const service2 = getNoSlotsNotificationService();
await service2.handleNotificationWhenNoSlots({
eventDetails: { ...baseEventDetails, eventSlug: "event2" },
orgDetails,
teamId: 123,
@@ -1,9 +1,10 @@
import type { Dayjs } from "@calcom/dayjs";
import { sendOrganizationAdminNoSlotsNotification } from "@calcom/emails";
import { RedisService } from "@calcom/features/redis/RedisService";
import type { IRedisService } from "@calcom/features/redis/IRedisService";
import { IS_PRODUCTION, WEBAPP_URL } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import type { MembershipRepository } from "@calcom/lib/server/repository/membership";
import type { TeamRepository } from "@calcom/lib/server/repository/team";
type EventDetails = {
username: string;
@@ -38,107 +39,83 @@ const constructDataHash = (eventDetails: EventDetails) => {
return JSON.stringify(obj);
};
export const handleNotificationWhenNoSlots = async ({
eventDetails,
orgDetails,
teamId,
}: {
eventDetails: EventDetails;
orgDetails: { currentOrgDomain: string | null };
teamId?: number;
}) => {
// Check for org
if (!orgDetails.currentOrgDomain || !teamId) return;
const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL;
if (!UPSTASH_ENV_FOUND) return;
export interface INoSlotsNotificationService {
teamRepo: TeamRepository;
membershipRepo: MembershipRepository;
redisClient: IRedisService;
}
// Check org has this setting enabled
const orgSettings = await prisma.team.findFirst({
where: {
export class NoSlotsNotificationService {
constructor(public readonly dependencies: INoSlotsNotificationService) {}
async handleNotificationWhenNoSlots({
eventDetails,
orgDetails,
teamId,
}: {
eventDetails: EventDetails;
orgDetails: { currentOrgDomain: string | null };
teamId?: number;
}) {
// Check for org
if (!orgDetails.currentOrgDomain || !teamId) return;
const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL;
if (!UPSTASH_ENV_FOUND) return;
// Check org has this setting enabled
const orgSettings = await this.dependencies.teamRepo.findOrganizationSettingsBySlug({
slug: orgDetails.currentOrgDomain,
isOrganization: true,
},
select: {
organizationSettings: {
select: {
adminGetsNoSlotsNotification: true,
},
},
},
});
if (!orgSettings?.organizationSettings?.adminGetsNoSlotsNotification) return;
const redis = new RedisService();
const usersUniqueKey = constructRedisKey(eventDetails, orgDetails.currentOrgDomain, teamId);
// Get only the required amount of data so the request is as small as possible
// We may need to get more data and check the startDate occurrence of this
// Not trigger email if the start months are the same
const usersExistingNoSlots =
(await redis.lrange(usersUniqueKey, 0, NO_SLOTS_COUNT_FOR_NOTIFICATION - 1)) ?? [];
await redis.lpush(usersUniqueKey, constructDataHash(eventDetails));
if (!usersExistingNoSlots.length) {
await redis.expire(usersUniqueKey, NO_SLOTS_NOTIFICATION_FREQUENCY);
}
// We add one as we know we just added one to the list - saves us re-fetching the data
if (usersExistingNoSlots.length + 1 === NO_SLOTS_COUNT_FOR_NOTIFICATION) {
// Get all team admins to send the email too
const foundAdmins = await prisma.membership.findMany({
where: {
team: {
id: teamId,
parentId: {
not: null,
},
},
role: {
in: ["ADMIN", "OWNER"],
},
},
select: {
user: {
select: {
email: true,
locale: true,
},
},
},
});
const teamSlug = await prisma.team.findUnique({
where: {
id: teamId,
},
select: {
slug: true,
},
});
// TODO: use new tasker as we dont want this blocking loading slots (Just out of scope for this PR)
// Tasker isn't 100% working with emails - will refactor after i have made changes to Tasker in another PR.
const emailsToSend: Array<Promise<void>> = [];
// const tasker = getTasker();
for (const admin of foundAdmins) {
const translation = await getTranslation(admin.user.locale ?? "en", "common");
if (!orgSettings?.organizationSettings?.adminGetsNoSlotsNotification) return;
const payload = {
language: translation,
to: {
email: admin.user.email,
},
user: eventDetails.username,
slug: eventDetails.eventSlug,
startTime: eventDetails.startTime.format("YYYY-MM-DD"),
endTime: eventDetails.endTime.format("YYYY-MM-DD"),
// For now navigate here - when impersonation via parameter has been pushed we will impersonate and then navigate to availability
editLink: `${WEBAPP_URL}/availability?type=team`,
teamSlug: teamSlug?.slug ?? "",
};
const usersUniqueKey = constructRedisKey(eventDetails, orgDetails.currentOrgDomain, teamId);
// Get only the required amount of data so the request is as small as possible
// We may need to get more data and check the startDate occurrence of this
// Not trigger email if the start months are the same
const usersExistingNoSlots =
(await this.dependencies.redisClient.lrange(usersUniqueKey, 0, NO_SLOTS_COUNT_FOR_NOTIFICATION - 1)) ??
[];
await this.dependencies.redisClient.lpush(usersUniqueKey, constructDataHash(eventDetails));
emailsToSend.push(sendOrganizationAdminNoSlotsNotification(payload));
if (!usersExistingNoSlots.length) {
await this.dependencies.redisClient.expire(usersUniqueKey, NO_SLOTS_NOTIFICATION_FREQUENCY);
}
// We add one as we know we just added one to the list - saves us re-fetching the data
if (usersExistingNoSlots.length + 1 === NO_SLOTS_COUNT_FOR_NOTIFICATION) {
// Get all team admins to send the email too
const foundAdmins = await this.dependencies.membershipRepo.findTeamAdminsByTeamId({
teamId,
});
const teamSlug = await this.dependencies.teamRepo.findTeamSlugById({
id: teamId,
});
// TODO: use new tasker as we dont want this blocking loading slots (Just out of scope for this PR)
// Tasker isn't 100% working with emails - will refactor after i have made changes to Tasker in another PR.
const emailsToSend: Array<Promise<void>> = [];
// const tasker = getTasker();
for (const admin of foundAdmins) {
const translation = await getTranslation(admin.user.locale ?? "en", "common");
const payload = {
language: translation,
to: {
email: admin.user.email,
},
user: eventDetails.username,
slug: eventDetails.eventSlug,
startTime: eventDetails.startTime.format("YYYY-MM-DD"),
endTime: eventDetails.endTime.format("YYYY-MM-DD"),
// For now navigate here - when impersonation via parameter has been pushed we will impersonate and then navigate to availability
editLink: `${WEBAPP_URL}/availability?type=team`,
teamSlug: teamSlug?.slug ?? "",
};
emailsToSend.push(sendOrganizationAdminNoSlotsNotification(payload));
}
await Promise.all(emailsToSend);
}
await Promise.all(emailsToSend);
}
};
}
@@ -61,7 +61,7 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { TRPCError } from "@trpc/server";
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
import type { NoSlotsNotificationService } from "./handleNotificationWhenNoSlots";
import type { GetScheduleOptions } from "./types";
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
@@ -108,6 +108,7 @@ export interface IAvailableSlotsService {
redisClient: IRedisService;
featuresRepo: FeaturesRepository;
qualifiedHostsService: QualifiedHostsService;
noSlotsNotificationService: NoSlotsNotificationService;
}
function withSlotsCache(
@@ -1438,8 +1439,7 @@ export class AvailableSlotsService {
// We only want to run this on single targeted events and not dynamic
if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) {
try {
// TODO: DI handleNotificationWhenNoSlots
await handleNotificationWhenNoSlots({
await this.dependencies.noSlotsNotificationService.handleNotificationWhenNoSlots({
eventDetails: {
username: input.usernameList?.[0],
startTime: startTime,
+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.311"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312"
"@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.311":
version: 0.0.311
resolution: "@calcom/platform-libraries@npm:0.0.311"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.312":
version: 0.0.312
resolution: "@calcom/platform-libraries@npm:0.0.312"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 7a38e8e0478dfe163a97ddab89285b98d7c7e86a73d3659bc29e31c059782708bfe2466e50657f6861e1c67bcfc66e1e3e44803ab3b5f65fd0fe656e880bb916
checksum: 46520737404a61f3aa96c6f1adfa81e67f645fd984f495662ca7c63184b0310117a1dad57ddbe7ea487f2e5a0c9cbe62749a9a43b3197a021b19e3ccadc1f5b2
languageName: node
linkType: hard