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:
co-authored by
unknown
Keith Williams
Devin AI
parent
2c105aceb8
commit
14e2fa6893
@@ -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
-8
@@ -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,
|
||||
|
||||
+247
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user