Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
const onBookingWriteToRecord = getAppData("onBookingWriteToRecord") ?? false;
const onBookingWriteToRecordFields = getAppData("onBookingWriteToRecordFields") ?? {};
const ignoreGuests = getAppData("ignoreGuests") ?? false;
const excludeSalesforceBookingsFromRR = getAppData("excludeSalesforceBookingsFromRR") ?? false;
const roundRobinSkipFallbackToLeadOwner = getAppData("roundRobinSkipFallbackToLeadOwner") ?? false;
const onCancelWriteToEventRecord = getAppData("onCancelWriteToEventRecord") ?? false;
const onCancelWriteToEventRecordFields = getAppData("onCancelWriteToEventRecordFields") ?? {};
Expand Down Expand Up @@ -440,6 +441,28 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
</>
) : null}

{/* Only show this toggle when the event type is Round Robin */}
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && (
<Section.SubSection>
<Section.SubSectionHeader
icon="refresh-ccw"
title={t("exclude_salesforce_bookings_from_round_robin")}
labelFor="exclude-salesforce-bookings-from-rr">
<Switch
id="exclude-salesforce-bookings-from-rr"
size="sm"
checked={excludeSalesforceBookingsFromRR}
onCheckedChange={(checked) => {
setAppData("excludeSalesforceBookingsFromRR", checked);
}}
/>
</Section.SubSectionHeader>
<p className="text-subtle mt-2 text-sm">
{t("exclude_salesforce_bookings_from_round_robin_description")}
</p>
</Section.SubSection>
)}

<Section.SubSection>
<WriteToObjectSettings
bookingAction={BookingActionEnum.ON_CANCEL}
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/salesforce/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const appDataSchema = eventTypeAppCardZod.extend({
onBookingWriteToRecord: z.boolean().optional(),
onBookingWriteToRecordFields: z.record(z.string(), writeToBookingEntry).optional(),
ignoreGuests: z.boolean().optional(),
excludeSalesforceBookingsFromRR: z.boolean().optional(),
onCancelWriteToEventRecord: z.boolean().optional(),
onCancelWriteToEventRecordFields: z.record(z.string(), writeToBookingEntry).optional(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ const RoundRobinHosts = ({
)}
/>
</>

<AddMembersWithSwitch
placeholder={t("add_a_member")}
teamId={teamId}
Expand Down
111 changes: 111 additions & 0 deletions packages/lib/server/getLuckyUser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { v4 as uuid } from "uuid";
import { expect, it, describe, vi, beforeAll } from "vitest";

import dayjs from "@calcom/dayjs";
import { EventTypeService } from "@calcom/lib/server/service/eventType";
import { buildUser, buildBooking } from "@calcom/lib/test/builder";
import { AttributeType, RRResetInterval, RRTimestampBasis } from "@calcom/prisma/enums";

Expand Down Expand Up @@ -1412,6 +1413,116 @@ describe("attribute weights and virtual queues", () => {
})
);
});

it("should exclude Salesforce bookings from round robin when excludeSalesforceBookingsFromRR is true", async () => {
const users: GetLuckyUserAvailableUsersType = [
buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test1@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T05:30:00.000Z"),
},
],
}),
buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "test2@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
}),
];

CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
prismaMock.user.findMany.mockResolvedValue(users);
prismaMock.host.findMany.mockResolvedValue([]);
prismaMock.booking.findMany.mockResolvedValue([]);

// Mock Salesforce app data with excludeSalesforceBookingsFromRR set to true
const mockEventTypeService = vi.spyOn(EventTypeService, "getEventTypeAppDataFromId");
mockEventTypeService.mockResolvedValue({ excludeSalesforceBookingsFromRR: true });

await getLuckyUser({
availableUsers: users,
eventType: {
id: 1,
isRRWeightsEnabled: false,
team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
});

const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];

// Verify that the query excludes Salesforce assignments
expect(queryArgs.where?.NOT?.assignmentReason?.some?.reasonEnum).toEqual("SALESFORCE_ASSIGNMENT");

