fix: v2 event types booking fields & enable booker atom google-meet location (#17321)
* fix: v2 phone system booking field * feat: atom support google-meet * feat: unknown booking field return type * tests * bump platform libraries * driveby: improve create-booking-input
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.52",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.53",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
+80
-1
@@ -915,7 +915,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle legacy event-types booking fields", () => {
|
||||
describe("Handle event-types booking fields", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
@@ -940,6 +940,7 @@ describe("Event types Endpoints", () => {
|
||||
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1092,6 +1093,21 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1207,6 +1223,21 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1234,6 +1265,54 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return event type with unknown bookingField", async () => {
|
||||
const unknownSystemField = {
|
||||
name: "unknown-whatever",
|
||||
type: "unknown-whatever",
|
||||
label: "your team",
|
||||
sources: [
|
||||
{
|
||||
id: "user",
|
||||
type: "user",
|
||||
label: "User",
|
||||
fieldRequired: true,
|
||||
},
|
||||
],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "FC Barcelona",
|
||||
};
|
||||
|
||||
const eventTypeInput = {
|
||||
title: "unknown field event type two",
|
||||
description: "unknown field event type description two",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "unknown-field-type-two",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [unknownSystemField],
|
||||
};
|
||||
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(unknownSystemField),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+25
-8
@@ -8,7 +8,6 @@ import {
|
||||
transformBookingFieldsInternalToApi,
|
||||
parseRecurringEvent,
|
||||
InternalLocationSchema,
|
||||
BookingFieldsSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
parseBookingLimit,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
parseEventTypeColor,
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
NoticeThresholdTransformedSchema,
|
||||
EventTypeOutput_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
OutputUnknownBookingField_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = {
|
||||
@@ -120,7 +121,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
const customName = databaseEventType?.eventName ?? undefined;
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: [];
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
@@ -196,14 +197,14 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
const parsedLocations = locations.map((location: any) => {
|
||||
for (const location of locations) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
return knownLocations.push(result.data);
|
||||
knownLocations.push(result.data);
|
||||
} else {
|
||||
return unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [...transformLocationsInternalToApi(knownLocations), ...unknownLocations];
|
||||
}
|
||||
@@ -216,10 +217,26 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
|
||||
transformBookingFields(bookingFields: any) {
|
||||
if (!bookingFields) return [];
|
||||
|
||||
return transformBookingFieldsInternalToApi(bookingFields);
|
||||
const knownBookingFields: (SystemField | CustomField)[] = [];
|
||||
const unknownBookingFields: OutputUnknownBookingField_2024_06_14[] = [];
|
||||
|
||||
for (const bookingField of bookingFields) {
|
||||
const result = BookingFieldSchema.safeParse(bookingField);
|
||||
if (result.success) {
|
||||
knownBookingFields.push(result.data);
|
||||
} else {
|
||||
unknownBookingFields.push({
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(bookingField),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
}
|
||||
|
||||
transformRecurringEvent(recurringEvent: any) {
|
||||
|
||||
@@ -5908,7 +5908,8 @@
|
||||
"type": "string",
|
||||
"example": "cal-video",
|
||||
"enum": [
|
||||
"cal-video"
|
||||
"cal-video",
|
||||
"google-meet"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -7067,7 +7068,7 @@
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialdId": {
|
||||
"credentialId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -12740,7 +12741,7 @@
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses.",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
"example": {
|
||||
"customField": "customValue"
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ import type {
|
||||
OrganizerPhoneLocation,
|
||||
} from "../internal/locations";
|
||||
|
||||
const apiToInternalintegrationsMapping: Record<Integration_2024_06_14, "integrations:daily"> = {
|
||||
const apiToInternalintegrationsMapping: Record<
|
||||
Integration_2024_06_14,
|
||||
"integrations:daily" | "integrations:google:meet"
|
||||
> = {
|
||||
"cal-video": "integrations:daily",
|
||||
"google-meet": "integrations:google:meet",
|
||||
};
|
||||
|
||||
export function transformLocationsApiToInternal(inputLocations: InputLocation_2024_06_14[] | undefined) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DefaultFieldOutput_2024_06_14,
|
||||
CustomFieldOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import type { OutputUnknownBookingField_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/outputs/booking-fields.output";
|
||||
|
||||
export function transformBookingFieldsInternalToApi(
|
||||
databaseBookingFields: (SystemField | CustomField)[]
|
||||
@@ -17,186 +18,201 @@ export function transformBookingFieldsInternalToApi(
|
||||
(field): field is CustomField => field.editable === "user"
|
||||
);
|
||||
|
||||
const responseDefaultFields: DefaultFieldOutput_2024_06_14[] = defaultFields.map((field) => {
|
||||
switch (field.name) {
|
||||
case "name":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "location":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "rescheduleReason":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "title":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "notes":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "guests":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
throw new Error(`Unsupported default booking field '${field.name}'.`);
|
||||
}
|
||||
});
|
||||
const responseDefaultFields: (DefaultFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
|
||||
defaultFields.map((field) => {
|
||||
switch (field.name) {
|
||||
case "name":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "location":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "rescheduleReason":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "title":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "notes":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "guests":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "attendeePhoneNumber":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(field),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const responseCustomFields: CustomFieldOutput_2024_06_14[] = customFields.map((field) => {
|
||||
switch (field.type) {
|
||||
case "phone":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "address":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "text":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "number":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "textarea":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiemail":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "boolean":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "select":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
options: field.options ? field.options.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiselect":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "checkbox":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "radio":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported booking field type '${field.type}'.`);
|
||||
}
|
||||
});
|
||||
const responseCustomFields: (CustomFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
|
||||
customFields.map((field) => {
|
||||
switch (field.type) {
|
||||
case "phone":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "address":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "text":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "number":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "textarea":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiemail":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "boolean":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "select":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
options: field.options ? field.options.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiselect":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "checkbox":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "radio":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(field),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return [...responseDefaultFields, ...responseCustomFields];
|
||||
}
|
||||
@@ -316,6 +332,12 @@ const NameSystemFieldSchema = SystemFieldSchema.extend({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const PhoneSystemFieldSchema = SystemFieldSchema.extend({
|
||||
name: z.literal("attendeePhoneNumber"),
|
||||
type: z.literal("phone"),
|
||||
required: z.boolean(),
|
||||
});
|
||||
|
||||
const EmailSystemFieldSchema = SystemFieldSchema.extend({
|
||||
name: z.literal("email"),
|
||||
type: z.literal("email"),
|
||||
@@ -359,10 +381,12 @@ type LocationReasonSystemField = z.infer<typeof LocationReasonSystemFieldSchema>
|
||||
type TitleSystemField = z.infer<typeof TitleSystemFieldSchema>;
|
||||
type NotesSystemField = z.infer<typeof NotesSystemFieldSchema>;
|
||||
type GuestsSystemField = z.infer<typeof GuestsSystemFieldSchema>;
|
||||
type PhoneSystemField = z.infer<typeof PhoneSystemFieldSchema>;
|
||||
|
||||
export type SystemField =
|
||||
| NameSystemField
|
||||
| EmailSystemField
|
||||
| PhoneSystemField
|
||||
| RescheduleReasonSystemField
|
||||
| LocationReasonSystemField
|
||||
| TitleSystemField
|
||||
@@ -374,6 +398,7 @@ export type CustomField = z.infer<typeof CustomFieldsSchema>;
|
||||
const SystemFieldsSchema = z.union([
|
||||
NameSystemFieldSchema,
|
||||
EmailSystemFieldSchema,
|
||||
PhoneSystemFieldSchema,
|
||||
LocationReasonSystemFieldSchema,
|
||||
RescheduleReasonSystemFieldSchema,
|
||||
TitleSystemFieldSchema,
|
||||
@@ -381,7 +406,8 @@ const SystemFieldsSchema = z.union([
|
||||
GuestsSystemFieldSchema,
|
||||
]);
|
||||
|
||||
export const BookingFieldsSchema = z.array(z.union([CustomFieldsSchema, SystemFieldsSchema]));
|
||||
export const BookingFieldSchema = z.union([CustomFieldsSchema, SystemFieldsSchema]);
|
||||
export const BookingFieldsSchema = z.array(BookingFieldSchema);
|
||||
|
||||
export const systemBeforeFieldName: NameSystemField = {
|
||||
type: "name",
|
||||
|
||||
@@ -776,6 +776,73 @@ describe("transformBookingFieldsInternalToApi", () => {
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform system phone field", () => {
|
||||
const transformedField: SystemField[] = [
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput = [
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformBookingFieldsInternalToApi(transformedField);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform unknown field", () => {
|
||||
const transformedField = [
|
||||
{
|
||||
name: "blabla",
|
||||
type: "blabla",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput = [
|
||||
{
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(transformedField[0]),
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = transformBookingFieldsInternalToApi(transformedField);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformIntervalLimitsInternalToApi", () => {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from "zod";
|
||||
|
||||
const integrationsApiAvailable = {
|
||||
"cal-video": z.literal("integrations:daily"),
|
||||
"google-meet": z.literal("integrations:google:meet"),
|
||||
};
|
||||
|
||||
// note(Lauris): these are read only aka they exist in database and are to be returned by a READ operation
|
||||
const integrationsApiUnavailable = {
|
||||
"google-meet": z.literal("integrations:google:meet"),
|
||||
zoom: z.literal("integrations:zoom"),
|
||||
"whereby-video": z.literal("integrations:whereby_video"),
|
||||
"whatsapp-video": z.literal("integrations:whatsapp_video"),
|
||||
|
||||
+4
-2
@@ -230,6 +230,8 @@ function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
|
||||
function isAtomSupportedLocation(
|
||||
location: EventTypeOutput_2024_06_14["locations"][number]
|
||||
): location is InputLocation_2024_06_14 {
|
||||
const supportedIntegrations = ["cal-video", "google-meet"];
|
||||
|
||||
return (
|
||||
location.type === "address" ||
|
||||
location.type === "attendeeAddress" ||
|
||||
@@ -237,7 +239,7 @@ function isAtomSupportedLocation(
|
||||
location.type === "phone" ||
|
||||
location.type === "attendeePhone" ||
|
||||
location.type === "attendeeDefined" ||
|
||||
(location.type === "integration" && location.integration === "cal-video")
|
||||
(location.type === "integration" && supportedIntegrations.includes(location.integration))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,7 +293,7 @@ function getBookingFields(
|
||||
function isCustomField(
|
||||
field: EventTypeOutput_2024_06_14["bookingFields"][number]
|
||||
): field is CustomFieldOutput_2024_06_14 {
|
||||
return !field.isDefault;
|
||||
return field.type !== "unknown" && !field.isDefault;
|
||||
}
|
||||
|
||||
function isDefaultEditableField(
|
||||
|
||||
@@ -129,6 +129,7 @@ export {
|
||||
InternalLocationsSchema,
|
||||
InternalLocationSchema,
|
||||
BookingFieldsSchema,
|
||||
BookingFieldSchema,
|
||||
// note(Lauris): constants
|
||||
systemBeforeFieldName,
|
||||
systemBeforeFieldEmail,
|
||||
|
||||
@@ -102,7 +102,7 @@ export class CreateBookingInputPipe implements PipeTransform {
|
||||
private isRecurringBookingInput(
|
||||
value: CreateBookingInput
|
||||
): value is CreateRecurringBookingInput_2024_08_13 {
|
||||
return value.hasOwnProperty("eventTypeId");
|
||||
return value.hasOwnProperty("recurrenceCount");
|
||||
}
|
||||
|
||||
private isInstantBookingInput(value: CreateBookingInput): value is CreateInstantBookingInput_2024_08_13 {
|
||||
|
||||
@@ -145,85 +145,7 @@ export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_202
|
||||
instant!: boolean;
|
||||
}
|
||||
|
||||
export class CreateRecurringBookingInput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "The start time of the booking in ISO 8601 format in UTC timezone.",
|
||||
example: "2024-08-13T09:00:00Z",
|
||||
})
|
||||
@IsDateString()
|
||||
start!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
description: "The ID of the event type that is booked.",
|
||||
example: 123,
|
||||
})
|
||||
@IsInt()
|
||||
eventTypeId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: Attendee,
|
||||
description: "The attendee's details.",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
attendee!: Attendee;
|
||||
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
description: "An optional list of guest emails attending the event.",
|
||||
example: ["guest1@example.com", "guest2@example.com"],
|
||||
required: false,
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
guests?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description:
|
||||
"Deprecated - use 'location' instead. Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.",
|
||||
example: "https://example.com/meeting",
|
||||
required: false,
|
||||
deprecated: true,
|
||||
})
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
meetingUrl?: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "Location for this booking. Displayed in email and calendar event.",
|
||||
example: "https://example.com/meeting",
|
||||
required: false,
|
||||
})
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
location?: string;
|
||||
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// @ApiProperty({
|
||||
// type: Object,
|
||||
// description: "Optional metadata for the booking.",
|
||||
// example: { key: "value" },
|
||||
// required: false,
|
||||
// })
|
||||
// @IsObject()
|
||||
// @IsOptional()
|
||||
// metadata!: Record<string, unknown>;
|
||||
|
||||
@ApiProperty({
|
||||
type: Object,
|
||||
description: "Booking field responses.",
|
||||
example: { customField: "customValue" },
|
||||
required: false,
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
bookingFieldsResponses?: Record<string, unknown>;
|
||||
|
||||
export class CreateRecurringBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 {
|
||||
@ApiPropertyOptional({
|
||||
type: Number,
|
||||
description: `The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than
|
||||
|
||||
+1
-1
@@ -521,7 +521,7 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
|
||||
boolean: BooleanFieldInput_2024_06_14,
|
||||
};
|
||||
|
||||
private reservedSystemSlugs = ["location", "rescheduleReason"];
|
||||
private reservedSystemSlugs = ["location", "rescheduleReason", "attendeePhoneNumber"];
|
||||
|
||||
async validate(bookingFields: { type: string; slug: string }[]) {
|
||||
if (!Array.isArray(bookingFields)) {
|
||||
|
||||
+4
-4
@@ -43,16 +43,16 @@ export class InputLinkLocation_2024_06_14 {
|
||||
public!: boolean;
|
||||
}
|
||||
|
||||
const integrations = ["cal-video"] as const;
|
||||
export type Integration_2024_06_14 = (typeof integrations)[number];
|
||||
export const supportedIntegrations = ["cal-video", "google-meet"] as const;
|
||||
export type Integration_2024_06_14 = (typeof supportedIntegrations)[number];
|
||||
|
||||
export class InputIntegrationLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "integration", description: "only allowed value for type is `integration`" })
|
||||
type!: "integration";
|
||||
|
||||
@IsIn(integrations)
|
||||
@DocsProperty({ example: integrations[0], enum: integrations })
|
||||
@IsIn(supportedIntegrations)
|
||||
@DocsProperty({ example: supportedIntegrations[0], enum: supportedIntegrations })
|
||||
integration!: Integration_2024_06_14;
|
||||
}
|
||||
|
||||
|
||||
+65
-6
@@ -205,6 +205,32 @@ export class GuestsDefaultFieldOutput_2024_06_14 {
|
||||
required!: false;
|
||||
}
|
||||
|
||||
export class PhoneDefaultFieldOutput_2024_06_14 {
|
||||
@IsBoolean()
|
||||
@DocsProperty({
|
||||
description: "This property is always true because it's a default field",
|
||||
example: true,
|
||||
default: true,
|
||||
})
|
||||
isDefault = true;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({
|
||||
default: "attendeePhoneNumber",
|
||||
})
|
||||
slug!: "attendeePhoneNumber";
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({
|
||||
default: "phone",
|
||||
})
|
||||
type!: "phone";
|
||||
|
||||
@IsBoolean()
|
||||
@DocsProperty()
|
||||
required!: boolean;
|
||||
}
|
||||
|
||||
export class PhoneFieldOutput_2024_06_14 extends PhoneFieldInput_2024_06_14 {
|
||||
@IsBoolean()
|
||||
@DocsProperty({
|
||||
@@ -315,6 +341,17 @@ export class BooleanFieldOutput_2024_06_14 extends BooleanFieldInput_2024_06_14
|
||||
isDefault = false;
|
||||
}
|
||||
|
||||
export class OutputUnknownBookingField_2024_06_14 {
|
||||
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
|
||||
type!: "unknown";
|
||||
|
||||
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
|
||||
slug!: "unknown";
|
||||
|
||||
@IsString()
|
||||
bookingField!: string;
|
||||
}
|
||||
|
||||
export type DefaultFieldOutput_2024_06_14 =
|
||||
| NameDefaultFieldOutput_2024_06_14
|
||||
| EmailDefaultFieldOutput_2024_06_14
|
||||
@@ -322,7 +359,8 @@ export type DefaultFieldOutput_2024_06_14 =
|
||||
| RescheduleReasonDefaultFieldOutput_2024_06_14
|
||||
| TitleDefaultFieldOutput_2024_06_14
|
||||
| NotesDefaultFieldOutput_2024_06_14
|
||||
| GuestsDefaultFieldOutput_2024_06_14;
|
||||
| GuestsDefaultFieldOutput_2024_06_14
|
||||
| PhoneDefaultFieldOutput_2024_06_14;
|
||||
|
||||
export type CustomFieldOutput_2024_06_14 =
|
||||
| PhoneFieldOutput_2024_06_14
|
||||
@@ -337,7 +375,10 @@ export type CustomFieldOutput_2024_06_14 =
|
||||
| RadioGroupFieldOutput_2024_06_14
|
||||
| BooleanFieldOutput_2024_06_14;
|
||||
|
||||
export type OutputBookingField_2024_06_14 = DefaultFieldOutput_2024_06_14 | CustomFieldOutput_2024_06_14;
|
||||
export type OutputBookingField_2024_06_14 =
|
||||
| DefaultFieldOutput_2024_06_14
|
||||
| CustomFieldOutput_2024_06_14
|
||||
| OutputUnknownBookingField_2024_06_14;
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
|
||||
@@ -349,6 +390,7 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
title: TitleDefaultFieldOutput_2024_06_14,
|
||||
notes: NotesDefaultFieldOutput_2024_06_14,
|
||||
guests: GuestsDefaultFieldOutput_2024_06_14,
|
||||
attendeePhoneNumber: PhoneDefaultFieldOutput_2024_06_14,
|
||||
};
|
||||
|
||||
private customOutputTypeMap: { [key: string]: new () => CustomFieldOutput_2024_06_14 } = {
|
||||
@@ -390,9 +432,13 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
|
||||
);
|
||||
}
|
||||
slugs.push(slug);
|
||||
if (slug !== "unknown") {
|
||||
slugs.push(slug);
|
||||
}
|
||||
|
||||
if (this.isDefaultField(field)) {
|
||||
if (this.isUnknownField(field)) {
|
||||
await this.validateUnknownField(field);
|
||||
} else if (this.isDefaultField(field)) {
|
||||
await this.validateDefaultField(field);
|
||||
} else {
|
||||
await this.validateCustomField(field);
|
||||
@@ -403,13 +449,26 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
}
|
||||
|
||||
isDefaultField(field: OutputBookingField_2024_06_14): field is DefaultFieldOutput_2024_06_14 {
|
||||
return field.isDefault === true;
|
||||
return field.type !== "unknown" && "isDefault" in field && field.isDefault === true;
|
||||
}
|
||||
|
||||
isUnknownField(field: OutputBookingField_2024_06_14): field is OutputUnknownBookingField_2024_06_14 {
|
||||
return field.type === "unknown";
|
||||
}
|
||||
|
||||
async validateUnknownField(field: OutputUnknownBookingField_2024_06_14) {
|
||||
const instance = plainToInstance(OutputUnknownBookingField_2024_06_14, field);
|
||||
const errors = await validate(instance);
|
||||
if (errors.length > 0) {
|
||||
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
|
||||
throw new BadRequestException(`Validation failed for ${field.slug} booking field: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async validateDefaultField(field: DefaultFieldOutput_2024_06_14) {
|
||||
const ClassType = this.defaultOutputNameMap[field.slug];
|
||||
if (!ClassType) {
|
||||
throw new BadRequestException(`Unsupported booking field slgu '${field.slug}'.`);
|
||||
throw new BadRequestException(`Unsupported default booking field slug '${field.slug}'.`);
|
||||
}
|
||||
|
||||
const instance = plainToInstance(ClassType, field);
|
||||
|
||||
Reference in New Issue
Block a user