Skip to content

Conversation

@hussam789
Copy link

@hussam789 hussam789 commented Oct 30, 2025

User description

PR #3


PR Type

Enhancement, Bug fix


Description

  • Transform destinationCalendar from single object to array to support multiple host destinations in collective bookings

  • Update calendar service implementations to handle array-based destination calendars with credential matching

  • Refactor event manager to process multiple calendar references and credentials for team events

  • Add credential ID parameter to createEvent method for proper calendar selection


Diagram Walkthrough

flowchart LR
  A["destinationCalendar<br/>Single Object"] -->|"Convert to Array"| B["destinationCalendar[]<br/>Multiple Calendars"]
  B -->|"Match by credentialId"| C["GoogleCalendarService<br/>LarkCalendarService<br/>Office365Service"]
  C -->|"Create/Update Events"| D["Calendar Events<br/>Per Credential"]
  E["EventManager<br/>CalendarManager"] -->|"Process Multiple<br/>References"| B
Loading

File Walkthrough

Relevant files
Enhancement
18 files
Calendar.d.ts
Change destinationCalendar to array type                                 
+4/-2     
class.ts
Update destinationCalendar property to array                         
+1/-1     
CalendarManager.ts
Add credentialId parameter and handle array destinations 
+24/-19 
EventManager.ts
Process multiple calendar references and credentials         
+124/-68
CalendarService.ts
Handle array destination calendars with credential matching
+22/-11 
CalendarService.ts
Extract first destination calendar from array                       
+8/-4     
CalendarService.ts
Extract main host destination calendar from array               
+3/-2     
CalendarService.ts
Handle array-based destination calendars in iCal                 
+7/-5     
handleNewBooking.ts
Collect team destination calendars for collective events 
+72/-43 
handleCancelBooking.ts
Process multiple calendar references for deletion               
+65/-44 
webhook.ts
Convert destination calendar to array format                         
+4/-4     
paypal-webhook.ts
Convert destination calendar to array format                         
+5/-1     
confirm.handler.ts
Convert destination calendar to array format                         
+5/-1     
editLocation.handler.ts
Convert destination calendar to array format                         
+5/-1     
requestReschedule.handler.ts
Convert destination calendar to array format                         
+3/-1     
deleteCredential.handler.ts
Convert destination calendar to array format                         
+5/-1     
BrokenIntegrationEmail.tsx
Extract main host destination calendar from array               
+3/-2     
bookingReminder.ts
Convert destination calendar to array format                         
+2/-2     
Tests
1 files
webhook.e2e.ts
Update test expectations for array destination calendar   
+1/-1     
Bug_fix
1 files
create.handler.ts
Fix conditional logic for billing configuration                   
+2/-2     
Formatting
1 files
builder.ts
Refactor import statement organization                                     
+2/-1     
Additional files
1 files
EventManager.d.ts +1/-0     

@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Unsafe calendar fallback

Description: When no matching destination calendar is found for the provided credentialId, the code
falls back to using the "primary" calendar, which may lead to events being created on an
unintended calendar without explicit confirmation.
CalendarService.ts [147-155]

Referred Code
const selectedCalendar = calEventRaw.destinationCalendar?.find(
  (cal) => cal.credentialId === credentialId
)?.externalId;

