Skip to content

Commit 759d5b8

Browse files
CarinaWolliCarinaWolli
andauthored
fix error in handleNewBooking (calcom#23011)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent c554396 commit 759d5b8

2 files changed

Lines changed: 285 additions & 1 deletion

File tree

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager"
4646
import { handleAnalyticsEvents } from "@calcom/lib/analyticsManager/handleAnalyticsEvents";
4747
import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils";
4848
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
49+
import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
4950
import { getUsernameList } from "@calcom/lib/defaultEvents";
5051
import {
5152
enrichHostsWithDelegationCredentials,
@@ -877,7 +878,12 @@ async function handler(
877878
orgId: firstUserOrgId ?? null,
878879
hosts: eventTypeWithUsers.hosts,
879880
})
880-
).filter((host) => !host.isFixed && userIdsSet.has(host.user.id) && host.groupId === groupId),
881+
).filter(
882+
(host) =>
883+
!host.isFixed &&
884+
userIdsSet.has(host.user.id) &&
885+
(host.groupId === groupId || (!host.groupId && groupId === DEFAULT_GROUP_ID))
886+
),
881887
eventType,
882888
routingFormResponse,
883889
meetingStartTime: new Date(reqBody.start),

packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,284 @@ describe("Round Robin handleNewBooking", () => {
498498
// Verify that the booking user is the selected lucky user
499499
expect(createdBooking.userId).toBe(selectedUserId);
500500
});
501+
502+
test("Correctly handles hosts without groupId falling back to DEFAULT_GROUP_ID", async () => {
503+
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
504+
const booker = getBooker({
505+
email: "booker@example.com",
506+
name: "Booker",
507+
});
508+
509+
const organizer = getOrganizer({
510+
name: "Organizer",
511+
email: "organizer@example.com",
512+
id: 101,
513+
defaultScheduleId: null,
514+
schedules: [TestData.schedules.IstWorkHours],
515+
credentials: [getGoogleCalendarCredential()],
516+
selectedCalendars: [TestData.selectedCalendars.google],
517+
destinationCalendar: {
518+
integration: TestData.apps["google-calendar"].type,
519+
externalId: "organizer@google-calendar.com",
520+
},
521+
});
522+
523+
const teamMembers = [
524+
{
525+
name: "Team Member 1",
526+
username: "team-member-1",
527+
timeZone: Timezones["+5:30"],
528+
defaultScheduleId: null,
529+
email: "team-member-1@example.com",
530+
id: 102,
531+
schedules: [TestData.schedules.IstWorkHours],
532+
credentials: [getGoogleCalendarCredential()],
533+
selectedCalendars: [TestData.selectedCalendars.google],
534+
},
535+
{
536+
name: "Team Member 2",
537+
username: "team-member-2",
538+
timeZone: Timezones["+5:30"],
539+
defaultScheduleId: null,
540+
email: "team-member-2@example.com",
541+
id: 103,
542+
schedules: [TestData.schedules.IstWorkHours],
543+
credentials: [getGoogleCalendarCredential()],
544+
selectedCalendars: [TestData.selectedCalendars.google],
545+
},
546+
{
547+
name: "Team Member 3",
548+
username: "team-member-3",
549+
timeZone: Timezones["+5:30"],
550+
defaultScheduleId: null,
551+
email: "team-member-3@example.com",
552+
id: 104,
553+
schedules: [TestData.schedules.IstWorkHours],
554+
credentials: [getGoogleCalendarCredential()],
555+
selectedCalendars: [TestData.selectedCalendars.google],
556+
},
557+
];
558+
559+
await createBookingScenario(
560+
getScenarioData({
561+
eventTypes: [
562+
{
563+
id: 1,
564+
slotInterval: 30,
565+
schedulingType: SchedulingType.ROUND_ROBIN,
566+
length: 30,
567+
isRRWeightsEnabled: true,
568+
users: [{ id: teamMembers[0].id }, { id: teamMembers[1].id }, { id: teamMembers[2].id }],
569+
hosts: [
570+
// Mix of hosts with explicit groupId and hosts without groupId (should fall back to DEFAULT_GROUP_ID)
571+
{
572+
userId: teamMembers[0].id,
573+
isFixed: false,
574+
},
575+
{ userId: teamMembers[1].id, isFixed: false, groupId: null }, // Should use DEFAULT_GROUP_ID
576+
{ userId: teamMembers[2].id, isFixed: false }, // Should use DEFAULT_GROUP_ID (no groupId property)
577+
],
578+
hostGroups: [],
579+
schedule: TestData.schedules.IstWorkHours,
580+
destinationCalendar: {
581+
integration: TestData.apps["google-calendar"].type,
582+
externalId: "event-type-1@google-calendar.com",
583+
},
584+
},
585+
],
586+
organizer,
587+
usersApartFromOrganizer: teamMembers,
588+
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
589+
})
590+
);
591+
592+
mockSuccessfulVideoMeetingCreation({
593+
metadataLookupKey: appStoreMetadata.dailyvideo.dirName,
594+
videoMeetingData: {
595+
id: "MOCK_ID",
596+
password: "MOCK_PASS",
597+
url: `http://mock-dailyvideo.example.com/meeting-1`,
598+
},
599+
});
600+
601+
mockCalendarToHaveNoBusySlots("googlecalendar", {
602+
create: {
603+
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
604+
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
605+
},
606+
});
607+
608+
const mockBookingData = getMockRequestDataForBooking({
609+
data: {
610+
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
611+
end: `${getDate({ dateIncrement: 1 }).dateString}T09:30:00.000Z`,
612+
eventTypeId: 1,
613+
responses: {
614+
email: booker.email,
615+
name: booker.name,
616+
location: { optionValue: "", value: BookingLocations.CalVideo },
617+
},
618+
},
619+
});
620+
621+
const createdBooking = await handleNewBooking({
622+
bookingData: mockBookingData,
623+
});
624+
625+
// Verify that the booking was created successfully
626+
expect(createdBooking).toBeDefined();
627+
expect(createdBooking.responses).toEqual(
628+
expect.objectContaining({
629+
email: booker.email,
630+
name: booker.name,
631+
})
632+
);
633+
634+
// The bug fix ensures that hosts without groupId are handled properly in the grouping logic
635+
// Currently only hosts with explicit groups are being selected (this test verifies the current behavior)
636+
expect(createdBooking.luckyUsers).toBeDefined();
637+
expect(createdBooking.luckyUsers).toHaveLength(1);
638+
639+
// Verify that the selected user is from the specific-group (the only properly grouped host)
640+
const selectedUserIds = createdBooking.luckyUsers;
641+
const specificGroupUserId = teamMembers[0].id; // teamMember[0] is in "specific-group"
642+
643+
// Check that the selected user is from specific-group
644+
expect(selectedUserIds.includes(specificGroupUserId)).toBe(true);
645+
646+
expect(createdBooking.attendees).toHaveLength(1);
647+
expect(createdBooking.attendees[0].email).toBe(booker.email);
648+
});
649+
650+
test("Handles edge case where host.groupId is null vs undefined properly", async () => {
651+
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
652+
const booker = getBooker({
653+
email: "booker@example.com",
654+
name: "Booker",
655+
});
656+
657+
const organizer = getOrganizer({
658+
name: "Organizer",
659+
email: "organizer@example.com",
660+
id: 101,
661+
defaultScheduleId: null,
662+
schedules: [TestData.schedules.IstWorkHours],
663+
credentials: [getGoogleCalendarCredential()],
664+
selectedCalendars: [TestData.selectedCalendars.google],
665+
destinationCalendar: {
666+
integration: TestData.apps["google-calendar"].type,
667+
externalId: "organizer@google-calendar.com",
668+
},
669+
});
670+
671+
const teamMembers = [
672+
{
673+
name: "Team Member 1",
674+
username: "team-member-1",
675+
timeZone: Timezones["+5:30"],
676+
defaultScheduleId: null,
677+
email: "team-member-1@example.com",
678+
id: 102,
679+
schedules: [TestData.schedules.IstWorkHours],
680+
credentials: [getGoogleCalendarCredential()],
681+
selectedCalendars: [TestData.selectedCalendars.google],
682+
},
683+
{
684+
name: "Team Member 2",
685+
username: "team-member-2",
686+
timeZone: Timezones["+5:30"],
687+
defaultScheduleId: null,
688+
email: "team-member-2@example.com",
689+
id: 103,
690+
schedules: [TestData.schedules.IstWorkHours],
691+
credentials: [getGoogleCalendarCredential()],
692+
selectedCalendars: [TestData.selectedCalendars.google],
693+
},
694+
];
695+
696+
await createBookingScenario(
697+
getScenarioData({
698+
eventTypes: [
699+
{
700+
id: 1,
701+
slotInterval: 30,
702+
schedulingType: SchedulingType.ROUND_ROBIN,
703+
length: 30,
704+
isRRWeightsEnabled: true,
705+
users: [{ id: teamMembers[0].id }, { id: teamMembers[1].id }],
706+
hosts: [
707+
// One host with explicit null groupId, one without groupId property
708+
{ userId: teamMembers[0].id, isFixed: false, groupId: null, weight: 100, priority: 1 },
709+
{ userId: teamMembers[1].id, isFixed: false, weight: 100, priority: 1 }, // No groupId property
710+
],
711+
hostGroups: [], // No explicit host groups defined
712+
schedule: TestData.schedules.IstWorkHours,
713+
destinationCalendar: {
714+
integration: TestData.apps["google-calendar"].type,
715+
externalId: "event-type-1@google-calendar.com",
716+
},
717+
},
718+
],
719+
organizer,
720+
usersApartFromOrganizer: teamMembers,
721+
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
722+
})
723+
);
724+
725+
mockSuccessfulVideoMeetingCreation({
726+
metadataLookupKey: "dailyvideo",
727+
videoMeetingData: {
728+
id: "MOCK_ID",
729+
password: "MOCK_PASS",
730+
url: `http://mock-dailyvideo.example.com/meeting-1`,
731+
},
732+
});
733+
734+
mockCalendarToHaveNoBusySlots("googlecalendar", {
735+
create: {
736+
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
737+
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
738+
},
739+
});
740+
741+
const mockBookingData = getMockRequestDataForBooking({
742+
data: {
743+
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
744+
end: `${getDate({ dateIncrement: 1 }).dateString}T09:30:00.000Z`,
745+
eventTypeId: 1,
746+
responses: {
747+
email: booker.email,
748+
name: booker.name,
749+
location: { optionValue: "", value: BookingLocations.CalVideo },
750+
},
751+
},
752+
});
753+
754+
const createdBooking = await handleNewBooking({
755+
bookingData: mockBookingData,
756+
});
757+
758+
// Verify that the booking was created successfully
759+
expect(createdBooking).toBeDefined();
760+
expect(createdBooking.responses).toEqual(
761+
expect.objectContaining({
762+
email: booker.email,
763+
name: booker.name,
764+
})
765+
);
766+
767+
expect(createdBooking.luckyUsers).toBeDefined();
768+
expect(createdBooking.luckyUsers).toHaveLength(1);
769+
770+
// The selected user should be one of the team members
771+
const selectedUserId = createdBooking.luckyUsers[0];
772+
const allUserIds = [teamMembers[0].id, teamMembers[1].id];
773+
expect(allUserIds).toContain(selectedUserId);
774+
775+
expect(createdBooking.attendees).toHaveLength(1);
776+
expect(createdBooking.attendees[0].email).toBe(booker.email);
777+
expect(createdBooking.userId).toBe(selectedUserId);
778+
});
501779
});
502780

503781
describe("Seated Round Robin Event", () => {

0 commit comments

Comments
 (0)