diff --git a/packages/features/bookings/lib/handleNewBooking/getBookingData.test.ts b/packages/features/bookings/lib/handleNewBooking/getBookingData.test.ts new file mode 100644 index 0000000000..f0019d509b --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getBookingData.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { z } from "zod"; + +import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations"; + +import { getBookingData } from "./getBookingData"; +import type { getEventTypeResponse } from "./getEventTypesFromDB"; + +// Mock the dependencies +vi.mock("@calcom/features/bookings/lib/getCalEventResponses", () => ({ + getCalEventResponses: vi.fn().mockReturnValue({ + userFieldsResponses: {}, + responses: {}, + }), +})); + +vi.mock("@calcom/lib/sentryWrapper", () => ({ + withReporting: (fn: Function) => fn, +})); + +const createMockEventType = (overrides: Partial = {}): getEventTypeResponse => { + return { + id: 1, + length: 30, + bookingFields: [ + { name: "name", type: "name", required: true }, + { name: "email", type: "email", required: true }, + { name: "location", type: "radioInput", required: false }, + ], + customInputs: [], + seatsPerTimeSlot: null, + ...overrides, + } as getEventTypeResponse; +}; + +const createMockSchema = () => { + return z.object({ + eventTypeId: z.number(), + start: z.string(), + end: z.string().optional(), + timeZone: z.string(), + language: z.string(), + metadata: z.record(z.string()), + responses: z.object({ + name: z.string(), + email: z.string(), + location: z + .object({ + value: z.string(), + optionValue: z.string().optional(), + }) + .optional(), + attendeePhoneNumber: z.string().optional(), + guests: z.array(z.string()).optional(), + smsReminderNumber: z.string().optional(), + notes: z.string().optional(), + rescheduleReason: z.string().optional(), + }), + }); +}; + +describe("getBookingData", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("location extraction", () => { + it("should ignore optionValue when location type is OrganizerDefaultConferencingAppType", async () => { + const mockEventType = createMockEventType(); + const schema = createMockSchema(); + + const reqBody = { + eventTypeId: 1, + start: "2024-01-15T10:00:00.000Z", + end: "2024-01-15T10:30:00.000Z", + timeZone: "America/New_York", + language: "en", + metadata: {}, + responses: { + name: "Test User", + email: "test@example.com", + location: { + value: OrganizerDefaultConferencingAppType, // "conferencing" + optionValue: "+15551234567", // Phone number that should be ignored + }, + }, + }; + + const result = await getBookingData({ + reqBody, + eventType: mockEventType, + schema, + }); + + // The location should be the conferencing type, NOT the phone number + expect(result.location).toBe(OrganizerDefaultConferencingAppType); + expect(result.location).not.toBe("+15551234567"); + }); + + it("should use optionValue for non-OrganizerDefaultConferencingAppType locations", async () => { + const mockEventType = createMockEventType(); + const schema = createMockSchema(); + + const reqBody = { + eventTypeId: 1, + start: "2024-01-15T10:00:00.000Z", + end: "2024-01-15T10:30:00.000Z", + timeZone: "America/New_York", + language: "en", + metadata: {}, + responses: { + name: "Test User", + email: "test@example.com", + location: { + value: "phone", + optionValue: "+15551234567", + }, + }, + }; + + const result = await getBookingData({ + reqBody, + eventType: mockEventType, + schema, + }); + + // For phone location type, optionValue should be used + expect(result.location).toBe("+15551234567"); + }); + + it("should fall back to value when optionValue is empty for non-conferencing locations", async () => { + const mockEventType = createMockEventType(); + const schema = createMockSchema(); + + const reqBody = { + eventTypeId: 1, + start: "2024-01-15T10:00:00.000Z", + end: "2024-01-15T10:30:00.000Z", + timeZone: "America/New_York", + language: "en", + metadata: {}, + responses: { + name: "Test User", + email: "test@example.com", + location: { + value: "integrations:daily", + optionValue: "", + }, + }, + }; + + const result = await getBookingData({ + reqBody, + eventType: mockEventType, + schema, + }); + + // Should fall back to value when optionValue is empty + expect(result.location).toBe("integrations:daily"); + }); + + it("should return empty string when location is not provided", async () => { + const mockEventType = createMockEventType(); + const schema = createMockSchema(); + + const reqBody = { + eventTypeId: 1, + start: "2024-01-15T10:00:00.000Z", + end: "2024-01-15T10:30:00.000Z", + timeZone: "America/New_York", + language: "en", + metadata: {}, + responses: { + name: "Test User", + email: "test@example.com", + // No location provided + }, + }; + + const result = await getBookingData({ + reqBody, + eventType: mockEventType, + schema, + }); + + expect(result.location).toBe(""); + }); + + it("should use OrganizerDefaultConferencingAppType value even when optionValue contains meeting URL", async () => { + const mockEventType = createMockEventType(); + const schema = createMockSchema(); + + const reqBody = { + eventTypeId: 1, + start: "2024-01-15T10:00:00.000Z", + end: "2024-01-15T10:30:00.000Z", + timeZone: "America/New_York", + language: "en", + metadata: {}, + responses: { + name: "Test User", + email: "test@example.com", + location: { + value: OrganizerDefaultConferencingAppType, + optionValue: "https://meet.google.com/abc-defg-hij", // Should be ignored + }, + }, + }; + + const result = await getBookingData({ + reqBody, + eventType: mockEventType, + schema, + }); + + // The location should be the conferencing type, NOT the URL + expect(result.location).toBe(OrganizerDefaultConferencingAppType); + }); + }); +}); diff --git a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts index c6fdea3bfd..d8bc341200 100644 --- a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts +++ b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts @@ -1,11 +1,10 @@ -import type z from "zod"; - +import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { withReporting } from "@calcom/lib/sentryWrapper"; import type { EventTypeCustomInput } from "@calcom/prisma/client"; - +import type z from "zod"; import { bookingCreateSchemaLegacyPropsForApi } from "../bookingCreateBodySchema"; import type { TgetBookingDataSchema } from "../getBookingDataSchema"; import type { getEventTypeResponse } from "./getEventTypesFromDB"; @@ -66,13 +65,24 @@ const _getBookingData = async ({ responses, seatsEnabled: !!eventType.seatsPerTimeSlot, }); + // Extract location value, but ignore optionValue when location is organizer's default app + let locationValue = ""; + if (responses.location) { + const locationType = responses.location.value; + if (locationType === OrganizerDefaultConferencingAppType) { + locationValue = locationType; + } else { + locationValue = responses.location.optionValue || locationType || ""; + } + } + return { ...parsedBody, name: responses.name, email: responses.email, attendeePhoneNumber: responses.attendeePhoneNumber, guests: responses.guests ? responses.guests : [], - location: responses.location?.optionValue || responses.location?.value || "", + location: locationValue, smsReminderNumber: responses.smsReminderNumber, notes: responses.notes || "", calEventUserFieldsResponses,