Files
calendar/packages/features/bookings/lib/handleNewBooking/loadUsers.ts
T
649faf4ff6 fix: Fairness when routing (#18550)
* 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>
2025-02-18 14:59:48 +00:00

138 lines
4.0 KiB
TypeScript

import { Prisma } from "@prisma/client";
import type { IncomingMessage } from "http";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import {
getRoutedUsersWithContactOwnerAndFixedUsers,
findMatchingHostsWithEventSegment,
getNormalizedHosts,
} from "@calcom/lib/bookings/getRoutedUsers";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withSelectedCalendars, UserRepository } from "@calcom/lib/server/repository/user";
import prisma, { userSelect } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { NewBookingEventType } from "./types";
const log = logger.getSubLogger({ prefix: ["[loadUsers]:handleNewBooking "] });
type EventType = Pick<
NewBookingEventType,
| "hosts"
| "users"
| "id"
| "schedulingType"
| "team"
| "assignAllTeamMembers"
| "assignRRMembersUsingSegment"
| "rrSegmentQueryValue"
>;
export const loadUsers = async ({
eventType,
dynamicUserList,
req,
routedTeamMemberIds,
contactOwnerEmail,
}: {
eventType: EventType;
dynamicUserList: string[];
req: IncomingMessage;
routedTeamMemberIds: number[] | null;
contactOwnerEmail: string | null;
}) => {
try {
const { currentOrgDomain } = orgDomainConfig(req);
const users = eventType.id
? await loadUsersByEventType(eventType)
: await loadDynamicUsers(dynamicUserList, currentOrgDomain);
const routedUsers = getRoutedUsersWithContactOwnerAndFixedUsers({
users,
routedTeamMemberIds,
contactOwnerEmail,
});
if (routedUsers.length) {
return routedUsers;
}
return users;
} catch (error) {
log.error("Unable to load users", safeStringify(error));
if (error instanceof HttpError || error instanceof Prisma.PrismaClientKnownRequestError) {
throw new HttpError({ statusCode: 400, message: error.message });
}
throw new HttpError({ statusCode: 500, message: "Unable to load users" });
}
};
const loadUsersByEventType = async (eventType: EventType): Promise<NewBookingEventType["users"]> => {
const { hosts, fallbackHosts } = getNormalizedHosts({
eventType: { ...eventType, hosts: eventType.hosts.filter(Boolean) },
});
const matchingHosts = await findMatchingHostsWithEventSegment({
eventType,
hosts: hosts ?? fallbackHosts,
});
return matchingHosts.map(({ user, isFixed, priority, weight, createdAt }) => ({
...user,
isFixed,
priority,
weight,
createdAt,
}));
};
const loadDynamicUsers = async (dynamicUserList: string[], currentOrgDomain: string | null) => {
if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
throw new Error("dynamicUserList is not properly defined or empty.");
}
return findUsersByUsername({
usernameList: dynamicUserList,
orgSlug: !!currentOrgDomain ? currentOrgDomain : null,
});
};
/**
* This method is mostly same as the one in UserRepository but it includes a lot more relations which are specific requirement here
* TODO: Figure out how to keep it in UserRepository and use it here
*/
export const findUsersByUsername = async ({
usernameList,
orgSlug,
}: {
orgSlug: string | null;
usernameList: string[];
}) => {
log.debug("findUsersByUsername", { usernameList, orgSlug });
const { where, profiles } = await UserRepository._getWhereClauseForFindingUsersByUsername({
orgSlug,
usernameList,
});
return (
await prisma.user.findMany({
where,
select: {
...userSelect.select,
credentials: {
select: credentialForCalendarServiceSelect,
},
metadata: true,
},
})
).map((_user) => {
const user = withSelectedCalendars(_user);
const profile = profiles?.find((profile) => profile.user.id === user.id) ?? null;
return {
...user,
organizationId: profile?.organizationId ?? null,
profile,
};
});
};
export type LoadedUsers = Awaited<ReturnType<typeof loadUsers>>;