diff --git a/packages/trpc/server/routers/viewer/eventTypes/__tests__/types.test.ts b/packages/trpc/server/routers/viewer/eventTypes/__tests__/types.test.ts new file mode 100644 index 0000000000..c8aa975b70 --- /dev/null +++ b/packages/trpc/server/routers/viewer/eventTypes/__tests__/types.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect } from "vitest"; + +import { ZUpdateInputSchema } from "../types"; + +describe("ZUpdateInputSchema", () => { + describe("aiPhoneCallConfig transformation", () => { + const baseValidInput = { + id: 1, + }; + + const baseAiPhoneCallConfig = { + generalPrompt: "Test prompt", + enabled: true, + beginMessage: "Hello", + yourPhoneNumber: "+1234567890", + numberToCall: "+0987654321", + templateType: "CUSTOM_TEMPLATE" as const, + }; + + it("should pass validation with valid aiPhoneCallConfig", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: baseAiPhoneCallConfig, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("should set yourPhoneNumber to empty string when falsy", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + yourPhoneNumber: "", + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.yourPhoneNumber).toBe(""); + } + }); + + it("should set numberToCall to empty string when falsy", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + numberToCall: "", + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.numberToCall).toBe(""); + } + }); + + it("should set guestName to undefined when null", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + guestName: null, + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.guestName).toBeUndefined(); + } + }); + + it("should set guestEmail to null when undefined", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + guestEmail: undefined, + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.guestEmail).toBeNull(); + } + }); + + it("should set guestCompany to null when undefined", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + guestCompany: undefined, + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.guestCompany).toBeNull(); + } + }); + + it("should preserve existing values when provided", () => { + const input = { + ...baseValidInput, + aiPhoneCallConfig: { + ...baseAiPhoneCallConfig, + guestName: "John Doe", + guestEmail: "john@example.com", + guestCompany: "Acme Inc", + }, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig?.guestName).toBe("John Doe"); + expect(result.data.aiPhoneCallConfig?.guestEmail).toBe("john@example.com"); + expect(result.data.aiPhoneCallConfig?.guestCompany).toBe("Acme Inc"); + } + }); + + it("should allow undefined aiPhoneCallConfig", () => { + const input = { + ...baseValidInput, + }; + + const result = ZUpdateInputSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.aiPhoneCallConfig).toBeUndefined(); + } + }); + }); +}); diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.test.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.test.ts new file mode 100644 index 0000000000..36540fad67 --- /dev/null +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "vitest"; + +import { Prisma } from "@calcom/prisma/client"; + +describe("update.handler", () => { + describe("bookingFields null to Prisma.DbNull transformation", () => { + function transformBookingFields( + bookingFields: null | undefined | Prisma.InputJsonValue + ): typeof Prisma.DbNull | Prisma.InputJsonValue | undefined { + return bookingFields === null ? Prisma.DbNull : (bookingFields as Prisma.InputJsonValue | undefined); + } + + it("should convert null to Prisma.DbNull", () => { + const result = transformBookingFields(null); + expect(result).toBe(Prisma.DbNull); + }); + + it("should pass through undefined as-is", () => { + const result = transformBookingFields(undefined); + expect(result).toBeUndefined(); + }); + + it("should pass through an array of booking fields as-is", () => { + const bookingFieldsArray = [ + { + name: "email", + type: "email", + label: "Email", + required: true, + hidden: false, + }, + { + name: "name", + type: "name", + label: "Name", + required: true, + hidden: false, + }, + ]; + + const result = transformBookingFields(bookingFieldsArray); + expect(result).toEqual(bookingFieldsArray); + }); + + it("should pass through an empty array as-is", () => { + const result = transformBookingFields([]); + expect(result).toEqual([]); + }); + + it("should distinguish between null and empty array", () => { + const nullResult = transformBookingFields(null); + const emptyArrayResult = transformBookingFields([]); + + expect(nullResult).toBe(Prisma.DbNull); + expect(emptyArrayResult).toEqual([]); + expect(nullResult).not.toEqual(emptyArrayResult); + }); + }); +}); diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts index ed5fcefa9f..cef6bd1253 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts @@ -240,7 +240,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }), description: newDescription, title: newTitle, - bookingFields, + bookingFields: + bookingFields === null ? Prisma.DbNull : (bookingFields as Prisma.InputJsonValue | undefined), maxActiveBookingsPerBooker, isRRWeightsEnabled, rrSegmentQueryValue: diff --git a/packages/trpc/server/routers/viewer/eventTypes/types.ts b/packages/trpc/server/routers/viewer/eventTypes/types.ts index 9c767ad409..b6d9b9c515 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/types.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/types.ts @@ -1,18 +1,245 @@ import { z } from "zod"; +import type { TemplateType } from "@calcom/features/calAIPhone/zod-utils"; import { templateTypeEnum } from "@calcom/features/calAIPhone/zod-utils"; import { MAX_SEATS_PER_TIME_SLOT } from "@calcom/lib/constants"; +import type { PeriodType, SchedulingType } from "@calcom/prisma/enums"; +import type { + CustomInputSchema, + EventTypeMetadata, + IntervalLimit, + EventTypeLocation, +} from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema, - stringOrNumber, rrSegmentQueryValueSchema, + eventTypeLocations, + recurringEventType, + intervalLimitsType, + eventTypeBookingFields, + eventTypeColor, } from "@calcom/prisma/zod-utils"; -import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; -import { DestinationCalendarSchema } from "@calcom/prisma/zod/modelSchema/DestinationCalendarSchema"; -import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; -const hashedLinkInputSchema = z +// ============================================================================ +// EXPLICIT TYPE DEFINITIONS +// ============================================================================ +// These explicit types prevent TypeScript from having to infer complex types +// through chains of .extend(), .partial(), .merge() etc. This significantly +// reduces type-checking time by giving TypeScript the answer directly. +// ============================================================================ + +type HashedLinkInput = { + link: string; + expiresAt?: Date | null; + maxUsageCount?: number | null; + usageCount?: number | null; +}; + +type AiPhoneCallConfig = { + generalPrompt: string; + enabled: boolean; + beginMessage: string | null; + yourPhoneNumber: string; + numberToCall: string; + guestName?: string | null; + guestEmail?: string | null; + guestCompany?: string | null; + templateType: TemplateType; +}; + +type CalVideoSettings = { + disableRecordingForGuests?: boolean | null; + disableRecordingForOrganizer?: boolean | null; + enableAutomaticTranscription?: boolean | null; + enableAutomaticRecordingForOrganizer?: boolean | null; + disableTranscriptionForGuests?: boolean | null; + disableTranscriptionForOrganizer?: boolean | null; + redirectUrlOnExit?: string | null; + requireEmailForGuests?: boolean | null; +} | null; + +type HostInput = { + userId: number; + profileId?: number | null; + isFixed?: boolean; + priority?: number | null; + weight?: number | null; + scheduleId?: number | null; + groupId?: string | null; +}; + +type HostGroupInput = { + id: string; + name: string; +}; + +type ChildInput = { + owner: { + id: number; + name: string; + email: string; + eventTypeSlugs: string[]; + }; + hidden: boolean; +}; + +type DestinationCalendarInput = { + integration: string; + externalId: string; +} | null; + +type RecurringEventInput = { + dtstart?: Date; + interval: number; + count: number; + freq: number; + until?: Date; + tzid?: string; +} | null; + +type EventTypeColorInput = { + lightEventTypeColor: string; + darkEventTypeColor: string; +} | null; + +/** + * Booking field type - minimal type definition for fields that need to be accessed. + * Only includes properties that are actually read in server code. + * Does NOT use an index signature to maintain compatibility with API v2 DTO classes. + */ +type BookingFieldInput = { + name: string; + hidden?: boolean; + required?: boolean; + type?: string; +}; + +/** + * RR Segment query value - using index signature for complex RAQB structure. + * The values need to be indexable (string keys) for downstream usage. + */ +type RRSegmentQueryValueInput = { + [key: string]: unknown; +} | null; + +/** + * Explicit type definition for event type update input. + * + * This type is defined explicitly rather than using z.infer<> on a complex + * schema chain to significantly reduce TypeScript type-checking time. + * The schema still validates all fields at runtime. + * + * All fields are optional (from .partial()) except `id` which is required. + */ +export type TUpdateInputSchema = { + // Required field + id: number; + + // Fields from EventTypeSchema (all optional due to .partial()) + periodType?: PeriodType; + schedulingType?: SchedulingType | null; + title?: string; + slug?: string; + description?: string | null; + interfaceLanguage?: string | null; + position?: number; + locations?: EventTypeLocation[] | null; + length?: number; + offsetStart?: number; + hidden?: boolean; + userId?: number | null; + profileId?: number | null; + teamId?: number | null; + useEventLevelSelectedCalendars?: boolean; + eventName?: string | null; + parentId?: number | null; + bookingFields?: BookingFieldInput[] | null; + timeZone?: string | null; + periodStartDate?: Date | null; + periodEndDate?: Date | null; + periodDays?: number | null; + periodCountCalendarDays?: boolean | null; + lockTimeZoneToggleOnBookingPage?: boolean; + lockedTimeZone?: string | null; + requiresConfirmation?: boolean; + requiresConfirmationWillBlockSlot?: boolean; + requiresConfirmationForFreeEmail?: boolean; + requiresBookerEmailVerification?: boolean; + canSendCalVideoTranscriptionEmails?: boolean; + autoTranslateDescriptionEnabled?: boolean; + autoTranslateInstantMeetingTitleEnabled?: boolean; + recurringEvent?: RecurringEventInput; + disableGuests?: boolean; + hideCalendarNotes?: boolean; + hideCalendarEventDetails?: boolean; + minimumBookingNotice?: number; + beforeEventBuffer?: number; + afterEventBuffer?: number; + seatsPerTimeSlot?: number | null; + onlyShowFirstAvailableSlot?: boolean; + showOptimizedSlots?: boolean | null; + disableCancelling?: boolean | null; + disableRescheduling?: boolean | null; + minimumRescheduleNotice?: number | null; + seatsShowAttendees?: boolean | null; + seatsShowAvailabilityCount?: boolean | null; + scheduleId?: number | null; + allowReschedulingCancelledBookings?: boolean | null; + price?: number; + currency?: string; + slotInterval?: number | null; + metadata?: EventTypeMetadata; + successRedirectUrl?: string | null; + forwardParamsSuccessRedirect?: boolean | null; + bookingLimits?: IntervalLimit | null; + durationLimits?: IntervalLimit | null; + isInstantEvent?: boolean; + instantMeetingExpiryTimeOffsetInSeconds?: number; + instantMeetingScheduleId?: number | null; + instantMeetingParameters?: string[]; + assignAllTeamMembers?: boolean; + assignRRMembersUsingSegment?: boolean; + rrSegmentQueryValue?: RRSegmentQueryValueInput; + useEventTypeDestinationCalendarEmail?: boolean; + isRRWeightsEnabled?: boolean; + maxLeadThreshold?: number | null; + includeNoShowInRRCalculation?: boolean; + allowReschedulingPastBookings?: boolean; + hideOrganizerEmail?: boolean; + maxActiveBookingsPerBooker?: number | null; + maxActiveBookingPerBookerOfferReschedule?: boolean; + customReplyToEmail?: string | null; + eventTypeColor?: EventTypeColorInput; + rescheduleWithSameRoundRobinHost?: boolean; + secondaryEmailId?: number | null; + useBookerTimezone?: boolean; + restrictionScheduleId?: number | null; + bookingRequiresAuthentication?: boolean; + rrHostSubsetEnabled?: boolean; + createdAt?: Date | null; + updatedAt?: Date | null; + + // Extended fields (all optional due to .partial()) + aiPhoneCallConfig?: AiPhoneCallConfig; + calVideoSettings?: CalVideoSettings; + calAiPhoneScript?: string; + customInputs?: CustomInputSchema[]; + destinationCalendar?: DestinationCalendarInput; + users?: number[]; + children?: ChildInput[]; + hosts?: HostInput[]; + schedule?: number | null; + instantMeetingSchedule?: number | null; + multiplePrivateLinks?: (string | HashedLinkInput)[]; + hostGroups?: HostGroupInput[]; +}; + +// ============================================================================ +// ZOD SCHEMAS +// ============================================================================ + +const hashedLinkInputSchema: z.ZodType = z .object({ link: z.string(), expiresAt: z.date().nullish(), @@ -21,7 +248,7 @@ const hashedLinkInputSchema = z }) .strict(); -const aiPhoneCallConfig = z +const aiPhoneCallConfigSchema: z.ZodType = z .object({ generalPrompt: z.string(), enabled: z.boolean(), @@ -35,7 +262,7 @@ const aiPhoneCallConfig = z }) .optional(); -const calVideoSettingsSchema = z +const calVideoSettingsSchema: z.ZodType = z .object({ disableRecordingForGuests: z.boolean().nullish(), disableRecordingForOrganizer: z.boolean().nullish(), @@ -49,7 +276,7 @@ const calVideoSettingsSchema = z .optional() .nullable(); -const hostSchema = z.object({ +const hostSchema: z.ZodType = z.object({ userId: z.number(), profileId: z.number().or(z.null()).optional(), isFixed: z.boolean().optional(), @@ -59,12 +286,12 @@ const hostSchema = z.object({ groupId: z.string().optional().nullable(), }); -const hostGroupSchema = z.object({ +const hostGroupSchema: z.ZodType = z.object({ id: z.string().uuid(), name: z.string(), }); -const childSchema = z.object({ +const childSchema: z.ZodType = z.object({ owner: z.object({ id: z.number(), name: z.string(), @@ -74,55 +301,132 @@ const childSchema = z.object({ hidden: z.boolean(), }); -/** Optional fields */ -const BaseEventTypeUpdateInput = EventTypeSchema.extend({ - isInstantEvent: z.boolean(), - instantMeetingParameters: z.array(z.string()), - instantMeetingExpiryTimeOffsetInSeconds: z.number(), - aiPhoneCallConfig, - calVideoSettings: calVideoSettingsSchema, - calAiPhoneScript: z.string(), - customInputs: z.array(customInputSchema), - destinationCalendar: DestinationCalendarSchema - .pick({ - integration: true, - externalId: true, - }) - .nullable(), - users: z.array(stringOrNumber), - children: z.array(childSchema), - hosts: z.array(hostSchema), - schedule: z.number().nullable(), - instantMeetingSchedule: z.number().nullable(), - multiplePrivateLinks: z.array(z.union([z.string(), hashedLinkInputSchema])), - assignAllTeamMembers: z.boolean(), - isRRWeightsEnabled: z.boolean(), - metadata: EventTypeMetaDataSchema, - bookingFields: eventTypeBookingFields, - assignRRMembersUsingSegment: z.boolean().optional(), - rrSegmentQueryValue: rrSegmentQueryValueSchema.optional(), - useEventLevelSelectedCalendars: z.boolean().optional(), - seatsPerTimeSlot: z.number().min(1).max(MAX_SEATS_PER_TIME_SLOT).nullable().optional(), - hostGroups: z.array(hostGroupSchema).optional(), -}) - .partial() - .extend(EventTypeSchema.pick({ id: true }).shape); +const destinationCalendarInputSchema: z.ZodType = z + .object({ + integration: z.string(), + externalId: z.string(), + }) + .nullable(); -export const ZUpdateInputSchema = BaseEventTypeUpdateInput.extend({ - aiPhoneCallConfig: aiPhoneCallConfig.refine( - (data) => { - if (!data) return true; - data.yourPhoneNumber = data.yourPhoneNumber || ""; - data.numberToCall = data.numberToCall || ""; - data.guestName = data.guestName ?? undefined; - data.guestEmail = data.guestEmail ?? null; - data.guestCompany = data.guestCompany ?? null; - return true; - }, - { - message: "Applying default values and transformations", - } - ), -}).strict(); -// only run infer over the simple type, excluding refines/transforms. -export type TUpdateInputSchema = z.infer; +/** + * Base schema for event type updates. + * + * Uses z.ZodType annotation to prevent TypeScript from + * inferring the complex type through the schema chain. This is a key + * optimization that reduces type-checking time significantly. + */ +const BaseEventTypeUpdateInput: z.ZodType = z + .object({ + // Required field + id: z.number().int(), + + // Fields from EventTypeSchema + periodType: z.enum(["UNLIMITED", "ROLLING", "ROLLING_WINDOW", "RANGE"]).optional(), + schedulingType: z.enum(["ROUND_ROBIN", "COLLECTIVE", "MANAGED"]).nullable().optional(), + title: z.string().min(1).optional(), + slug: z.string().optional(), + description: z.string().nullable().optional(), + interfaceLanguage: z.string().nullable().optional(), + position: z.number().int().optional(), + locations: eventTypeLocations.nullable().optional(), + length: z.number().min(1).optional(), + offsetStart: z.number().int().optional(), + hidden: z.boolean().optional(), + userId: z.number().int().nullable().optional(), + profileId: z.number().int().nullable().optional(), + teamId: z.number().int().nullable().optional(), + useEventLevelSelectedCalendars: z.boolean().optional(), + eventName: z.string().nullable().optional(), + parentId: z.number().int().nullable().optional(), + bookingFields: eventTypeBookingFields.nullable().optional(), + timeZone: z.string().nullable().optional(), + periodStartDate: z.coerce.date().nullable().optional(), + periodEndDate: z.coerce.date().nullable().optional(), + periodDays: z.number().int().nullable().optional(), + periodCountCalendarDays: z.boolean().nullable().optional(), + lockTimeZoneToggleOnBookingPage: z.boolean().optional(), + lockedTimeZone: z.string().nullable().optional(), + requiresConfirmation: z.boolean().optional(), + requiresConfirmationWillBlockSlot: z.boolean().optional(), + requiresConfirmationForFreeEmail: z.boolean().optional(), + requiresBookerEmailVerification: z.boolean().optional(), + canSendCalVideoTranscriptionEmails: z.boolean().optional(), + autoTranslateDescriptionEnabled: z.boolean().optional(), + autoTranslateInstantMeetingTitleEnabled: z.boolean().optional(), + recurringEvent: recurringEventType.nullable().optional(), + disableGuests: z.boolean().optional(), + hideCalendarNotes: z.boolean().optional(), + hideCalendarEventDetails: z.boolean().optional(), + minimumBookingNotice: z.number().min(0).optional(), + beforeEventBuffer: z.number().int().optional(), + afterEventBuffer: z.number().int().optional(), + seatsPerTimeSlot: z.number().min(1).max(MAX_SEATS_PER_TIME_SLOT).nullable().optional(), + onlyShowFirstAvailableSlot: z.boolean().optional(), + showOptimizedSlots: z.boolean().nullable().optional(), + disableCancelling: z.boolean().nullable().optional(), + disableRescheduling: z.boolean().nullable().optional(), + minimumRescheduleNotice: z.number().min(0).nullable().optional(), + seatsShowAttendees: z.boolean().nullable().optional(), + seatsShowAvailabilityCount: z.boolean().nullable().optional(), + scheduleId: z.number().int().nullable().optional(), + allowReschedulingCancelledBookings: z.boolean().nullable().optional(), + price: z.number().int().optional(), + currency: z.string().optional(), + slotInterval: z.number().int().nullable().optional(), + metadata: EventTypeMetaDataSchema.optional(), + successRedirectUrl: z.string().nullable().optional(), + forwardParamsSuccessRedirect: z.boolean().nullable().optional(), + bookingLimits: intervalLimitsType.nullable().optional(), + durationLimits: intervalLimitsType.nullable().optional(), + isInstantEvent: z.boolean().optional(), + instantMeetingExpiryTimeOffsetInSeconds: z.number().int().optional(), + instantMeetingScheduleId: z.number().int().nullable().optional(), + instantMeetingParameters: z.string().array().optional(), + assignAllTeamMembers: z.boolean().optional(), + assignRRMembersUsingSegment: z.boolean().optional(), + rrSegmentQueryValue: rrSegmentQueryValueSchema.nullable().optional(), + useEventTypeDestinationCalendarEmail: z.boolean().optional(), + isRRWeightsEnabled: z.boolean().optional(), + maxLeadThreshold: z.number().int().nullable().optional(), + includeNoShowInRRCalculation: z.boolean().optional(), + allowReschedulingPastBookings: z.boolean().optional(), + hideOrganizerEmail: z.boolean().optional(), + maxActiveBookingsPerBooker: z.number().int().nullable().optional(), + maxActiveBookingPerBookerOfferReschedule: z.boolean().optional(), + customReplyToEmail: z.string().nullable().optional(), + eventTypeColor: eventTypeColor.nullable().optional(), + rescheduleWithSameRoundRobinHost: z.boolean().optional(), + secondaryEmailId: z.number().int().nullable().optional(), + useBookerTimezone: z.boolean().optional(), + restrictionScheduleId: z.number().int().nullable().optional(), + bookingRequiresAuthentication: z.boolean().optional(), + rrHostSubsetEnabled: z.boolean().optional(), + createdAt: z.coerce.date().nullable().optional(), + updatedAt: z.coerce.date().nullable().optional(), + + // Extended fields + aiPhoneCallConfig: aiPhoneCallConfigSchema, + calVideoSettings: calVideoSettingsSchema, + calAiPhoneScript: z.string().optional(), + customInputs: z.array(customInputSchema).optional(), + destinationCalendar: destinationCalendarInputSchema.optional(), + users: z.array(z.number()).optional(), + children: z.array(childSchema).optional(), + hosts: z.array(hostSchema).optional(), + schedule: z.number().nullable().optional(), + instantMeetingSchedule: z.number().nullable().optional(), + multiplePrivateLinks: z.array(z.union([z.string(), hashedLinkInputSchema])).optional(), + hostGroups: z.array(hostGroupSchema).optional(), + }) + .strict(); + +export const ZUpdateInputSchema = BaseEventTypeUpdateInput.superRefine((data, _ctx) => { + // Apply transformations to aiPhoneCallConfig if present + if (data.aiPhoneCallConfig) { + data.aiPhoneCallConfig.yourPhoneNumber = data.aiPhoneCallConfig.yourPhoneNumber || ""; + data.aiPhoneCallConfig.numberToCall = data.aiPhoneCallConfig.numberToCall || ""; + data.aiPhoneCallConfig.guestName = data.aiPhoneCallConfig.guestName ?? undefined; + data.aiPhoneCallConfig.guestEmail = data.aiPhoneCallConfig.guestEmail ?? null; + data.aiPhoneCallConfig.guestCompany = data.aiPhoneCallConfig.guestCompany ?? null; + } +});