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:
co-authored by
supalarry
Morgan Vernay
parent
6de454b071
commit
2e2a6c73b1
+42
-11
@@ -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);
|
||||
|
||||
|
||||
+37
-2
@@ -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 () => {
|
||||
|
||||
+38
@@ -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,
|
||||
|
||||
+6
-6
@@ -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 {
|
||||
|
||||
+23
-89
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user