* Partial push of new filters * Let's always return the passed host array when a filter doesn't need to be applied * fix: isFixed typing in same round robin host check * Add main bulk of rewritten logic for routed fairness support * Remove console.log * Add getAttributeWithEnabledWeights to AttributeRepository * Add OOO entry repository * Use getOrderedListOfLuckyUsers in lead threshold filtering * Shortfall already includes weight * test against minShortfall * first attempt to fix fallbacks * Added testcase updates * Partially fix filterHosts test * Some test updates * Removed email field from normalization * fix no available users found error * fix unit test * remove fallback from same host reschedule return * fixes and improvements for findQualifiedHosts * include fallback logic in handleNewBooking * use fallback in handleNewBooking * fix for contact owner in handleNewBooking * WIP - comment * pass routingFormResponseId * fix type error * pass routingFormResponse * fixes in treshold logic * remove reason * merge qualified and fallback availabilities * fix type error * add and fix tests * clean up test file * fix filterHostsBySameRoundRobinHost tests * fix findQualifiedHosts.test.ts * fix getRoutedUsers.test.ts * fix tests * fix isRerouting check * fix start time for two weeks check * fix fallback hosts * fix test * fallback hosts should also include qualified hosts * fix type error * fix typos * add contact owner to fallbacks * fix typo * fix qualifiedRRUsers * set fixed users when no hosts * add tets + code clean up * add test for reschedule with same host * fix typo * add more tests for findQualifiedHosts * remove skip from test * add comment * remove unused code * fix fallback users in loadAndValidateUsers * improve naming for fallback * rename missed fallback variable * fix reschedule with same rr host in handleNewBooking * fixes for fallback slots * add more tests + small fixes for contact owner * fix tests and type errors * add logs for fallback * add type:json to logger * Fix small merge issue * fix end time if two weeks are cut off * add more tests for contact owner availability check * add new logic for two weeks fallback * fix reschedule same RR host test --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
671 lines
16 KiB
TypeScript
671 lines
16 KiB
TypeScript
import prismaMock from "../../../tests/libs/__mocks__/prismaMock";
|
|
|
|
import { vi, it, describe, expect, afterEach } from "vitest";
|
|
import type { Mock } from "vitest";
|
|
|
|
import { SchedulingType } from "@calcom/prisma/enums";
|
|
|
|
import { filterHostsByLeadThreshold } from "./filterHostsByLeadThreshold";
|
|
import { findQualifiedHosts } from "./findQualifiedHosts";
|
|
import * as getRoutedUsers from "./getRoutedUsers";
|
|
|
|
// Mock the filterHostsByLeadThreshold function
|
|
vi.mock("./filterHostsByLeadThreshold", () => {
|
|
return {
|
|
filterHostsByLeadThreshold: vi.fn(),
|
|
};
|
|
});
|
|
|
|
// Clear call history after each test
|
|
afterEach(() => {
|
|
(filterHostsByLeadThreshold as Mock).mockClear();
|
|
});
|
|
|
|
describe("findQualifiedHosts", async () => {
|
|
it("should return qualified hosts based on mock of filterHostsByLeadThreshold", async () => {
|
|
const hosts = [
|
|
{
|
|
isFixed: true,
|
|
createdAt: new Date(),
|
|
user: {
|
|
id: 2,
|
|
email: "hellouser2@email.com",
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
priority: undefined,
|
|
weight: undefined,
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: new Date(),
|
|
user: {
|
|
id: 1,
|
|
email: "hellouser@email.com",
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
priority: undefined,
|
|
weight: undefined,
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: new Date(),
|
|
user: {
|
|
id: 3,
|
|
email: "hellouser3@email.com",
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
priority: undefined,
|
|
weight: undefined,
|
|
},
|
|
];
|
|
|
|
const rrHosts = hosts.filter((host) => !host.isFixed);
|
|
const fixedHosts = hosts.filter((host) => host.isFixed);
|
|
const rrHostsAfterFairness = [rrHosts[2]];
|
|
|
|
// Configure the mock return value
|
|
(filterHostsByLeadThreshold as Mock).mockResolvedValue(rrHostsAfterFairness);
|
|
|
|
// Define the input for the test
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toStrictEqual({
|
|
qualifiedRRHosts: rrHostsAfterFairness,
|
|
fixedHosts,
|
|
allFallbackRRHosts: rrHosts,
|
|
});
|
|
});
|
|
|
|
it("should return hosts after valid input with users", async () => {
|
|
const users = [
|
|
{
|
|
email: "hello@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
{
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
];
|
|
|
|
// Define the input for the test
|
|
const eventType = {
|
|
id: 1,
|
|
hosts: [],
|
|
users,
|
|
schedulingType: null,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [],
|
|
fixedHosts: users.map((user) => ({
|
|
user: user,
|
|
isFixed: true,
|
|
email: user.email,
|
|
createdAt: null,
|
|
})),
|
|
});
|
|
|
|
expect(filterHostsByLeadThreshold).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return only the crm contact owner match & other users + contact owner as fallback ", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
weight: 100,
|
|
priority: 2,
|
|
createdAt: oneYearAgo,
|
|
isFixed: false,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
weight: 100,
|
|
priority: 2,
|
|
createdAt: oneYearAgo,
|
|
isFixed: false,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
weight: 100,
|
|
priority: 2,
|
|
createdAt: oneYearAgo,
|
|
isFixed: false,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
(filterHostsByLeadThreshold as Mock).mockResolvedValue(hosts);
|
|
|
|
// Define the input for the test
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: "hello1@gmail.com",
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [
|
|
{
|
|
...hosts[0],
|
|
},
|
|
],
|
|
allFallbackRRHosts: hosts,
|
|
fixedHosts: [],
|
|
});
|
|
});
|
|
|
|
// it("should return only the crm contact owner match & other users + contact owner as fallback (with routing and segment filtering)", async () => {
|
|
|
|
// });
|
|
|
|
it("should return only routed members + contact owner as fallback for crm contact owner match", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
(filterHostsByLeadThreshold as Mock).mockResolvedValue([hosts[0], hosts[1]]);
|
|
|
|
// Define the input for the test
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [1],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: "hello3@gmail.com",
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [hosts[2]],
|
|
allFallbackRRHosts: [hosts[0], hosts[2]],
|
|
fixedHosts: [],
|
|
});
|
|
});
|
|
|
|
it("if it's a reschedule with same host, it should only return this host and the fixed hosts", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: true,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
prismaMock.booking.findFirst.mockResolvedValue({ userId: 2 });
|
|
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [],
|
|
rescheduleUid: "recheduleUid",
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [hosts[1]],
|
|
fixedHosts: [hosts[2]],
|
|
});
|
|
});
|
|
|
|
it("should return early if segment matching results in only one host", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
vi.spyOn(getRoutedUsers, "findMatchingHostsWithEventSegment").mockImplementation(async () => [hosts[0]]);
|
|
|
|
const filterSpy = vi.spyOn(Array.prototype, "filter");
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [0, 1, 2],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// check if we returned early after segment matching only returned one host
|
|
expect(filterSpy).toHaveBeenCalledTimes(2);
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [hosts[0]],
|
|
fixedHosts: [],
|
|
});
|
|
});
|
|
|
|
it("should filter for segment matching and routed team member ids", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
vi.spyOn(getRoutedUsers, "findMatchingHostsWithEventSegment").mockImplementation(async () => [
|
|
hosts[0],
|
|
hosts[1],
|
|
]);
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [2, 3],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [hosts[1]],
|
|
fixedHosts: [],
|
|
});
|
|
});
|
|
|
|
it("should filter for fairness and return fallback with segment filtering and routed team member ids", async () => {
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
|
|
const hosts = [
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello1@gmail.com",
|
|
id: 1,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello2@gmail.com",
|
|
id: 2,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
{
|
|
isFixed: false,
|
|
createdAt: oneYearAgo,
|
|
weight: undefined,
|
|
priority: undefined,
|
|
user: {
|
|
email: "hello3@gmail.com",
|
|
id: 3,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const eventType = {
|
|
id: 1,
|
|
hosts,
|
|
users: [],
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
maxLeadThreshold: null,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
assignAllTeamMembers: true,
|
|
assignRRMembersUsingSegment: false,
|
|
rrSegmentQueryValue: null,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
id: 1,
|
|
parentId: null,
|
|
},
|
|
};
|
|
|
|
vi.spyOn(getRoutedUsers, "findMatchingHostsWithEventSegment").mockImplementation(async () => [
|
|
hosts[0],
|
|
hosts[1],
|
|
hosts[2],
|
|
]);
|
|
|
|
const rrHostsAfterFairness = [hosts[2]];
|
|
|
|
// Configure the mock return value
|
|
(filterHostsByLeadThreshold as Mock).mockResolvedValue(rrHostsAfterFairness);
|
|
|
|
// Call the function under test
|
|
const result = await findQualifiedHosts({
|
|
eventType,
|
|
routedTeamMemberIds: [2, 3],
|
|
rescheduleUid: null,
|
|
contactOwnerEmail: null,
|
|
routingFormResponse: null,
|
|
});
|
|
|
|
// Verify the result
|
|
expect(result).toEqual({
|
|
qualifiedRRHosts: [hosts[2]],
|
|
allFallbackRRHosts: [hosts[1], hosts[2]],
|
|
fixedHosts: [],
|
|
});
|
|
});
|
|
});
|