From b1b8b0135e0fed1618ea654659db962caaddc083 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 20 Mar 2025 20:19:14 +0530 Subject: [PATCH] refactor: event type builder (#19459) --- .../features/CalendarEventBuilder.test.ts | 690 ++++++++++++++++++ packages/features/CalendarEventBuilder.ts | 275 +++++++ .../features/bookings/lib/handleNewBooking.ts | 134 ++-- packages/lib/timeFormat.ts | 2 +- 4 files changed, 1049 insertions(+), 52 deletions(-) create mode 100644 packages/features/CalendarEventBuilder.test.ts create mode 100644 packages/features/CalendarEventBuilder.ts diff --git a/packages/features/CalendarEventBuilder.test.ts b/packages/features/CalendarEventBuilder.test.ts new file mode 100644 index 0000000000..0d1cd058db --- /dev/null +++ b/packages/features/CalendarEventBuilder.test.ts @@ -0,0 +1,690 @@ +import type { TFunction } from "next-i18next"; +import { describe, expect, it, vi } from "vitest"; + +import dayjs from "@calcom/dayjs"; +import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; +import type { Person } from "@calcom/types/Calendar"; + +describe("CalendarEventBuilder", () => { + const mockTranslate = vi.fn(() => "foo") as TFunction; + const mockStartTime = dayjs().add(1, "day").format(); + const mockEndTime = dayjs().add(1, "day").add(30, "minutes").format(); + + it("should create a basic calendar event", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + additionalNotes: "Some notes", + }) + .withEventType({ + slug: "test-slug", + description: "Test description", + id: 123, + }) + .build(); + + expect(event.bookerUrl).toBe("https://cal.com/user/test-slug"); + expect(event.title).toBe("Test Event"); + expect(event.startTime).toBe(mockStartTime); + expect(event.endTime).toBe(mockEndTime); + expect(event.additionalNotes).toBe("Some notes"); + }); + + it("should create an event with event type details", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + description: "Test description", + id: 123, + hideCalendarNotes: true, + hideCalendarEventDetails: false, + }) + .build(); + + expect(event.type).toBe("test-slug"); + expect(event.description).toBe("Test description"); + expect(event.eventTypeId).toBe(123); + expect(event.hideCalendarNotes).toBe(true); + expect(event.hideCalendarEventDetails).toBe(false); + }); + + it("should create an event with organizer details", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withOrganizer({ + id: 456, + name: "John Doe", + email: "john@example.com", + username: "johndoe", + timeZone: "America/New_York", + language: { + translate: mockTranslate, + locale: "en", + }, + }) + .build(); + + expect(event.organizer).toEqual({ + id: 456, + name: "John Doe", + email: "john@example.com", + username: "johndoe", + timeZone: "America/New_York", + language: { + translate: mockTranslate, + locale: "en", + }, + }); + }); + + it("should handle nameless organizer", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withOrganizer({ + id: 456, + name: null, + email: "john@example.com", + timeZone: "America/New_York", + language: { + translate: mockTranslate, + locale: "en", + }, + }) + .build(); + + expect(event.organizer.name).toBe("Nameless"); + }); + + it("should create an event with attendees", () => { + const attendees: Person[] = [ + { + email: "attendee1@example.com", + name: "Attendee One", + timeZone: "Europe/London", + language: { + translate: mockTranslate, + locale: "en", + }, + }, + { + email: "attendee2@example.com", + name: "Attendee Two", + timeZone: "Europe/Paris", + language: { + translate: mockTranslate, + locale: "fr", + }, + }, + ]; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAttendees(attendees) + .build(); + + expect(event.attendees).toEqual(attendees); + }); + + it("should create an event with metadata and responses", () => { + const customInputs = { question1: "answer1" }; + const responses = { + name: { label: "your_name", value: "Owner 1", isHidden: false }, + email: { + label: "email_address", + value: "owner1-dunder@example.com", + isHidden: false, + }, + }; + const userFieldsResponses = {}; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withMetadataAndResponses({ + additionalNotes: "Some notes", + customInputs, + responses, + userFieldsResponses, + }) + .build(); + + expect(event.additionalNotes).toBe("Some notes"); + expect(event.customInputs).toEqual(customInputs); + expect(event.responses).toEqual(responses); + expect(event.userFieldsResponses).toEqual(userFieldsResponses); + }); + + it("should create an event with location", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withLocation({ + location: "Conference Room A", + conferenceCredentialId: 789, + }) + .build(); + + expect(event.location).toBe("Conference Room A"); + expect(event.conferenceCredentialId).toBe(789); + }); + + it("should create an event with destination calendar", () => { + const destinationCalendar = { + id: 1, + integration: "google_calendar", + externalId: "external123", + primaryEmail: "primary@example.com", + userId: null, + eventTypeId: null, + credentialId: null, + delegationCredentialId: null, + domainWideDelegationCredentialId: null, + }; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withDestinationCalendar([destinationCalendar]) + .build(); + + expect(event.destinationCalendar).toEqual([destinationCalendar]); + }); + + it("should create an event with identifiers", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withIdentifiers({ + iCalUID: "ical-123", + iCalSequence: 2, + }) + .build(); + + expect(event.iCalUID).toBe("ical-123"); + expect(event.iCalSequence).toBe(2); + }); + + it("should create an event with confirmation settings", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withConfirmation({ + requiresConfirmation: true, + isConfirmedByDefault: false, + }) + .build(); + + expect(event.requiresConfirmation).toBe(true); + expect(event.oneTimePassword).toBeUndefined(); + }); + + it("should set oneTimePassword to null when isConfirmedByDefault is true", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withConfirmation({ + requiresConfirmation: true, + isConfirmedByDefault: true, + }) + .build(); + + expect(event.requiresConfirmation).toBe(true); + expect(event.oneTimePassword).toBeNull(); + }); + + it("should create an event with platform variables", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withPlatformVariables({ + platformClientId: "client-123", + platformRescheduleUrl: "https://platform.com/reschedule", + platformCancelUrl: "https://platform.com/cancel", + platformBookingUrl: "https://platform.com/booking", + }) + .build(); + + expect(event.platformClientId).toBe("client-123"); + expect(event.platformRescheduleUrl).toBe("https://platform.com/reschedule"); + expect(event.platformCancelUrl).toBe("https://platform.com/cancel"); + expect(event.platformBookingUrl).toBe("https://platform.com/booking"); + }); + + it("should create an event with apps status", () => { + const appsStatus = [ + { + appName: "google-calendar", + type: "google_calendar", + success: 1, + failures: 0, + errors: [], + warnings: [], + }, + { + appName: "Google Meet", + type: "conferencing", + success: 1, + failures: 0, + errors: [], + warnings: undefined, + }, + ]; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAppsStatus(appsStatus) + .build(); + + expect(event.appsStatus).toEqual(appsStatus); + }); + + it("should create an event with video call data", () => { + const videoCallData = { + type: "google_meet", + id: "123", + url: "https://meet.example.com/123", + password: "password123", + }; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withVideoCallData(videoCallData) + .build(); + + expect(event.videoCallData).toEqual(videoCallData); + }); + + it("should create an event with team information", () => { + const team = { + name: "Engineering Team", + members: [ + { + email: "member1@example.com", + name: "Member One", + timeZone: "America/Chicago", + language: { + translate: mockTranslate, + locale: "en", + }, + }, + ], + id: 101, + }; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withTeam(team) + .build(); + + expect(event.team).toEqual(team); + }); + + it("should create an event with recurring event information", () => { + const recurringEvent = { + count: 5, + freq: 2, + interval: 1, + }; + + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withRecurring(recurringEvent) + .build(); + + expect(event.recurringEvent).toEqual(recurringEvent); + }); + + it("should create an event with attendee seat ID", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAttendeeSeatId("seat-123") + .build(); + + expect(event.attendeeSeatId).toBe("seat-123"); + }); + + it("should create an event with UID", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withUid("booking-uid-123") + .build(); + + expect(event.uid).toBe("booking-uid-123"); + }); + + it("should create an event with one-time password", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withOneTimePassword("otp123") + .build(); + + expect(event.oneTimePassword).toBe("otp123"); + }); + + it("should create an event with recurring event ID", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withRecurringEventId("recurring-123") + .build(); + + expect(event.existingRecurringEvent).toEqual({ + recurringEventId: "recurring-123", + }); + }); + + it("should create a complete calendar event with all properties", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Complete Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + additionalNotes: "Complete test notes", + }) + .withEventType({ + slug: "complete-test", + description: "Complete test description", + id: 123, + hideCalendarNotes: true, + hideCalendarEventDetails: false, + }) + .withOrganizer({ + id: 456, + name: "John Doe", + email: "john@example.com", + username: "johndoe", + timeZone: "America/New_York", + language: { + translate: mockTranslate, + locale: "en", + }, + }) + .withAttendees([ + { + email: "attendee@example.com", + name: "Attendee", + timeZone: "Europe/London", + language: { + translate: mockTranslate, + locale: "en", + }, + }, + ]) + .withMetadataAndResponses({ + customInputs: { question1: "answer1" }, + responses: { + name: { label: "your_name", value: "Owner 1", isHidden: false }, + email: { + label: "email_address", + value: "owner1-dunder@example.com", + isHidden: false, + }, + }, + }) + .withLocation({ + location: "Conference Room A", + }) + .withDestinationCalendar([ + { + id: 1, + integration: "google_calendar", + externalId: "external123", + primaryEmail: "primary@example.com", + userId: 1, + eventTypeId: 123, + credentialId: 1, + delegationCredentialId: null, + domainWideDelegationCredentialId: null, + }, + ]) + .withIdentifiers({ + iCalUID: "ical-123", + iCalSequence: 2, + }) + .withConfirmation({ + requiresConfirmation: true, + isConfirmedByDefault: false, + }) + .withPlatformVariables({ + platformClientId: "client-123", + }) + .withAppsStatus([ + { + appName: "google-calendar", + type: "google_calendar", + success: 1, + failures: 0, + errors: [], + warnings: [], + }, + ]) + .withVideoCallData({ + type: "google_meet", + id: "123", + url: "https://meet.example.com/123", + password: "password123", + }) + .withTeam({ + name: "Engineering Team", + members: [], + id: 101, + }) + .withRecurring({ + count: 5, + freq: 2, + interval: 1, + }) + .withAttendeeSeatId("seat-123") + .withUid("booking-uid-123") + .withOneTimePassword("otp123") + .build(); + + // Test that all properties are set correctly + expect(event.title).toBe("Complete Test Event"); + expect(event.type).toBe("complete-test"); + expect(event.organizer.name).toBe("John Doe"); + expect(event.attendees).toHaveLength(1); + expect(event.location).toBe("Conference Room A"); + expect(event.iCalUID).toBe("ical-123"); + expect(event.requiresConfirmation).toBe(true); + expect(event.platformClientId).toBe("client-123"); + expect(event.appsStatus).toHaveLength(1); + expect(event.videoCallData?.url).toBe("https://meet.example.com/123"); + expect(event.team?.name).toBe("Engineering Team"); + expect(event.recurringEvent?.count).toBe(5); + expect(event.attendeeSeatId).toBe("seat-123"); + expect(event.uid).toBe("booking-uid-123"); + expect(event.oneTimePassword).toBe("otp123"); + }); + + it("should throw an error when building without required fields", () => { + const builder = new CalendarEventBuilder(); + expect(() => builder.build()).toThrow("Missing required fields for calendar event"); + }); + + it("should create an event from an existing event", () => { + const existingEvent = { + title: "Existing Event", + startTime: mockStartTime, + endTime: mockEndTime, + type: "existing-type", + bookerUrl: "https://cal.com/user/test-slug", + }; + + const event = CalendarEventBuilder.fromEvent(existingEvent) + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Updated Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .build(); + + expect(event.title).toBe("Updated Event"); + expect(event.type).toBe("existing-type"); + }); +}); diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts new file mode 100644 index 0000000000..bef812ac33 --- /dev/null +++ b/packages/features/CalendarEventBuilder.ts @@ -0,0 +1,275 @@ +import type { Prisma } from "@prisma/client"; +import type { TFunction } from "next-i18next"; + +import type { TimeFormat } from "@calcom/lib/timeFormat"; +import type { SchedulingType } from "@calcom/prisma/enums"; +import type { CalendarEvent, Person, CalEventResponses, AppsStatus } from "@calcom/types/Calendar"; +import type { VideoCallData } from "@calcom/types/VideoApiAdapter"; + +export class CalendarEventBuilder { + private event: Partial; + + constructor(existingEvent?: Partial) { + this.event = existingEvent || {}; + } + + static fromEvent(event: Partial) { + return new CalendarEventBuilder(event); + } + + withBasicDetails({ + bookerUrl, + title, + startTime, + endTime, + additionalNotes, + }: { + bookerUrl: string; + title: string; + startTime: string; + endTime: string; + additionalNotes?: string; + }) { + this.event = { + ...this.event, + bookerUrl, + title, + startTime, + endTime, + additionalNotes, + }; + return this; + } + + withEventType(eventType: { + slug: string; + description?: string | null; + id: number; + hideCalendarNotes?: boolean; + hideCalendarEventDetails?: boolean; + schedulingType?: SchedulingType | null; + seatsPerTimeSlot?: number | null; + seatsShowAttendees?: boolean | null; + seatsShowAvailabilityCount?: boolean | null; + }) { + this.event = { + ...this.event, + type: eventType.slug, + description: eventType.description, + eventTypeId: eventType.id, + hideCalendarNotes: eventType.hideCalendarNotes, + hideCalendarEventDetails: eventType.hideCalendarEventDetails, + schedulingType: eventType.schedulingType, + seatsPerTimeSlot: eventType.seatsPerTimeSlot, + // if seats are not enabled we should default true + seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, + seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, + }; + return this; + } + + withOrganizer(organizer: { + id: number; + name: string | null; + email: string; + username?: string; + timeZone: string; + timeFormat?: TimeFormat; + language: { + translate: TFunction; + locale: string; + }; + }) { + this.event = { + ...this.event, + organizer: { + id: organizer.id, + name: organizer.name || "Nameless", + email: organizer.email, + username: organizer.username, + timeZone: organizer.timeZone, + language: organizer.language, + timeFormat: organizer.timeFormat, + }, + }; + return this; + } + + withAttendees(attendees: Person[]) { + this.event = { + ...this.event, + attendees, + }; + return this; + } + + withMetadataAndResponses({ + additionalNotes, + customInputs, + responses, + userFieldsResponses, + }: { + additionalNotes?: string | null; + customInputs?: Prisma.JsonObject | null; + responses?: CalEventResponses | null; + userFieldsResponses?: CalEventResponses | null; + }) { + this.event = { + ...this.event, + additionalNotes, + customInputs, + responses, + userFieldsResponses, + }; + return this; + } + + withLocation({ + location, + conferenceCredentialId, + }: { + location: string | null; + conferenceCredentialId?: number; + }) { + this.event = { + ...this.event, + location, + conferenceCredentialId, + }; + return this; + } + + withDestinationCalendar(destinationCalendar: CalendarEvent["destinationCalendar"]) { + this.event = { + ...this.event, + destinationCalendar, + }; + return this; + } + + withIdentifiers({ iCalUID, iCalSequence }: { iCalUID?: string; iCalSequence?: number }) { + this.event = { + ...this.event, + iCalUID: iCalUID ?? this.event.iCalUID, + iCalSequence: iCalSequence ?? this.event.iCalSequence, + }; + return this; + } + + withConfirmation({ + requiresConfirmation, + isConfirmedByDefault, + }: { + requiresConfirmation: boolean; + isConfirmedByDefault: boolean; + }) { + this.event = { + ...this.event, + requiresConfirmation, + oneTimePassword: isConfirmedByDefault ? null : undefined, + }; + return this; + } + + withPlatformVariables({ + platformClientId, + platformRescheduleUrl, + platformCancelUrl, + platformBookingUrl, + }: { + platformClientId?: string | null; + platformRescheduleUrl?: string | null; + platformCancelUrl?: string | null; + platformBookingUrl?: string | null; + }) { + this.event = { + ...this.event, + platformClientId, + platformRescheduleUrl, + platformCancelUrl, + platformBookingUrl, + }; + return this; + } + + withAppsStatus(appsStatus?: AppsStatus[]) { + this.event = { + ...this.event, + appsStatus, + }; + return this; + } + + withVideoCallData(videoCallData?: VideoCallData) { + this.event = { + ...this.event, + videoCallData, + }; + return this; + } + + withTeam(team?: { name: string; members: Person[]; id: number }) { + this.event = { + ...this.event, + team, + }; + return this; + } + + withRecurring(recurringEvent?: { count: number; freq: number; interval: number }) { + this.event = { + ...this.event, + recurringEvent, + }; + return this; + } + + withAttendeeSeatId(attendeeSeatId?: string) { + this.event = { + ...this.event, + attendeeSeatId, + }; + return this; + } + + withUid(uid: string | null) { + this.event = { + ...this.event, + uid, + }; + return this; + } + + withRecurringEventId(recurringEventId: string) { + this.event = { + ...this.event, + existingRecurringEvent: { + recurringEventId, + }, + }; + return this; + } + + withOneTimePassword(oneTimePassword?: string | null) { + this.event = { + ...this.event, + oneTimePassword, + }; + return this; + } + + build(): CalendarEvent { + // Validate required fields + if ( + !this.event.startTime || + !this.event.endTime || + !this.event.type || + !this.event.bookerUrl || + !this.event.title + ) { + throw new Error("Missing required fields for calendar event"); + } + + return this.event as CalendarEvent; + } +} diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index c5ed9563de..180ff3095d 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -25,6 +25,7 @@ import { sendScheduledEmailsAndSMS, } from "@calcom/emails"; import getICalUID from "@calcom/emails/lib/getICalUID"; +import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; @@ -964,16 +965,28 @@ async function handler( }; const eventName = getEventName(eventNameObject); - let evt: CalendarEvent = { - bookerUrl, - type: eventType.slug, - title: eventName, //this needs to be either forced in english, or fetched for each attendee and organizer separately - description: eventType.description, - additionalNotes, - customInputs, - startTime: dayjs(reqBody.start).utc().format(), - endTime: dayjs(reqBody.end).utc().format(), - organizer: { + + let evt: CalendarEvent = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl, + title: eventName, + startTime: dayjs(reqBody.start).utc().format(), + endTime: dayjs(reqBody.end).utc().format(), + additionalNotes, + }) + .withEventType({ + slug: eventType.slug, + description: eventType.description, + id: eventType.id, + hideCalendarNotes: eventType.hideCalendarNotes, + hideCalendarEventDetails: eventType.hideCalendarEventDetails, + schedulingType: eventType.schedulingType, + seatsPerTimeSlot: eventType.seatsPerTimeSlot, + // if seats are not enabled we should default true + seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, + seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, + }) + .withOrganizer({ id: organizerUser.id, name: organizerUser.name || "Nameless", email: organizerEmail, @@ -981,39 +994,42 @@ async function handler( timeZone: organizerUser.timeZone, language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" }, timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat), - }, - responses: reqBody.calEventResponses || null, - userFieldsResponses: reqBody.calEventUserFieldsResponses || null, - attendees: attendeesList, - location: platformBookingLocation ?? bookingLocation, // Will be processed by the EventManager later. - conferenceCredentialId, - destinationCalendar, - hideCalendarNotes: eventType.hideCalendarNotes, - hideCalendarEventDetails: eventType.hideCalendarEventDetails, - requiresConfirmation: !isConfirmedByDefault, - eventTypeId: eventType.id, - // if seats are not enabled we should default true - seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, - seatsPerTimeSlot: eventType.seatsPerTimeSlot, - seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, - schedulingType: eventType.schedulingType, - iCalUID, - iCalSequence, - platformClientId, - platformRescheduleUrl, - platformCancelUrl, - platformBookingUrl, - oneTimePassword: isConfirmedByDefault ? null : undefined, - }; + }) + .withAttendees(attendeesList) + .withMetadataAndResponses({ + additionalNotes, + customInputs, + responses: reqBody.calEventResponses || null, + userFieldsResponses: reqBody.calEventUserFieldsResponses || null, + }) + .withLocation({ + location: platformBookingLocation ?? bookingLocation, // Will be processed by the EventManager later. + conferenceCredentialId, + }) + .withDestinationCalendar(destinationCalendar) + .withIdentifiers({ iCalUID, iCalSequence }) + .withConfirmation({ + requiresConfirmation: !isConfirmedByDefault, + isConfirmedByDefault, + }) + .withPlatformVariables({ + platformClientId, + platformRescheduleUrl, + platformCancelUrl, + platformBookingUrl, + }) + .build(); if (req.body.thirdPartyRecurringEventId) { - evt.existingRecurringEvent = { - recurringEventId: req.body.thirdPartyRecurringEventId, - }; + evt = CalendarEventBuilder.fromEvent(evt) + .withRecurringEventId(req.body.thirdPartyRecurringEventId) + .build(); } if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") { - evt.destinationCalendar?.push(...teamDestinationCalendars); + evt = CalendarEventBuilder.fromEvent(evt) + .withDestinationCalendar([...(evt.destinationCalendar ?? []), ...teamDestinationCalendars]) + .build(); } // data needed for triggering webhooks @@ -1075,11 +1091,13 @@ async function handler( ); if (isTeamEventType) { - evt.team = { - members: teamMembers, - name: eventType.team?.name || "Nameless", - id: eventType.team?.id ?? 0, - }; + evt = CalendarEventBuilder.fromEvent(evt) + .withTeam({ + members: teamMembers, + name: eventType.team?.name || "Nameless", + id: eventType.team?.id ?? 0, + }) + .build(); } // For seats, if the booking already exists then we want to add the new attendee to the existing booking @@ -1140,9 +1158,13 @@ async function handler( // Rescheduling logic for the original seated event was handled in handleSeats // We want to use new booking logic for the new time slot originalRescheduledBooking = null; - evt.iCalUID = getICalUID({ - attendeeId: bookingSeat?.attendeeId, - }); + evt = CalendarEventBuilder.fromEvent(evt) + .withIdentifiers({ + iCalUID: getICalUID({ + attendeeId: bookingSeat?.attendeeId, + }), + }) + .build(); } } @@ -1232,8 +1254,14 @@ async function handler( } } - evt.uid = booking.uid ?? null; - evt.oneTimePassword = booking.oneTimePassword ?? null; + evt = CalendarEventBuilder.fromEvent(evt) + .withUid(booking.uid ?? null) + .build(); + + evt = CalendarEventBuilder.fromEvent(evt) + .withOneTimePassword(booking.oneTimePassword ?? null) + .build(); + if (booking && booking.id && eventType.seatsPerTimeSlot) { const currentAttendee = booking.attendees.find( (attendee) => @@ -1327,9 +1355,13 @@ async function handler( evt.iCalUID = undefined; } else { // In case of rescheduling, we need to keep the previous host destination calendar - evt.destinationCalendar = originalRescheduledBooking?.destinationCalendar - ? [originalRescheduledBooking?.destinationCalendar] - : evt.destinationCalendar; + evt = CalendarEventBuilder.fromEvent(evt) + .withDestinationCalendar( + originalRescheduledBooking?.destinationCalendar + ? [originalRescheduledBooking?.destinationCalendar] + : evt.destinationCalendar + ) + .build(); } const updateManager = await eventManager.reschedule( diff --git a/packages/lib/timeFormat.ts b/packages/lib/timeFormat.ts index 3b98c36da5..4864df4119 100644 --- a/packages/lib/timeFormat.ts +++ b/packages/lib/timeFormat.ts @@ -24,7 +24,7 @@ export const getIs24hClockFromLocalStorage = () => { return is24hFromLocalstorage === "true"; }; -export const getTimeFormatStringFromUserTimeFormat = (timeFormat: number | null | undefined) => { +export const getTimeFormatStringFromUserTimeFormat = (timeFormat: number | null | undefined): TimeFormat => { return timeFormat === 24 ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR; };