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
@@ -198,7 +198,7 @@ class DestinationCalendar {
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
class ConnectedCalendarsData {
|
||||
export class ConnectedCalendarsData {
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
connectedCalendars!: ConnectedCalendar[];
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
CredentialsRepository,
|
||||
CredentialsWithUserEmail,
|
||||
} from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
UnauthorizedException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { User } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -34,9 +32,7 @@ export class CalendarsService {
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly appsRepository: AppsRepository,
|
||||
private readonly calendarsRepository: CalendarsRepository,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
|
||||
) {}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
const origin = req.get("origin") ?? req.get("host");
|
||||
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
|
||||
}
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import { IntegrationLocation } from "@calcom/lib";
|
||||
|
||||
type BaseEventType = {
|
||||
length: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type EventTypeWithLocation = BaseEventType & {
|
||||
locations: IntegrationLocation[];
|
||||
};
|
||||
|
||||
const thirtyMinutes: BaseEventType = {
|
||||
length: 30,
|
||||
slug: "thirty-minutes",
|
||||
title: "30 Minutes",
|
||||
};
|
||||
|
||||
const thirtyMinutesVideo: EventTypeWithLocation = {
|
||||
length: 30,
|
||||
slug: "thirty-minutes-video",
|
||||
title: "30 Minutes",
|
||||
locations: [{ type: "integrations:daily" }],
|
||||
};
|
||||
|
||||
const sixtyMinutes: BaseEventType = {
|
||||
length: 60,
|
||||
slug: "sixty-minutes",
|
||||
title: "60 Minutes",
|
||||
};
|
||||
|
||||
const sixtyMinutesVideo: EventTypeWithLocation = {
|
||||
length: 60,
|
||||
slug: "sixty-minutes-video",
|
||||
title: "60 Minutes",
|
||||
locations: [{ type: "integrations:daily" }],
|
||||
};
|
||||
|
||||
export const DEFAULT_EVENT_TYPES = {
|
||||
thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" },
|
||||
thirtyMinutesVideo: {
|
||||
length: 30,
|
||||
slug: "thirty-minutes-video",
|
||||
title: "30 Minutes",
|
||||
locations: [{ type: "integrations:daily" }],
|
||||
},
|
||||
sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" },
|
||||
sixtyMinutesVideo: {
|
||||
length: 60,
|
||||
slug: "sixty-minutes-video",
|
||||
title: "60 Minutes",
|
||||
locations: [{ type: "integrations:daily" }],
|
||||
},
|
||||
thirtyMinutes,
|
||||
thirtyMinutesVideo,
|
||||
sixtyMinutes,
|
||||
sixtyMinutesVideo,
|
||||
};
|
||||
|
||||
+416
-32
@@ -20,7 +20,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
||||
import { BookingWindowPeriodInputTypeEnum_2024_06_14, FrequencyInput } from "@calcom/platform-enums";
|
||||
import {
|
||||
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
||||
BookerLayoutsInputEnum_2024_06_14,
|
||||
ConfirmationPolicyEnum,
|
||||
NoticeThresholdUnitEnum,
|
||||
FrequencyInput,
|
||||
} from "@calcom/platform-enums";
|
||||
import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
@@ -230,11 +236,36 @@ describe("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,
|
||||
},
|
||||
recurrence: {
|
||||
frequency: FrequencyInput.weekly,
|
||||
interval: 2,
|
||||
occurrences: 10,
|
||||
disabled: false,
|
||||
},
|
||||
requiresBookerEmailVerification: false,
|
||||
hideCalendarNotes: false,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
color: {
|
||||
darkThemeHex: "#292929",
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -257,7 +288,19 @@ describe("Event types Endpoints", () => {
|
||||
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
|
||||
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||
expect(createdEventType.bookerLayouts).toEqual(body.bookerLayouts);
|
||||
expect(createdEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
|
||||
expect(createdEventType.recurrence).toEqual(body.recurrence);
|
||||
expect(createdEventType.customName).toEqual(body.customName);
|
||||
expect(createdEventType.requiresBookerEmailVerification).toEqual(
|
||||
body.requiresBookerEmailVerification
|
||||
);
|
||||
|
||||
expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||
expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
body.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(createdEventType.color).toEqual(body.color);
|
||||
|
||||
const responseBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
@@ -272,6 +315,317 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
|
||||
const fetchedEventType = responseBody.data?.[0];
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
|
||||
eventType.requiresBookerEmailVerification
|
||||
);
|
||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 2",
|
||||
slug: "coding-class-2",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone: "+37120993151",
|
||||
public: true,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when trying to enable seats for an event type with multiple locations", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 3",
|
||||
slug: "coding-class-3",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone: "+37120993151",
|
||||
public: true,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
lengthInMinutes: body.lengthInMinutes,
|
||||
locations: body.locations,
|
||||
scheduleId: firstSchedule.id,
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with seats enabled and confirmationPolicy enabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 4",
|
||||
slug: "coding-class-4",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
scheduleId: firstSchedule.id,
|
||||
confirmationPolicy: {
|
||||
type: ConfirmationPolicyEnum.ALWAYS,
|
||||
blockUnconfirmedBookingsInBooker: false,
|
||||
},
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when trying to enable seats for an event type with confirmationPolicy enabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 5",
|
||||
slug: "coding-class-5",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
confirmationPolicy: {
|
||||
type: ConfirmationPolicyEnum.ALWAYS,
|
||||
blockUnconfirmedBookingsInBooker: false,
|
||||
},
|
||||
scheduleId: firstSchedule.id,
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
console.log("createdEventType: ", createdEventType);
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
lengthInMinutes: body.lengthInMinutes,
|
||||
confirmationPolicy: body.confirmationPolicy,
|
||||
scheduleId: firstSchedule.id,
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when trying to set multiple locations for an event type with seats enabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 6",
|
||||
slug: "coding-class-6",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
scheduleId: firstSchedule.id,
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
lengthInMinutes: body.lengthInMinutes,
|
||||
seats: body.seats,
|
||||
scheduleId: firstSchedule.id,
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone: "+37120993151",
|
||||
public: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with confirmationPolicy enabled and seats enabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 7",
|
||||
slug: "coding-class-7",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
confirmationPolicy: {
|
||||
type: ConfirmationPolicyEnum.ALWAYS,
|
||||
blockUnconfirmedBookingsInBooker: false,
|
||||
},
|
||||
scheduleId: firstSchedule.id,
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should return an error when trying to enable confirmationPolicy for an event type with seats enabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 8",
|
||||
slug: "coding-class-8",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
seats: {
|
||||
seatsPerTimeSlot: 4,
|
||||
showAttendeeInfo: true,
|
||||
showAvailabilityCount: true,
|
||||
},
|
||||
scheduleId: firstSchedule.id,
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const createdEventType = createResponse.body.data;
|
||||
|
||||
expect(createdEventType).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
lengthInMinutes: body.lengthInMinutes,
|
||||
seats: body.seats,
|
||||
scheduleId: firstSchedule.id,
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send({
|
||||
confirmationPolicy: {
|
||||
type: ConfirmationPolicyEnum.ALWAYS,
|
||||
blockUnconfirmedBookingsInBooker: false,
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const newTitle = "Coding class in Italian!";
|
||||
|
||||
@@ -293,11 +647,32 @@ describe("Event types Endpoints", () => {
|
||||
value: 40,
|
||||
rolling: false,
|
||||
},
|
||||
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.ALWAYS,
|
||||
blockUnconfirmedBookingsInBooker: false,
|
||||
},
|
||||
recurrence: {
|
||||
frequency: FrequencyInput.monthly,
|
||||
interval: 4,
|
||||
occurrences: 10,
|
||||
disabled: false,
|
||||
},
|
||||
requiresBookerEmailVerification: true,
|
||||
hideCalendarNotes: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
color: {
|
||||
darkThemeHex: "#292929",
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -323,7 +698,18 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
|
||||
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
|
||||
expect(updatedEventType.bookerLayouts).toEqual(body.bookerLayouts);
|
||||
expect(updatedEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
|
||||
expect(updatedEventType.recurrence).toEqual(body.recurrence);
|
||||
expect(updatedEventType.customName).toEqual(body.customName);
|
||||
expect(updatedEventType.requiresBookerEmailVerification).toEqual(
|
||||
body.requiresBookerEmailVerification
|
||||
);
|
||||
expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||
expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
body.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(updatedEventType.color).toEqual(body.color);
|
||||
|
||||
eventType.title = newTitle;
|
||||
eventType.scheduleId = secondSchedule.id;
|
||||
@@ -332,7 +718,14 @@ describe("Event types Endpoints", () => {
|
||||
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
||||
eventType.offsetStart = updatedEventType.offsetStart;
|
||||
eventType.bookingWindow = updatedEventType.bookingWindow;
|
||||
eventType.bookerLayouts = updatedEventType.bookerLayouts;
|
||||
eventType.confirmationPolicy = updatedEventType.confirmationPolicy;
|
||||
eventType.recurrence = updatedEventType.recurrence;
|
||||
eventType.customName = updatedEventType.customName;
|
||||
eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification;
|
||||
eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes;
|
||||
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
|
||||
eventType.color = updatedEventType.color;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,38 +766,18 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
|
||||
const fetchedEventType = responseBody.data?.[0];
|
||||
|
||||
expect(fetchedEventType?.id).toEqual(eventType.id);
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
|
||||
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
|
||||
eventType.requiresBookerEmailVerification
|
||||
);
|
||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and eventSlug`, async () => {
|
||||
@@ -430,7 +803,18 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
|
||||
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
|
||||
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
|
||||
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
|
||||
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
|
||||
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
|
||||
expect(fetchedEventType.customName).toEqual(eventType.customName);
|
||||
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
|
||||
eventType.requiresBookerEmailVerification
|
||||
);
|
||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it(`/GET/:id not existing`, async () => {
|
||||
|
||||
+27
-9
@@ -3,7 +3,9 @@ import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
|
||||
import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output";
|
||||
import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output";
|
||||
import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output";
|
||||
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
@@ -23,14 +25,15 @@ import {
|
||||
HttpStatus,
|
||||
Delete,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -45,7 +48,11 @@ import {
|
||||
required: true,
|
||||
})
|
||||
export class EventTypesController_2024_06_14 {
|
||||
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService_2024_06_14,
|
||||
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@@ -60,11 +67,16 @@ export class EventTypesController_2024_06_14 {
|
||||
@Body() body: CreateEventTypeInput_2024_06_14,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<CreateEventTypeOutput_2024_06_14> {
|
||||
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||
const transformedBody = await this.inputEventTypesService.transformAndValidateCreateEventTypeInput(
|
||||
user.id,
|
||||
body
|
||||
);
|
||||
|
||||
const eventType = await this.eventTypesService.createUserEventType(user, transformedBody);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventType),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +101,7 @@ export class EventTypesController_2024_06_14 {
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventType),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,7 +113,7 @@ export class EventTypesController_2024_06_14 {
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventTypes,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,15 +128,21 @@ export class EventTypesController_2024_06_14 {
|
||||
})
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeInput_2024_06_14,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<UpdateEventTypeOutput_2024_06_14> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
const transformedBody = await this.inputEventTypesService.transformAndValidateUpdateEventTypeInput(
|
||||
body,
|
||||
user.id,
|
||||
eventTypeId
|
||||
);
|
||||
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, transformedBody, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventType),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||
@@ -22,6 +27,11 @@ import { Module } from "@nestjs/common";
|
||||
UsersRepository,
|
||||
UsersService,
|
||||
SchedulesRepository_2024_06_11,
|
||||
EventTypeResponseTransformPipe,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
],
|
||||
controllers: [EventTypesController_2024_06_14],
|
||||
exports: [
|
||||
|
||||
@@ -4,36 +4,10 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
getEventTypeById,
|
||||
transformBookingFieldsApiToInternal,
|
||||
transformLocationsApiToInternal,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type InputEventTransformed = Omit<
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
| "lengthInMinutes"
|
||||
| "locations"
|
||||
| "bookingFields"
|
||||
| "bookingLimitsCount"
|
||||
| "onlyShowFirstAvailableSlot"
|
||||
| "bookingLimitsDuration"
|
||||
| "offsetStart"
|
||||
| "periodType"
|
||||
| "periodDays"
|
||||
| "periodCountCalendarDays"
|
||||
| "periodStartDate"
|
||||
| "periodEndDate"
|
||||
| "recurrence"
|
||||
> & {
|
||||
length: number;
|
||||
slug: string;
|
||||
locations?: ReturnType<typeof transformLocationsApiToInternal>;
|
||||
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesRepository_2024_06_14 {
|
||||
constructor(
|
||||
@@ -42,7 +16,10 @@ export class EventTypesRepository_2024_06_14 {
|
||||
private usersService: UsersService
|
||||
) {}
|
||||
|
||||
async createUserEventType(userId: number, body: InputEventTransformed) {
|
||||
async createUserEventType(
|
||||
userId: number,
|
||||
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar">
|
||||
) {
|
||||
return this.dbWrite.prisma.eventType.create({
|
||||
data: {
|
||||
...body,
|
||||
@@ -57,7 +34,19 @@ export class EventTypesRepository_2024_06_14 {
|
||||
async getEventTypeWithSeats(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { users: { select: { id: true } }, seatsPerTimeSlot: true },
|
||||
select: {
|
||||
users: { select: { id: true } },
|
||||
seatsPerTimeSlot: true,
|
||||
locations: true,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWithMetaData(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { metadata: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +56,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
id: eventTypeId,
|
||||
userId,
|
||||
},
|
||||
include: { users: true, schedule: true },
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,7 +65,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
include: { users: true, schedule: true },
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,7 +87,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
include: { users: true, schedule: true },
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,7 +106,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
slug: slug,
|
||||
},
|
||||
},
|
||||
include: { users: true, schedule: true },
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { Injectable, PipeTransform } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
import {
|
||||
DatabaseEventType,
|
||||
OutputEventTypesService_2024_06_14,
|
||||
} from "../services/output-event-types.service";
|
||||
|
||||
type EventTypeResponse = DatabaseEventType & { ownerId: number };
|
||||
|
||||
@Injectable()
|
||||
export class EventTypeResponseTransformPipe implements PipeTransform {
|
||||
constructor(private readonly outputEventTypesService: OutputEventTypesService_2024_06_14) {}
|
||||
|
||||
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
|
||||
return plainToClass(
|
||||
EventTypeOutput_2024_06_14,
|
||||
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
|
||||
{ strategy: "exposeAll" }
|
||||
);
|
||||
}
|
||||
|
||||
// Implementing function overloading to ensure correct return types based on input type:
|
||||
transform(value: EventTypeResponse[]): EventTypeOutput_2024_06_14[];
|
||||
|
||||
transform(value: EventTypeResponse): EventTypeOutput_2024_06_14;
|
||||
|
||||
transform(
|
||||
value: EventTypeResponse | EventTypeResponse[]
|
||||
): EventTypeOutput_2024_06_14 | EventTypeOutput_2024_06_14[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.transformEventType(item));
|
||||
} else {
|
||||
return this.transformEventType(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-49
@@ -1,7 +1,5 @@
|
||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
@@ -13,20 +11,13 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly usersService: UsersService,
|
||||
@@ -35,22 +26,13 @@ export class EventTypesService_2024_06_14 {
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11
|
||||
) {}
|
||||
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
|
||||
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const {
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
periodType = undefined,
|
||||
periodDays = undefined,
|
||||
periodCountCalendarDays = undefined,
|
||||
periodStartDate = undefined,
|
||||
periodEndDate = undefined,
|
||||
recurrence = undefined,
|
||||
...bodyTransformed
|
||||
} = this.inputEventTypesService.transformInputCreateEventType(body);
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: bodyTransformed,
|
||||
input: rest,
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -62,15 +44,7 @@ export class EventTypesService_2024_06_14 {
|
||||
await updateEventType({
|
||||
input: {
|
||||
id: eventTypeCreated.id,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
periodType,
|
||||
periodDays,
|
||||
periodCountCalendarDays,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
recurrence,
|
||||
...bodyTransformed,
|
||||
...body,
|
||||
},
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
@@ -86,10 +60,13 @@ export class EventTypesService_2024_06_14 {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`);
|
||||
}
|
||||
|
||||
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||
return {
|
||||
ownerId: user.id,
|
||||
...eventType,
|
||||
};
|
||||
}
|
||||
|
||||
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) {
|
||||
async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) {
|
||||
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||
if (existsWithSlug) {
|
||||
throw new BadRequestException("User already has an event type with this slug.");
|
||||
@@ -109,7 +86,10 @@ export class EventTypesService_2024_06_14 {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||
return {
|
||||
ownerId: user.id,
|
||||
...eventType,
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypesByUsername(username: string) {
|
||||
@@ -117,7 +97,7 @@ export class EventTypesService_2024_06_14 {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
return this.getUserEventTypes(user.id);
|
||||
return await this.getUserEventTypes(user.id);
|
||||
}
|
||||
|
||||
async getUserToCreateEvent(user: UserWithProfile) {
|
||||
@@ -147,17 +127,19 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(userId, eventType);
|
||||
return this.outputEventTypesService.getResponseEventType(userId, eventType);
|
||||
|
||||
return {
|
||||
ownerId: userId,
|
||||
...eventType,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserEventTypes(userId: number) {
|
||||
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
|
||||
|
||||
const eventTypePromises = eventTypes.map(async (eventType) => {
|
||||
return await this.outputEventTypesService.getResponseEventType(userId, eventType);
|
||||
return eventTypes.map((eventType) => {
|
||||
return { ownerId: userId, ...eventType };
|
||||
});
|
||||
|
||||
return await Promise.all(eventTypePromises);
|
||||
}
|
||||
|
||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||
@@ -190,7 +172,7 @@ export class EventTypesService_2024_06_14 {
|
||||
return await getEventTypesPublic(user.id);
|
||||
}
|
||||
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise<EventTypeOutput_2024_06_14[]> {
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
||||
const { username, eventSlug, usernames } = queryParams;
|
||||
|
||||
if (username && eventSlug) {
|
||||
@@ -218,12 +200,12 @@ export class EventTypesService_2024_06_14 {
|
||||
usersFiltered.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
return this.outputEventTypesService.getResponseEventType(0, {
|
||||
return {
|
||||
ownerId: 0,
|
||||
...dynamicEvent,
|
||||
users: usersFiltered,
|
||||
isInstantEvent: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async createUserDefaultEventTypes(userId: number) {
|
||||
@@ -239,12 +221,12 @@ export class EventTypesService_2024_06_14 {
|
||||
return defaultEventTypes;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) {
|
||||
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body);
|
||||
|
||||
await updateEventType({
|
||||
input: { id: eventTypeId, ...bodyTransformed },
|
||||
input: { id: eventTypeId, ...body },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -259,7 +241,10 @@ export class EventTypesService_2024_06_14 {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
|
||||
return {
|
||||
ownerId: user.id,
|
||||
...eventType,
|
||||
};
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
|
||||
|
||||
+259
-6
@@ -1,4 +1,8 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformBookingFieldsApiToInternal,
|
||||
@@ -9,12 +13,85 @@ import {
|
||||
systemBeforeFieldName,
|
||||
systemBeforeFieldEmail,
|
||||
systemAfterFieldRescheduleReason,
|
||||
EventTypeMetaDataSchema,
|
||||
systemBeforeFieldLocation,
|
||||
transformBookerLayoutsApiToInternal,
|
||||
transformConfirmationPolicyApiToInternal,
|
||||
transformEventColorsApiToInternal,
|
||||
validateCustomEventName,
|
||||
transformSeatsApiToInternal,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
DestinationCalendar_2024_06_14,
|
||||
InputEventTransformed_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
interface ValidationContext {
|
||||
eventTypeId?: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
locations?: InputEventTransformed_2024_06_14["locations"];
|
||||
requiresConfirmation?: boolean;
|
||||
eventName?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InputEventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
async transformAndValidateCreateEventTypeInput(
|
||||
userId: UserWithProfile["id"],
|
||||
inputEventType: CreateEventTypeInput_2024_06_14
|
||||
) {
|
||||
const transformedBody = this.transformInputCreateEventType(inputEventType);
|
||||
|
||||
await this.validateEventTypeInputs({
|
||||
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
|
||||
locations: transformedBody.locations,
|
||||
requiresConfirmation: transformedBody.requiresConfirmation,
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.validateInputUseDestinationCalendarEmail(userId));
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
|
||||
async transformAndValidateUpdateEventTypeInput(
|
||||
inputEventType: UpdateEventTypeInput_2024_06_14,
|
||||
userId: UserWithProfile["id"],
|
||||
eventTypeId: number
|
||||
) {
|
||||
const transformedBody = await this.transformInputUpdateEventType(inputEventType, eventTypeId);
|
||||
|
||||
await this.validateEventTypeInputs({
|
||||
eventTypeId: eventTypeId,
|
||||
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
|
||||
locations: transformedBody.locations,
|
||||
requiresConfirmation: transformedBody.requiresConfirmation,
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.validateInputUseDestinationCalendarEmail(userId));
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
|
||||
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
|
||||
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
|
||||
{
|
||||
@@ -30,9 +107,16 @@ export class InputEventTypesService_2024_06_14 {
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookerLayouts,
|
||||
confirmationPolicy,
|
||||
color,
|
||||
recurrence,
|
||||
seats,
|
||||
customName,
|
||||
useDestinationCalendarEmail,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
|
||||
const hasMultipleLocations = (locations || defaultLocations).length > 1;
|
||||
const eventType = {
|
||||
@@ -45,25 +129,47 @@ export class InputEventTypesService_2024_06_14 {
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
metadata: {
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
},
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
requiresConfirmationWillBlockSlot:
|
||||
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
|
||||
eventTypeColor: this.transformInputEventTypeColor(color),
|
||||
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||
...this.transformInputSeatOptions(seats),
|
||||
eventName: customName,
|
||||
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
|
||||
};
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
|
||||
async transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14, eventTypeId: number) {
|
||||
const {
|
||||
lengthInMinutes,
|
||||
locations,
|
||||
bookingFields,
|
||||
scheduleId,
|
||||
bookingLimitsCount,
|
||||
bookingLimitsDuration,
|
||||
bookingWindow,
|
||||
bookerLayouts,
|
||||
confirmationPolicy,
|
||||
color,
|
||||
recurrence,
|
||||
seats,
|
||||
customName,
|
||||
useDestinationCalendarEmail,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
|
||||
const metadataTransformed = !!eventTypeDb?.metadata
|
||||
? EventTypeMetaDataSchema.parse(eventTypeDb.metadata)
|
||||
: {};
|
||||
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
const hasMultipleLocations = !!(locations && locations?.length > 1);
|
||||
|
||||
const eventType = {
|
||||
@@ -73,13 +179,25 @@ export class InputEventTypesService_2024_06_14 {
|
||||
bookingFields: bookingFields
|
||||
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
|
||||
: undefined,
|
||||
schedule: scheduleId,
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
metadata: {
|
||||
...metadataTransformed,
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
},
|
||||
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
requiresConfirmationWillBlockSlot:
|
||||
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
|
||||
eventTypeColor: this.transformInputEventTypeColor(color),
|
||||
...this.transformInputSeatOptions(seats),
|
||||
eventName: customName,
|
||||
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
|
||||
};
|
||||
|
||||
return eventType;
|
||||
@@ -114,7 +232,142 @@ export class InputEventTypesService_2024_06_14 {
|
||||
return !!res ? res : {};
|
||||
}
|
||||
|
||||
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
|
||||
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
|
||||
}
|
||||
|
||||
transformInputConfirmationPolicy(
|
||||
requiresConfirmation: CreateEventTypeInput_2024_06_14["confirmationPolicy"]
|
||||
) {
|
||||
return transformConfirmationPolicyApiToInternal(requiresConfirmation);
|
||||
}
|
||||
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
|
||||
return transformRecurrenceApiToInternal(recurrence);
|
||||
}
|
||||
|
||||
transformInputEventTypeColor(color: CreateEventTypeInput_2024_06_14["color"]) {
|
||||
return transformEventColorsApiToInternal(color);
|
||||
}
|
||||
|
||||
transformInputSeatOptions(seats: CreateEventTypeInput_2024_06_14["seats"]) {
|
||||
return transformSeatsApiToInternal(seats);
|
||||
}
|
||||
async validateEventTypeInputs({
|
||||
eventTypeId,
|
||||
seatsPerTimeSlot,
|
||||
locations,
|
||||
requiresConfirmation,
|
||||
eventName,
|
||||
}: ValidationContext) {
|
||||
let seatsPerTimeSlotDb: number | null = null;
|
||||
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
|
||||
let requiresConfirmationDb = false;
|
||||
|
||||
if (eventTypeId != null) {
|
||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
|
||||
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
|
||||
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
|
||||
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
|
||||
}
|
||||
|
||||
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
|
||||
|
||||
const locationsFinal = locations !== undefined ? locations : locationsDb;
|
||||
const requiresConfirmationFinal =
|
||||
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
|
||||
|
||||
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
|
||||
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
|
||||
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
|
||||
this.validateRequiresConfirmationSeatsDisabledRule(requiresConfirmationFinal, seatsEnabledFinal);
|
||||
|
||||
if (eventName) {
|
||||
await this.validateCustomEventNameInput(eventName);
|
||||
}
|
||||
}
|
||||
validateSeatsSingleLocationRule(
|
||||
seatsEnabled: boolean,
|
||||
locations: ReturnType<typeof this.transformInputLocations>
|
||||
) {
|
||||
if (seatsEnabled && locations.length > 1) {
|
||||
throw new BadRequestException(
|
||||
"Seats Validation failed: Seats are enabled but more than one location provided."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
|
||||
if (seatsEnabled && requiresConfirmation) {
|
||||
throw new BadRequestException(
|
||||
"Seats Validation failed: Seats are enabled but requiresConfirmation is true."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateMultipleLocationsSeatsDisabledRule(
|
||||
locations: ReturnType<typeof this.transformInputLocations>,
|
||||
seatsEnabled: boolean
|
||||
) {
|
||||
if (locations.length > 1 && seatsEnabled) {
|
||||
throw new BadRequestException("Locations Validation failed: Multiple locations but seats are enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
validateRequiresConfirmationSeatsDisabledRule(requiresConfirmation: boolean, seatsEnabled: boolean) {
|
||||
if (requiresConfirmation && seatsEnabled) {
|
||||
throw new BadRequestException(
|
||||
"RequiresConfirmation Validation failed: Seats are enabled but requiresConfirmation is true."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async validateCustomEventNameInput(value: string) {
|
||||
const validationResult = validateCustomEventName(value);
|
||||
if (validationResult !== true) {
|
||||
throw new BadRequestException(`Invalid event name variables: ${validationResult}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async validateInputDestinationCalendar(
|
||||
userId: number,
|
||||
destinationCalendar: DestinationCalendar_2024_06_14
|
||||
) {
|
||||
const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId);
|
||||
|
||||
const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat();
|
||||
|
||||
const matchedCalendar = allCals.find(
|
||||
(cal) =>
|
||||
cal.externalId === destinationCalendar.externalId &&
|
||||
cal.integration === destinationCalendar.integration
|
||||
);
|
||||
|
||||
if (!matchedCalendar) {
|
||||
throw new BadRequestException("Invalid destinationCalendarId: Calendar does not exist");
|
||||
}
|
||||
|
||||
if (matchedCalendar.readOnly) {
|
||||
throw new BadRequestException("Invalid destinationCalendarId: Calendar does not have write permission");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async validateInputUseDestinationCalendarEmail(userId: number) {
|
||||
const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId);
|
||||
|
||||
const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat();
|
||||
|
||||
const primaryCalendar = allCals.find((cal) => cal.primary);
|
||||
|
||||
if (!primaryCalendar) {
|
||||
throw new BadRequestException(
|
||||
"Validation failed: A primary connected calendar is required to set useDestinationCalendarEmail"
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+98
-12
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule } from "@prisma/client";
|
||||
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
||||
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
@@ -15,11 +15,25 @@ import {
|
||||
transformIntervalLimitsInternalToApi,
|
||||
transformFutureBookingLimitsInternalToApi,
|
||||
transformRecurrenceInternalToApi,
|
||||
transformBookerLayoutsInternalToApi,
|
||||
transformRequiresConfirmationInternalToApi,
|
||||
transformEventTypeColorsInternalToApi,
|
||||
parseEventTypeColor,
|
||||
transformSeatsInternalToApi,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
NoticeThresholdTransformedSchema,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
type EventTypeRelations = {
|
||||
users: User[];
|
||||
schedule: Schedule | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
};
|
||||
export type DatabaseEventType = EventType & EventTypeRelations;
|
||||
|
||||
type Input = Pick<
|
||||
DatabaseEventType,
|
||||
@@ -58,11 +72,19 @@ type Input = Pick<
|
||||
| "periodCountCalendarDays"
|
||||
| "periodStartDate"
|
||||
| "periodEndDate"
|
||||
| "requiresBookerEmailVerification"
|
||||
| "hideCalendarNotes"
|
||||
| "eventTypeColor"
|
||||
| "seatsShowAttendees"
|
||||
| "requiresConfirmationWillBlockSlot"
|
||||
| "eventName"
|
||||
| "destinationCalendar"
|
||||
| "useEventTypeDestinationCalendarEmail"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class OutputEventTypesService_2024_06_14 {
|
||||
async getResponseEventType(ownerId: number, databaseEventType: Input) {
|
||||
getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 {
|
||||
const {
|
||||
id,
|
||||
length,
|
||||
@@ -74,8 +96,6 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
slug,
|
||||
schedulingType,
|
||||
requiresConfirmation,
|
||||
price,
|
||||
currency,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
@@ -87,17 +107,34 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
scheduleId,
|
||||
onlyShowFirstAvailableSlot,
|
||||
offsetStart,
|
||||
requiresBookerEmailVerification,
|
||||
hideCalendarNotes,
|
||||
seatsShowAttendees,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
const customName = databaseEventType?.eventName ?? undefined;
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||
: [];
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users);
|
||||
const users = this.transformUsers(databaseEventType.users || []);
|
||||
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
|
||||
const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits);
|
||||
const color = this.transformEventTypeColor(databaseEventType.eventTypeColor);
|
||||
const bookerLayouts = this.transformBookerLayouts(
|
||||
metadata.bookerLayouts as unknown as BookerLayoutsTransformedSchema
|
||||
);
|
||||
const confirmationPolicy = this.transformRequiresConfirmation(
|
||||
!!databaseEventType.requiresConfirmation,
|
||||
!!databaseEventType.requiresConfirmationWillBlockSlot,
|
||||
metadata.requiresConfirmationThreshold as NoticeThresholdTransformedSchema
|
||||
);
|
||||
delete metadata["bookerLayouts"];
|
||||
delete metadata["requiresConfirmationThreshold"];
|
||||
const seats = this.transformSeats(seatsPerTimeSlot, seatsShowAttendees, seatsShowAvailabilityCount);
|
||||
const bookingWindow = this.transformBookingWindow({
|
||||
periodType: databaseEventType.periodType,
|
||||
periodDays: databaseEventType.periodDays,
|
||||
@@ -105,6 +142,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
periodStartDate: databaseEventType.periodStartDate,
|
||||
periodEndDate: databaseEventType.periodEndDate,
|
||||
} as TransformFutureBookingsLimitSchema_2024_06_14);
|
||||
const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -121,16 +159,12 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
minimumBookingNotice,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
schedulingType,
|
||||
metadata,
|
||||
requiresConfirmation,
|
||||
price,
|
||||
currency,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
seatsPerTimeSlot,
|
||||
forwardParamsSuccessRedirect,
|
||||
successRedirectUrl,
|
||||
seatsShowAvailabilityCount,
|
||||
isInstantEvent,
|
||||
users,
|
||||
scheduleId,
|
||||
@@ -139,6 +173,15 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
onlyShowFirstAvailableSlot,
|
||||
offsetStart,
|
||||
bookingWindow,
|
||||
bookerLayouts,
|
||||
confirmationPolicy,
|
||||
requiresBookerEmailVerification,
|
||||
hideCalendarNotes,
|
||||
color,
|
||||
seats,
|
||||
customName,
|
||||
destinationCalendar,
|
||||
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,6 +190,14 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
|
||||
}
|
||||
|
||||
transformDestinationCalendar(destinationCalendar?: DestinationCalendar | null) {
|
||||
if (!destinationCalendar) return undefined;
|
||||
return {
|
||||
integration: destinationCalendar.integration,
|
||||
externalId: destinationCalendar.externalId,
|
||||
};
|
||||
}
|
||||
|
||||
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
|
||||
if (!bookingFields) return [];
|
||||
|
||||
@@ -189,4 +240,39 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
|
||||
return transformFutureBookingLimitsInternalToApi(bookingLimits);
|
||||
}
|
||||
|
||||
transformBookerLayouts(bookerLayouts: BookerLayoutsTransformedSchema) {
|
||||
if (!bookerLayouts) return undefined;
|
||||
return transformBookerLayoutsInternalToApi(bookerLayouts);
|
||||
}
|
||||
|
||||
transformRequiresConfirmation(
|
||||
requiresConfirmation: boolean,
|
||||
requiresConfirmationWillBlockSlot: boolean,
|
||||
requiresConfirmationThreshold?: NoticeThresholdTransformedSchema
|
||||
) {
|
||||
return transformRequiresConfirmationInternalToApi(
|
||||
requiresConfirmation,
|
||||
requiresConfirmationWillBlockSlot,
|
||||
requiresConfirmationThreshold
|
||||
);
|
||||
}
|
||||
|
||||
transformEventTypeColor(eventTypeColor: any) {
|
||||
if (!eventTypeColor) return undefined;
|
||||
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
|
||||
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
|
||||
}
|
||||
|
||||
transformSeats(
|
||||
seatsPerTimeSlot: number | null,
|
||||
seatsShowAttendees: boolean | null,
|
||||
seatsShowAvailabilityCount: boolean | null
|
||||
) {
|
||||
return transformSeatsInternalToApi({
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees: !!seatsShowAttendees,
|
||||
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+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