Files
calendar/packages/features/users/lib/getRoutedUsers.ts
T
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
75d611c2e8 chore: Integrate creation/rescheduling booking audit for Recurring/regular booking/seated bookings (#26046)
* Integrate creation/rescheduling booking audit

* fix: add missing hostUserUuid to booking audit test data

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix-ci

* feat: enhance booking audit with seat reference

- Added support for seat reference in booking audit actions.
- Updated localization for booking creation to include seat information.
- Modified relevant services to pass attendee seat ID during booking creation.

* fix: update test data to match schema requirements

- Add seatReferenceUid: null to default mock audit log data
- Add seatReferenceUid: null to multiple audit logs test case
- Convert SEAT_RESCHEDULED test data to use numeric timestamps instead of ISO strings

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Allow nullish seatReferenceUid

* feat: enhance booking audit to support rescheduledBy information

- Updated booking audit actions to include rescheduledBy details, allowing tracking of who rescheduled a booking.
- Refactored related services to accommodate the new rescheduledBy parameter in booking events.
- Adjusted type definitions and function signatures to reflect the changes in the booking audit context.

* Avoid possible run time issue

* Fix imoport path

* fix failing test due to merge from main\

* Pass useruuid

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-13 13:16:16 +00:00

217 lines
6.5 KiB
TypeScript

import { enrichHostsWithDelegationCredentials } from "@calcom/app-store/delegationCredential";
import { findTeamMembersMatchingAttributeLogic } from "@calcom/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
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;
uuid: string;
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;
}