feat: v2 api phone based organization team event types (#20134)

* refactor: input event types service remove unused code

* feat: toggle if email field is hidden

* chore: ensure event type has email or attendee phone field required

* chore: ensure non-org team cant have phone only event type

* chore: ensure users cant have phone only event type

* chore: ensure org team can have phone only event type

* chore: bump libraries

* docs

* refactor: confirm failing test response message

* fix: unit test

* chore: default organizer booking location docs

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Lauris SkraucisandMorgan authored and GitHub committed 2025-03-18 11:34:35 +02:00
1 parent e9f20b1a0b
commit 94a26031a8
21 files changed
+6243 -110

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.151",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
};
const defaultResponseBookingFieldLocation = {
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
.expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should create an event type", async () => {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "radioInput",
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
@@ -14,6 +14,8 @@ import {
updateEventType,
getEventTypesPublic,
EventTypesPublic,
SystemField,
CustomField,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
if (!isEmailFieldRequiredAndVisible) {
throw new BadRequestException(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -37,8 +37,6 @@ import {
} from "@calcom/platform-types";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
interface ValidationContext {
eventTypeId?: number;
seatsPerTimeSlot?: number | null;
@@ -51,7 +49,6 @@ interface ValidationContext {
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
) {}
@@ -128,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -182,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
: {};
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -220,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
@@ -252,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
throw new BadRequestException(
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
);
}
return bookingFields;
}
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const attendeePhoneNumberField = bookingFields.find(
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
);
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
const isAttendeePhoneNumberFieldRequiredAndVisible =
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
@@ -81,7 +81,7 @@ export class OrganizationsEventTypesController {
bodyEventType
);
const eventType = await this.organizationsEventTypesService.createTeamEventType(
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
user,
teamId,
orgId,
@@ -212,7 +212,7 @@ export class OrganizationsEventTypesController {
bodyEventType
);
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType(
eventTypeId,
teamId,
transformedBody,
@@ -581,6 +581,123 @@ describe("Organizations Event Types Endpoints", () => {
});
});
it("should be able to create phone-only event type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "organizersDefaultApp",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teammate1.id,
mandatory: true,
priority: "high",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
expect(data.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: false,
label: "Email",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: true,
hidden: false,
label: "Phone number",
disableOnPrefill: false,
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
it("should be able to configure phone-only event type", async () => {
const body: UpdateTeamEventTypeInput_2024_06_14 = {
bookingFields: [
@@ -588,13 +705,14 @@ describe("Organizations Event Types Endpoints", () => {
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: true,
hidden: false,
},
],
};
@@ -622,6 +740,7 @@ describe("Organizations Event Types Endpoints", () => {
required: false,
label: "Email",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
@@ -635,7 +754,7 @@ describe("Organizations Event Types Endpoints", () => {
type: "phone",
slug: "attendeePhoneNumber",
required: true,
hidden: true,
hidden: false,
label: "Phone number",
disableOnPrefill: false,
},
@@ -776,7 +895,14 @@ describe("Organizations Event Types Endpoints", () => {
expect(fetchedEventType.bookingFields).toEqual([
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
disableOnPrefill: false,
isDefault: true,
@@ -26,7 +26,7 @@ export class OrganizationsEventTypesService {
private readonly usersService: UsersService
) {}
async createTeamEventType(
async createOrganizationTeamEventType(
user: UserWithProfile,
teamId: number,
orgId: number,
@@ -48,7 +48,7 @@ export class OrganizationsEventTypesService {
},
});
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
}
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
@@ -91,13 +91,13 @@ export class OrganizationsEventTypesService {
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
}
async updateTeamEventType(
async updateOrganizationTeamEventType(
eventTypeId: number,
teamId: number,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true);
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
@@ -167,6 +167,56 @@ describe("Organizations Event Types Endpoints", () => {
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "organizersDefaultApp",
},
],
schedulingType: "COLLECTIVE",
hosts: [
{
userId: teamMember1.id,
},
{
userId: teamMember2.id,
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post(`/v2/teams/${team.id}/event-types`)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should create a collective team event-type", async () => {
const body: CreateTeamEventTypeInput_2024_06_14 = {
title: `teams-event-types-collective-${randomString()}`,
@@ -181,7 +181,8 @@ export class TeamsEventTypesController {
eventTypeId,
teamId,
transformedBody,
user
user,
false
);
return {
@@ -30,6 +30,10 @@ export class TeamsEventTypesService {
teamId: number,
body: TransformedCreateTeamEventTypeInput
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
if (body.bookingFields) {
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
@@ -44,7 +48,7 @@ export class TeamsEventTypesService {
},
});
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user, false);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
@@ -105,8 +109,14 @@ export class TeamsEventTypesService {
eventTypeId: number,
teamId: number,
body: TransformedUpdateTeamEventTypeInput,
user: UserWithProfile
user: UserWithProfile,
// note(Lauris): once phone only event types / bookings are enabled for simple users remove isOrg parameter (right now only organization team event types support hidden / non-required email field)
isOrg: boolean
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
if (!isOrg && body.bookingFields) {
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.validateEventTypeExists(teamId, eventTypeId);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
await updateEventType({
+56 -6
View File
@@ -3756,6 +3756,26 @@
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter bookings that have been created after this date string.",
"example": "?afterCreatedAt=2025-03-07T10:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter bookings that have been created before this date string.",
"example": "?beforeCreatedAt=2025-03-14T11:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
@@ -8251,7 +8271,7 @@
},
"slug": {
"type": "string",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
"example": "some-slug"
},
"label": {
@@ -9097,7 +9117,12 @@
"type": "string"
},
"required": {
"type": "object"
"type": "object",
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"hidden": {
"type": "boolean",
"description": "Can be set to true only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"placeholder": {
"type": "string"
@@ -9110,7 +9135,6 @@
"required": [
"type",
"label",
"required",
"placeholder"
]
},
@@ -9833,7 +9857,12 @@
},
"required": {
"type": "object",
"default": true
"default": true,
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both. Can only be hidden\n for organization team event types when also providing attendee phone number booking field."
},
"placeholder": {
"type": "string"
@@ -9855,7 +9884,6 @@
},
"required": [
"type",
"required",
"isDefault",
"slug"
]
@@ -10633,7 +10661,7 @@
},
"slug": {
"type": "string",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
"example": "some-slug"
},
"label": {
@@ -16468,6 +16496,19 @@
"type"
]
},
"BookingInputOrganizersDefaultAppLocation_2024_08_13": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "organizersDefaultApp",
"description": "only available for team event types and the only allowed value for type is `organizersDefaultApp` - it refers to the default app defined by the organizer."
}
},
"required": [
"type"
]
},
"ValidateBookingLocation_2024_08_13": {
"type": "object",
"properties": {}
@@ -16617,6 +16658,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
@@ -16707,6 +16751,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
@@ -16803,6 +16850,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
+56 -6
View File
@@ -3552,6 +3552,26 @@
"type": "string"
}
},
{
"name": "afterCreatedAt",
"required": false,
"in": "query",
"description": "Filter bookings that have been created after this date string.",
"example": "?afterCreatedAt=2025-03-07T10:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "beforeCreatedAt",
"required": false,
"in": "query",
"description": "Filter bookings that have been created before this date string.",
"example": "?beforeCreatedAt=2025-03-14T11:00:00.000Z",
"schema": {
"type": "string"
}
},
{
"name": "afterUpdatedAt",
"required": false,
@@ -7731,7 +7751,7 @@
},
"slug": {
"type": "string",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
"example": "some-slug"
},
"label": {
@@ -8424,7 +8444,12 @@
"type": "string"
},
"required": {
"type": "object"
"type": "object",
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"hidden": {
"type": "boolean",
"description": "Can be set to true only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"placeholder": {
"type": "string"
@@ -8434,7 +8459,7 @@
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&email=bob@gmail.com`, the email field will be prefilled with this value and disabled."
}
},
"required": ["type", "label", "required", "placeholder"]
"required": ["type", "label", "placeholder"]
},
"TitleDefaultFieldInput_2024_06_14": {
"type": "object",
@@ -9100,7 +9125,12 @@
},
"required": {
"type": "object",
"default": true
"default": true,
"description": "Can be set to false only for organization team event types and if you also pass booking field {type: \"phone\", slug: \"attendeePhoneNumber\", required: true, hidden: false, label: \"whatever label\"} of booking field type PhoneFieldInput_2024_06_14 - this is done\n to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.\n If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both. Can only be hidden\n for organization team event types when also providing attendee phone number booking field."
},
"placeholder": {
"type": "string"
@@ -9120,7 +9150,7 @@
"default": "email"
}
},
"required": ["type", "required", "isDefault", "slug"]
"required": ["type", "isDefault", "slug"]
},
"NameDefaultFieldOutput_2024_06_14": {
"type": "object",
@@ -9787,7 +9817,7 @@
},
"slug": {
"type": "string",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
"description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking. Special slug is `attendeePhoneNumber` - if you create\n a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {\"type\": \"email\", \"required\": false, \"hidden\": true} to the email booking field input in the request body.",
"example": "some-slug"
},
"label": {
@@ -14829,6 +14859,17 @@
},
"required": ["type"]
},
"BookingInputOrganizersDefaultAppLocation_2024_08_13": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "organizersDefaultApp",
"description": "only available for team event types and the only allowed value for type is `organizersDefaultApp` - it refers to the default app defined by the organizer."
}
},
"required": ["type"]
},
"ValidateBookingLocation_2024_08_13": {
"type": "object",
"properties": {}
@@ -14972,6 +15013,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
@@ -15055,6 +15099,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
@@ -15143,6 +15190,9 @@
},
{
"$ref": "#/components/schemas/BookingInputPhoneLocation_2024_08_13"
},
{
"$ref": "#/components/schemas/BookingInputOrganizersDefaultAppLocation_2024_08_13"
}
]
},
@@ -214,6 +214,7 @@ describe("transformBookingFieldsApiToInternal", () => {
label: "Your email",
placeholder: "bob@gmail.com",
disableOnPrefill: true,
hidden: false,
},
];
@@ -147,6 +147,7 @@ function getBaseProperties(field: InputBookingField): CustomField | SystemField
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
required: field.required,
hidden: !!field.hidden,
};
}
@@ -290,7 +291,9 @@ function fieldIsCustomSystemRescheduleReason(
}
function fieldIsDefaultAttendeePhone(field: InputBookingField): field is PhoneDefaultFieldOutput_2024_06_14 {
return "slug" in field && field.slug === "attendeePhoneNumber";
const isPhone = "type" in field && field.type === "phone";
const isAttendeePhoneNumber = "slug" in field && field.slug === "attendeePhoneNumber";
return isPhone && isAttendeePhoneNumber;
}
function fieldIsDefaultSystemLocation(
@@ -73,6 +73,7 @@ export function transformBookingFieldsInternalToApi(
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: !!field.disableOnPrefill,
hidden: !!field.hidden,
} satisfies EmailDefaultFieldOutput_2024_06_14;
case "location":
return {
@@ -358,6 +358,7 @@ describe("transformBookingFieldsInternalToApi", () => {
disableOnPrefill: false,
label: undefined,
placeholder: undefined,
hidden: false,
},
];
@@ -385,6 +386,7 @@ describe("transformBookingFieldsInternalToApi", () => {
placeholder: "custom placeholder",
disableOnPrefill: true,
label: "custom label",
hidden: false,
},
];
@@ -31,6 +31,7 @@ import {
BookingInputIntegrationLocation_2024_08_13,
BookingInputLinkLocation_2024_08_13,
BookingInputPhoneLocation_2024_08_13,
BookingInputOrganizersDefaultAppLocation_2024_08_13,
ValidateBookingLocation_2024_08_13,
} from "./location.input";
import { ValidateMetadata } from "./validators/validate-metadata";
@@ -117,6 +118,7 @@ class Attendee {
BookingInputIntegrationLocation_2024_08_13,
BookingInputLinkLocation_2024_08_13,
BookingInputPhoneLocation_2024_08_13,
BookingInputOrganizersDefaultAppLocation_2024_08_13,
ValidateBookingLocation_2024_08_13
)
export class CreateBookingInput_2024_08_13 {
@@ -189,6 +191,7 @@ export class CreateBookingInput_2024_08_13 {
{ $ref: getSchemaPath(BookingInputIntegrationLocation_2024_08_13) },
{ $ref: getSchemaPath(BookingInputLinkLocation_2024_08_13) },
{ $ref: getSchemaPath(BookingInputPhoneLocation_2024_08_13) },
{ $ref: getSchemaPath(BookingInputOrganizersDefaultAppLocation_2024_08_13) },
],
})
@Type(() => Object)
@@ -91,7 +91,7 @@ export class BookingInputOrganizersDefaultAppLocation_2024_08_13 {
@DocsProperty({
example: "organizersDefaultApp",
description:
"only allowed value for type is `organizersDefaultApp` - it refers to the default app defined by the organizer.",
"only available for team event types and the only allowed value for type is `organizersDefaultApp` - it refers to the default app defined by the organizer.",
})
type!: "organizersDefaultApp";
}
@@ -116,9 +116,22 @@ export class EmailDefaultFieldInput_2024_06_14 {
@IsBoolean()
@IsOptional()
@DocsProperty()
@DocsPropertyOptional({
description: `Can be set to false only for organization team event types and if you also pass booking field {type: "phone", slug: "attendeePhoneNumber", required: true, hidden: false, label: "whatever label"} of booking field type PhoneFieldInput_2024_06_14 - this is done
to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.
If true show under event type settings but don't show this booking field in the Booker. If false show in both.`,
})
required = true;
@IsBoolean()
@IsOptional()
@DocsPropertyOptional({
description: `Can be set to true only for organization team event types and if you also pass booking field {type: "phone", slug: "attendeePhoneNumber", required: true, hidden: false, label: "whatever label"} of booking field type PhoneFieldInput_2024_06_14 - this is done
to enable phone only bookings where during the booking attendee can provide only their phone number and not provide email, so you must pass to the email booking field {hidden: true, required: false}.
If true show under event type settings but don't show this booking field in the Booker. If false show in both.`,
})
hidden?: boolean;
@IsString()
@IsOptional()
@DocsProperty()
@@ -306,8 +319,8 @@ export class PhoneFieldInput_2024_06_14 {
@IsString()
@DocsProperty({
description:
"Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking",
description: `Unique identifier for the field in format \`some-slug\`. It is used to access response to this booking field during the booking. Special slug is \`attendeePhoneNumber\` - if you create
a phone input field with this slug for organization team event type you can create an organization team event type that can be booked using phone without requiring an email by setting {"type": "email", "required": false, "hidden": true} to the email booking field input in the request body.`,
example: "some-slug",
})
slug!: string;
@@ -99,6 +99,13 @@ export class EmailDefaultFieldOutput_2024_06_14 extends EmailDefaultFieldInput_2
@IsBoolean()
@DocsProperty()
required!: boolean;
@IsBoolean()
@DocsProperty({
description: `If true show under event type settings but don't show this booking field in the Booker. If false show in both. Can only be hidden
for organization team event types when also providing attendee phone number booking field.`,
})
hidden!: boolean;
}
export class LocationDefaultFieldOutput_2024_06_14 {
+5803 -65
View File
File diff suppressed because it is too large. Load diff