Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3788,6 +3788,10 @@
"configure_agent_to_handle_incoming_calls": "Configure agent to handle incoming calls",
"incoming_calls": "Incoming Calls",
"outgoing_calls": "Outgoing Calls",
"booking_fields_email_and_phone_both_hidden": "Both Email and Attendee Phone Number cannot be hidden",
"booking_fields_email_or_phone_required": "At least Email or Attendee Phone Number must be a required field",
"booking_fields_phone_required_when_email_hidden": "Attendee Phone Number must be required when Email is hidden",
"booking_fields_email_required_when_phone_hidden": "Email must be required when Attendee Phone Number is hidden",
"inbound_agent_setup_success": "Inbound agent setup successful",
"inbound_agent_configured": "Inbound agent configured",
"setup_inbound_agent": "Set up Inbound Agent",
Expand Down
60 changes: 60 additions & 0 deletions packages/features/bookings/lib/getBookingResponsesSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,66 @@ describe("getBookingResponsesSchema", () => {
);
});

test(`hidden required email field should not be validated`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone",
required: true,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "John",
email: "",
attendeePhoneNumber: "+919999999999",
});
expect(parsedResponses.success).toBe(true);
});

test(`hidden required phone field should not be validated`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
},
{
name: "attendeePhoneNumber",
type: "phone",
required: true,
hidden: true,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "John",
email: "john@example.com",
attendeePhoneNumber: "",
});
expect(parsedResponses.success).toBe(true);
});

test(`firstName is required and lastName is optional by default`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function preprocess<T extends z.ZodType>({
}

if (bookingField.type === "email") {
if (!bookingField.hidden && checkOptional ? true : bookingField.required) {
if (!bookingField.hidden && (checkOptional || bookingField.required)) {
// Email RegExp to validate if the input is a valid email
if (!emailSchema.safeParse(value).success) {
ctx.addIssue({
Expand Down
161 changes: 160 additions & 1 deletion packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";

import type { authedProcedure } from "../../../procedures/authedProcedure";
import { createEventPbacProcedure } from "../util";
import { createEventPbacProcedure, ensureEmailOrPhoneNumberIsPresent } from "../util";

// Mock dependencies
vi.mock("@calcom/features/pbac/services/permission-check.service");
Expand Down Expand Up @@ -513,4 +513,163 @@ describe("createEventPbacProcedure", () => {
);
});
});

describe("ensureEmailOrPhoneNumberIsPresent", () => {
it("should throw error when both email and phone are hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: true,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_and_phone_both_hidden",
})
);
});

it("should throw error when neither email nor phone is required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: false,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_or_phone_required",
})
);
});

it("should throw error when email is hidden and phone is not required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: false,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_phone_required_when_email_hidden",
})
);
});

it("should throw error when phone is hidden and email is not required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: true,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_required_when_phone_hidden",
})
);
});

it("should pass when email is visible and required while phone is hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: true,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});

it("should pass when phone is visible and required while email is hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: false,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});

it("should pass when both email and phone are visible and required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: false,
},
];

expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});
});
});
16 changes: 14 additions & 2 deletions packages/trpc/server/routers/viewer/eventTypes/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,25 @@ export function ensureEmailOrPhoneNumberIsPresent(fields: TUpdateInputSchema["bo
if (emailField?.hidden && attendeePhoneNumberField?.hidden) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Both Email and Attendee Phone Number cannot be hidden`,
message: "booking_fields_email_and_phone_both_hidden",
});
}
if (!emailField?.required && !attendeePhoneNumberField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `At least Email or Attendee Phone Number need to be required field.`,
message: "booking_fields_email_or_phone_required",
});
}
if (emailField?.hidden && !attendeePhoneNumberField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "booking_fields_phone_required_when_email_hidden",
});
}
if (attendeePhoneNumberField?.hidden && !emailField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "booking_fields_email_required_when_phone_hidden",
});
}
}
Expand Down
Loading