mockEventTypeService.mockRestore();
});

it("should include Salesforce bookings in round robin when excludeSalesforceBookingsFromRR is false", async () => {
const users: GetLuckyUserAvailableUsersType = [
buildUser({
id: 1,
username: "test1",
name: "Test User 1",
email: "test1@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T05:30:00.000Z"),
},
],
}),
buildUser({
id: 2,
username: "test2",
name: "Test User 2",
email: "test2@example.com",
bookings: [
{
createdAt: new Date("2022-01-25T04:30:00.000Z"),
},
],
}),
];

CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]);
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
prismaMock.user.findMany.mockResolvedValue(users);
prismaMock.host.findMany.mockResolvedValue([]);
prismaMock.booking.findMany.mockResolvedValue([]);

// Mock Salesforce app data with excludeSalesforceBookingsFromRR set to false
const mockEventTypeService = vi.spyOn(EventTypeService, "getEventTypeAppDataFromId");
mockEventTypeService.mockResolvedValue({ excludeSalesforceBookingsFromRR: false });

await getLuckyUser({
availableUsers: users,
eventType: {
id: 1,
isRRWeightsEnabled: false,
team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
});

const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];

// Verify that the query does NOT exclude Salesforce assignments
expect(queryArgs.where?.NOT?.assignmentReason?.some?.reasonEnum).toBeUndefined();

mockEventTypeService.mockRestore();
});
});