calendar.events.insert(
  {
    auth: myGoogleAuth,
    calendarId: selectedCalendar || "primary",
    requestBody: payload,
Calendar ownership validation

Description: Missing explicit validation of destinationCalendar array items (e.g., ensuring
credentialId/externalId belong to the organizer or team) could allow misrouting events to
unauthorized calendars if upstream data is tampered.
EventManager.ts [329-383]

Referred Code
 *
 * When the optional uid is set, it will be used instead of the auto generated uid.
 *
 * @param event
 * @param noMail
 * @private
 */
private async createAllCalendarEvents(event: CalendarEvent) {
  let createdEvents: EventResult<NewCalendarEventType>[] = [];
  if (event.destinationCalendar && event.destinationCalendar.length > 0) {
    for (const destination of event.destinationCalendar) {
      if (destination.credentialId) {
        let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
        if (!credential) {
          // Fetch credential from DB
          const credentialFromDB = await prisma.credential.findUnique({
            include: {
              app: {
                select: {
                  slug: true,
                },


 ... (clipped 34 lines)
Broad deletion risk

Description: Cancel flow iterates over all calendar references and may delete events across all
connected calendar credentials when references lack credentialId, risking unintended
deletions if references are inconsistent or manipulated.
handleCancelBooking.ts [418-483]

Referred Code
const bookingCalendarReference = bookingToDelete.references.filter((reference) =>
  reference.type.includes("_calendar")
);

if (bookingCalendarReference.length > 0) {
  for (const reference of bookingCalendarReference) {
    const { credentialId, uid, externalCalendarId } = reference;
    // If the booking calendar reference contains a credentialId
    if (credentialId) {
      // Find the correct calendar credential under user credentials
      let calendarCredential = bookingToDelete.user.credentials.find(
        (credential) => credential.id === credentialId
      );
      if (!calendarCredential) {
        // get credential from DB
        const foundCalendarCredential = await prisma.credential.findUnique({
          where: {
            id: credentialId,
          },
        });
        if (foundCalendarCredential) {


 ... (clipped 45 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: New flows creating/updating/deleting calendar events across multiple credentials lack
explicit audit logs of actor, action, and outcome, making it unclear if critical actions
are recorded.

Referred Code
 *
 * When the optional uid is set, it will be used instead of the auto generated uid.
 *
 * @param event
 * @param noMail
 * @private
 */
private async createAllCalendarEvents(event: CalendarEvent) {
  let createdEvents: EventResult<NewCalendarEventType>[] = [];
  if (event.destinationCalendar && event.destinationCalendar.length > 0) {
    for (const destination of event.destinationCalendar) {
      if (destination.credentialId) {
        let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
        if (!credential) {
          // Fetch credential from DB
          const credentialFromDB = await prisma.credential.findUnique({
            include: {
              app: {
                select: {
                  slug: true,
                },


 ... (clipped 261 lines)
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Swallowed errors: The createEvent path catches errors and returns undefined on 404 and other cases without
surfacing actionable context, risking silent failures across multi-calendar creation.

Referred Code
): Promise<EventResult<NewCalendarEventType>> => {
  const uid: string = getUid(calEvent);
  const calendar = await getCalendar(credential);
  let success = true;
  let calError: string | undefined = undefined;

  // Check if the disabledNotes flag is set to true
  if (calEvent.hideCalendarNotes) {
    calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
  }

  // TODO: Surface success/error messages coming from apps to improve end user visibility
  const creationResult = calendar
    ? await calendar
        .createEvent(calEvent, credential.id)
        .catch(async (error: { code: number; calError: string }) => {
          success = false;
          /**
           * There is a time when selectedCalendar externalId doesn't match witch certain credential
           * so google returns 404.
           * */


 ... (clipped 27 lines)
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Potential PII logging: The error log in createEvent logs the entire event object which may include PII, risking
exposure of sensitive data in logs.

Referred Code
? await calendar
    .createEvent(calEvent, credential.id)
    .catch(async (error: { code: number; calError: string }) => {
      success = false;
      /**
       * There is a time when selectedCalendar externalId doesn't match witch certain credential
       * so google returns 404.
       * */
      if (error?.code === 404) {
        return undefined;
      }
      if (error?.calError) {
        calError = error.calError;
      }
      log.error("createEvent failed", JSON.stringify(error), calEvent);
      // @TODO: This code will be off till we can investigate an error with it
      //https://github.com/calcom/cal.com/issues/3949
      // await sendBrokenIntegrationEmail(calEvent, "calendar");
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Limited validation: Newly added user loading and team destination calendar aggregation rely on database values
without visible validation or sanitization for fields like usernames and
destinationCalendar arrays.

Referred Code
    message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
  };
  log.warn({
    message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
  });
  throw new HttpError({ statusCode: 400, message: error.message });
}

const loadUsers = async () => {
  try {
    if (!eventTypeId) {
      if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
        throw new Error("dynamicUserList is not properly defined or empty.");
      }

      const users = await prisma.user.findMany({
        where: {
          username: { in: dynamicUserList },
        },
        select: {
          ...userSelect.select,


 ... (clipped 345 lines)
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Inconsistently implement multi-calendar event creation

The PR inconsistently implements multi-calendar event creation. While
GoogleCalendarService uses the new credentialId to select the correct calendar,
other services like LarkCalendarService and Office365CalendarService default to
the first calendar, which needs to be fixed.

Examples:

packages/app-store/googlecalendar/lib/CalendarService.ts [87-149]
  async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
    const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
      ...rest,
      responseStatus: "accepted",
    }));
    // TODO: Check every other CalendarService for team members
    const teamMembers =
      calEventRaw.team?.members.map((m) => ({
        email: m.email,
        displayName: m.name,

 ... (clipped 53 lines)
packages/app-store/larkcalendar/lib/CalendarService.ts [125-129]
  async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
    let eventId = "";
    let eventRespData;
    const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
    const calendarId = mainHostDestinationCalendar?.externalId;

Solution Walkthrough:

Before:

// In GoogleCalendarService (correctly implemented)
class GoogleCalendarService {
  async createEvent(calEvent: CalendarEvent, credentialId: number) {
    const selectedCalendar = calEvent.destinationCalendar?.find(
      (cal) => cal.credentialId === credentialId
    )?.externalId;
    // ... creates event on the correct user's calendar
  }
}

// In LarkCalendarService and others (incorrectly implemented)
class LarkCalendarService {
  async createEvent(event: CalendarEvent) { // Missing credentialId
    const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
    const calendarId = mainHostDestinationCalendar?.externalId;
    // ... always creates event on the first calendar in the list
  }
}

After:

// In GoogleCalendarService (correct)
class GoogleCalendarService {
  async createEvent(calEvent: CalendarEvent, credentialId: number) {
    const selectedCalendar = calEvent.destinationCalendar?.find(
      (cal) => cal.credentialId === credentialId
    )?.externalId;
    // ... creates event on the correct user's calendar
  }
}

// In LarkCalendarService and others (corrected)
class LarkCalendarService {
  async createEvent(event: CalendarEvent, credentialId: number) { // Added credentialId
    const selectedCalendar = event.destinationCalendar?.find(
      (cal) => cal.credentialId === credentialId
    )?.externalId;
    // ... creates event on the correct user's calendar
  }
}
Suggestion importance[1-10]: 9

__

Why: This is a critical flaw, as the core feature of supporting multiple destination calendars for collective events is only correctly implemented for GoogleCalendarService, leaving it broken for other major calendar integrations.

High
Possible issue
Fix incorrect calendar selection logic

In updateEvent, fix the calendar selection logic by using this.credential.id to
find the correct destination calendar instead of incorrectly using
externalCalendarId in the find condition.

packages/app-store/googlecalendar/lib/CalendarService.ts [254-256]

-const selectedCalendar = externalCalendarId
-  ? externalCalendarId
-  : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+const selectedCalendar =
+  externalCalendarId ||
+  event.destinationCalendar?.find((cal) => cal.credentialId === this.credential.id)?.externalId;
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a logical flaw where externalCalendarId (which is null or undefined) is used to find a calendar, which will always fail. The proposed fix to use this.credential.id is a valid approach to correctly identify the calendar associated with the current credential.

Medium
Preserve original error in catch block

In the loadUsers function's catch block, re-throw HttpError and
Prisma.PrismaClientKnownRequestError instances directly to preserve their
original context and status codes.

packages/features/bookings/lib/handleNewBooking.ts [724-763]

-const loadUsers = async () =>
-  !eventTypeId
-    ? await prisma.user.findMany({
+const loadUsers = async () => {
+  try {
+    if (!eventTypeId) {
+      if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
+        throw new Error("dynamicUserList is not properly defined or empty.");
+      }
+
+      const users = await prisma.user.findMany({
         where: {
-          username: {
-            in: dynamicUserList,
-          },
+          username: { in: dynamicUserList },
         },
         select: {
           ...userSelect.select,
-          credentials: true, // Don't leak to client
+          credentials: true,
           metadata: true,
-          organization: {
-            select: {
-              slug: true,
-            },
-          },
         },
-      })
-    : eventType.hosts?.length
-    ? eventType.hosts.map(({ user, isFixed }) => ({
+      });
+
+      return users;
+    } else {
+      const hosts = eventType.hosts || [];
+
+      if (!Array.isArray(hosts)) {
+        throw new Error("eventType.hosts is not properly defined.");
+      }
+
+      const users = hosts.map(({ user, isFixed }) => ({
         ...user,
         isFixed,
-      }))
-    : eventType.users || [];
+      }));
 
+      return users.length ? users : eventType.users;
+    }
+  } catch (error) {
+    if (error instanceof HttpError || error instanceof Prisma.PrismaClientKnownRequestError) {
+      throw error;
+    }
+    throw new HttpError({ statusCode: 500, message: "Unable to load users" });
+  }
+};
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that re-throwing specific errors (HttpError, Prisma.PrismaClientKnownRequestError) instead of wrapping them in a new generic HttpError would preserve valuable error context and status codes, improving error handling and debugging.

Medium
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants