refactor: circular deps between app store and lib [6] (#23971)
* move delegation credential repository to features * mv credential repository to features * update imports * mv * fix * fix * fix * fix * fix * update imports * update imports * update eslint rule * fix * fix * mv getConnectedDestinationCalendars * fix import errors * mv getCalendarsEvents * remove getUsersCredentials * wip * revert eslint rule change for now * fix type checks * fix * format * cleanup * fix * fix * fix * fix * fix tests * migrate getUserAvailability * migrate * fix tests * fix type checks * fix * fix * migrate crmManager * update imports * migrate raqbUtils to appstore * migrate getLuckyUser to features * migrate findTeamMembersMatchingAttributeLogic to appstore * update imports * fix * fix * fix test * fix unit tests * fix * fix * add eslint config
This commit is contained in:
1 parent
445b307972
commit
bb68cd73ef
114 files changed
+263
-245
No files matched your search
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { getRoutedUsersWithContactOwnerAndFixedUsers } from "./getRoutedUsers";
|
||||
|
||||
vi.mock("@calcom/prisma", () => {
|
||||
return {
|
||||
default: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("getRoutedUsersWithContactOwnerAndFixedUsers", () => {
|
||||
const users = [
|
||||
{ id: 1, email: "user1@example.com", isFixed: false },
|
||||
{ id: 2, email: "user2@example.com", isFixed: true },
|
||||
{ id: 3, email: "user3@example.com", isFixed: false },
|
||||
{ id: 4, email: "owner@example.com", isFixed: false },
|
||||
];
|
||||
|
||||
const usersWithoutFixedHosts = users.map((user) => ({ ...user, isFixed: false }));
|
||||
|
||||
it("should return all users when routedTeamMemberIds is null", () => {
|
||||
const result = getRoutedUsersWithContactOwnerAndFixedUsers({
|
||||
routedTeamMemberIds: null,
|
||||
users,
|
||||
contactOwnerEmail: "owner@example.com",
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
it("should return all users when routedTeamMemberIds is empty - We don't want to enter a scenario where we have no team members to be booked", () => {
|
||||
const result = getRoutedUsersWithContactOwnerAndFixedUsers({
|
||||
routedTeamMemberIds: [],
|
||||
users,
|
||||
contactOwnerEmail: "owner@example.com",
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
it("should filter users based on routedTeamMemberIds, isFixed, and contactOwnerEmail", () => {
|
||||
const result = getRoutedUsersWithContactOwnerAndFixedUsers({
|
||||
routedTeamMemberIds: [1, 3],
|
||||
users,
|
||||
contactOwnerEmail: "owner@example.com",
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ id: 1, email: "user1@example.com", isFixed: false },
|
||||
{ id: 2, email: "user2@example.com", isFixed: true },
|
||||
{ id: 3, email: "user3@example.com", isFixed: false },
|
||||
{ id: 4, email: "owner@example.com", isFixed: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return an empty array when neither fixed, nor routedTeamMemberIds, nor contactOwnerEmail match", () => {
|
||||
const result = getRoutedUsersWithContactOwnerAndFixedUsers({
|
||||
routedTeamMemberIds: [5],
|
||||
users: usersWithoutFixedHosts,
|
||||
contactOwnerEmail: "nonexistent@example.com",
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return fixed hosts even if routedTeamMemberIds and contactOwnerEmail are invalid ", () => {
|
||||
const result = getRoutedUsersWithContactOwnerAndFixedUsers({
|
||||
routedTeamMemberIds: [5],
|
||||
users,
|
||||
contactOwnerEmail: "nonexistent@example.com",
|
||||
});
|
||||
expect(result).toEqual([{ id: 2, email: "user2@example.com", isFixed: true }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
import { enrichHostsWithDelegationCredentials } from "@calcom/app-store/delegationCredential";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { findTeamMembersMatchingAttributeLogic } from "@calcom/app-store/_utils/raqb/findTeamMembersMatchingAttributeLogic";
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { RRResetInterval } from "@calcom/prisma/client";
|
||||
import type { RRTimestampBasis } from "@calcom/prisma/enums";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[getRoutedUsers]"] });
|
||||
|
||||
export const getRoutedUsersWithContactOwnerAndFixedUsers = <
|
||||
T extends { id: number; isFixed?: boolean; email: string }
|
||||
>({
|
||||
routedTeamMemberIds,
|
||||
users,
|
||||
contactOwnerEmail,
|
||||
}: {
|
||||
routedTeamMemberIds: number[] | null;
|
||||
users: T[];
|
||||
contactOwnerEmail: string | null;
|
||||
}) => {
|
||||
// We don't want to enter a scenario where we have no team members to be booked
|
||||
// So, let's just fallback to regular flow if no routedTeamMemberIds are provided
|
||||
if (!routedTeamMemberIds || !routedTeamMemberIds.length) {
|
||||
return users;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"filtering users as per routedTeamMemberIds",
|
||||
safeStringify({ routedTeamMemberIds, contactOwnerEmail })
|
||||
);
|
||||
return users.filter(
|
||||
(user) => routedTeamMemberIds.includes(user.id) || user.isFixed || user.email === contactOwnerEmail
|
||||
);
|
||||
};
|
||||
|
||||
async function findMatchingTeamMembersIdsForEventRRSegment(eventType: EventType) {
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSegmentationDisabled = !eventType.assignAllTeamMembers || !eventType.assignRRMembersUsingSegment;
|
||||
|
||||
if (isSegmentationDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!eventType.team || !eventType.team.parentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { teamMembersMatchingAttributeLogic } = await findTeamMembersMatchingAttributeLogic({
|
||||
attributesQueryValue: eventType.rrSegmentQueryValue ?? null,
|
||||
teamId: eventType.team.id,
|
||||
orgId: eventType.team.parentId,
|
||||
});
|
||||
if (!teamMembersMatchingAttributeLogic) {
|
||||
return teamMembersMatchingAttributeLogic;
|
||||
}
|
||||
return teamMembersMatchingAttributeLogic.map((member) => member.userId);
|
||||
}
|
||||
|
||||
type BaseUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type BaseHost<User extends BaseUser> = {
|
||||
isFixed: boolean;
|
||||
createdAt: Date;
|
||||
priority?: number | null;
|
||||
weight?: number | null;
|
||||
weightAdjustment?: number | null;
|
||||
user: User;
|
||||
groupId: string | null;
|
||||
};
|
||||
|
||||
export type EventType = {
|
||||
assignAllTeamMembers: boolean;
|
||||
assignRRMembersUsingSegment: boolean;
|
||||
rrSegmentQueryValue: AttributesQueryValue | null | undefined;
|
||||
team: {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
rrResetInterval: RRResetInterval | null;
|
||||
rrTimestampBasis: RRTimestampBasis;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export function getNormalizedHosts<User extends BaseUser, Host extends BaseHost<User>>({
|
||||
eventType,
|
||||
}: {
|
||||
eventType: {
|
||||
schedulingType: SchedulingType | null;
|
||||
hosts?: Host[];
|
||||
users: User[];
|
||||
};
|
||||
}) {
|
||||
if (eventType.hosts?.length && eventType.schedulingType) {
|
||||
return {
|
||||
hosts: eventType.hosts.map((host) => ({
|
||||
isFixed: host.isFixed,
|
||||
user: host.user,
|
||||
priority: host.priority,
|
||||
weight: host.weight,
|
||||
createdAt: host.createdAt,
|
||||
groupId: host.groupId,
|
||||
})),
|
||||
fallbackHosts: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
hosts: null,
|
||||
fallbackHosts: eventType.users.map((user) => {
|
||||
return {
|
||||
isFixed: !eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE,
|
||||
email: user.email,
|
||||
user: user,
|
||||
createdAt: null,
|
||||
groupId: null,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
type BaseUserWithCredentialPayload = BaseUser & { credentials: CredentialPayload[] };
|
||||
export async function getNormalizedHostsWithDelegationCredentials<
|
||||
User extends BaseUserWithCredentialPayload,
|
||||
Host extends BaseHost<User>
|
||||
>({
|
||||
eventType,
|
||||
}: {
|
||||
eventType: {
|
||||
schedulingType: SchedulingType | null;
|
||||
hosts?: Host[];
|
||||
users: User[];
|
||||
teamId?: number;
|
||||
};
|
||||
}) {
|
||||
if (eventType.hosts?.length && eventType.schedulingType) {
|
||||
const hostsWithoutDelegationCredential = eventType.hosts.map((host) => ({
|
||||
isFixed: host.isFixed,
|
||||
user: host.user,
|
||||
priority: host.priority,
|
||||
weight: host.weight,
|
||||
createdAt: host.createdAt,
|
||||
groupId: host.groupId,
|
||||
}));
|
||||
const firstHost = hostsWithoutDelegationCredential[0];
|
||||
const firstUserOrgId = await getOrgIdFromMemberOrTeamId({
|
||||
memberId: firstHost?.user?.id ?? null,
|
||||
teamId: eventType.teamId,
|
||||
});
|
||||
const hostsEnrichedWithDelegationCredential = await enrichHostsWithDelegationCredentials({
|
||||
orgId: firstUserOrgId ?? null,
|
||||
hosts: hostsWithoutDelegationCredential ?? null,
|
||||
});
|
||||
return {
|
||||
hosts: hostsEnrichedWithDelegationCredential,
|
||||
fallbackHosts: null,
|
||||
};
|
||||
} else {
|
||||
const hostsWithoutDelegationCredential = eventType.users.map((user) => {
|
||||
return {
|
||||
isFixed: !eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE,
|
||||
email: user.email,
|
||||
user: user,
|
||||
createdAt: null,
|
||||
};
|
||||
});
|
||||
const firstHost = hostsWithoutDelegationCredential[0];
|
||||
const firstUserOrgId = await getOrgIdFromMemberOrTeamId({
|
||||
memberId: firstHost?.user?.id ?? null,
|
||||
teamId: eventType.teamId,
|
||||
});
|
||||
const hostsEnrichedWithDelegationCredential = await enrichHostsWithDelegationCredentials({
|
||||
orgId: firstUserOrgId ?? null,
|
||||
hosts: hostsWithoutDelegationCredential ?? null,
|
||||
});
|
||||
return {
|
||||
hosts: null,
|
||||
fallbackHosts: hostsEnrichedWithDelegationCredential,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We don't allow fixed hosts when segment matching is enabled
|
||||
// If this ever changes, we need to update this function and return fixed hosts
|
||||
export async function findMatchingHostsWithEventSegment<User extends BaseUser>({
|
||||
eventType,
|
||||
hosts,
|
||||
}: {
|
||||
eventType: EventType;
|
||||
hosts: {
|
||||
isFixed: boolean;
|
||||
user: User;
|
||||
priority?: number | null;
|
||||
weight?: number | null;
|
||||
createdAt: Date | null;
|
||||
groupId: string | null;
|
||||
}[];
|
||||
}) {
|
||||
const matchingRRTeamMembers = await findMatchingTeamMembersIdsForEventRRSegment({
|
||||
...eventType,
|
||||
rrSegmentQueryValue: eventType.rrSegmentQueryValue ?? null,
|
||||
});
|
||||
const segmentedRoundRobinHosts = hosts.filter((host) => {
|
||||
if (!matchingRRTeamMembers) return true;
|
||||
return matchingRRTeamMembers.includes(host.user.id);
|
||||
});
|
||||
return segmentedRoundRobinHosts;
|
||||
}
|
||||
Reference in new issue
Block a user