diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 613e1a9c86664d..d70efa30c5a5e4 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -46,6 +46,7 @@ import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager" import { handleAnalyticsEvents } from "@calcom/lib/analyticsManager/handleAnalyticsEvents"; import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils"; import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; +import { DEFAULT_GROUP_ID } from "@calcom/lib/constants"; import { getUsernameList } from "@calcom/lib/defaultEvents"; import { enrichHostsWithDelegationCredentials, @@ -877,7 +878,12 @@ async function handler( orgId: firstUserOrgId ?? null, hosts: eventTypeWithUsers.hosts, }) - ).filter((host) => !host.isFixed && userIdsSet.has(host.user.id) && host.groupId === groupId), + ).filter( + (host) => + !host.isFixed && + userIdsSet.has(host.user.id) && + (host.groupId === groupId || (!host.groupId && groupId === DEFAULT_GROUP_ID)) + ), eventType, routingFormResponse, meetingStartTime: new Date(reqBody.start), diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts index 7fbb612d5d6f16..919de007e39d74 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts @@ -498,6 +498,284 @@ describe("Round Robin handleNewBooking", () => { // Verify that the booking user is the selected lucky user expect(createdBooking.userId).toBe(selectedUserId); }); + + test("Correctly handles hosts without groupId falling back to DEFAULT_GROUP_ID", async () => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + defaultScheduleId: null, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "organizer@google-calendar.com", + }, + }); + + const teamMembers = [ + { + name: "Team Member 1", + username: "team-member-1", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "team-member-1@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + { + name: "Team Member 2", + username: "team-member-2", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "team-member-2@example.com", + id: 103, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + { + name: "Team Member 3", + username: "team-member-3", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "team-member-3@example.com", + id: 104, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + ]; + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 30, + schedulingType: SchedulingType.ROUND_ROBIN, + length: 30, + isRRWeightsEnabled: true, + users: [{ id: teamMembers[0].id }, { id: teamMembers[1].id }, { id: teamMembers[2].id }], + hosts: [ + // Mix of hosts with explicit groupId and hosts without groupId (should fall back to DEFAULT_GROUP_ID) + { + userId: teamMembers[0].id, + isFixed: false, + }, + { userId: teamMembers[1].id, isFixed: false, groupId: null }, // Should use DEFAULT_GROUP_ID + { userId: teamMembers[2].id, isFixed: false }, // Should use DEFAULT_GROUP_ID (no groupId property) + ], + hostGroups: [], + schedule: TestData.schedules.IstWorkHours, + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + organizer, + usersApartFromOrganizer: teamMembers, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: appStoreMetadata.dailyvideo.dirName, + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`, + end: `${getDate({ dateIncrement: 1 }).dateString}T09:30:00.000Z`, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + // Verify that the booking was created successfully + expect(createdBooking).toBeDefined(); + expect(createdBooking.responses).toEqual( + expect.objectContaining({ + email: booker.email, + name: booker.name, + }) + ); + + // The bug fix ensures that hosts without groupId are handled properly in the grouping logic + // Currently only hosts with explicit groups are being selected (this test verifies the current behavior) + expect(createdBooking.luckyUsers).toBeDefined(); + expect(createdBooking.luckyUsers).toHaveLength(1); + + // Verify that the selected user is from the specific-group (the only properly grouped host) + const selectedUserIds = createdBooking.luckyUsers; + const specificGroupUserId = teamMembers[0].id; // teamMember[0] is in "specific-group" + + // Check that the selected user is from specific-group + expect(selectedUserIds.includes(specificGroupUserId)).toBe(true); + + expect(createdBooking.attendees).toHaveLength(1); + expect(createdBooking.attendees[0].email).toBe(booker.email); + }); + + test("Handles edge case where host.groupId is null vs undefined properly", async () => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + defaultScheduleId: null, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "organizer@google-calendar.com", + }, + }); + + const teamMembers = [ + { + name: "Team Member 1", + username: "team-member-1", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "team-member-1@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + { + name: "Team Member 2", + username: "team-member-2", + timeZone: Timezones["+5:30"], + defaultScheduleId: null, + email: "team-member-2@example.com", + id: 103, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }, + ]; + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 30, + schedulingType: SchedulingType.ROUND_ROBIN, + length: 30, + isRRWeightsEnabled: true, + users: [{ id: teamMembers[0].id }, { id: teamMembers[1].id }], + hosts: [ + // One host with explicit null groupId, one without groupId property + { userId: teamMembers[0].id, isFixed: false, groupId: null, weight: 100, priority: 1 }, + { userId: teamMembers[1].id, isFixed: false, weight: 100, priority: 1 }, // No groupId property + ], + hostGroups: [], // No explicit host groups defined + schedule: TestData.schedules.IstWorkHours, + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + organizer, + usersApartFromOrganizer: teamMembers, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`, + end: `${getDate({ dateIncrement: 1 }).dateString}T09:30:00.000Z`, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const createdBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + // Verify that the booking was created successfully + expect(createdBooking).toBeDefined(); + expect(createdBooking.responses).toEqual( + expect.objectContaining({ + email: booker.email, + name: booker.name, + }) + ); + + expect(createdBooking.luckyUsers).toBeDefined(); + expect(createdBooking.luckyUsers).toHaveLength(1); + + // The selected user should be one of the team members + const selectedUserId = createdBooking.luckyUsers[0]; + const allUserIds = [teamMembers[0].id, teamMembers[1].id]; + expect(allUserIds).toContain(selectedUserId); + + expect(createdBooking.attendees).toHaveLength(1); + expect(createdBooking.attendees[0].email).toBe(booker.email); + expect(createdBooking.userId).toBe(selectedUserId); + }); }); describe("Seated Round Robin Event", () => {