From 280544205e531d97eb31380098690c0507783810 Mon Sep 17 00:00:00 2001 From: shikhar-8 Date: Thu, 27 Nov 2025 23:09:44 +0530 Subject: [PATCH 1/3] refactor: replace loggedInUsersTz with timeZone in busy-times API - Add new timeZone parameter to CalendarBusyTimesInput - Mark loggedInUsersTz as deprecated - Update getBusyTimes endpoint to prioritize timeZone parameter - Maintain backward compatibility with loggedInUsersTz - Add validation to ensure at least one timezone parameter is provided - Update API documentation with new parameter example Fixes #25423 --- .../controllers/calendars.controller.ts | 13 ++++++++--- .../calendars/inputs/busy-times.input.ts | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts index cd17f220bfe2e3..63ce10b9dae903 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts @@ -114,20 +114,27 @@ export class CalendarsController { @ApiOperation({ summary: "Get busy times", description: - "Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`", + "Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?timeZone=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`. Note: loggedInUsersTz is deprecated, use timeZone instead.", }) async getBusyTimes( @Query() queryParams: CalendarBusyTimesInput, @GetUser() user: UserWithProfile ): Promise { - const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams; + const { loggedInUsersTz, timeZone, dateFrom, dateTo, calendarsToLoad } = queryParams; + + // Prefer timeZone, fallback to loggedInUsersTz for backward compatibility + const timezone = timeZone || loggedInUsersTz; + + if (!timezone) { + throw new BadRequestException("Either timeZone or loggedInUsersTz must be provided"); + } const busyTimes = await this.calendarsService.getBusyTimes( calendarsToLoad, user.id, dateFrom, dateTo, - loggedInUsersTz + timezone ); return { diff --git a/packages/platform/types/calendars/inputs/busy-times.input.ts b/packages/platform/types/calendars/inputs/busy-times.input.ts index 4feb14ae622ba2..62fafc4359eb60 100644 --- a/packages/platform/types/calendars/inputs/busy-times.input.ts +++ b/packages/platform/types/calendars/inputs/busy-times.input.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { Transform } from "class-transformer"; -import { IsNumber, IsString, IsArray, ValidateNested, IsDateString } from "class-validator"; +import { IsNumber, IsString, IsArray, ValidateNested, IsDateString, IsOptional, ValidateIf } from "class-validator"; export class Calendar { @Transform(({ value }: { value: string }) => value && parseInt(value)) @@ -16,12 +16,27 @@ export class Calendar { export class CalendarBusyTimesInput { @ApiProperty({ - required: true, - description: "The timezone of the logged in user represented as a string", + required: false, + deprecated: true, + description: "Deprecated: Use timeZone instead. The timezone of the user represented as a string", + example: "America/New_York", + }) + @IsOptional() + @IsString() + loggedInUsersTz?: string; + + @ApiProperty({ + required: false, + description: "The timezone for the busy times query represented as a string", example: "America/New_York", }) + @IsOptional() + @IsString() + timeZone?: string; + + @ValidateIf((o) => !o.timeZone && !o.loggedInUsersTz) @IsString() - loggedInUsersTz!: string; + private readonly _timezoneValidation?: never; @ApiProperty({ required: false, From a4ac0f9149fd80e27f685f6510189022260694ff Mon Sep 17 00:00:00 2001 From: shikhar-8 Date: Thu, 27 Nov 2025 23:45:01 +0530 Subject: [PATCH 2/3] fix: improve timezone validation in CalendarBusyTimesInput - Use custom ValidatorConstraint instead of problematic ValidateIf - Properly validate that at least one timezone parameter is provided - Improve code quality and clarity --- .../types/calendars/inputs/busy-times.input.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/platform/types/calendars/inputs/busy-times.input.ts b/packages/platform/types/calendars/inputs/busy-times.input.ts index 62fafc4359eb60..96bf54cbd95a83 100644 --- a/packages/platform/types/calendars/inputs/busy-times.input.ts +++ b/packages/platform/types/calendars/inputs/busy-times.input.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { Transform } from "class-transformer"; -import { IsNumber, IsString, IsArray, ValidateNested, IsDateString, IsOptional, ValidateIf } from "class-validator"; +import { IsNumber, IsString, IsArray, ValidateNested, IsDateString, IsOptional, ValidatorConstraint, ValidatorConstraintInterface, Validate } from "class-validator"; export class Calendar { @Transform(({ value }: { value: string }) => value && parseInt(value)) @@ -14,6 +14,18 @@ export class Calendar { externalId!: string; } +@ValidatorConstraint({ name: "TimezoneRequired", async: false }) +export class TimezoneRequiredValidator implements ValidatorConstraintInterface { + validate(value: any) { + return !!value.timeZone || !!value.loggedInUsersTz; + } + + defaultMessage() { + return "Either timeZone or loggedInUsersTz must be provided"; + } +} + +@Validate(TimezoneRequiredValidator) export class CalendarBusyTimesInput { @ApiProperty({ required: false, @@ -34,10 +46,6 @@ export class CalendarBusyTimesInput { @IsString() timeZone?: string; - @ValidateIf((o) => !o.timeZone && !o.loggedInUsersTz) - @IsString() - private readonly _timezoneValidation?: never; - @ApiProperty({ required: false, description: "The starting date for the busy times query", From ff247d9966542a65f8e0820bbb7af4c1348389a7 Mon Sep 17 00:00:00 2001 From: shikhar-8 Date: Thu, 27 Nov 2025 18:38:33 +0000 Subject: [PATCH 3/3] Fix #25432: Preserve API-provided email when email field is hidden When email field is hidden and phone confirmation is enabled, preserve explicitly provided emails instead of overwriting them with auto-generated phone emails. Added unit test to verify the fix works. --- .../lib/getBookingResponsesSchema.test.ts | 35 +++++++++++++++++++ .../bookings/lib/getBookingResponsesSchema.ts | 3 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 249baee9697bda..405427fbcdbf49 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -206,6 +206,41 @@ describe("getBookingResponsesSchema", () => { expect(parsedResponses.success).toBe(true); }); + test(`hidden email field should preserve explicitly provided email from API`, 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: "john@example.com", + attendeePhoneNumber: "+919999999999", + }); + expect(parsedResponses.success).toBe(true); + if (!parsedResponses.success) { + throw new Error("Should not reach here"); + } + // When email is explicitly provided via API, it should be preserved even if the field is hidden + expect(parsedResponses.data.email).toBe("john@example.com"); + }); + test(`hidden required phone field should not be validated`, async () => { const schema = getBookingResponsesSchema({ bookingFields: [ diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index a637f45e5d6571..d1dd775a4daaf7 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -136,7 +136,8 @@ function preprocess({ const isEmailFieldHidden = !!emailField?.hidden; // To prevent using user's session email as attendee's email, we set email to empty string - if (isEmailFieldHidden && !isAttendeePhoneNumberFieldHidden) { + // But only if email wasn't explicitly provided (e.g., from API request) + if (isEmailFieldHidden && !isAttendeePhoneNumberFieldHidden && !responses["email"]) { responses["email"] = ""; }