diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts index abd2f7cf98..47aad9a7b8 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.module.ts @@ -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, diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts index 3a54a2d9c6..1ef165399a 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/schedules.repository.ts @@ -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, + }); + } } diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts index 6f94cdc95a..1973dbc578 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/schedules.service.ts @@ -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 { 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 { 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 { 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 { + 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 { + 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 { 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 { 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) { + checkUserOwnsSchedule(userId: number, schedule: Pick): void { if (userId !== schedule.userId) { throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`); } diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts b/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts new file mode 100644 index 0000000000..9f5fde010a --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/schedules/inputs/teams-schedules.input.ts @@ -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; +} diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts index 229cca73b1..83db1cf15a 100644 --- a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts +++ b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.controller.ts @@ -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 { - 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 { - const schedules = await this.schedulesService.getUserSchedules(userId); + const { eventTypeId } = queryParams; + const schedules = await this.schedulesService.getUserSchedules(userId, eventTypeId); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts index 75865f2923..121716afea 100644 --- a/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/schedules/organizations-teams-schedules.e2e-spec.ts @@ -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 = 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 = 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 = 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); diff --git a/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts index 01eef437b4..067ef98d2e 100644 --- a/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts +++ b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts @@ -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, + }, + }, + }, + }, + }, + }); + } } diff --git a/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts b/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts index e1badc9221..8523b1e764 100644 --- a/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts +++ b/apps/api/v2/src/modules/teams/schedules/services/teams-schedules.service.ts @@ -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 { + 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 { + 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(); + + 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[] })[] + ); } } diff --git a/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts b/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts index 89d34c13cd..9392ee5875 100644 --- a/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts +++ b/apps/api/v2/src/modules/teams/schedules/teams-schedules.module.ts @@ -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,