refactor: evt calendar event builder (#27203)

* refactor: evt creation

* refactor: evt creation

* refactor: evt creation

* fix: type

* test: update unit tests

* chore: add comments

* fix: handle undefined in withConditional to match NonNullable<T> type

Co-Authored-By: unknown <>

* refactor: evt

* fix: type err

* refactor: remove add videoCallData

* refactor: use error with code

* test: update unit test

* refactor: feedback

* refactor: remove conditional

* refactor: more strict types

* fix: type err

* chor: remove un used

* refactor:  improve recurring event handling in RegularBookingService

Removed unnecessary whitespace and clarified comments regarding the attachment of recurring configurations in the booking handler. Adjusted logic to ensure that recurring settings are only applied when relevant.

* refactor: test

* fix: type error

* refactor: improve code

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Udit Takkar
2026-03-19 03:50:48 +09:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 3c57960b7d
commit f2a44279fe
12 changed files with 512 additions and 825 deletions
+116 -296
View File
@@ -1,7 +1,8 @@
import dayjs from "@calcom/dayjs";
import { type BookingForCalEventBuilder, CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import type { BookingForCalEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { Person } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import type { TFunction } from "i18next";
import { describe, expect, it, vi } from "vitest";
@@ -32,18 +33,42 @@ 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();
const defaultOrganizer = {
id: 123,
name: "Organizer",
email: "organizer@example.com",
timeZone: "America/New_York",
language: {
translate: mockTranslate,
locale: "en",
},
};
const defaultAttendees = [
{
name: "Attendee",
email: "attendee@example.com",
timeZone: "Europe/London",
language: {
translate: mockTranslate,
locale: "en",
},
},
];
const createBuilder = (overrides: Partial<CalendarEvent> = {}) =>
new CalendarEventBuilder({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
type: "test-slug",
organizer: defaultOrganizer,
attendees: defaultAttendees,
...overrides,
});
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",
})
const event = createBuilder({ additionalNotes: "Some notes" })
.withEventType({
slug: "test-slug",
description: "Test description",
id: 123,
})
@@ -60,15 +85,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
description: "Test description",
id: 123,
hideCalendarNotes: true,
@@ -87,73 +105,27 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const organizer = {
id: 456,
name: "John Doe",
email: "john@example.com",
username: "johndoe",
timeZone: "America/New_York",
language: {
translate: mockTranslate,
locale: "en",
},
};
const event = createBuilder({ organizer })
.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).not.toBeNull();
if (event) {
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).not.toBeNull();
if (event) {
expect(event.organizer.name).toBe("Nameless");
expect(event.organizer).toEqual(organizer);
}
});
@@ -179,18 +151,10 @@ describe("CalendarEventBuilder", () => {
},
];
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder({ attendees, bookerUrl: "https://cal.com/user" })
.withEventType({
slug: "test-slug",
id: 123,
})
.withAttendees(attendees)
.build();
expect(event).not.toBeNull();
@@ -211,15 +175,8 @@ describe("CalendarEventBuilder", () => {
};
const userFieldsResponses = {};
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withMetadataAndResponses({
@@ -240,15 +197,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withLocation({
@@ -279,15 +229,8 @@ describe("CalendarEventBuilder", () => {
domainWideDelegationCredentialId: null,
};
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withDestinationCalendar([destinationCalendar])
@@ -300,15 +243,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withIdentifiers({
@@ -325,15 +261,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withConfirmation({
@@ -350,15 +279,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withConfirmation({
@@ -375,15 +297,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withPlatformVariables({
@@ -423,15 +338,8 @@ describe("CalendarEventBuilder", () => {
},
];
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withAppsStatus(appsStatus)
@@ -451,15 +359,8 @@ describe("CalendarEventBuilder", () => {
password: "password123",
};
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withVideoCallData(videoCallData)
@@ -488,15 +389,8 @@ describe("CalendarEventBuilder", () => {
id: 101,
};
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withTeam(team)
@@ -515,15 +409,8 @@ describe("CalendarEventBuilder", () => {
interval: 1,
};
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withRecurring(recurringEvent)
@@ -536,15 +423,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withAttendeeSeatId("seat-123")
@@ -557,15 +437,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withUid("booking-uid-123")
@@ -578,15 +451,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withOneTimePassword("otp123")
@@ -599,15 +465,8 @@ describe("CalendarEventBuilder", () => {
});
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,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withRecurringEventId("recurring-123")
@@ -622,15 +481,8 @@ describe("CalendarEventBuilder", () => {
});
it("should create an event with assignment reason", () => {
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withAssignmentReason({
@@ -649,15 +501,8 @@ describe("CalendarEventBuilder", () => {
});
it("should create an event with assignment reason without details", () => {
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withAssignmentReason({
@@ -676,15 +521,8 @@ describe("CalendarEventBuilder", () => {
});
it("should create an event with null assignment reason", () => {
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
const event = createBuilder()
.withEventType({
slug: "test-slug",
id: 123,
})
.withAssignmentReason(null)
@@ -697,43 +535,43 @@ describe("CalendarEventBuilder", () => {
});
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",
})
const organizer = {
id: 456,
name: "John Doe",
email: "john@example.com",
username: "johndoe",
timeZone: "America/New_York",
language: {
translate: mockTranslate,
locale: "en",
},
};
const attendees = [
{
email: "attendee@example.com",
name: "Attendee",
timeZone: "Europe/London",
language: {
translate: mockTranslate,
locale: "en",
},
},
];
const event = createBuilder({
type: "complete-test",
organizer,
attendees,
title: "Complete Test Event",
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: {
@@ -826,11 +664,6 @@ describe("CalendarEventBuilder", () => {
}
});
it("should return null when building without required fields", () => {
const builder = new CalendarEventBuilder();
expect(builder.build()).toBeNull();
});
it("should create an event from an existing event", () => {
const existingEvent = {
title: "Existing Event",
@@ -838,16 +671,11 @@ describe("CalendarEventBuilder", () => {
endTime: mockEndTime,
type: "existing-type",
bookerUrl: "https://cal.com/user/test-slug",
organizer: defaultOrganizer,
attendees: defaultAttendees,
};
const event = CalendarEventBuilder.fromEvent(existingEvent)
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Updated Event",
startTime: mockStartTime,
endTime: mockEndTime,
})
.build();
const event = CalendarEventBuilder.fromEvent({ ...existingEvent, title: "Updated Event" }).build();
expect(event).not.toBeNull();
if (event) {
@@ -857,16 +685,8 @@ describe("CalendarEventBuilder", () => {
});
it("should propagate disableCancelling and disableRescheduling", () => {
const event = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: "https://cal.com/user/test-slug",
title: "Test Event",
startTime: mockStartTime,
endTime: mockEndTime,
additionalNotes: "Some notes",
})
const event = createBuilder({ additionalNotes: "Some notes" })
.withEventType({
slug: "test-slug",
description: "Test description",
id: 123,
disableCancelling: true,
@@ -1546,7 +1366,6 @@ describe("CalendarEventBuilder", () => {
const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking);
const builtFromBooking = eventFromBooking.build();
const manualBuilder = new CalendarEventBuilder();
const organizerPerson = {
id: 8,
name: "Match Host",
@@ -1563,17 +1382,20 @@ describe("CalendarEventBuilder", () => {
language: { translate: mockTranslate, locale: "en" },
};
const manualBuilder = createBuilder({
type: "match-event",
organizer: organizerPerson,
attendees: [attendeePerson],
bookerUrl: "https://cal.com",
title: "Match Test",
startTime: new Date(mockStartTime).toISOString(),
endTime: new Date(mockEndTime).toISOString(),
additionalNotes: "Match test description",
});
const manualEvent = manualBuilder
.withBasicDetails({
bookerUrl: "https://cal.com",
title: "Match Test",
startTime: new Date(mockStartTime).toISOString(),
endTime: new Date(mockEndTime).toISOString(),
additionalNotes: "Match test description",
})
.withEventType({
id: 700,
slug: "match-event",
description: "Match event type",
hideCalendarNotes: false,
hideCalendarEventDetails: false,
@@ -1586,8 +1408,6 @@ describe("CalendarEventBuilder", () => {
disableRescheduling: false,
disableCancelling: false,
})
.withOrganizer(organizerPerson)
.withAttendees([attendeePerson])
.withLocation({ location: "Test Location" })
.withIdentifiers({ iCalUID: "match-ical", iCalSequence: 1 })
.withConfirmation({ requiresConfirmation: false, isConfirmedByDefault: true })
+68 -107
View File
@@ -9,26 +9,26 @@ import {
} from "@calcom/features/eventtypes/di/EventTypeService.container";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import { getTranslation } from "@calcom/i18n/server";
import { getTimeFormatStringFromUserTimeFormat, type TimeFormat } from "@calcom/lib/timeFormat";
import type { Attendee, BookingSeat, DestinationCalendar, Prisma, User } from "@calcom/prisma/client";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type {
Attendee,
BookingReference,
BookingSeat,
DestinationCalendar,
Prisma,
User,
} from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import { bookingResponses as bookingResponsesSchema } from "@calcom/prisma/zod-utils";
import type { AppsStatus, CalEventResponses, CalendarEvent, Person } from "@calcom/types/Calendar";
import type { VideoCallData } from "@calcom/types/VideoApiAdapter";
import type { TFunction } from "i18next";
const APP_TYPE_TO_NAME_MAP = new Map<string, string>(ALL_APPS.map((app) => [app.type, app.name]));
export type BookingForCalEventBuilder = NonNullable<
Awaited<ReturnType<BookingRepository["getBookingForCalEventBuilder"]>>
type CalendarEventRequiredFields = Required<
Pick<CalendarEvent, "startTime" | "endTime" | "type" | "bookerUrl" | "title" | "organizer" | "attendees">
>;
export type BookingMetaOptions = {
conferenceCredentialId?: number;
platformClientId?: string;
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
};
type CalendarEventBuilderInit = CalendarEventRequiredFields & Partial<CalendarEvent>;
const APP_TYPE_TO_NAME_MAP = new Map<string, string>(ALL_APPS.map((app) => [app.type, app.name]));
async function _buildPersonFromUser(
user: Pick<User, "id" | "name" | "locale" | "username" | "email" | "timeFormat" | "timeZone">
@@ -65,14 +65,26 @@ async function _buildPersonFromAttendee(
} satisfies Person;
}
export class CalendarEventBuilder {
private event: Partial<CalendarEvent>;
export type BuiltCalendarEvent = Omit<CalendarEvent, "bookerUrl"> & { bookerUrl: string };
export type BookingForCalEventBuilder = NonNullable<
Awaited<ReturnType<BookingRepository["getBookingForCalEventBuilder"]>>
>;
export type BookingMetaOptions = {
conferenceCredentialId?: number;
platformClientId?: string;
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
};
constructor(existingEvent?: Partial<CalendarEvent>) {
this.event = existingEvent || {};
export class CalendarEventBuilder {
private event: CalendarEventBuilderInit;
constructor(existingEvent: CalendarEventBuilderInit) {
this.event = existingEvent;
}
static fromEvent(event: Partial<CalendarEvent>) {
static fromEvent(event: CalendarEventBuilderInit) {
return new CalendarEventBuilder(event);
}
@@ -88,7 +100,6 @@ export class CalendarEventBuilder {
if (!user) throw new Error(`Booking ${uid} is missing an organizer — user may have been deleted.`);
if (!eventType) throw new Error(`Booking ${uid} is missing eventType — it may have been deleted.`);
const builder = new CalendarEventBuilder();
const {
description,
attendees,
@@ -146,18 +157,21 @@ export class CalendarEventBuilder {
const recurring = parseRecurringEvent(eventType.recurringEvent) ?? undefined;
const builder = new CalendarEventBuilder({
bookerUrl,
title,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
type: eventType.slug,
organizer: organizerPerson,
attendees: attendeesList,
additionalNotes,
});
// Base builder setup
builder
.withBasicDetails({
bookerUrl,
title,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
additionalNotes,
})
.withEventType({
id: eventType.id,
slug: eventType.slug,
description: eventType.description,
hideCalendarNotes: eventType.hideCalendarNotes,
hideCalendarEventDetails: eventType.hideCalendarEventDetails,
@@ -170,8 +184,6 @@ export class CalendarEventBuilder {
disableRescheduling: eventType.disableRescheduling ?? false,
disableCancelling: eventType.disableCancelling ?? false,
})
.withOrganizer(organizerPerson)
.withAttendees(attendeesList)
.withMetadataAndResponses({
additionalNotes,
customInputs: parsedCustomInputs,
@@ -227,7 +239,7 @@ export class CalendarEventBuilder {
}
// Video
if (videoCallData && videoCallData.url) {
if (videoCallData?.url) {
builder.withVideoCallData({
...videoCallData,
id: videoCallData.id ?? "",
@@ -237,7 +249,7 @@ export class CalendarEventBuilder {
}
references
.filter((r) => r && r.type)
.filter((r) => r?.type)
.forEach((ref) => {
const appName = APP_TYPE_TO_NAME_MAP.get(ref.type) || ref.type.replace("_", "-");
appsStatus.push({
@@ -289,32 +301,7 @@ export class CalendarEventBuilder {
return builder;
}
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;
@@ -330,7 +317,6 @@ export class CalendarEventBuilder {
}) {
this.event = {
...this.event,
type: eventType.slug,
description: eventType.description,
eventTypeId: eventType.id,
hideCalendarNotes: eventType.hideCalendarNotes,
@@ -348,43 +334,6 @@ export class CalendarEventBuilder {
return this;
}
withOrganizer(organizer: {
id: number;
name: string | null;
email: string;
username?: string;
usernameInOrg?: 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,
usernameInOrg: organizer.usernameInOrg,
timeZone: organizer.timeZone,
language: organizer.language,
timeFormat: organizer.timeFormat,
},
};
return this;
}
withAttendees(attendees: Person[]) {
this.event = {
...this.event,
attendees,
};
return this;
}
withMetadataAndResponses({
additionalNotes,
customInputs,
@@ -491,6 +440,9 @@ export class CalendarEventBuilder {
}
withTeam(team?: { name: string; members: Person[]; id: number }) {
if (!team) {
return this;
}
this.event = {
...this.event,
team,
@@ -522,7 +474,10 @@ export class CalendarEventBuilder {
return this;
}
withRecurringEventId(recurringEventId: string) {
withRecurringEventId(recurringEventId?: string | null) {
if (!recurringEventId) {
return this;
}
this.event = {
...this.event,
existingRecurringEvent: {
@@ -572,18 +527,24 @@ export class CalendarEventBuilder {
return this;
}
build(): CalendarEvent | null {
// Validate required fields
if (
!this.event.startTime ||
!this.event.endTime ||
!this.event.type ||
!this.event.bookerUrl ||
!this.event.title
) {
return null;
}
withVideoCallDataFromReferences(bookingReferences: BookingReference[]): this {
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
return this.event as CalendarEvent;
if (videoCallReference) {
this.event = {
...this.event,
videoCallData: {
type: videoCallReference.type,
id: videoCallReference.meetingId,
password: videoCallReference?.meetingPassword,
url: videoCallReference.meetingUrl,
},
};
}
return this;
}
build(): BuiltCalendarEvent {
return this.event;
}
}
@@ -1,24 +0,0 @@
import type { BookingReference } from "@calcom/prisma/client";
import type { CalendarEvent } from "@calcom/types/Calendar";
/** Updates the evt object with video call data found from booking references
*
* @param bookingReferences
* @param evt
*
* @returns updated evt with video call data
*/
export const addVideoCallDataToEvent = (bookingReferences: BookingReference[], evt: CalendarEvent) => {
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
if (videoCallReference) {
evt.videoCallData = {
type: videoCallReference.type,
id: videoCallReference.meetingId,
password: videoCallReference?.meetingPassword,
url: videoCallReference.meetingUrl,
};
}
return evt;
};
@@ -1,194 +0,0 @@
// or wherever it's from
import { SchedulingType } from "@calcom/prisma/enums";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildEventForTeamEventType } from "../../service/RegularBookingService";
vi.mock("@calcom/i18n/server", () => ({
getTranslation: vi.fn().mockResolvedValue("translated"),
}));
vi.mock("@calcom/prisma", () => {
return {
default: vi.fn(),
prisma: {},
};
});
const withTeamSpy = vi.fn().mockReturnThis();
const withDestinationCalendarSpy = vi.fn().mockReturnThis();
vi.mock("@calcom/features/CalendarEventBuilder", () => {
return {
CalendarEventBuilder: {
fromEvent: vi.fn().mockImplementation((_evt) => ({
withDestinationCalendar: withDestinationCalendarSpy,
withTeam: withTeamSpy,
build: vi.fn().mockImplementation(function () {
return {
destinationCalendar: [],
team: {}, // <- you won't use this result anyway
};
}),
})),
},
};
});
vi.mock("@calcom/app-store/_utils/calendars/processExternalId", () => ({
default: vi.fn((dc) => `external-${dc?.externalId ?? "id"}`),
}));
vi.mock("@calcom/app-store/delegationCredential", () => ({
enrichHostsWithDelegationCredentials: vi.fn(),
getUsersCredentialsIncludeServiceAccountKey: vi.fn(),
getCredentialForSelectedCalendar: vi.fn(),
}));
vi.mock("@calcom/features/abuse-scoring/lib/hooks", () => ({
onEventTypeChange: vi.fn(),
onSignup: vi.fn(),
onBookingCreated: vi.fn(),
}));
vi.mock("@calcom/features/di/watchlist/containers/SpamCheckService.container", () => ({
getSpamCheckService: vi.fn().mockReturnValue({
checkForSpam: vi.fn().mockResolvedValue({ isSpam: false }),
}),
}));
vi.mock("@calcom/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain", () => ({
checkIfFreeEmailDomain: vi.fn().mockResolvedValue(false),
}));
const baseUser = (overrides: Record<string, unknown> = {}) => ({
id: 1,
name: "Alice",
email: "alice@example.com",
timeZone: "Europe/Paris",
locale: "fr",
destinationCalendar: {
id: 123,
integration: "google",
externalId: "ext-123",
primaryEmail: "alice@example.com",
userId: 1,
eventTypeId: null,
credentialId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
},
isFixed: true,
...overrides,
});
describe("buildEventForTeamEventType", () => {
it("throws if schedulingType is null", async () => {
await expect(
buildEventForTeamEventType({
existingEvent: {},
users: [],
organizerUser: { email: "organizer@example.com" },
schedulingType: null,
})
).rejects.toThrow("Scheduling type is required for team event type");
});
it("filters out the organizer", async () => {
await buildEventForTeamEventType({
existingEvent: {},
users: [baseUser({ email: "organizer@example.com" })],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.COLLECTIVE,
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).not.toContain("organizer@example.com");
});
it("includes destinationCalendars for COLLECTIVE", async () => {
await buildEventForTeamEventType({
existingEvent: { destinationCalendar: [] },
users: [baseUser({ id: 2 })],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.COLLECTIVE,
});
const withDestinationCalendarArgs = withDestinationCalendarSpy.mock.calls[0][0];
expect(withDestinationCalendarArgs).not.toHaveLength(0);
});
it("does not include destinationCalendars for ROUND_ROBIN", async () => {
await buildEventForTeamEventType({
existingEvent: { destinationCalendar: [] },
users: [baseUser({ id: 2 })],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.ROUND_ROBIN,
});
const withDestinationCalendarArgs = withDestinationCalendarSpy.mock.calls[0][0];
expect(withDestinationCalendarArgs).toHaveLength(0);
});
it("includes one non-fixed user for ROUND_ROBIN when fixed users exist", async () => {
await buildEventForTeamEventType({
existingEvent: {},
users: [
baseUser({ id: 2, isFixed: false, email: "nonfixed@example.com" }),
baseUser({ id: 3, isFixed: true, email: "fixed@example.com" }),
],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.ROUND_ROBIN,
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed@example.com");
});
it("includes only the first non-fixed user for ROUND_ROBIN when multiple exist", async () => {
await buildEventForTeamEventType({
existingEvent: {},
users: [
baseUser({ id: 3, isFixed: true, email: "fixed@example.com" }),
baseUser({ id: 2, isFixed: false, email: "nonfixed1@example.com" }),
baseUser({ id: 4, isFixed: false, email: "nonfixed2@example.com" }),
],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.ROUND_ROBIN,
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed1@example.com");
expect(memberEmails).toContain("nonfixed2@example.com");
});
it("builds a team with fallback name and id", async () => {
await buildEventForTeamEventType({
existingEvent: {},
users: [baseUser()],
organizerUser: { email: "organizer@example.com" },
schedulingType: SchedulingType.COLLECTIVE,
team: null,
});
// now inspect what was passed into withTeam()
const teamArgs = withTeamSpy.mock.calls[0][0];
expect(teamArgs.name).toBe("Nameless");
expect(teamArgs.id).toBe(0);
});
beforeEach(() => {
vi.clearAllMocks();
});
});
@@ -0,0 +1,176 @@
import { vi, describe, it, expect, beforeEach } from "vitest";
import { SchedulingType } from "@calcom/prisma/enums";
import { computeTeamData } from "../../service/RegularBookingService";
vi.mock("@calcom/i18n/server", () => ({
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
}));
vi.mock("@calcom/prisma", () => {
return {
default: vi.fn(),
prisma: {},
};
});
vi.mock("@calcom/app-store/_utils/calendars/processExternalId", () => ({
default: vi.fn((dc) => `external-${dc?.externalId ?? "id"}`),
}));
const baseUser = (overrides: Record<string, unknown> = {}) => ({
id: 1,
name: "Alice",
email: "alice@example.com",
timeZone: "Europe/Paris",
locale: "fr",
destinationCalendar: {
id: 123,
integration: "google",
externalId: "ext-123",
primaryEmail: "alice@example.com",
userId: 1,
eventTypeId: null,
credentialId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
},
isFixed: true,
...overrides,
});
describe("computeTeamData", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns empty arrays if isTeamEventType is false", async () => {
const result = await computeTeamData({
isTeamEventType: false,
schedulingType: SchedulingType.COLLECTIVE,
users: [baseUser()],
organizerEmail: "organizer@example.com",
});
expect(result.teamMembers).toHaveLength(0);
expect(result.teamDestinationCalendars).toHaveLength(0);
});
it("returns empty arrays if schedulingType is null", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: null,
users: [baseUser()],
organizerEmail: "organizer@example.com",
});
expect(result.teamMembers).toHaveLength(0);
expect(result.teamDestinationCalendars).toHaveLength(0);
});
it("filters out the organizer from team members", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.COLLECTIVE,
users: [baseUser({ email: "organizer@example.com" }), baseUser({ id: 2, email: "member@example.com" })],
organizerEmail: "organizer@example.com",
});
const memberEmails = result.teamMembers.map((m) => m.email);
expect(memberEmails).not.toContain("organizer@example.com");
expect(memberEmails).toContain("member@example.com");
});
it("includes destinationCalendars for COLLECTIVE scheduling type", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.COLLECTIVE,
users: [baseUser({ id: 2, email: "member@example.com" })],
organizerEmail: "organizer@example.com",
});
expect(result.teamDestinationCalendars).toHaveLength(1);
expect(result.teamDestinationCalendars[0].externalId).toBe("external-ext-123");
});
it("does not include destinationCalendars for ROUND_ROBIN scheduling type", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.ROUND_ROBIN,
users: [baseUser({ id: 2, email: "member@example.com" })],
organizerEmail: "organizer@example.com",
});
expect(result.teamDestinationCalendars).toHaveLength(0);
});
it("includes both fixed and non-fixed users for ROUND_ROBIN", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.ROUND_ROBIN,
users: [
baseUser({ id: 2, isFixed: false, email: "nonfixed@example.com" }),
baseUser({ id: 3, isFixed: true, email: "fixed@example.com" }),
],
organizerEmail: "organizer@example.com",
});
const memberEmails = result.teamMembers.map((m) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed@example.com");
});
it("includes all non-fixed users for ROUND_ROBIN when multiple exist", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.ROUND_ROBIN,
users: [
baseUser({ id: 3, isFixed: true, email: "fixed@example.com" }),
baseUser({ id: 2, isFixed: false, email: "nonfixed1@example.com" }),
baseUser({ id: 4, isFixed: false, email: "nonfixed2@example.com" }),
],
organizerEmail: "organizer@example.com",
});
const memberEmails = result.teamMembers.map((m) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed1@example.com");
expect(memberEmails).toContain("nonfixed2@example.com");
});
it("returns team members with correct structure", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.COLLECTIVE,
users: [baseUser({ id: 2, email: "member@example.com", name: "Member Name" })],
organizerEmail: "organizer@example.com",
});
expect(result.teamMembers).toHaveLength(1);
expect(result.teamMembers[0]).toEqual({
id: 2,
email: "member@example.com",
name: "Member Name",
firstName: "",
lastName: "",
timeZone: "Europe/Paris",
language: {
translate: expect.any(Function),
locale: "fr",
},
});
});
it("handles users without destinationCalendar for COLLECTIVE", async () => {
const result = await computeTeamData({
isTeamEventType: true,
schedulingType: SchedulingType.COLLECTIVE,
users: [baseUser({ id: 2, email: "member@example.com", destinationCalendar: null })],
organizerEmail: "organizer@example.com",
});
expect(result.teamMembers).toHaveLength(1);
expect(result.teamDestinationCalendars).toHaveLength(0);
});
});
@@ -1,8 +1,8 @@
import { cloneDeep } from "lodash";
import { sendRescheduledSeatEmailAndSMS } from "@calcom/emails/email-manager";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import type EventManager from "@calcom/features/bookings/lib/EventManager";
import { addVideoCallDataToEvent } from "@calcom/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent";
import { getTranslation } from "@calcom/i18n/server";
import prisma from "@calcom/prisma";
import type { Person, CalendarEvent } from "@calcom/types/Calendar";
@@ -57,7 +57,7 @@ const attendeeRescheduleSeatedBooking = async (
originalRescheduledBooking = null;
const evtWithVideoCallData = originalBookingReferences
? addVideoCallDataToEvent(originalBookingReferences, evt)
? CalendarEventBuilder.fromEvent(evt).withVideoCallDataFromReferences(originalBookingReferences).build()
: evt;
await sendRescheduledSeatEmailAndSMS(evtWithVideoCallData, seatAttendee as Person, eventType.metadata);
@@ -103,7 +103,7 @@ const attendeeRescheduleSeatedBooking = async (
await eventManager.updateCalendarAttendees(copyEvent, newTimeSlotBooking);
const copyEventWithVideoCallData = newTimeSlotBooking.references
? addVideoCallDataToEvent(newTimeSlotBooking.references, copyEvent)
? CalendarEventBuilder.fromEvent(copyEvent).withVideoCallDataFromReferences(newTimeSlotBooking.references).build()
: copyEvent;
await sendRescheduledSeatEmailAndSMS(
@@ -8,7 +8,7 @@ import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
import type { createLoggerWithEventDetails } from "../../../handleNewBooking/logger";
import type { SeatedBooking, RescheduleSeatedBookingObject, NewTimeSlotBooking } from "../../types";
@@ -118,7 +118,9 @@ const combineTwoSeatedBookings = async (
evt.attendees = updatedBookingAttendees;
evt = { ...addVideoCallDataToEvent(updatedNewBooking.references, evt), bookerUrl: evt.bookerUrl };
evt = CalendarEventBuilder.fromEvent(evt)
.withVideoCallDataFromReferences(updatedNewBooking.references)
.build();
const copyEvent = cloneDeep(evt);
@@ -5,7 +5,7 @@ import type EventManager from "@calcom/features/bookings/lib/EventManager";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import type { Booking } from "../../../handleNewBooking/createBooking";
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus";
@@ -67,7 +67,9 @@ const moveSeatedBookingToNewTimeSlot = async (
cancellationReason: rescheduleReason,
});
evt = { ...addVideoCallDataToEvent(newBooking.references, evt), bookerUrl: evt.bookerUrl };
evt = CalendarEventBuilder.fromEvent(evt)
.withVideoCallDataFromReferences(newBooking.references)
.build();
const copyEvent = cloneDeep(evt);
+2 -3
View File
@@ -3,6 +3,7 @@ import type z from "zod";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { TraceContext } from "@calcom/lib/tracing";
import type { Prisma } from "@calcom/prisma/client";
import type { BuiltCalendarEvent } from "@calcom/features/CalendarEventBuilder";
import type { AppsStatus, CalendarEvent } from "@calcom/types/Calendar";
import type { BookingEventHandlerService } from "../../onBookingEvents/BookingEventHandlerService";
@@ -29,9 +30,7 @@ export type NewSeatedBookingObject = {
rescheduleUid: string | undefined;
reqBookingUid: string | undefined;
eventType: NewBookingEventType;
evt: Omit<CalendarEvent, "bookerUrl"> & {
bookerUrl: string;
};
evt: BuiltCalendarEvent;
invitee: Invitee;
allCredentials: Awaited<ReturnType<typeof getAllCredentialsIncludeServiceAccountKey>>;
organizerUser: OrganizerUser;
@@ -36,6 +36,7 @@ import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEvent
import type { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService";
import type { BookingRescheduledPayload } from "@calcom/features/bookings/lib/onBookingEvents/types.d";
import type { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker";
import type { BuiltCalendarEvent } from "@calcom/features/CalendarEventBuilder";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
@@ -111,7 +112,6 @@ import { getAllCredentialsIncludeServiceAccountKey } from "../getAllCredentialsF
import { refreshCredentials } from "../getAllCredentialsForUsersOnEvent/refreshCredentials";
import getBookingDataSchema from "../getBookingDataSchema";
import type { LuckyUserService } from "../getLuckyUser";
import { addVideoCallDataToEvent } from "../handleNewBooking/addVideoCallDataToEvent";
import {
buildBookingCreatedAuditData,
buildBookingRescheduledAuditData,
@@ -285,29 +285,34 @@ const buildDryRunEventManager = () => {
};
};
export const buildEventForTeamEventType = async ({
existingEvent: evt,
users,
organizerUser,
type TeamMember = {
id: number;
email: string;
name: string;
firstName: string;
lastName: string;
timeZone: string;
language: { translate: Awaited<ReturnType<typeof getTranslation>>; locale: string };
};
export async function computeTeamData({
isTeamEventType,
schedulingType,
team,
users,
organizerEmail,
}: {
existingEvent: Partial<CalendarEvent>;
isTeamEventType: boolean;
schedulingType: SchedulingType | null;
users: (Pick<User, "id" | "name" | "timeZone" | "locale" | "email"> & {
destinationCalendar: DestinationCalendar | null;
isFixed?: boolean;
})[];
organizerUser: { email: string };
schedulingType: SchedulingType | null;
team?: {
id: number;
name: string;
} | null;
}) => {
// not null assertion.
if (!schedulingType) {
throw new Error("Scheduling type is required for team event type");
organizerEmail: string;
}): Promise<{ teamMembers: TeamMember[]; teamDestinationCalendars: DestinationCalendar[] }> {
if (!isTeamEventType || !schedulingType) {
return { teamMembers: [], teamDestinationCalendars: [] };
}
const teamDestinationCalendars: DestinationCalendar[] = [];
const fixedUsers = users.filter((user) => user.isFixed);
const nonFixedUsers = users.filter((user) => !user.isFixed);
@@ -316,9 +321,8 @@ export const buildEventForTeamEventType = async ({
// Organizer or user owner of this event type it's not listed as a team member.
const teamMemberPromises = filteredUsers
.filter((user) => user.email !== organizerUser.email)
.filter((user) => user.email !== organizerEmail)
.map(async (user) => {
// TODO: Add back once EventManager tests are ready https://github.com/calcom/cal.com/pull/14610#discussion_r1567817120
// push to teamDestinationCalendars if it's a team event but collective only
if (schedulingType === "COLLECTIVE" && user.destinationCalendar) {
teamDestinationCalendars.push({
@@ -326,7 +330,6 @@ export const buildEventForTeamEventType = async ({
externalId: processExternalId(user.destinationCalendar),
});
}
return {
id: user.id,
email: user.email ?? "",
@@ -342,37 +345,8 @@ export const buildEventForTeamEventType = async ({
});
const teamMembers = await Promise.all(teamMemberPromises);
const updatedEvt = CalendarEventBuilder.fromEvent(evt)
?.withDestinationCalendar([...(evt.destinationCalendar ?? []), ...teamDestinationCalendars])
.build();
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with destination calendar due to missing required fields",
});
}
evt = updatedEvt;
const teamEvt = CalendarEventBuilder.fromEvent(evt)
?.withTeam({
members: teamMembers,
name: team?.name || "Nameless",
id: team?.id ?? 0,
})
.build();
if (!teamEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build team event due to missing required fields",
});
}
return teamEvt;
};
return { teamMembers, teamDestinationCalendars };
}
function buildTroubleshooterData({
eventType,
@@ -1520,18 +1494,43 @@ async function handler(
optionValue: "",
};
// Only attach recurring config when this booking belongs to a recurring series.
const computedRecurringEvent =
reqBody.recurringEventId && eventType.recurringEvent
? { ...eventType.recurringEvent, count: recurringCount ?? eventType.recurringEvent.count }
: undefined;
const { teamMembers, teamDestinationCalendars } = await computeTeamData({
isTeamEventType,
schedulingType: eventType.schedulingType,
users,
organizerEmail: organizerUser.email,
});
const teamInfo = eventType.team;
const eventName = getEventName(eventNameObject);
const builtEvt = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl,
title: eventName,
startTime: dayjs(reqBody.start).utc().format(),
endTime: dayjs(reqBody.end).utc().format(),
additionalNotes,
})
let evt: BuiltCalendarEvent = new CalendarEventBuilder({
bookerUrl,
title: eventName,
startTime: dayjs(reqBody.start).utc().format(),
endTime: dayjs(reqBody.end).utc().format(),
type: eventType.slug,
organizer: {
id: organizerUser.id,
name: organizerUser.name || "Nameless",
email: organizerEmail,
username: organizerUser.username || undefined,
usernameInOrg: organizerOrganizationProfile?.username || undefined,
timeZone: organizerUser.timeZone,
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),
},
attendees: attendeesList,
additionalNotes,
})
.withEventType({
slug: eventType.slug,
description: eventType.description,
id: eventType.id,
hideCalendarNotes: eventType.hideCalendarNotes,
@@ -1546,17 +1545,6 @@ async function handler(
disableRescheduling: eventType.disableRescheduling ?? false,
disableCancelling: eventType.disableCancelling ?? false,
})
.withOrganizer({
id: organizerUser.id,
name: organizerUser.name || "Nameless",
email: organizerEmail,
username: organizerUser.username || undefined,
usernameInOrg: organizerOrganizationProfile?.username || undefined,
timeZone: organizerUser.timeZone,
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),
})
.withAttendees(attendeesList)
.withMetadataAndResponses({
additionalNotes,
customInputs,
@@ -1567,7 +1555,11 @@ async function handler(
location: platformBookingLocation ?? bookingLocation, // Will be processed by the EventManager later.
conferenceCredentialId,
})
.withDestinationCalendar(destinationCalendar)
.withDestinationCalendar(
teamDestinationCalendars.length > 0
? [...(destinationCalendar ?? []), ...teamDestinationCalendars]
: destinationCalendar
)
.withIdentifiers({ iCalUID, iCalSequence })
.withConfirmation({
requiresConfirmation: !isConfirmedByDefault,
@@ -1581,6 +1573,17 @@ async function handler(
})
.withOrganization(organizerOrganizationId)
.withHashedLink(hasHashedBookingLink ? (reqBody.hashedLink ?? null) : null)
.withRecurring(computedRecurringEvent ?? undefined)
.withRecurringEventId(input.bookingData.thirdPartyRecurringEventId)
.withTeam(
isTeamEventType
? {
members: teamMembers,
name: teamInfo?.name || "Nameless",
id: teamInfo?.id ?? 0,
}
: undefined
)
.withHideBranding(
await getEventTypeService().shouldHideBrandingForEventType(eventType.id, {
team: eventType.team
@@ -1597,46 +1600,6 @@ async function handler(
)
.build();
if (!builtEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build calendar event due to missing required fields",
});
}
let evt: CalendarEvent = builtEvt;
if (input.bookingData.thirdPartyRecurringEventId) {
const updatedEvt = CalendarEventBuilder.fromEvent(evt)
?.withRecurringEventId(input.bookingData.thirdPartyRecurringEventId)
.build();
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with recurring event ID due to missing required fields",
});
}
evt = updatedEvt;
}
if (isTeamEventType) {
const teamEvt = await buildEventForTeamEventType({
existingEvent: evt,
schedulingType: eventType.schedulingType,
users,
team: eventType.team,
organizerUser,
});
if (!teamEvt) {
throw new HttpError({ statusCode: 400, message: "Failed to build team event" });
}
evt = teamEvt;
}
// data needed for triggering webhooks
const eventTypeInfo: EventTypeInfo = {
eventTitle: eventType.title,
@@ -1799,7 +1762,7 @@ async function handler(
rescheduleUid,
reqBookingUid: reqBody.bookingUid,
eventType,
evt: { ...evt, seatsPerTimeSlot: eventType.seatsPerTimeSlot, bookerUrl },
evt,
invitee,
allCredentials,
organizerUser,
@@ -1874,24 +1837,10 @@ async function handler(
})
.build();
if (!updatedEvt) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with new identifiers due to missing required fields",
});
}
evt = updatedEvt;
}
}
if (reqBody.recurringEventId && eventType.recurringEvent) {
// Overriding the recurring event configuration count to be the actual number of events booked for
// the recurring event (equal or less than recurring event configuration count)
eventType.recurringEvent = Object.assign({}, eventType.recurringEvent, { count: recurringCount });
evt.recurringEvent = eventType.recurringEvent;
}
const changedOrganizer =
!!originalRescheduledBooking &&
(eventType.schedulingType === SchedulingType.ROUND_ROBIN ||
@@ -2002,44 +1951,22 @@ async function handler(
}
}
const updatedEvtWithUid = CalendarEventBuilder.fromEvent(evt)
?.withUid(booking.uid ?? null)
evt = CalendarEventBuilder.fromEvent(evt)
.withUid(booking.uid ?? null)
.build();
if (!updatedEvtWithUid) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with UID due to missing required fields",
});
}
evt = updatedEvtWithUid;
const updatedEvtWithPassword = CalendarEventBuilder.fromEvent(evt)
?.withOneTimePassword(booking.oneTimePassword ?? null)
evt = CalendarEventBuilder.fromEvent(evt)
.withOneTimePassword(booking.oneTimePassword ?? null)
.build();
if (!updatedEvtWithPassword) {
throw new HttpError({
statusCode: 400,
message: "Failed to build event with one-time password due to missing required fields",
});
}
evt = updatedEvtWithPassword;
// Add assignment reason to evt for emails
if (assignmentReason) {
const updatedEvtWithAssignmentReason = CalendarEventBuilder.fromEvent(evt)
?.withAssignmentReason({
evt = CalendarEventBuilder.fromEvent(evt)
.withAssignmentReason({
category: getAssignmentReasonCategory(assignmentReason.reasonEnum),
details: assignmentReason.reasonString ?? null,
})
.build();
if (updatedEvtWithAssignmentReason) {
evt = updatedEvtWithAssignmentReason;
}
}
if (booking && booking.id && eventType.seatsPerTimeSlot) {
@@ -2121,7 +2048,9 @@ async function handler(
// cancel workflow reminders from previous rescheduled booking
await WorkflowRepository.deleteAllWorkflowReminders(originalRescheduledBooking.workflowReminders);
evt = addVideoCallDataToEvent(originalRescheduledBooking.references, evt);
evt = CalendarEventBuilder.fromEvent(evt)
.withVideoCallDataFromReferences(originalRescheduledBooking.references)
.build();
evt.rescheduledBy = reqBody.rescheduledBy;
// If organizer is changed in RR event then we need to delete the previous host destination calendar events
@@ -485,29 +485,28 @@ export class ManagedEventManualReassignmentService {
})
);
const builder = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl,
title: newBooking.title,
startTime: dayjs(newBooking.startTime).utc().format(),
endTime: dayjs(newBooking.endTime).utc().format(),
additionalNotes: newBooking.description || undefined,
})
const builder = new CalendarEventBuilder({
bookerUrl,
title: newBooking.title,
startTime: dayjs(newBooking.startTime).utc().format(),
endTime: dayjs(newBooking.endTime).utc().format(),
type: targetEventTypeDetails.slug,
organizer: {
id: newUser.id,
name: newUser.name || "Nameless",
email: newUser.email,
timeZone: newUser.timeZone,
language: { translate: newUserT, locale: newUser.locale ?? "en" },
},
attendees,
additionalNotes: newBooking.description || undefined,
})
.withEventType({
id: targetEventTypeDetails.id,
slug: targetEventTypeDetails.slug,
description: newBooking.description,
hideOrganizerEmail: targetEventTypeDetails.hideOrganizerEmail,
schedulingType: targetEventTypeDetails.schedulingType,
})
.withOrganizer({
id: newUser.id,
name: newUser.name,
email: newUser.email,
timeZone: newUser.timeZone,
language: { translate: newUserT, locale: newUser.locale ?? "en" },
})
.withAttendees(attendees)
.withLocation({
location: bookingLocation || null,
conferenceCredentialId: conferenceCredentialId ?? undefined,
@@ -698,30 +697,29 @@ export class ManagedEventManualReassignmentService {
})
);
const emailBuilder = new CalendarEventBuilder()
.withBasicDetails({
bookerUrl: bookerUrlForEmail,
title: newBooking.title,
startTime: dayjs(newBooking.startTime).utc().format(),
endTime: dayjs(newBooking.endTime).utc().format(),
additionalNotes: newBooking.description || undefined,
})
.withEventType({
id: targetEventTypeDetails.id,
slug: targetEventTypeDetails.slug,
description: newBooking.description,
schedulingType: parentEventType.schedulingType,
seatsPerTimeSlot: targetEventTypeDetails.seatsPerTimeSlot,
})
.withOrganizer({
const emailBuilder = new CalendarEventBuilder({
bookerUrl: bookerUrlForEmail,
title: newBooking.title,
startTime: dayjs(newBooking.startTime).utc().format(),
endTime: dayjs(newBooking.endTime).utc().format(),
type: targetEventTypeDetails.slug,
organizer: {
id: newUser.id,
name: newUser.name,
name: newUser.name || "Nameless",
email: newUser.email,
timeZone: newUser.timeZone,
timeFormat: getTimeFormatStringFromUserTimeFormat(newUser.timeFormat),
language: { translate: newUserT, locale: newUser.locale ?? "en" },
},
attendees: attendeesForEmail,
additionalNotes: newBooking.description || undefined,
})
.withEventType({
id: targetEventTypeDetails.id,
description: newBooking.description,
schedulingType: parentEventType.schedulingType,
seatsPerTimeSlot: targetEventTypeDetails.seatsPerTimeSlot,
})
.withAttendees(attendeesForEmail)
.withLocation({
location: bookingLocation || null,
})
@@ -1,5 +1,8 @@
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { addVideoCallDataToEvent } from "@calcom/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent";
import type { BuiltCalendarEvent } from "@calcom/features/CalendarEventBuilder";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { getTranslation } from "@calcom/i18n/server";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -12,6 +15,7 @@ const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async
include: {
user: {
select: {
id: true,
name: true,
email: true,
locale: true,
@@ -24,6 +28,11 @@ const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async
select: {
slug: true,
bookingFields: true,
team: {
select: {
parentId: true,
},
},
},
},
attendees: {
@@ -51,6 +60,12 @@ const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async
throw new Error(`event type not found for booking ${bookingUid}`);
}
const organizerOrganizationId = booking.eventType.team?.parentId
? booking.eventType.team.parentId
: await ProfileRepository.findFirstOrganizationIdForUser({
userId: booking.user.id,
});
const bookerUrl = await getBookerBaseUrl(organizerOrganizationId ?? null);
const organizerT = await getTranslation(booking.user?.locale ?? "en", "common");
const attendeePromises = [];
@@ -68,12 +83,13 @@ const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async
const attendeeList = await Promise.all(attendeePromises);
let calendarEvent: CalendarEvent = {
let calendarEvent: BuiltCalendarEvent = {
uid: bookingUid,
type: booking.eventType.slug,
title: booking.title,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
bookerUrl,
organizer: {
email: booking.user.email,
name: booking.user.name || "Nameless",
@@ -89,7 +105,9 @@ const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async
}),
};
calendarEvent = addVideoCallDataToEvent(booking.references, calendarEvent);
calendarEvent = CalendarEventBuilder.fromEvent(calendarEvent)
.withVideoCallDataFromReferences(booking.references)
.build();
return calendarEvent;
};