Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetBusyTimesOutput> {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "John",
email: "[email protected]",
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("[email protected]");
});

test(`hidden required phone field should not be validated`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ function preprocess<T extends z.ZodType>({
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"] = "";
}

Expand Down
31 changes: 27 additions & 4 deletions packages/platform/types/calendars/inputs/busy-times.input.ts
Original file line number Diff line number Diff line change
@@ -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, ValidatorConstraint, ValidatorConstraintInterface, Validate } from "class-validator";

export class Calendar {
@Transform(({ value }: { value: string }) => value && parseInt(value))
Expand All @@ -14,14 +14,37 @@ 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)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Validate is a property decorator; applying it to the class prevents the TimezoneRequiredValidator from ever running, so both timezone fields become optional and requests without any timezone now pass validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform/types/calendars/inputs/busy-times.input.ts, line 28:

<comment>`@Validate` is a property decorator; applying it to the class prevents the TimezoneRequiredValidator from ever running, so both timezone fields become optional and requests without any timezone now pass validation.</comment>

<file context>
@@ -14,14 +14,37 @@ export class Calendar {
+  }
+}
+
+@Validate(TimezoneRequiredValidator)
 export class CalendarBusyTimesInput {
   @ApiProperty({
</file context>
Fix with Cubic

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()
loggedInUsersTz!: string;
timeZone?: string;

@ApiProperty({
required: false,
Expand Down
Loading