feat: add advanced tab attributes to api/v2/event-types (#16314)

* added bookerLayouts

* added requiresConfirmation

* added tests

* undo extra changes

* undo extra change

* fixed imports

* added requiresBookerEmailVerification

* added hideCalendarNotes

* added lockTimeZoneToggleOnBookingPage

* added eventTypeColor

* undo extra changes

* added seats

* added requiresConfirmationWillBlockSlot to requiresConfirmation

* refactor: Implement BaseEventType class to avoid duplication across EventType input/output classes

## D-R-Y ##

* temp-fix: will revert later

* Correct placement of users array from EventTypeOutput to TeamEventTypeOutput

* Refactor: Shifted transformation logic to controllers using interceptors and pipes for a cleaner service layer.

* small fix

* Refactor: Moved transformation logic from service to controller using interceptors and pipes

** organisations/event-types

* feat: adde tests for `validateEventTypeInputs`

* fix: failing test

* fix: layout validator

* renamed blockCalendarForUnconfirmedBookings to blockUnconfirmedBookingsInBooker

* testing changes

* added disabled state

* fix merege conflict

* renamed requiresConfirmation to confirmationPolicy

* renamed OutputEventTypesResponseInterceptor to OutputTeamEventTypesResponseInterceptor

* renamed `darkThemeColor` -> `darkThemeHex` and `lightThemeColor` -> `lightThemeHex`

* renamed `eventTypeColor` -> `color`

* changed `requiresConfirmation` -> `confirmationPolicy`

* added tests for disabled state

* added class-validators to `disabled?: false`

* Revert: Split create and update input/output classes

* feat: Refactor responses to use DTOs with plainToClass for explicit property control

	•	Updated all endpoints to return response DTOs (e.g., CreateTeamEventTypeOutput, GetTeamEventTypeOutput) to ensure consistent Swagger documentation generation.
	•	Introduced plainToClass for transforming responses, enabling explicit control over which properties are returned in the response, following Morgan’s recommendation.
	•	Replaced generic HandlerResponse type with class-validator based DTOs for each endpoint, ensuring TypeScript enforces correct response structure.
	•	Applied strategy: "excludeAll" to limit response properties to only those explicitly defined in the DTO, preventing unintended fields from being included in the API responses.

* added pipes to transform output data

* add clean script to platform-types package.json

* added customName

* added destination calendar

* fixed type errors

* added useDestinationCalendarEmail

* refactor: api-reqest and api-response

* Update event-type.output.ts

* fixed errors

* Update yarn.lock

* added some missed properties to output

* Update constants.ts

* removed return type

* fixed some type errors

* reuse types

* Improve readability of validation functions

* Update CHANGELOG.md

* chore: post publish platform libraries

* Update documentation.json

* fix: reset platform libraries to 0.0.0

* fixup! fix: reset platform libraries to 0.0.0

* Update event-type.tranformed.ts

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Somay Chauhan
2024-09-24 13:36:05 +03:00
committed by GitHub
co-authored by supalarry Morgan Vernay
parent 6de454b071
commit 2e2a6c73b1
68 changed files with 2859 additions and 6305 deletions
@@ -1,5 +1,6 @@
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";
@@ -15,7 +16,10 @@ import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/e
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 { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -34,22 +38,33 @@ import {
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
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/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@DocsTags("Organizations Event Types")
export class OrganizationsEventTypesController {
constructor(private readonly organizationsEventTypesService: OrganizationsEventTypesService) {}
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@@ -61,16 +76,22 @@ export class OrganizationsEventTypesController {
@Param("orgId", ParseIntPipe) orgId: number,
@Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14
): Promise<CreateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput(
user.id,
teamId,
bodyEventType
);
const eventType = await this.organizationsEventTypesService.createTeamEventType(
user,
teamId,
orgId,
bodyEventType
transformedBody
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -90,7 +111,9 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventType,
data: (await this.outputTeamEventTypesResponsePipe.transform(
eventType
)) as TeamEventTypeOutput_2024_06_14,
};
}
@@ -125,12 +148,13 @@ export class OrganizationsEventTypesController {
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
return {
status: SUCCESS_STATUS,
data: eventType ? [eventType] : [],
data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []),
};
}
@@ -138,7 +162,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -155,7 +179,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -169,16 +193,23 @@ export class OrganizationsEventTypesController {
@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.organizationsEventTypesService.updateTeamEventType(
eventTypeId,
teamId,
bodyEventType,
transformedBody,
user
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -189,7 +220,7 @@ export class OrganizationsEventTypesController {
@HttpCode(HttpStatus.OK)
async deleteTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId") eventTypeId: number
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<DeleteTeamEventTypeOutput> {
const eventType = await this.organizationsEventTypesService.deleteTeamEventType(teamId, eventTypeId);
@@ -17,7 +17,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateTeamEventTypeInput_2024_06_14,
@@ -276,6 +281,30 @@ describe("Organizations Event Types Endpoints", () => {
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,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
};
return request(app.getHttpServer())
@@ -297,6 +326,12 @@ describe("Organizations Event Types Endpoints", () => {
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.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
collectiveEventType = responseBody.data;
});
@@ -444,7 +479,7 @@ describe("Organizations Event Types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`)
.send(body)
.expect(404);
.expect(400);
});
it("should update collective event-type", async () => {
@@ -0,0 +1,38 @@
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class OutputTeamEventTypesResponsePipe implements PipeTransform {
constructor(private readonly outputOrganizationsEventTypesService: OutputOrganizationsEventTypesService) {}
private async transformEventType(item: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14> {
return plainToClass(
TeamEventTypeOutput_2024_06_14,
await this.outputOrganizationsEventTypesService.getResponseTeamEventType(item),
{ strategy: "exposeAll" }
);
}
// Implementing function overloading to ensure correct return types based on input type:
async transform(value: DatabaseTeamEventType[]): Promise<TeamEventTypeOutput_2024_06_14[]>;
async transform(value: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]> {
if (Array.isArray(value)) {
return await Promise.all(value.map((item) => this.transformEventType(item)));
} else {
return await this.transformEventType(value);
}
}
}
@@ -7,6 +7,7 @@ import { OrganizationsOptionsAttributesController } from "@/modules/organization
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
@@ -79,6 +80,7 @@ import { Module } from "@nestjs/common";
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
OutputTeamEventTypesResponsePipe,
],
exports: [
OrganizationsService,
@@ -12,7 +12,7 @@ export class OrganizationsEventTypesRepository {
id: eventTypeId,
teamId,
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -24,7 +24,7 @@ export class OrganizationsEventTypesRepository {
slug: eventTypeSlug,
},
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -33,21 +33,21 @@ export class OrganizationsEventTypesRepository {
where: {
teamId,
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true, hosts: true },
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 },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -60,7 +60,7 @@ export class OrganizationsEventTypesRepository {
},
skip,
take,
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -2,7 +2,7 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import {
CreateTeamEventTypeInput_2024_06_14,
@@ -19,37 +19,89 @@ export class InputOrganizationsEventTypesService {
private readonly usersRepository: UsersRepository,
private readonly orgEventTypesRepository: OrganizationsEventTypesRepository
) {}
async transformAndValidateCreateTeamEventTypeInput(
userId: number,
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
const transformedBody = await this.transformInputCreateTeamEventType(teamId, inputEventType);
await this.inputEventTypesService.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.inputEventTypesService.validateInputDestinationCalendar(
userId,
transformedBody.destinationCalendar
));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
async transformAndValidateUpdateTeamEventTypeInput(
userId: number,
eventTypeId: number,
teamId: number,
inputEventType: UpdateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
const transformedBody = await this.transformInputUpdateTeamEventType(eventTypeId, teamId, inputEventType);
await this.inputEventTypesService.validateEventTypeInputs({
eventTypeId: eventTypeId,
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.inputEventTypesService.validateInputDestinationCalendar(
userId,
transformedBody.destinationCalendar
));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
async transformInputCreateTeamEventType(
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
const {
hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest
} = inputEventType;
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
const children = await this.getChildEventTypesForManagedEventType(null, inputEventType, teamId);
const metadata = rest.schedulingType === "MANAGED" ? { managedEventConfig: {} } : undefined;
const metadata =
rest.schedulingType === "MANAGED"
? { managedEventConfig: {}, ...eventType.metadata }
: eventType.metadata;
const teamEventType = {
...eventType,
hosts: assignAllTeamMembers
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
: this.transformInputHosts(hosts, inputEventType.schedulingType),
// note(Lauris): we don't populate hosts for managed event-types because they are handled by the children
hosts: !(rest.schedulingType === "MANAGED")
? assignAllTeamMembers
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
: this.transformInputHosts(hosts, inputEventType.schedulingType)
: undefined,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
metadata,
children,
};
return teamEventType;
@@ -62,7 +114,7 @@ export class InputOrganizationsEventTypesService {
) {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputUpdateEventType(rest);
const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId);
const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!dbEventType) {
@@ -86,14 +138,16 @@ export class InputOrganizationsEventTypesService {
}
async getChildEventTypesForManagedEventType(
eventTypeId: number,
eventTypeId: number | null,
inputEventType: UpdateTeamEventTypeInput_2024_06_14,
teamId: number
) {
const eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
if (!eventType || eventType.schedulingType !== "MANAGED") {
return undefined;
let eventType = null;
if (eventTypeId) {
eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
if (!eventType || eventType.schedulingType !== "MANAGED") {
return undefined;
}
}
const ownersIds = await this.getOwnersIdsForManagedEventType(teamId, inputEventType, eventType);
@@ -110,7 +164,7 @@ export class InputOrganizationsEventTypesService {
async getOwnersIdsForManagedEventType(
teamId: number,
inputEventType: UpdateTeamEventTypeInput_2024_06_14,
eventType: { children: { userId: number | null }[] }
eventType: { children: { userId: number | null }[] } | null
) {
if (inputEventType.assignAllTeamMembers) {
return await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
@@ -122,7 +176,7 @@ export class InputOrganizationsEventTypesService {
}
// note(Lauris): when API user DOES NOT update managed event type users, but we still need existing managed event type users to know which event-types to update
return eventType.children.map((child) => child.userId).filter((id) => !!id) as number[];
return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[];
}
async getOwnersForManagedEventType(userIds: number[]) {
@@ -170,6 +224,16 @@ export class InputOrganizationsEventTypesService {
),
}));
}
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
if (hosts && hosts.length) {
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
}
}
}
}
function getPriorityValue(priority: keyof typeof HostPriority): number {
@@ -2,31 +2,23 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
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 { 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 { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class OrganizationsEventTypesService {
constructor(
private readonly inputService: InputOrganizationsEventTypesService,
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly dbWrite: PrismaWriteService,
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputService: OutputOrganizationsEventTypesService,
private readonly membershipsRepository: MembershipsRepository,
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly usersService: UsersService
) {}
@@ -34,24 +26,14 @@ export class OrganizationsEventTypesService {
user: UserWithProfile,
teamId: number,
orgId: number,
body: CreateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, body.hosts);
body: InputTeamEventTransformed_2024_06_14
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hosts,
assignAllTeamMembers,
locations,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, locations, ...rest },
input: { teamId: teamId, ...rest },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -60,31 +42,7 @@ export class OrganizationsEventTypesService {
},
});
return await this.updateTeamEventType(
eventTypeCreated.id,
teamId,
{
hosts: body.hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest,
},
user
);
}
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
if (hosts && hosts.length) {
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
}
}
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
@@ -110,17 +68,17 @@ export class OrganizationsEventTypesService {
};
}
async getTeamEventType(teamId: number, eventTypeId: number) {
async getTeamEventType(teamId: number, eventTypeId: number): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
return null;
}
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug(
teamId,
eventTypeSlug
@@ -130,46 +88,28 @@ export class OrganizationsEventTypesService {
return null;
}
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
async getTeamEventTypes(teamId: number) {
const eventTypes = await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputService.getResponseTeamEventType(eventType);
});
return await Promise.all(eventTypePromises);
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
}
async getTeamsEventTypes(orgId: number, skip = 0, take = 250) {
const eventTypes = await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputService.getResponseTeamEventType(eventType);
});
return await Promise.all(eventTypePromises);
async getTeamsEventTypes(orgId: number, skip = 0, take = 250): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take);
}
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: UpdateTeamEventTypeInput_2024_06_14,
body: InputTeamEventTransformed_2024_06_14,
user: UserWithProfile
) {
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
await this.validateEventTypeExists(teamId, eventTypeId);
await this.validateHosts(teamId, body.hosts);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
const bodyTransformed = await this.inputService.transformInputUpdateTeamEventType(
eventTypeId,
teamId,
body
);
await updateEventType({
input: { id: eventTypeId, ...bodyTransformed },
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -185,18 +125,12 @@ export class OrganizationsEventTypesService {
}
if (eventType.schedulingType !== "MANAGED") {
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id);
const childrenEventTypes = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id);
const eventTypes = [eventType, ...children];
const eventTypePromises = eventTypes.map(async (e) => {
return await this.outputService.getResponseTeamEventType(e);
});
return await Promise.all(eventTypePromises);
return [eventType, ...childrenEventTypes];
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
@@ -2,16 +2,21 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, Host } from "@prisma/client";
import type { EventType, User, Schedule, Host, DestinationCalendar } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
type DatabaseEventType = EventType & EventTypeRelations;
type EventTypeRelations = {
users: User[];
schedule: Schedule | null;
hosts: Host[];
destinationCalendar?: DestinationCalendar | null;
};
export type DatabaseTeamEventType = EventType & EventTypeRelations;
type Input = Pick<
DatabaseEventType,
DatabaseTeamEventType,
| "id"
| "length"
| "title"
@@ -52,6 +57,14 @@ type Input = Pick<
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
| "requiresBookerEmailVerification"
| "hideCalendarNotes"
| "lockTimeZoneToggleOnBookingPage"
| "eventTypeColor"
| "seatsShowAttendees"
| "requiresConfirmationWillBlockSlot"
| "eventName"
| "useEventTypeDestinationCalendarEmail"
>;
@Injectable()
@@ -65,7 +78,7 @@ export class OutputOrganizationsEventTypesService {
async getResponseTeamEventType(databaseEventType: Input) {
const { teamId, userId, parentId, assignAllTeamMembers } = databaseEventType;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ownerId, users, ...rest } = await this.outputEventTypesService.getResponseEventType(
const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType(
0,
databaseEventType
);
@@ -80,6 +93,7 @@ export class OutputOrganizationsEventTypesService {
teamId,
ownerId: userId,
parentEventTypeId: parentId,
schedulingType: databaseEventType.schedulingType,
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
};
}