describe("get interval times", () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/lib/server/getLuckyUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { acrossQueryValueCompatiblity } from "@calcom/lib/raqb/raqbUtils";
import { raqbQueryValueSchema } from "@calcom/lib/raqb/zod";
import { safeStringify } from "@calcom/lib/safeStringify";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { EventTypeService } from "@calcom/lib/server/service/eventType";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import type { SelectedCalendar } from "@calcom/prisma/client";
Expand Down Expand Up @@ -479,6 +480,7 @@ async function getBookingsOfInterval({
virtualQueuesData,
interval,
includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR = false,
rrTimestampBasis,
meetingStartTime,
}: {
Expand All @@ -487,6 +489,7 @@ async function getBookingsOfInterval({
virtualQueuesData: VirtualQueuesDataType | null;
interval: RRResetInterval;
includeNoShowInRRCalculation: boolean;
excludeSalesforceBookingsFromRR?: boolean;
rrTimestampBasis: RRTimestampBasis;
meetingStartTime?: Date;
}) {
Expand All @@ -498,6 +501,7 @@ async function getBookingsOfInterval({
endDate: getIntervalEndDate({ interval, rrTimestampBasis, meetingStartTime }),
virtualQueuesData,
includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR,
rrTimestampBasis,
});
}
Expand Down Expand Up @@ -630,6 +634,11 @@ async function fetchAllDataNeededForCalculations<
const startTime = performance.now();

const { availableUsers, allRRHosts, eventType, meetingStartTime } = getLuckyUserParams;

// Get Salesforce app data to check excludeSalesforceBookingsFromRR setting
const salesforceAppData = await EventTypeService.getEventTypeAppDataFromId(eventType.id, "salesforce");
const excludeSalesforceBookingsFromRR = salesforceAppData?.excludeSalesforceBookingsFromRR ?? false;

const notAvailableHosts = (function getNotAvailableHosts() {
const availableUserIds = new Set(availableUsers.map((user) => user.id));
return allRRHosts.reduce(
Expand Down Expand Up @@ -686,6 +695,7 @@ async function fetchAllDataNeededForCalculations<
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR,
rrTimestampBasis,
meetingStartTime,
}),
Expand All @@ -696,6 +706,7 @@ async function fetchAllDataNeededForCalculations<
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR,
rrTimestampBasis,
meetingStartTime,
}),
Expand All @@ -708,6 +719,7 @@ async function fetchAllDataNeededForCalculations<
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR,
rrTimestampBasis,
meetingStartTime,
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/server/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,8 @@
"number_of_options": "{{count}} options",
"reschedule_with_same_round_robin_host_title": "Reschedule with same Round-Robin host",
"reschedule_with_same_round_robin_host_description": "Rescheduled events will be assigned to the same host as initially scheduled",
"exclude_salesforce_bookings_from_round_robin": "Only count Round-Robin assigned bookings",
"exclude_salesforce_bookings_from_round_robin_description": "When enabled, only bookings assigned via Round-Robin logic count towards Round-Robin calculations. Bookings assigned due to Salesforce ownership will not count.",
"disable_input_if_prefilled": "Disable input if the URL identifier is prefilled",
"booking_limits": "Booking Limits",
"booking_limits_team_description": "Booking limits for team members across all team event types",
Expand Down
19 changes: 17 additions & 2 deletions packages/lib/server/repository/booking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { withReporting } from "@calcom/lib/sentryWrapper";
import type { PrismaClient } from "@calcom/prisma";
import { bookingMinimalSelect } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { RRTimestampBasis } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { BookingStatus, AssignmentReasonEnum, RRTimestampBasis } from "@calcom/prisma/enums";

import { UserRepository } from "./user";

Expand Down Expand Up @@ -49,6 +48,7 @@ const buildWhereClauseForActiveBookings = ({
users,
virtualQueuesData,
includeNoShowInRRCalculation = false,
excludeSalesforceBookingsFromRR = false,
rrTimestampBasis,
}: {
eventTypeId: number;
Expand All @@ -63,6 +63,7 @@ const buildWhereClauseForActiveBookings = ({
};
} | null;
includeNoShowInRRCalculation: boolean;
excludeSalesforceBookingsFromRR?: boolean;
rrTimestampBasis: RRTimestampBasis;
}): Prisma.BookingWhereInput => ({
OR: [
Expand Down Expand Up @@ -111,6 +112,17 @@ const buildWhereClauseForActiveBookings = ({
},
}
: {}),
...(excludeSalesforceBookingsFromRR
? {
NOT: {
assignmentReason: {
some: {
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
},
},
},
}
: {}),
});

export class BookingRepository {
Expand Down Expand Up @@ -304,6 +316,7 @@ export class BookingRepository {
endDate,
virtualQueuesData,
includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR = false,
rrTimestampBasis,
}: {
users: { id: number; email: string }[];
Expand All @@ -318,6 +331,7 @@ export class BookingRepository {
};
} | null;
includeNoShowInRRCalculation: boolean;
excludeSalesforceBookingsFromRR: boolean;
rrTimestampBasis: RRTimestampBasis;
}) {
const allBookings = await this.prismaClient.booking.findMany({
Expand All @@ -328,6 +342,7 @@ export class BookingRepository {
users,
virtualQueuesData,
includeNoShowInRRCalculation,
excludeSalesforceBookingsFromRR,
rrTimestampBasis,
}),
select: {
Expand Down
1 change: 1 addition & 0 deletions packages/lib/server/repository/eventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ export class EventTypeRepository {
rrSegmentQueryValue: true,
isRRWeightsEnabled: true,
rescheduleWithSameRoundRobinHost: true,
excludeSalesforceBookingsFromRR: true,
successRedirectUrl: true,
forwardParamsSuccessRedirect: true,
currency: true,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
durationLimits: null,
assignAllTeamMembers: false,
rescheduleWithSameRoundRobinHost: false,
excludeSalesforceBookingsFromRR: false,
price: 0,
currency: "usd",
slotInterval: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "EventType" ADD COLUMN "excludeSalesforceBookingsFromRR" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ model EventType {
/// @zod.custom(imports.eventTypeColor)
eventTypeColor Json?
rescheduleWithSameRoundRobinHost Boolean @default(false)
excludeSalesforceBookingsFromRR Boolean @default(false)

secondaryEmailId Int?
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)
Expand Down
1 change: 1 addition & 0 deletions packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
allowReschedulingPastBookings: true,
hideOrganizerEmail: true,
rescheduleWithSameRoundRobinHost: true,
excludeSalesforceBookingsFromRR: true,
maxLeadThreshold: true,
customReplyToEmail: true,
};
Expand Down
Loading