feat: support phone based booking api (#17635)
* chore: save progress * feat: support phone based booking using API v2 * chore: change name of the variable * fix: update event types and booking fields for improved validation and new properties - Updated imports in input and output event types services to use the correct library version. - Added new `avatarUrl` and `successRedirectUrl` properties to the Swagger documentation for user and booking schemas. - Enhanced booking fields validation by changing the `required` property to a boolean type. - Improved descriptions and examples in the OpenAPI specifications for better clarity. - Refactored booking fields input and output classes to include new properties and ensure proper validation. * fix: booking with phone number * test: add event type update test * test: add test for creating booking * chore: update unit test * fix: tests * fix: add turbo * chore: remove emial * refactor: make email optional and move it to service * fix: test * chore: bump platform-libraries * fix: e2e test --------- Co-authored-by: supalarry <laurisskraucis@gmail.com>
This commit is contained in:
1 parent
20a5775f31
commit
c9c5493de6
19 files changed
+6318
-101
No files matched your search
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.79",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.81",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
let team1EventTypeId: number;
|
||||
let team2EventTypeId: number;
|
||||
let phoneOnlyEventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -205,6 +206,83 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
team1EventTypeId = team1EventType.id;
|
||||
|
||||
const phoneOnlyEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team1.id },
|
||||
},
|
||||
title: "Phone Only Event Type",
|
||||
slug: "phone-only-event-type",
|
||||
length: 15,
|
||||
assignAllTeamMembers: false,
|
||||
hosts: {
|
||||
connectOrCreate: [
|
||||
{
|
||||
where: {
|
||||
userId_eventTypeId: {
|
||||
userId: teamUser.id,
|
||||
eventTypeId: team1EventTypeId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: teamUser.id,
|
||||
isFixed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
label: "your name",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
variant: "fullName",
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "your_name",
|
||||
variantsConfig: {
|
||||
variants: {
|
||||
fullName: {
|
||||
fields: [{ name: "fullName", type: "text", label: "your name", required: true }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
label: "your email",
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system",
|
||||
required: false,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
label: "phone_number",
|
||||
sources: [{ id: "user", type: "user", label: "User", fieldRequired: true }],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [{ id: "reschedule", label: "Reschedule View" }],
|
||||
sources: [{ id: "default", type: "default", label: "Default" }],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
phoneOnlyEventTypeId = phoneOnlyEventType.id;
|
||||
|
||||
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
@@ -322,6 +400,60 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: "919876543210@sms.cal.com",
|
||||
phoneNumber: body.attendee.phoneNumber,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
@@ -398,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
});
|
||||
@@ -436,7 +568,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewBooking,
|
||||
handleNewRecurringBooking,
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
|
||||
@@ -160,9 +160,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
? {
|
||||
...inputBooking.bookingFieldsResponses,
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
}
|
||||
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email },
|
||||
: {
|
||||
name: inputBooking.attendee.name,
|
||||
email: inputBooking.attendee.email ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ type DatabaseBooking = Booking & {
|
||||
email: string;
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
phoneNumber?: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
@@ -121,6 +122,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location,
|
||||
|
||||
+1
@@ -128,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: this.getDefaultBookingFields(isOrgTeamEvent);
|
||||
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users || []);
|
||||
|
||||
+92
@@ -577,6 +577,98 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign all members to managed event-type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
assignAllTeamMembers: true,
|
||||
|
||||
@@ -7778,6 +7778,9 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7789,6 +7792,7 @@
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"required",
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
@@ -8488,6 +8492,10 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object",
|
||||
"default": true
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8504,16 +8512,13 @@
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "email"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"required",
|
||||
"isDefault",
|
||||
"slug",
|
||||
"required"
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"NameDefaultFieldOutput_2024_06_14": {
|
||||
@@ -10112,7 +10117,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -12538,7 +12543,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -14471,6 +14476,11 @@
|
||||
"description": "The time zone of the attendee.",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"description": "The phone number of the attendee in international format.",
|
||||
"example": "+919876543210"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -14524,7 +14534,6 @@
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"timeZone"
|
||||
]
|
||||
},
|
||||
@@ -15132,6 +15141,10 @@
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
|
||||
@@ -7206,6 +7206,9 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7214,7 +7217,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", "placeholder"]
|
||||
"required": ["type", "label", "required", "placeholder"]
|
||||
},
|
||||
"TitleDefaultFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -7858,6 +7861,10 @@
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"type": "object",
|
||||
"default": true
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7874,12 +7881,9 @@
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"default": "email"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["type", "isDefault", "slug", "required"]
|
||||
"required": ["type", "required", "isDefault", "slug"]
|
||||
},
|
||||
"NameDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -9307,7 +9311,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -11433,7 +11437,7 @@
|
||||
},
|
||||
"customName": {
|
||||
"type": "string",
|
||||
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"description": "Customizable event name with valid variables:\n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},\n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},\n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
|
||||
"example": "{Event type title} between {Organiser} and {Scheduler}"
|
||||
},
|
||||
"destinationCalendar": {
|
||||
@@ -13017,6 +13021,11 @@
|
||||
"description": "The time zone of the attendee.",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"description": "The phone number of the attendee in international format.",
|
||||
"example": "+919876543210"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -13068,7 +13077,7 @@
|
||||
"default": "en"
|
||||
}
|
||||
},
|
||||
"required": ["name", "email", "timeZone"]
|
||||
"required": ["name", "timeZone"]
|
||||
},
|
||||
"CreateBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
@@ -13628,6 +13637,10 @@
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
|
||||
@@ -162,7 +162,7 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
|
||||
if (bookingField.type === "email") {
|
||||
if (!bookingField.hidden) {
|
||||
if (!bookingField.hidden && bookingField.required) {
|
||||
// Email RegExp to validate if the input is a valid email
|
||||
if (!emailSchema.safeParse(value).success) {
|
||||
ctx.addIssue({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SMSLockState } from "@calcom/prisma/enums";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[twilioProvider]"] });
|
||||
|
||||
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE;
|
||||
const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE || process.env.IS_E2E;
|
||||
|
||||
function createTwilioClient() {
|
||||
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN && process.env.TWILIO_MESSAGING_SID) {
|
||||
|
||||
@@ -187,6 +187,7 @@ describe("transformBookingFieldsApiToInternal", () => {
|
||||
label: "Your email",
|
||||
placeholder: "bob@gmail.com",
|
||||
disableOnPrefill: true,
|
||||
required: true,
|
||||
};
|
||||
|
||||
const input: InputBookingField_2024_06_14[] = [bookingField];
|
||||
|
||||
@@ -113,6 +113,7 @@ function getBaseProperties(field: InputBookingField): CustomField | SystemField
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: !!field.disableOnPrefill,
|
||||
required: field.required,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,52 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import type { ValidationArguments, ValidationOptions } from "class-validator";
|
||||
import {
|
||||
IsInt,
|
||||
IsDateString,
|
||||
IsTimeZone,
|
||||
IsEnum,
|
||||
IsEmail,
|
||||
ValidateNested,
|
||||
IsArray,
|
||||
IsString,
|
||||
isEmail,
|
||||
IsOptional,
|
||||
IsUrl,
|
||||
IsObject,
|
||||
IsBoolean,
|
||||
Min,
|
||||
registerDecorator,
|
||||
Validate,
|
||||
} from "class-validator";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
|
||||
import type { BookingLanguageType } from "./language";
|
||||
import { BookingLanguage } from "./language";
|
||||
import { ValidateMetadata } from "./validators/validate-metadata";
|
||||
|
||||
function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
|
||||
return function (target: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "requireEmailOrPhone",
|
||||
target: target.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const obj = args.object as Attendee;
|
||||
|
||||
const hasPhoneNumber = !!obj.phoneNumber && obj.phoneNumber.trim().length > 0;
|
||||
const hasEmail = !!obj.email && obj.email.trim().length > 0;
|
||||
return hasPhoneNumber || hasEmail;
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return "At least one contact method (email or phone number) must be provided";
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
@@ -29,13 +56,17 @@ class Attendee {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "The email of the attendee.",
|
||||
example: "john.doe@example.com",
|
||||
})
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
@IsOptional()
|
||||
@Validate((value: string) => !value || isEmail(value), {
|
||||
message: "Invalid email format",
|
||||
})
|
||||
@RequireEmailOrPhone()
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
@@ -45,6 +76,17 @@ class Attendee {
|
||||
@IsTimeZone()
|
||||
timeZone!: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "The phone number of the attendee in international format.",
|
||||
example: "+919876543210",
|
||||
})
|
||||
@IsOptional()
|
||||
@Validate((value: string) => !value || isValidPhoneNumber(value), {
|
||||
message: "Invalid phone number format. Please use international format.",
|
||||
})
|
||||
phoneNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: BookingLanguage,
|
||||
description: "The preferred language of the attendee. Used for booking confirmation.",
|
||||
|
||||
@@ -44,6 +44,12 @@ class Attendee {
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
absent!: boolean;
|
||||
|
||||
@ApiPropertyOptional({ type: String, example: "+1234567890" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
export class SeatedAttendee extends Attendee {
|
||||
|
||||
+5
@@ -66,6 +66,11 @@ export class EmailDefaultFieldInput_2024_06_14 {
|
||||
@DocsProperty()
|
||||
label?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@DocsProperty()
|
||||
required = true;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@DocsProperty()
|
||||
|
||||
+3
-3
@@ -379,9 +379,9 @@ export class UpdateEventTypeInput_2024_06_14 {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsPropertyOptional({
|
||||
description: `Customizable event name with valid variables:
|
||||
{Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},
|
||||
{Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},
|
||||
description: `Customizable event name with valid variables:
|
||||
{Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},
|
||||
{Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},
|
||||
{HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}`,
|
||||
example: "{Event type title} between {Organiser} and {Scheduler}",
|
||||
})
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
"HUBSPOT_CLIENT_ID",
|
||||
"HUBSPOT_CLIENT_SECRET",
|
||||
"INTEGRATION_TEST_MODE",
|
||||
"IS_E2E",
|
||||
"INTERCOM_SECRET",
|
||||
"INTERCOM_SECRET",
|
||||
"INSIGHTS_DATABASE_URL",
|
||||
|
||||
Reference in new issue
Block a user