Skip to content

Commit 2408337

Browse files
fix: Don't force reschedule with same RR host if reschedule is actually rerouting (#17511)
* Dont force reschedule with same RR host if routed there * Add unit tests
1 parent 119edb5 commit 2408337

File tree

7 files changed

+499
-19
lines changed

7 files changed

+499
-19
lines changed

apps/web/test/lib/getSchedule.test.ts

+280-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import dayjs from "@calcom/dayjs";
1919
import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums";
2020
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
2121

22-
import { expect } from "./getSchedule/expects";
22+
import { expect, expectedSlotsForSchedule } from "./getSchedule/expects";
2323
import { setupAndTeardown } from "./getSchedule/setupAndTeardown";
2424
import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils";
2525

@@ -89,7 +89,8 @@ describe("getSchedule", () => {
8989
});
9090
});
9191

92-
describe("Round robin lead skip - CRM", async () => {
92+
// TODO: Move these inside describe('Team Event')
93+
describe("Round robin lead skip(i.e. use contact owner specified by teamMemberEmail) - CRM", async () => {
9394
test("correctly get slots for event with only round robin hosts", async () => {
9495
vi.setSystemTime("2024-05-21T00:00:13Z");
9596

@@ -367,7 +368,7 @@ describe("getSchedule", () => {
367368
}
368369
);
369370
});
370-
test("correctly get slots for event with only round robin hosts", async () => {
371+
test("correctly get slots for event with only round robin hosts - When no availability is found", async () => {
371372
vi.setSystemTime("2024-05-21T00:00:13Z");
372373

373374
const plus1DateString = "2024-05-22";
@@ -496,6 +497,119 @@ describe("getSchedule", () => {
496497
}
497498
);
498499
});
500+
test("Negative case(i.e. skipContactOwner is true) - when teamMemberEmail is provided but not used", async () => {
501+
vi.setSystemTime("2024-05-21T00:00:13Z");
502+
503+
const plus1DateString = "2024-05-22";
504+
const plus2DateString = "2024-05-23";
505+
506+
const crmCredential = {
507+
id: 1,
508+
type: "salesforce_crm",
509+
key: {
510+
clientId: "test-client-id",
511+
},
512+
userId: 1,
513+
teamId: null,
514+
appId: "salesforce",
515+
invalid: false,
516+
user: { email: "test@test.com" },
517+
};
518+
519+
await createCredentials([crmCredential]);
520+
521+
mockCrmApp("salesforce", {
522+
getContacts: [
523+
{
524+
id: "contact-id",
525+
email: "test@test.com",
526+
ownerEmail: "example@example.com",
527+
},
528+
],
529+
createContacts: [{ id: "contact-id", email: "test@test.com" }],
530+
});
531+
532+
await createBookingScenario({
533+
eventTypes: [
534+
{
535+
id: 1,
536+
slotInterval: 60,
537+
length: 60,
538+
hosts: [
539+
{
540+
userId: 101,
541+
isFixed: false,
542+
},
543+
{
544+
userId: 102,
545+
isFixed: false,
546+
},
547+
],
548+
schedulingType: "ROUND_ROBIN",
549+
metadata: {
550+
apps: {
551+
salesforce: {
552+
enabled: true,
553+
appCategories: ["crm"],
554+
roundRobinLeadSkip: true,
555+
},
556+
},
557+
},
558+
},
559+
],
560+
users: [
561+
{
562+
...TestData.users.example,
563+
email: "example@example.com",
564+
id: 101,
565+
schedules: [TestData.schedules.IstEveningShift],
566+
},
567+
{
568+
...TestData.users.example,
569+
email: "example1@example.com",
570+
id: 102,
571+
schedules: [TestData.schedules.IstMorningShift],
572+
defaultScheduleId: 2,
573+
},
574+
],
575+
bookings: [],
576+
});
577+
578+
const scheduleWhenContactOwnerIsSkipped = await getSchedule({
579+
input: {
580+
eventTypeId: 1,
581+
eventTypeSlug: "",
582+
startTime: `${plus1DateString}T18:30:00.000Z`,
583+
endTime: `${plus2DateString}T18:29:59.999Z`,
584+
timeZone: Timezones["+5:30"],
585+
isTeamEvent: true,
586+
teamMemberEmail: "example@example.com",
587+
skipContactOwner: true,
588+
orgSlug: null,
589+
},
590+
});
591+
592+
// Both users slot would be available as contact owner(example@example.com) is skipped and we fallback to all users of RR
593+
expect(scheduleWhenContactOwnerIsSkipped).toHaveTimeSlots(
594+
[
595+
`04:30:00.000Z`,
596+
`05:30:00.000Z`,
597+
`06:30:00.000Z`,
598+
`07:30:00.000Z`,
599+
`08:30:00.000Z`,
600+
`09:30:00.000Z`,
601+
`10:30:00.000Z`,
602+
`11:30:00.000Z`,
603+
`12:30:00.000Z`,
604+
`13:30:00.000Z`,
605+
`14:30:00.000Z`,
606+
`15:30:00.000Z`,
607+
],
608+
{
609+
dateString: plus2DateString,
610+
}
611+
);
612+
});
499613
});
500614

501615
describe("User Event", () => {
@@ -2163,5 +2277,168 @@ describe("getSchedule", () => {
21632277
}
21642278
);
21652279
});
2280+
2281+
test("Reschedule: should reschedule with same host if rescheduleWithSameRoundRobinHost is true", async () => {
2282+
vi.setSystemTime("2024-05-21T00:00:13Z");
2283+
2284+
const plus1DateString = "2024-05-22";
2285+
const plus2DateString = "2024-05-23";
2286+
2287+
// An event with common schedule
2288+
await createBookingScenario({
2289+
eventTypes: [
2290+
{
2291+
id: 1,
2292+
slotInterval: 60,
2293+
length: 60,
2294+
rescheduleWithSameRoundRobinHost: true,
2295+
hosts: [
2296+
{
2297+
userId: 101,
2298+
isFixed: false,
2299+
},
2300+
{
2301+
userId: 102,
2302+
isFixed: false,
2303+
},
2304+
],
2305+
schedulingType: "ROUND_ROBIN",
2306+
},
2307+
],
2308+
users: [
2309+
{
2310+
...TestData.users.example,
2311+
email: "example@example.com",
2312+
id: 101,
2313+
schedules: [TestData.schedules.IstEveningShift],
2314+
defaultScheduleId: 1,
2315+
},
2316+
{
2317+
...TestData.users.example,
2318+
email: "example1@example.com",
2319+
id: 102,
2320+
schedules: [TestData.schedules.IstMorningShift],
2321+
defaultScheduleId: 2,
2322+
},
2323+
],
2324+
bookings: [
2325+
{
2326+
uid: "BOOKING_TO_RESCHEDULE_UID",
2327+
userId: 101,
2328+
attendees: [
2329+
{
2330+
email: "IntegrationTestUser102@example.com",
2331+
},
2332+
],
2333+
eventTypeId: 1,
2334+
status: "ACCEPTED",
2335+
startTime: `${plus2DateString}T04:00:00.000Z`,
2336+
endTime: `${plus2DateString}T04:15:00.000Z`,
2337+
},
2338+
],
2339+
});
2340+
2341+
const schedule = await getSchedule({
2342+
input: {
2343+
eventTypeId: 1,
2344+
eventTypeSlug: "",
2345+
startTime: `${plus1DateString}T18:30:00.000Z`,
2346+
endTime: `${plus2DateString}T18:29:59.999Z`,
2347+
timeZone: Timezones["+5:30"],
2348+
isTeamEvent: true,
2349+
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
2350+
},
2351+
});
2352+
2353+
// expect only slots of IstEveningShift as this is the slots for the original host of the booking
2354+
expect(schedule).toHaveTimeSlots(
2355+
[`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`],
2356+
{
2357+
dateString: plus2DateString,
2358+
}
2359+
);
2360+
});
2361+
2362+
test("Reschedule: should reschedule as per routedTeamMemberIds(instead of same host) even if rescheduleWithSameRoundRobinHost is true but it is a rerouting scenario", async () => {
2363+
vi.setSystemTime("2024-05-21T00:00:13Z");
2364+
2365+
const plus1DateString = "2024-05-22";
2366+
const plus2DateString = "2024-05-23";
2367+
2368+
// An event with common schedule
2369+
await createBookingScenario({
2370+
eventTypes: [
2371+
{
2372+
id: 1,
2373+
slotInterval: 60,
2374+
length: 60,
2375+
rescheduleWithSameRoundRobinHost: true,
2376+
hosts: [
2377+
{
2378+
userId: 101,
2379+
isFixed: false,
2380+
},
2381+
{
2382+
userId: 102,
2383+
isFixed: false,
2384+
},
2385+
],
2386+
schedulingType: "ROUND_ROBIN",
2387+
},
2388+
],
2389+
users: [
2390+
{
2391+
...TestData.users.example,
2392+
email: "example@example.com",
2393+
id: 101,
2394+
schedules: [TestData.schedules.IstEveningShift],
2395+
defaultScheduleId: 1,
2396+
},
2397+
{
2398+
...TestData.users.example,
2399+
email: "example1@example.com",
2400+
id: 102,
2401+
schedules: [TestData.schedules.IstMorningShift],
2402+
defaultScheduleId: 2,
2403+
},
2404+
],
2405+
bookings: [
2406+
{
2407+
uid: "BOOKING_TO_RESCHEDULE_UID",
2408+
userId: 101,
2409+
attendees: [
2410+
{
2411+
email: "IntegrationTestUser102@example.com",
2412+
},
2413+
],
2414+
eventTypeId: 1,
2415+
status: "ACCEPTED",
2416+
startTime: `${plus2DateString}T04:00:00.000Z`,
2417+
endTime: `${plus2DateString}T04:15:00.000Z`,
2418+
},
2419+
],
2420+
});
2421+
2422+
const schedule = await getSchedule({
2423+
input: {
2424+
eventTypeId: 1,
2425+
eventTypeSlug: "",
2426+
startTime: `${plus1DateString}T18:30:00.000Z`,
2427+
endTime: `${plus2DateString}T18:29:59.999Z`,
2428+
timeZone: Timezones["+5:30"],
2429+
isTeamEvent: true,
2430+
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
2431+
routedTeamMemberIds: [102],
2432+
},
2433+
});
2434+
2435+
// expect only slots of IstEveningShift as this is the slots for the original host of the booking
2436+
expect(schedule).toHaveTimeSlots(
2437+
expectedSlotsForSchedule.IstMorningShift.interval["1hr"].allPossibleSlotsStartingAt430,
2438+
{
2439+
dateString: plus2DateString,
2440+
}
2441+
);
2442+
});
21662443
});
21672444
});

