feat: v2 teams event types (#18085)

* feat: v2 teams event-types endpoints

* refactor: rely on teams event types service and repository

* finish mergin main into this branch

* test: teams event types

* refactor: move teams event types output from org to teams folder

* refactor: move teams event types output from org to teams folder

* chore: finish merge main

* refactor: logger name and unused import

* chore: finish main merge

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
Lauris SkraucisandSomay Chauhan authored and GitHub committed 2024-12-17 20:25:42 +01:00
1 parent ae7bbaf414
commit 2af5cba0d7
24 files changed
+1619 -222

No files matched your search

@@ -9,6 +9,7 @@ import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { SlotsModule } from "@/modules/slots/slots.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
@@ -20,6 +21,7 @@ import { Module } from "@nestjs/common";
ProviderModule,
SchedulesModule_2024_04_15,
SchedulesModule_2024_06_11,
TeamsEventTypesModule,
MeModule,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
@@ -8,12 +8,13 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule],
providers: [
EventTypesAtomService,
ConferencingAtomsService,
@@ -2,10 +2,9 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
@@ -50,7 +49,7 @@ export class EventTypesAtomService {
private readonly dbWrite: PrismaWriteService,
private readonly dbRead: PrismaReadService,
private readonly eventTypeService: EventTypesService_2024_06_14,
private readonly teamEventTypeService: OrganizationsEventTypesService
private readonly teamEventTypeService: TeamsEventTypesService
) {}
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
@@ -1,6 +1,5 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -11,15 +10,15 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
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 { CreateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output";
import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output";
import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output";
import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output";
import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -62,7 +61,6 @@ export class OrganizationsEventTypesController {
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@@ -180,7 +178,11 @@ export class OrganizationsEventTypesController {
@Query() queryParams: SkipTakePagination
): Promise<GetTeamEventTypesOutput> {
const { skip, take } = queryParams;
const eventTypes = await this.organizationsEventTypesService.getTeamsEventTypes(orgId, skip, take);
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
orgId,
skip,
take
);
return {
status: SUCCESS_STATUS,
@@ -15,8 +15,8 @@ import {
OrgTeamMembershipsOutputResponseDto,
OrgTeamMembershipOutputResponseDto,
} from "@/modules/organizations/outputs/organization-teams-memberships.output";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import {
Controller,
UseGuards,
@@ -51,7 +51,7 @@ export class OrganizationsTeamsMembershipsController {
constructor(
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
private oganizationsEventTypesService: OrganizationsEventTypesService,
private teamsEventTypesService: TeamsEventTypesService,
private readonly organizationsRepository: OrganizationsRepository
) {}
@@ -119,7 +119,7 @@ export class OrganizationsTeamsMembershipsController {
membershipId
);
await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return {
status: SUCCESS_STATUS,
@@ -39,6 +39,7 @@ import { OrganizationsService } from "@/modules/organizations/services/organizat
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { UsersModule } from "@/modules/users/users.module";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
@@ -54,6 +55,7 @@ import { Module } from "@nestjs/common";
RedisModule,
EmailModule,
EventTypesModule_2024_06_14,
TeamsEventTypesModule,
TeamsModule,
],
providers: [
@@ -1,91 +1,10 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getTeamEventType(teamId: number, eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: {
id: eventTypeId,
teamId,
},
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
return this.dbRead.prisma.eventType.findUnique({
where: {
teamId_slug: {
teamId,
slug: eventTypeSlug,
},
},
include: {
users: true,
schedule: true,
hosts: true,
destinationCalendar: true,
team: {
select: {
bannerUrl: true,
name: true,
logoUrl: true,
slug: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
},
},
});
}
async getTeamEventTypes(teamId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
teamId,
},
include: {
users: true,
schedule: true,
hosts: true,
destinationCalendar: true,
team: {
select: {
bannerUrl: true,
name: true,
logoUrl: true,
slug: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
},
},
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeChildren(eventTypeId: number) {
return this.dbRead.prisma.eventType.findMany({
where: { parentId: eventTypeId },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getTeamsEventTypes(orgId: number, skip: number, take: number) {
constructor(private readonly dbRead: PrismaReadService) {}
async getOrganizationTeamsEventTypes(orgId: number, skip: number, take: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
team: {
@@ -97,33 +16,4 @@ export class OrganizationsEventTypesRepository {
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeByIdWithChildren(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { children: true },
});
}
async deleteUserManagedTeamEventTypes(userId: number, teamId: number) {
return this.dbWrite.prisma.eventType.deleteMany({
where: {
parent: {
teamId,
},
userId,
},
});
}
async removeUserFromTeamEventTypesHosts(userId: number, teamId: number) {
return this.dbWrite.prisma.host.deleteMany({
where: {
userId,
eventType: {
teamId,
},
},
});
}
}
@@ -1,5 +1,5 @@
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
@@ -17,7 +17,7 @@ export class InputOrganizationsEventTypesService {
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly teamsRepository: TeamsRepository,
private readonly usersRepository: UsersRepository,
private readonly orgEventTypesRepository: OrganizationsEventTypesRepository
private readonly teamsEventTypesRepository: TeamsEventTypesRepository
) {}
async transformAndValidateCreateTeamEventTypeInput(
userId: number,
@@ -115,7 +115,7 @@ export class InputOrganizationsEventTypesService {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId);
const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId);
const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!dbEventType) {
throw new BadRequestException("Event type to update not found");
@@ -144,7 +144,7 @@ export class InputOrganizationsEventTypesService {
) {
let eventType = null;
if (eventTypeId) {
eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
eventType = await this.teamsEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
if (!eventType || eventType.schedulingType !== "MANAGED") {
return undefined;
}
@@ -1,14 +1,13 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { createEventType } from "@calcom/platform-libraries";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -16,10 +15,9 @@ export class OrganizationsEventTypesService {
private readonly logger = new Logger("OrganizationsEventTypesService");
constructor(
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly dbWrite: PrismaWriteService,
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly teamsEventTypesService: TeamsEventTypesService,
private readonly membershipsRepository: MembershipsRepository,
private readonly usersService: UsersService
) {}
@@ -44,15 +42,7 @@ export class OrganizationsEventTypesService {
},
});
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
}
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
@@ -71,34 +61,23 @@ export class OrganizationsEventTypesService {
}
async getTeamEventType(teamId: number, eventTypeId: number): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
return null;
}
return eventType;
return this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId);
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug(
teamId,
eventTypeSlug
);
if (!eventType) {
return null;
}
return eventType;
return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug);
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
return await this.teamsEventTypesService.getTeamEventTypes(teamId);
}
async getTeamsEventTypes(orgId: number, skip = 0, take = 250): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take);
async getOrganizationsTeamsEventTypes(
orgId: number,
skip = 0,
take = 250
): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
}
async updateTeamEventType(
@@ -107,57 +86,10 @@ export class OrganizationsEventTypesService {
body: InputTeamEventTransformed_2024_06_14,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
await this.validateEventTypeExists(teamId, eventTypeId);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
await updateEventType({
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
const eventType = await this.organizationEventTypesRepository.getEventTypeById(eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
if (eventType.schedulingType !== "MANAGED") {
return eventType;
}
const childrenEventTypes = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id);
return [eventType, ...childrenEventTypes];
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
const existingEventType = await this.organizationEventTypesRepository.getTeamEventType(
teamId,
eventTypeId
);
if (!existingEventType) {
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
}
return this.eventTypesRepository.deleteEventType(eventTypeId);
}
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
try {
await this.organizationEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
await this.organizationEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
} catch (err) {
this.logger.error("Could not remove user from all team event-types.", {
error: err,
userId,
teamId,
});
}
return this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId);
}
}
@@ -1,5 +1,5 @@
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, Host, DestinationCalendar } from "@prisma/client";
@@ -77,7 +77,7 @@ type Input = Pick<
export class OutputOrganizationsEventTypesService {
constructor(
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly usersRepository: UsersRepository
) {}
@@ -117,7 +117,7 @@ export class OutputOrganizationsEventTypesService {
}
async getManagedEventTypeHosts(eventTypeId: number) {
const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventTypeId);
const children = await this.teamsEventTypesRepository.getEventTypeChildren(eventTypeId);
const transformedHosts: TeamEventTypeResponseHost[] = [];
for (const child of children) {
if (child.userId) {
@@ -0,0 +1,570 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.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";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomNumber } from "test/utils/randomNumber";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateTeamEventTypeInput_2024_06_14,
Host,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
describe("Organizations Event Types Endpoints", () => {
describe("User Authentication - User is Org Admin", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let team: Team;
let falseTestTeam: Team;
const userEmail = `auth-${randomNumber()}@api.com`;
let userAdmin: User;
const teammate1Email = "teammate111@team.com";
const teammate2Email = "teammate221@team.com";
const falseTestUserEmail = "false-user@false-team.com";
let teamMember1: User;
let teamMember2: User;
let falseTestUser: User;
let collectiveEventType: TeamEventTypeOutput_2024_06_14;
let managedEventType: TeamEventTypeOutput_2024_06_14;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
userAdmin = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
role: "ADMIN",
});
teamMember1 = await userRepositoryFixture.create({
email: teammate1Email,
username: teammate1Email,
});
teamMember2 = await userRepositoryFixture.create({
email: teammate2Email,
username: teammate2Email,
});
falseTestUser = await userRepositoryFixture.create({
email: falseTestUserEmail,
username: falseTestUserEmail,
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
isOrganization: false,
});
falseTestTeam = await teamsRepositoryFixture.create({
name: "Outside org team",
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: userAdmin.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teamMember1.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teamMember2.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: falseTestUser.id } },
team: { connect: { id: falseTestTeam.id } },
accepted: true,
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should not be able to create event-type for user outside team", async () => {
const userId = falseTestUser.id;
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation",
slug: "coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId,
mandatory: true,
priority: "high",
},
],
};
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
});
it("should create a collective team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation collective",
slug: "coding-consultation collective",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "select",
label: "select which language is your codebase in",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
},
],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "collective",
hosts: [
{
userId: teamMember1.id,
},
{
userId: teamMember2.id,
},
],
bookingLimitsCount: {
day: 2,
week: 5,
},
onlyShowFirstAvailableSlot: true,
bookingLimitsDuration: {
day: 60,
week: 100,
},
offsetStart: 30,
bookingWindow: {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
value: 30,
rolling: true,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
},
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
};
return request(app.getHttpServer())
.post(`/v2/teams/${team.id}/event-types`)
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("COLLECTIVE");
evaluateHost(body.hosts[0], data.hosts[0]);
evaluateHost(body.hosts[1], data.hosts[1]);
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(data.offsetStart).toEqual(body.offsetStart);
expect(data.bookingWindow).toEqual(body.bookingWindow);
expect(data.bookerLayouts).toEqual(body.bookerLayouts);
expect(data.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
collectiveEventType = responseBody.data;
});
});
it("should create a managed team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Coding consultation managed",
slug: "coding-consultation-managed",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
schedulingType: "MANAGED",
hosts: [
{
userId: teamMember1.id,
mandatory: true,
priority: "high",
},
{
userId: teamMember2.id,
mandatory: false,
priority: "low",
},
],
};
return request(app.getHttpServer())
.post(`/v2/teams/${team.id}/event-types`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.length).toEqual(3);
const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id);
const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id);
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
expect(teammate1EventTypes.length).toEqual(1);
expect(teammate2EventTypes.length).toEqual(1);
expect(teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED").length).toEqual(
1
);
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
expect(responseTeamEvent).toBeDefined();
if (!responseTeamEvent) {
throw new Error("Team event not found");
}
const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teamMember1.id);
expect(responseTeammate1Event).toBeDefined();
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teamMember2.id);
expect(responseTeammate2Event).toBeDefined();
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
managedEventType = responseTeamEvent;
});
});
it("should not get a non existing event-type", async () => {
return request(app.getHttpServer()).get(`/v2/teams/${team.id}/event-types/999999`).expect(404);
});
it("should get a team event-type", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.title).toEqual(collectiveEventType.title);
expect(data.hosts.length).toEqual(2);
evaluateHost(collectiveEventType.hosts[0], data.hosts[0]);
evaluateHost(collectiveEventType.hosts[1], data.hosts[1]);
collectiveEventType = responseBody.data;
});
});
it("should get team event-types", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${team.id}/event-types`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.length).toEqual(2);
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED");
expect(eventTypeCollective?.title).toEqual(collectiveEventType.title);
expect(eventTypeCollective?.hosts.length).toEqual(2);
expect(eventTypeManaged?.title).toEqual(managedEventType.title);
expect(eventTypeManaged?.hosts.length).toEqual(2);
evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]);
evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]);
});
});
it("should not be able to update non existing event-type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
title: "Clean code consultation",
};
return request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/event-types/999999`)
.send(body)
.expect(400);
});
it("should update collective event-type", async () => {
const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [
{
userId: teamMember1.id,
},
];
const body: UpdateTeamEventTypeInput_2024_06_14 = {
hosts: newHosts,
};
return request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`)
.send(body)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const eventType = responseBody.data;
expect(eventType.title).toEqual(collectiveEventType.title);
expect(eventType.hosts.length).toEqual(1);
evaluateHost(eventType.hosts[0], newHosts[0]);
});
});
it("should update managed event-type", async () => {
const newTitle = "Coding consultation managed updated";
const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [
{
userId: teamMember1.id,
mandatory: true,
priority: "medium",
},
];
const body: UpdateTeamEventTypeInput_2024_06_14 = {
title: newTitle,
hosts: newHosts,
};
return request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/event-types/${managedEventType.id}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.length).toEqual(2);
const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id);
const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id);
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
const managedTeamEventTypes = teamEventTypes.filter(
(eventType) => eventType.schedulingType === "MANAGED"
);
expect(teammate1EventTypes.length).toEqual(1);
expect(teammate1EventTypes[0].title).toEqual(newTitle);
expect(teammate2EventTypes.length).toEqual(0);
expect(managedTeamEventTypes.length).toEqual(1);
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false);
expect(
teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title
).toEqual(newTitle);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.title).toEqual(newTitle);
expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false);
const responseTeammate1Event = responseBody.data.find(
(eventType) => eventType.ownerId === teamMember1.id
);
expect(responseTeammate1Event).toBeDefined();
expect(responseTeammate1Event?.title).toEqual(newTitle);
managedEventType = responseBody.data[0];
});
});
it("should assign all members to managed event-type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
assignAllTeamMembers: true,
};
return request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/event-types/${managedEventType.id}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
// note(Lauris): we expect 4 because we have 2 team members, 1 team admin and 4th is the event object itself.
expect(data.length).toEqual(4);
const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id);
const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id);
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
const managedTeamEventTypes = teamEventTypes.filter(
(eventType) => eventType.schedulingType === "MANAGED"
);
expect(teammate1EventTypes.length).toEqual(1);
expect(teammate2EventTypes.length).toEqual(1);
expect(managedTeamEventTypes.length).toEqual(1);
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
const responseTeamEvent = responseBody.data.find(
(eventType) => eventType.schedulingType === "MANAGED"
);
expect(responseTeamEvent).toBeDefined();
expect(responseTeamEvent?.teamId).toEqual(team.id);
expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true);
const responseTeammate1Event = responseBody.data.find(
(eventType) => eventType.ownerId === teamMember1.id
);
expect(responseTeammate1Event).toBeDefined();
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
const responseTeammate2Event = responseBody.data.find(
(eventType) => eventType.ownerId === teamMember2.id
);
expect(responseTeammate1Event).toBeDefined();
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
if (responseTeamEvent) {
managedEventType = responseTeamEvent;
}
});
});
it("should delete event-type not part of the team", async () => {
return request(app.getHttpServer()).delete(`/v2/teams/${team.id}/event-types/99999`).expect(404);
});
it("should delete collective event-type", async () => {
return request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`)
.expect(200);
});
it("should delete managed event-type", async () => {
return request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/event-types/${managedEventType.id}`)
.expect(200);
});
function evaluateHost(expected: Host, received: Host | undefined) {
expect(expected.userId).toEqual(received?.userId);
expect(expected.mandatory).toEqual(received?.mandatory);
expect(expected.priority).toEqual(received?.priority);
}
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(userAdmin.email);
await userRepositoryFixture.deleteByEmail(teamMember1.email);
await userRepositoryFixture.deleteByEmail(teamMember2.email);
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(falseTestTeam.id);
await app.close();
});
});
});
@@ -0,0 +1,208 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output";
import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output";
import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
UseGuards,
Get,
Post,
Param,
ParseIntPipe,
Body,
Patch,
Delete,
HttpCode,
HttpStatus,
NotFoundException,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
SkipTakePagination,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
export type EventTypeHandlerResponse = {
data: DatabaseTeamEventType[] | DatabaseTeamEventType;
status: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
};
@Controller({
path: "/v2/teams/:teamId/event-types",
version: API_VERSIONS_VALUES,
})
@DocsTags("Teams / Event Types")
export class TeamsEventTypesController {
constructor(
private readonly teamsEventTypesService: TeamsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, RolesGuard)
@Post("/")
@ApiOperation({ summary: "Create an event type" })
async createTeamEventType(
@GetUser() user: UserWithProfile,
@Param("teamId", ParseIntPipe) teamId: number,
@Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14
): Promise<CreateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput(
user.id,
teamId,
bodyEventType
);
const eventType = await this.teamsEventTypesService.createTeamEventType(user, teamId, transformedBody);
return {
status: SUCCESS_STATUS,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@Get("/:eventTypeId")
@ApiOperation({ summary: "Get an event type" })
async getTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId") eventTypeId: number
): Promise<GetTeamEventTypeOutput> {
const eventType = await this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return {
status: SUCCESS_STATUS,
data: (await this.outputTeamEventTypesResponsePipe.transform(
eventType
)) as TeamEventTypeOutput_2024_06_14,
};
}
@Roles("TEAM_ADMIN")
@Post("/:eventTypeId/create-phone-call")
@UseGuards(ApiAuthGuard, RolesGuard)
@ApiOperation({ summary: "Create a phone call" })
async createPhoneCall(
@Param("eventTypeId") eventTypeId: number,
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreatePhoneCallInput,
@GetUser() user: UserWithProfile
): Promise<CreatePhoneCallOutput> {
const data = await handleCreatePhoneCall({
user: {
id: user.id,
timeZone: user.timeZone,
profile: { organization: { id: orgId } },
},
input: { ...body, eventTypeId },
});
return {
status: SUCCESS_STATUS,
data,
};
}
@Get("/")
@ApiOperation({ summary: "Get a team event type" })
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
if (eventSlug) {
const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
return {
status: SUCCESS_STATUS,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []),
};
}
const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId);
return {
status: SUCCESS_STATUS,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@Patch("/:eventTypeId")
@ApiOperation({ summary: "Update a team event type" })
async updateTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@GetUser() user: UserWithProfile,
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
): Promise<UpdateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput(
user.id,
eventTypeId,
teamId,
bodyEventType
);
const eventType = await this.teamsEventTypesService.updateTeamEventType(
eventTypeId,
teamId,
transformedBody,
user
);
return {
status: SUCCESS_STATUS,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@Roles("TEAM_ADMIN")
@UseGuards(ApiAuthGuard, RolesGuard)
@Delete("/:eventTypeId")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Delete a team event type" })
async deleteTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<DeleteTeamEventTypeOutput> {
const eventType = await this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId);
return {
status: SUCCESS_STATUS,
data: {
id: eventTypeId,
title: eventType.title,
},
};
}
}
@@ -0,0 +1,148 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class TeamsEventTypesService {
private readonly logger = new Logger("TeamsEventTypesService");
constructor(
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly dbWrite: PrismaWriteService,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly usersService: UsersService
) {}
async createTeamEventType(
user: UserWithProfile,
teamId: number,
body: InputTeamEventTransformed_2024_06_14
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, ...rest },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
const eventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
}
async getUserToCreateTeamEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id;
return {
id: user.id,
role: user.role,
organizationId: null,
organization: { id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null },
profile: { id: profileId || null },
metadata: user.metadata,
};
}
async getTeamEventType(teamId: number, eventTypeId: number): Promise<DatabaseTeamEventType | null> {
const eventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
return null;
}
return eventType;
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise<DatabaseTeamEventType | null> {
const eventType = await this.teamsEventTypesRepository.getTeamEventTypeBySlug(teamId, eventTypeSlug);
if (!eventType) {
return null;
}
return eventType;
}
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.teamsEventTypesRepository.getTeamEventTypes(teamId);
}
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: InputTeamEventTransformed_2024_06_14,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
await this.validateEventTypeExists(teamId, eventTypeId);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
await updateEventType({
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
if (eventType.schedulingType !== "MANAGED") {
return eventType;
}
const childrenEventTypes = await this.teamsEventTypesRepository.getEventTypeChildren(eventType.id);
return [eventType, ...childrenEventTypes];
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
const existingEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
}
return this.eventTypesRepository.deleteEventType(eventTypeId);
}
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
try {
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
} catch (err) {
this.logger.error("Could not remove user from all team event-types.", {
error: err,
userId,
teamId,
});
}
}
}
@@ -0,0 +1,36 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TeamsEventTypesController } from "@/modules/teams/event-types/controllers/teams-event-types.controller";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [
PrismaModule,
RedisModule,
MembershipsModule,
EventTypesModule_2024_06_14,
UsersModule,
TeamsModule,
],
providers: [
TeamsEventTypesRepository,
TeamsEventTypesService,
InputOrganizationsEventTypesService,
OrganizationsTeamsRepository,
OutputTeamEventTypesResponsePipe,
OutputOrganizationsEventTypesService,
],
exports: [TeamsEventTypesRepository, TeamsEventTypesService],
controllers: [TeamsEventTypesController],
})
export class TeamsEventTypesModule {}
@@ -0,0 +1,116 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class TeamsEventTypesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getTeamEventType(teamId: number, eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: {
id: eventTypeId,
teamId,
},
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
return this.dbRead.prisma.eventType.findUnique({
where: {
teamId_slug: {
teamId,
slug: eventTypeSlug,
},
},
include: {
users: true,
schedule: true,
hosts: true,
destinationCalendar: true,
team: {
select: {
bannerUrl: true,
name: true,
logoUrl: true,
slug: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
},
},
});
}
async getTeamEventTypes(teamId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
teamId,
},
include: {
users: true,
schedule: true,
hosts: true,
destinationCalendar: true,
team: {
select: {
bannerUrl: true,
name: true,
logoUrl: true,
slug: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
},
},
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeChildren(eventTypeId: number) {
return this.dbRead.prisma.eventType.findMany({
where: { parentId: eventTypeId },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeByIdWithChildren(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { children: true },
});
}
async deleteUserManagedTeamEventTypes(userId: number, teamId: number) {
return this.dbWrite.prisma.eventType.deleteMany({
where: {
parent: {
teamId,
},
userId,
},
});
}
async removeUserFromTeamEventTypesHosts(userId: number, teamId: number) {
return this.dbWrite.prisma.host.deleteMany({
where: {
userId,
eventType: {
teamId,
},
},
});
}
}
@@ -2,7 +2,7 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
@@ -45,7 +45,7 @@ export class TeamsMembershipsController {
constructor(
private teamsMembershipsService: TeamsMembershipsService,
private oganizationsEventTypesService: OrganizationsEventTypesService
private teamsEventTypesService: TeamsEventTypesService
) {}
@Roles("TEAM_ADMIN")
@@ -149,7 +149,7 @@ export class TeamsMembershipsController {
): Promise<DeleteTeamMembershipOutput> {
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return {
status: SUCCESS_STATUS,
@@ -2,13 +2,14 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
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 { TeamsMembershipsController } from "@/modules/teams/memberships/controllers/teams-memberships.controller";
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule],
imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule, TeamsEventTypesModule],
providers: [TeamsMembershipsRepository, TeamsMembershipsService],
controllers: [TeamsMembershipsController],
})
+251
View File
@@ -5554,6 +5554,257 @@
]
}
},
"/v2/teams/{teamId}/event-types": {
"post": {
"operationId": "TeamsEventTypesController_createTeamEventType",
"summary": "Create an event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTeamEventTypeOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
},
"get": {
"operationId": "TeamsEventTypesController_getTeamEventTypes",
"summary": "Get a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventSlug",
"required": false,
"in": "query",
"description": "Slug of team event type to return.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTeamEventTypesOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
}
},
"/v2/teams/{teamId}/event-types/{eventTypeId}": {
"get": {
"operationId": "TeamsEventTypesController_getTeamEventType",
"summary": "Get an event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTeamEventTypeOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
},
"patch": {
"operationId": "TeamsEventTypesController_updateTeamEventType",
"summary": "Update a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateTeamEventTypeOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
},
"delete": {
"operationId": "TeamsEventTypesController_deleteTeamEventType",
"summary": "Delete a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteTeamEventTypeOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
}
},
"/v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": {
"post": {
"operationId": "TeamsEventTypesController_createPhoneCall",
"summary": "Create a phone call",
"parameters": [
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePhoneCallInput"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePhoneCallOutput"
}
}
}
}
},
"tags": [
"Teams / Event Types"
]
}
},
"/v2/teams/{teamId}/memberships": {
"post": {
"operationId": "TeamsMembershipsController_createTeamMembership",
+239
View File
@@ -5271,6 +5271,245 @@
"tags": ["Teams"]
}
},
"/v2/teams/{teamId}/event-types": {
"post": {
"operationId": "TeamsEventTypesController_createTeamEventType",
"summary": "Create an event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTeamEventTypeOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
},
"get": {
"operationId": "TeamsEventTypesController_getTeamEventTypes",
"summary": "Get a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventSlug",
"required": false,
"in": "query",
"description": "Slug of team event type to return.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTeamEventTypesOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
}
},
"/v2/teams/{teamId}/event-types/{eventTypeId}": {
"get": {
"operationId": "TeamsEventTypesController_getTeamEventType",
"summary": "Get an event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetTeamEventTypeOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
},
"patch": {
"operationId": "TeamsEventTypesController_updateTeamEventType",
"summary": "Update a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateTeamEventTypeOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
},
"delete": {
"operationId": "TeamsEventTypesController_deleteTeamEventType",
"summary": "Delete a team event type",
"parameters": [
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteTeamEventTypeOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
}
},
"/v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": {
"post": {
"operationId": "TeamsEventTypesController_createPhoneCall",
"summary": "Create a phone call",
"parameters": [
{
"name": "eventTypeId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePhoneCallInput"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreatePhoneCallOutput"
}
}
}
}
},
"tags": ["Teams / Event Types"]
}
},
"/v2/teams/{teamId}/memberships": {
"post": {
"operationId": "TeamsMembershipsController_createTeamMembership",