* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1270 lines
35 KiB
TypeScript
1270 lines
35 KiB
TypeScript
import CalendarManagerMock from "@calcom/features/calendars/lib/__mocks__/CalendarManager";
|
|
import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
|
|
|
|
import { expect, it, describe, vi, beforeAll } from "vitest";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { getLuckyUserService } from "@calcom/features/di/containers/LuckyUser";
|
|
import { buildUser, buildBooking } from "@calcom/lib/test/builder";
|
|
import { RRResetInterval, RRTimestampBasis } from "@calcom/prisma/enums";
|
|
|
|
import { getIntervalStartDate, getIntervalEndDate } from "./getLuckyUser";
|
|
|
|
const luckyUserService = getLuckyUserService();
|
|
|
|
type NonEmptyArray<T> = [T, ...T[]];
|
|
type GetLuckyUserAvailableUsersType = NonEmptyArray<ReturnType<typeof buildUser>>;
|
|
|
|
beforeAll(() => {
|
|
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
|
|
});
|
|
|
|
it("can find lucky user with maximize availability", 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"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T06: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({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: false,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [],
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
});
|
|
|
|
it("can find lucky user with maximize availability and priority ranking", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 2,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T06: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({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
|
|
|
// both users have medium priority (one user has no priority set, default to medium) so pick least recently booked
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: false,
|
|
team: {
|
|
rrResetInterval: RRResetInterval.MONTH,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
},
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [],
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
|
|
const userLowest = buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 0,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
const userMedium = buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 2,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const userHighest = buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 4,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const usersWithPriorities: GetLuckyUserAvailableUsersType = [userLowest, userMedium, userHighest];
|
|
prismaMock.user.findMany.mockResolvedValue(usersWithPriorities);
|
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
// pick the user with the highest priority
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: usersWithPriorities,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: false,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [],
|
|
})
|
|
).resolves.toStrictEqual(usersWithPriorities[2]);
|
|
|
|
const userLow = buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 0,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T02:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
const userHighLeastRecentBooking = buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "tes2t@example.com",
|
|
priority: 3,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const userHighRecentBooking = buildUser({
|
|
id: 3,
|
|
username: "test3",
|
|
name: "Test User 3",
|
|
email: "test3@example.com",
|
|
priority: 3,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const usersWithSamePriorities: GetLuckyUserAvailableUsersType = [
|
|
userLow,
|
|
userHighLeastRecentBooking,
|
|
userHighRecentBooking,
|
|
];
|
|
prismaMock.user.findMany.mockResolvedValue(usersWithSamePriorities);
|
|
prismaMock.booking.findMany.mockResolvedValue([]);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
|
|
// pick the least recently booked user of the two with the highest priority
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: usersWithSamePriorities,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: false,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [],
|
|
})
|
|
).resolves.toStrictEqual(usersWithSamePriorities[1]);
|
|
});
|
|
|
|
describe("maximize availability and weights", () => {
|
|
it("can find lucky user if hosts have same weights", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 3,
|
|
weight: 100,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 3,
|
|
weight: 100,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 2,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 3,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T04:30:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, monthly interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-01T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("can find lucky user if hosts have different weights", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 3,
|
|
weight: 200,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T08:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 3,
|
|
weight: 100,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T08:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 2,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 3,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.DAY, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[0]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, daily interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-20T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("can find lucky user with weights and adjusted weights", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 3,
|
|
weight: 150,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 3,
|
|
weight: 100,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 2,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 3,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T07:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T03:30:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.DAY, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[0]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, daily interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-20T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("applies calibration when user had OOO entries this month", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
bookings: [],
|
|
}),
|
|
buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
bookings: [],
|
|
}),
|
|
];
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([
|
|
{
|
|
start: dayjs().subtract(10, "day").toDate(),
|
|
end: dayjs().subtract(5, "day").toDate(),
|
|
userId: users[0].id,
|
|
},
|
|
]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([
|
|
{
|
|
userId: allRRHosts[0].user.id,
|
|
weight: allRRHosts[0].weight,
|
|
createdAt: allRRHosts[0].createdAt,
|
|
},
|
|
]);
|
|
|
|
// bookings of current month
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 1,
|
|
createdAt: dayjs().subtract(2, "days").toDate(),
|
|
}),
|
|
// happened during OOO of userId 1
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: dayjs().subtract(6, "days").toDate(),
|
|
}),
|
|
// happened during OOO of userId 1
|
|
buildBooking({
|
|
id: 5,
|
|
userId: 2,
|
|
createdAt: dayjs().subtract(7, "days").toDate(),
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, monthly interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-01T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("applies calibration when user had full day calendar events this month", 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"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T06: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"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
data: [
|
|
{
|
|
start: dayjs().utc().startOf("month").toDate(),
|
|
end: dayjs().utc().startOf("month").add(3, "day").toDate(),
|
|
timeZone: "UTC",
|
|
},
|
|
],
|
|
})
|
|
.mockResolvedValue([]);
|
|
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([
|
|
{
|
|
userId: allRRHosts[0].user.id,
|
|
weight: allRRHosts[0].weight,
|
|
createdAt: allRRHosts[0].createdAt,
|
|
},
|
|
]);
|
|
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: dayjs().startOf("month").add(10, "day").toDate(),
|
|
}),
|
|
// happened during OOO
|
|
buildBooking({
|
|
id: 2,
|
|
userId: 2,
|
|
createdAt: dayjs().startOf("month").add(5, "hour").toDate(),
|
|
}),
|
|
// happened during OOO
|
|
buildBooking({
|
|
id: 3,
|
|
userId: 2,
|
|
createdAt: dayjs().startOf("month").add(20, "hour").toDate(),
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, monthly interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-01T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("skips OOO calibration when there is only one host", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
bookings: [],
|
|
}),
|
|
];
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
|
|
// Mock OOO entry for the single host
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([
|
|
{
|
|
start: dayjs().subtract(10, "day").toDate(),
|
|
end: dayjs().subtract(5, "day").toDate(),
|
|
userId: users[0].id,
|
|
},
|
|
]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([
|
|
{
|
|
userId: allRRHosts[0].user.id,
|
|
weight: allRRHosts[0].weight,
|
|
createdAt: allRRHosts[0].createdAt,
|
|
},
|
|
]);
|
|
|
|
// Mock some bookings during the OOO period (though there's only one host)
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: dayjs().subtract(7, "days").toDate(),
|
|
}),
|
|
]);
|
|
|
|
// Should return the only available user without throwing division by zero error
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[0]);
|
|
});
|
|
|
|
it("applies calibration to newly added hosts so they are not penalized unfairly compared to their peers", 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"),
|
|
},
|
|
{
|
|
createdAt: new Date("2022-01-25T06: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"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
const middleOfMonth = new Date(
|
|
Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), 14, 12, 0, 0)
|
|
);
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: middleOfMonth,
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([
|
|
{
|
|
userId: allRRHosts[0].user.id,
|
|
weight: allRRHosts[0].weight,
|
|
createdAt: allRRHosts[0].createdAt,
|
|
},
|
|
]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: dayjs(middleOfMonth).subtract(2, "days").toDate(),
|
|
}),
|
|
buildBooking({
|
|
id: 5,
|
|
userId: 2,
|
|
createdAt: dayjs(middleOfMonth).subtract(5, "days").toDate(),
|
|
}),
|
|
]);
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[1]);
|
|
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([]);
|
|
prismaMock.booking.findMany.mockResolvedValueOnce([
|
|
// Mock 6: All hosts
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: dayjs(middleOfMonth).add(2, "days").toDate(),
|
|
}),
|
|
buildBooking({
|
|
id: 5,
|
|
userId: 2,
|
|
createdAt: dayjs(middleOfMonth).add(5, "days").toDate(),
|
|
}),
|
|
]);
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[0]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, monthly interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-01T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("attribute weights and virtual queues", () => {
|
|
it("prepareQueuesAndAttributesData returns undefined attributeWeights and virtualQueuesData when stubbed", async () => {
|
|
const queuesAndAttributesData = await luckyUserService.prepareQueuesAndAttributesData({
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: {
|
|
parentId: 1,
|
|
rrResetInterval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
},
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [
|
|
{
|
|
user: {
|
|
id: 1,
|
|
email: "test1@example.com",
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
createdAt: new Date(),
|
|
weight: 10,
|
|
},
|
|
{
|
|
user: {
|
|
id: 2,
|
|
email: "test2@example.com",
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
createdAt: new Date(),
|
|
weight: 150,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(queuesAndAttributesData).toEqual({
|
|
attributeWeights: undefined,
|
|
virtualQueuesData: undefined,
|
|
});
|
|
});
|
|
|
|
it("uses attribute weights and counts only bookings within virtual queue", async () => {
|
|
const users: GetLuckyUserAvailableUsersType = [
|
|
buildUser({
|
|
id: 1,
|
|
username: "test1",
|
|
name: "Test User 1",
|
|
email: "test1@example.com",
|
|
priority: 1,
|
|
weight: 150,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
buildUser({
|
|
id: 2,
|
|
username: "test2",
|
|
name: "Test User 2",
|
|
email: "test2@example.com",
|
|
priority: 3,
|
|
weight: 50,
|
|
bookings: [
|
|
{
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
prismaMock.outOfOfficeEntry.findMany.mockResolvedValue([]);
|
|
|
|
prismaMock.user.findMany.mockResolvedValue(users);
|
|
prismaMock.host.findMany.mockResolvedValue([]);
|
|
prismaMock.booking.findMany.mockResolvedValue([
|
|
buildBooking({
|
|
id: 1,
|
|
userId: 1,
|
|
createdAt: new Date("2022-01-25T06:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 3,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
}),
|
|
buildBooking({
|
|
id: 4,
|
|
userId: 2,
|
|
createdAt: new Date("2022-01-25T05:30:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const allRRHosts = [
|
|
{
|
|
user: {
|
|
id: users[0].id,
|
|
email: users[0].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[0].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
{
|
|
user: {
|
|
id: users[1].id,
|
|
email: users[1].email,
|
|
credentials: [],
|
|
userLevelSelectedCalendars: [],
|
|
},
|
|
weight: users[1].weight,
|
|
createdAt: new Date(0),
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: users,
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: true,
|
|
team: {
|
|
parentId: 1,
|
|
rrResetInterval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
},
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts,
|
|
})
|
|
).resolves.toStrictEqual(users[0]);
|
|
|
|
const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
|
|
|
|
// Today: 2021-06-20T11:59:59Z, daily interval
|
|
expect(queryArgs.where?.createdAt).toEqual(
|
|
expect.objectContaining({
|
|
gte: new Date("2021-06-20T00:00:00Z"),
|
|
lte: new Date("2021-06-20T11:59:59.000Z"),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("get interval times", () => {
|
|
it("should get correct interval start time with meeting started timestamp basis and DAY interval", () => {
|
|
const meetingStartTime = new Date("2024-03-15T14:30:00Z");
|
|
const result = getIntervalStartDate({
|
|
interval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.START_TIME,
|
|
meetingStartTime,
|
|
});
|
|
expect(result).toEqual(new Date("2024-03-15T00:00:00Z"));
|
|
});
|
|
|
|
it("should get correct interval start time with meeting started timestamp basis and MONTH interval", () => {
|
|
const meetingStartTime = new Date("2024-03-15T14:30:00Z");
|
|
const result = getIntervalStartDate({
|
|
interval: RRResetInterval.MONTH,
|
|
rrTimestampBasis: RRTimestampBasis.START_TIME,
|
|
meetingStartTime,
|
|
});
|
|
expect(result).toEqual(new Date("2024-03-01T00:00:00Z"));
|
|
});
|
|
|
|
it("should get correct interval start time with created at timestamp basis and DAY interval", () => {
|
|
const result = getIntervalStartDate({
|
|
interval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
});
|
|
expect(result).toEqual(new Date("2021-06-20T00:00:00Z")); // Based on the mocked system time
|
|
});
|
|
|
|
it("should get correct interval start time with created at timestamp basis and MONTH interval", () => {
|
|
const result = getIntervalStartDate({
|
|
interval: RRResetInterval.MONTH,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
});
|
|
expect(result).toEqual(new Date("2021-06-01T00:00:00Z")); // Based on the mocked system time
|
|
});
|
|
|
|
it("should get correct interval end time with meeting started timestamp basis and DAY interval", () => {
|
|
const meetingStartTime = new Date("2024-03-15T14:30:00Z");
|
|
const result = getIntervalEndDate({
|
|
interval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.START_TIME,
|
|
meetingStartTime,
|
|
});
|
|
expect(result).toEqual(new Date("2024-03-15T23:59:59.999Z"));
|
|
});
|
|
|
|
it("should get correct interval end time with meeting started timestamp basis and MONTH interval", () => {
|
|
const meetingStartTime = new Date("2024-03-15T14:30:00Z");
|
|
const result = getIntervalEndDate({
|
|
interval: RRResetInterval.MONTH,
|
|
rrTimestampBasis: RRTimestampBasis.START_TIME,
|
|
meetingStartTime,
|
|
});
|
|
expect(result).toEqual(new Date("2024-03-31T23:59:59.999Z"));
|
|
});
|
|
|
|
it("should get correct interval end time with created at timestamp basis", () => {
|
|
const result = getIntervalEndDate({
|
|
interval: RRResetInterval.DAY,
|
|
rrTimestampBasis: RRTimestampBasis.CREATED_AT,
|
|
});
|
|
expect(result).toEqual(new Date("2021-06-20T11:59:59Z")); // Based on the mocked system time
|
|
});
|
|
});
|
|
|
|
it("returns the single user correctly without fetching data when only one user available", async () => {
|
|
const singleUser = buildUser({
|
|
id: 42,
|
|
username: "singleuser",
|
|
name: "Single User",
|
|
email: "singleuser@example.com",
|
|
bookings: [],
|
|
});
|
|
|
|
// Create spies to verify no data fetching occurs
|
|
const spyCalendar = vi.spyOn(CalendarManagerMock, "getBusyCalendarTimes");
|
|
const spyPrismaUser = vi.spyOn(prismaMock.user, "findMany");
|
|
const spyPrismaHost = vi.spyOn(prismaMock.host, "findMany");
|
|
const spyPrismaBooking = vi.spyOn(prismaMock.booking, "findMany");
|
|
const spyPrismaOOO = vi.spyOn(prismaMock.outOfOfficeEntry, "findMany");
|
|
|
|
await expect(
|
|
luckyUserService.getLuckyUser({
|
|
availableUsers: [singleUser],
|
|
eventType: {
|
|
id: 1,
|
|
isRRWeightsEnabled: false,
|
|
team: { rrResetInterval: RRResetInterval.MONTH, rrTimestampBasis: RRTimestampBasis.CREATED_AT },
|
|
includeNoShowInRRCalculation: false,
|
|
},
|
|
allRRHosts: [],
|
|
})
|
|
).resolves.toStrictEqual(singleUser);
|
|
|
|
// Verify no expensive operations were called
|
|
expect(spyCalendar).not.toHaveBeenCalled();
|
|
expect(spyPrismaUser).not.toHaveBeenCalled();
|
|
expect(spyPrismaHost).not.toHaveBeenCalled();
|
|
expect(spyPrismaBooking).not.toHaveBeenCalled();
|
|
expect(spyPrismaOOO).not.toHaveBeenCalled();
|
|
});
|