apps/web/test/lib/getSchedule/expects.ts

+26
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ export const expectedSlotsForSchedule = {
3030
},
3131
},
3232
},
33+
IstMorningShift: {
34+
interval: {
35+
"1hr": {
36+
allPossibleSlotsStartingAt430: [
37+
"04:30:00.000Z",
38+
"05:30:00.000Z",
39+
"06:30:00.000Z",
40+
"07:30:00.000Z",
41+
"08:30:00.000Z",
42+
"09:30:00.000Z",
43+
"10:30:00.000Z",
44+
"11:30:00.000Z",
45+
],
46+
allPossibleSlotsStartingAt4: [
47+
"04:00:00.000Z",
48+
"05:00:00.000Z",
49+
"06:00:00.000Z",
50+
"07:00:00.000Z",
51+
"08:00:00.000Z",
52+
"09:00:00.000Z",
53+
"10:00:00.000Z",
54+
"11:00:00.000Z",
55+
],
56+
},
57+
},
58+
},
3359
};
3460

3561
declare global {

apps/web/test/utils/bookingScenario/bookingScenario.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,9 @@ export function getOrganizer({
12831283

12841284
export function getScenarioData(
12851285
{
1286+
/**
1287+
* organizer has no special meaning. It is a regular user. It is supposed to be deprecated along with `usersApartFromOrganizer` and we should introduce a new `users` field instead
1288+
*/
12861289
organizer,
12871290
eventTypes,
12881291
usersApartFromOrganizer = [],

packages/features/bookings/lib/handleNewBooking.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -619,10 +619,16 @@ async function handler(
619619
// freeUsers is ensured
620620
const originalRescheduledBookingUserId =
621621
originalRescheduledBooking && originalRescheduledBooking.userId;
622-
const isSameRoundRobinHost =
622+
623+
const isRouting = !!routedTeamMemberIds;
624+
const isRerouting = originalRescheduledBookingUserId && isRouting;
625+
const shouldUseSameRRHost =
623626
!!originalRescheduledBookingUserId &&
624627
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
625-
eventType.rescheduleWithSameRoundRobinHost;
628+
eventType.rescheduleWithSameRoundRobinHost &&
629+
// If it is rerouting, we should not force reschedule with same host.
630+
// It will be unexpected plus could cause unavailable slots as original host might not be part of routedTeamMemberIds
631+
!isRerouting;
626632

627633
const userIdsSet = new Set(users.map((user) => user.id));
628634

@@ -646,7 +652,7 @@ async function handler(
646652
});
647653
}
648654

649-
const newLuckyUser = isSameRoundRobinHost
655+
const newLuckyUser = shouldUseSameRRHost
650656
? freeUsers.find((user) => user.id === originalRescheduledBookingUserId)
651657
: await getLuckyUser({
652658
// find a lucky user that is not already in the luckyUsers array

0 commit comments

Comments
 (0)