feat: api v2 getSchedules that are applied to team event type (#25912)

* chore: update teams schedules module

* chore: update schedules and team events repository

* chore: input for teams schedules

* feat: add logic to filter schedules based on event type id

* fix: bad import

* chore: add input for get user schedules

* feat: add logic in user schedule to filter schedules based on event type id

* chore: update e2e tests

* fix: add EventTypesModule_2024_06_14 import to SchedulesModule_2024_06_11

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* fix: import path

* chore: implement cubic feedback

* fix: replace .finally() with try/finally in E2E tests

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* chore: implement PR feedback

* fix: correct schedule precedence order for team hosts

Fix the effective schedule resolution to follow the intended precedence:
host schedule > user default schedule > event type schedule

This addresses Cubic AI review feedback with confidence 9/10:
- schedules.service.ts: Fix precedence in getUserSchedulesForEventType
- teams-schedules.service.ts: Fix precedence in getTeamSchedulesForEventType

Co-Authored-By: unknown <>

* fixup

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Rajiv Sahal
2026-01-20 17:55:38 +05:30
committed by GitHub
co-authored by unknown Keith Williams Devin AI
parent 2c105aceb8
commit 14e2fa6893
9 changed files with 478 additions and 38 deletions
@@ -1,3 +1,4 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesController_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/controllers/schedules.controller";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
@@ -9,7 +10,7 @@ import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, UsersModule, TokensModule],
imports: [PrismaModule, UsersModule, TokensModule, EventTypesModule_2024_06_14],
providers: [
SchedulesRepository_2024_06_11,
SchedulesService_2024_06_11,
@@ -244,4 +244,31 @@ export class SchedulesRepository_2024_06_11 {
take,
});
}
async getSchedulesByIds(scheduleIds: number[], skip: number, take: number) {
return this.dbRead.prisma.schedule.findMany({
where: {
id: {
in: scheduleIds,
},
},
select: {
id: true,
userId: true,
name: true,
timeZone: true,
availability: {
select: {
days: true,
startTime: true,
endTime: true,
date: true,
},
},
},
orderBy: { id: 'asc' },
skip,
take,
});
}
}
@@ -1,15 +1,15 @@
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import type {
CreateScheduleInput_2024_06_11,
ScheduleOutput_2024_06_11,
UpdateScheduleInput_2024_06_11,
} from "@calcom/platform-types";
import type { Schedule } from "@calcom/prisma/client";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { UsersRepository } from "@/modules/users/users.repository";
@Injectable()
export class SchedulesService_2024_06_11 {
@@ -17,10 +17,11 @@ export class SchedulesService_2024_06_11 {
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly inputSchedulesService: InputSchedulesService_2024_06_11,
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
private readonly usersRepository: UsersRepository
private readonly usersRepository: UsersRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14
) {}
async createUserDefaultSchedule(userId: number, timeZone: string) {
async createUserDefaultSchedule(userId: number, timeZone: string): Promise<ScheduleOutput_2024_06_11> {
const defaultSchedule = {
isDefault: true,
name: "Default schedule",
@@ -45,7 +46,7 @@ export class SchedulesService_2024_06_11 {
return this.outputSchedulesService.getResponseSchedule(createdSchedule);
}
async getUserScheduleDefault(userId: number) {
async getUserScheduleDefault(userId: number): Promise<ScheduleOutput_2024_06_11 | null> {
const user = await this.usersRepository.findById(userId);
if (!user?.defaultScheduleId) return null;
@@ -56,7 +57,7 @@ export class SchedulesService_2024_06_11 {
return this.outputSchedulesService.getResponseSchedule(defaultSchedule);
}
async getUserSchedule(userId: number, scheduleId: number) {
async getUserSchedule(userId: number, scheduleId: number): Promise<ScheduleOutput_2024_06_11> {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
if (!existingSchedule) {
@@ -68,16 +69,71 @@ export class SchedulesService_2024_06_11 {
return this.outputSchedulesService.getResponseSchedule(existingSchedule);
}
async getUserSchedules(userId: number) {
const schedules = await this.schedulesRepository.getSchedulesByUserId(userId);
return Promise.all(
schedules.map(async (schedule) => {
return this.outputSchedulesService.getResponseSchedule(schedule);
})
);
async getUserSchedules(userId: number, eventTypeId?: number): Promise<ScheduleOutput_2024_06_11[]> {
if (!eventTypeId) {
const schedules = await this.schedulesRepository.getSchedulesByUserId(userId);
return Promise.all(
schedules.map(async (schedule) => {
return this.outputSchedulesService.getResponseSchedule(schedule);
})
);
}
return this.getUserSchedulesForEventType(userId, eventTypeId);
}
async updateUserSchedule(userId: number, scheduleId: number, bodySchedule: UpdateScheduleInput_2024_06_11) {
private async getUserSchedulesForEventType(
userId: number,
eventTypeId: number
): Promise<ScheduleOutput_2024_06_11[]> {
const userEventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
let effectiveScheduleId: number | null = null;
if (userEventType) {
const user = await this.usersRepository.findById(userId);
effectiveScheduleId = userEventType.scheduleId ?? user?.defaultScheduleId ?? null;
} else {
// if its not a user owned event type, check if it's a team event type where user one of the host
const eventType = await this.eventTypesRepository.getEventTypeByIdWithHosts(eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
const userHost = eventType.hosts?.find((host) => host.userId === userId);
if (!userHost) {
throw new NotFoundException(`User ${userId} is not associated with event type ${eventTypeId}`);
}
const user = await this.usersRepository.findById(userId);
effectiveScheduleId = eventType.scheduleId ?? userHost.scheduleId ?? user?.defaultScheduleId ?? null;
}
if (!effectiveScheduleId) {
return [];
}
const schedule = await this.schedulesRepository.getScheduleById(effectiveScheduleId);
if (!schedule) {
return [];
}
if (schedule.userId !== userId) {
throw new ForbiddenException(`User ${userId} does not have access to schedule ${effectiveScheduleId}`);
}
return [await this.outputSchedulesService.getResponseSchedule(schedule)];
}
async updateUserSchedule(
userId: number,
scheduleId: number,
bodySchedule: UpdateScheduleInput_2024_06_11
): Promise<ScheduleOutput_2024_06_11> {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
if (!existingSchedule) {
@@ -106,7 +162,7 @@ export class SchedulesService_2024_06_11 {
return this.outputSchedulesService.getResponseSchedule(updatedSchedule);
}
async deleteUserSchedule(userId: number, scheduleId: number) {
async deleteUserSchedule(userId: number, scheduleId: number): Promise<Schedule> {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
if (!existingSchedule) {
@@ -118,7 +174,7 @@ export class SchedulesService_2024_06_11 {
return this.schedulesRepository.deleteScheduleById(scheduleId);
}
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">): void {
if (userId !== schedule.userId) {
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
}
@@ -0,0 +1,27 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsNumber, IsOptional } from "class-validator";
import { SkipTakePagination } from "@calcom/platform-types";
export class GetTeamSchedulesQuery extends SkipTakePagination {
@ApiPropertyOptional({
description: "Filter schedules by event type ID",
example: 1,
})
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@IsOptional()
eventTypeId?: number;
}
export class GetUserSchedulesQuery {
@ApiPropertyOptional({
description: "Filter schedules by event type ID",
example: 1,
})
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@IsOptional()
eventTypeId?: number;
}
@@ -14,12 +14,16 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
import {
GetTeamSchedulesQuery,
GetUserSchedulesQuery,
} from "@/modules/organizations/teams/schedules/inputs/teams-schedules.input";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId",
@@ -43,14 +47,14 @@ export class OrganizationsTeamsSchedulesController {
@DocsTags("Orgs / Teams / Schedules")
@ApiOperation({ summary: "Get all team member schedules" })
async getTeamSchedules(
// note(Lauirs): putting orgId so swagger is generacted correctly
// note(Lauris): putting orgId so swagger is generated correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
@Query() queryParams: GetTeamSchedulesQuery
): Promise<GetSchedulesOutput_2024_06_11> {
const { skip, take } = queryParams;
const { skip, take, eventTypeId } = queryParams;
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take, eventTypeId);
return {
status: SUCCESS_STATUS,
@@ -67,12 +71,14 @@ export class OrganizationsTeamsSchedulesController {
summary: "Get schedules of a team member",
})
async getUserSchedules(
// note(Lauirs): putting orgId and teamId so swagger is generacted correctly
// note(Lauris): putting orgId and teamId so swagger is generated correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) _teamId: number,
@Param("userId", ParseIntPipe) userId: number
@Param("userId", ParseIntPipe) userId: number,
@Query() queryParams: GetUserSchedulesQuery
): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(userId);
const { eventTypeId } = queryParams;
const schedules = await this.schedulesService.getUserSchedules(userId, eventTypeId);
return {
status: SUCCESS_STATUS,
@@ -4,11 +4,13 @@ import type {
GetSchedulesOutput_2024_06_11,
ScheduleOutput_2024_06_11,
} from "@calcom/platform-types";
import type { Schedule, Team, User } from "@calcom/prisma/client";
import type { EventType, Schedule, Team, User } from "@calcom/prisma/client";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
@@ -19,6 +21,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { AppModule } from "@/app.module";
import { bootstrap } from "@/bootstrap";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -32,6 +35,9 @@ describe("Organizations Teams Schedules Endpoints", () => {
let teamsRepositoryFixture: TeamRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let hostsRepositoryFixture: HostsRepositoryFixture;
let prismaWriteService: PrismaWriteService;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -41,6 +47,11 @@ describe("Organizations Teams Schedules Endpoints", () => {
let userSchedule: Schedule;
let user2Schedule: Schedule;
let userEventTypeSchedule: Schedule;
let teamEventTypeSchedule: Schedule;
let userEventType: EventType;
let teamEventType: EventType;
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
const userEmail2 = `organizations-teams-schedules-member-${randomString()}@api.com`;
@@ -66,6 +77,9 @@ describe("Organizations Teams Schedules Endpoints", () => {
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
prismaWriteService = moduleRef.get(PrismaWriteService);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
@@ -97,6 +111,26 @@ describe("Organizations Teams Schedules Endpoints", () => {
timeZone: "America/New_York",
});
userEventTypeSchedule = await scheduleRepositoryFixture.create({
user: {
connect: {
id: user2.id,
},
},
name: `organizations-teams-schedules-user-event-type-schedule-${randomString()}`,
timeZone: "America/New_York",
});
teamEventTypeSchedule = await scheduleRepositoryFixture.create({
user: {
connect: {
id: user2.id,
},
},
name: `organizations-teams-schedules-team-event-type-schedule-${randomString()}`,
timeZone: "America/New_York",
});
nonOrgUser = await userRepositoryFixture.create({
email: nonOrgUserEmail,
username: nonOrgUserEmail,
@@ -204,6 +238,50 @@ describe("Organizations Teams Schedules Endpoints", () => {
},
});
userEventType = await eventTypesRepositoryFixture.create(
{
title: `User Event Type ${randomString()}`,
slug: `user-event-type-${randomString()}`,
length: 30,
schedule: {
connect: {
id: userEventTypeSchedule.id,
},
},
},
user2.id
);
teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
title: `Team Event Type ${randomString()}`,
slug: `team-event-type-${randomString()}`,
length: 30,
team: {
connect: {
id: orgTeam.id,
},
},
});
await hostsRepositoryFixture.create({
user: {
connect: {
id: user2.id,
},
},
eventType: {
connect: {
id: teamEventType.id,
},
},
schedule: {
connect: {
id: teamEventTypeSchedule.id,
},
},
isFixed: false,
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -236,7 +314,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(2);
expect(responseBody.data.length).toBeGreaterThanOrEqual(2);
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
@@ -254,7 +333,173 @@ describe("Organizations Teams Schedules Endpoints", () => {
});
});
describe("eventTypeId query parameter", () => {
describe("GET /users/:userId/schedules", () => {
it("should filter user schedules by user-owned event type", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${userEventType.id}`
)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleOutput_2024_06_11[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].id).toEqual(userEventTypeSchedule.id);
expect(responseBody.data[0].name).toEqual(userEventTypeSchedule.name);
});
});
it("should filter user schedules by team event type where user is a host", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${teamEventType.id}`
)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ScheduleOutput_2024_06_11[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(1);
expect(responseBody.data[0].id).toEqual(teamEventTypeSchedule.id);
expect(responseBody.data[0].name).toEqual(teamEventTypeSchedule.name);
});
});
it("should return 404 when event type does not exist", async () => {
return request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=999999`
)
.expect(404);
});
it("should return 404 when user is not associated with event type", async () => {
const otherUserEventType = await eventTypesRepositoryFixture.create(
{
title: `Other User Event Type ${randomString()}`,
slug: `other-user-event-type-${randomString()}`,
length: 30,
},
user.id
);
try {
await request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${otherUserEventType.id}`
)
.expect(404);
} finally {
await eventTypesRepositoryFixture.delete(otherUserEventType.id);
}
});
it("should return empty array when event type has no schedule and user has no default schedule", async () => {
const eventTypeWithoutSchedule = await eventTypesRepositoryFixture.create(
{
title: `Event Type Without Schedule ${randomString()}`,
slug: `event-type-no-schedule-${randomString()}`,
length: 30,
},
user2.id
);
const user2Original = await prismaWriteService.prisma.user.findUnique({
where: { id: user2.id },
select: { defaultScheduleId: true },
});
const originalDefaultScheduleId = user2Original?.defaultScheduleId ?? null;
await prismaWriteService.prisma.user.update({
where: { id: user2.id },
data: { defaultScheduleId: null },
});
try {
const response = await request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules?eventTypeId=${eventTypeWithoutSchedule.id}`
)
.expect(200);
const responseBody: ApiSuccessResponse<ScheduleOutput_2024_06_11[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(0);
} finally {
await prismaWriteService.prisma.user.update({
where: { id: user2.id },
data: { defaultScheduleId: originalDefaultScheduleId },
});
await eventTypesRepositoryFixture.delete(eventTypeWithoutSchedule.id);
}
});
});
describe("GET /schedules (team schedules)", () => {
it("should filter team schedules by event type", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=${teamEventType.id}`)
.expect(200)
.then((response) => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toBeGreaterThanOrEqual(1);
const user2ScheduleFound = responseBody.data.find(
(schedule) => schedule.id === teamEventTypeSchedule.id
);
expect(user2ScheduleFound).toBeDefined();
expect(user2ScheduleFound?.id).toEqual(teamEventTypeSchedule.id);
});
});
it("should return 404 when team event type does not exist", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=999999`)
.expect(404);
});
it("should return empty array when event type has no hosts with schedules", async () => {
const teamEventTypeNoHosts = await eventTypesRepositoryFixture.createTeamEventType({
title: `Team Event Type No Hosts ${randomString()}`,
slug: `team-event-type-no-hosts-${randomString()}`,
length: 30,
team: {
connect: {
id: orgTeam.id,
},
},
});
try {
const response = await request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules?eventTypeId=${teamEventTypeNoHosts.id}`
)
.expect(200);
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(0);
} finally {
await eventTypesRepositoryFixture.delete(teamEventTypeNoHosts.id);
}
});
});
});
afterAll(async () => {
if (userEventType) {
await eventTypesRepositoryFixture.delete(userEventType.id);
}
if (teamEventType) {
await eventTypesRepositoryFixture.delete(teamEventType.id);
}
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
await userRepositoryFixture.deleteByEmail(nonOrgUser.email);
@@ -157,4 +157,28 @@ export class TeamsEventTypesRepository {
},
});
}
async getByIdIncludeHostsAndUserDefaultSchedule(eventTypeId: number, teamId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: {
id: eventTypeId,
teamId,
},
select: {
id: true,
scheduleId: true,
hosts: {
select: {
scheduleId: true,
userId: true,
user: {
select: {
defaultScheduleId: true,
},
},
},
},
},
});
}
}
@@ -1,20 +1,73 @@
import type { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
import type { Availability, Schedule } from "@calcom/prisma/client";
import { Injectable, NotFoundException } from "@nestjs/common";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { Injectable } from "@nestjs/common";
@Injectable()
export class TeamsSchedulesService {
constructor(
private readonly teamsRepository: TeamsRepository,
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository
) {}
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
async getTeamSchedules(
teamId: number,
skip = 0,
take = 250,
eventTypeId?: number
): Promise<ScheduleOutput_2024_06_11[]> {
if (!eventTypeId) {
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
return this.outputSchedulesService.getResponseSchedules(schedules);
return this.outputSchedulesService.getResponseSchedules(schedules);
}
return this.getTeamSchedulesForEventType(teamId, eventTypeId, skip, take);
}
private async getTeamSchedulesForEventType(
teamId: number,
eventTypeId: number,
skip: number,
take: number
): Promise<ScheduleOutput_2024_06_11[]> {
const eventType = await this.teamsEventTypesRepository.getByIdIncludeHostsAndUserDefaultSchedule(
eventTypeId,
teamId
);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found in team ${teamId}`);
}
const effectiveScheduleIds = new Set<number>();
for (const host of eventType.hosts) {
const effectiveScheduleId = eventType.scheduleId ?? host.scheduleId ?? host.user.defaultScheduleId;
if (effectiveScheduleId) {
effectiveScheduleIds.add(effectiveScheduleId);
}
}
if (effectiveScheduleIds.size === 0) {
return [];
}
const schedules = await this.schedulesRepository.getSchedulesByIds(
Array.from(effectiveScheduleIds),
skip,
take
);
return this.outputSchedulesService.getResponseSchedules(
schedules as (Schedule & { availability: Availability[] })[]
);
}
}
@@ -8,6 +8,7 @@ import { OrganizationSchedulesRepository } from "@/modules/organizations/schedul
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { StripeService } from "@/modules/stripe/stripe.service";
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
@@ -20,7 +21,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, RedisModule],
imports: [PrismaModule, MembershipsModule, RedisModule, TeamsEventTypesModule],
providers: [
TeamsRepository,
TeamsService,