diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 4f1e6f4508de93..b83e5fc0c4be26 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 556356dd7919e3..249baee9697bda 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -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 & 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 & 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: [ diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index d3e61a883dde0e..d7269224551e10 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -174,7 +174,7 @@ function preprocess({ } 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({ diff --git a/packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts b/packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts index 1bf45f4e5b008e..38d27edc0d9889 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts @@ -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"); @@ -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(); + }); + }); }); diff --git a/packages/trpc/server/routers/viewer/eventTypes/util.ts b/packages/trpc/server/routers/viewer/eventTypes/util.ts index 9f459128e74e6b..aba7dab84be33c 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/util.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/util.ts @@ -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", }); } }