* feat: include record IDs in Salesforce assignment reason strings - Add recordId parameter to assignmentReasonHandler function - Include Contact ID, Lead ID, and Account ID in assignment reason strings - Update entire call chain to pass record IDs from CRM service - Maintain backward compatibility with optional recordId parameter Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: resolve lint warnings in assignment reason handler implementation - Change Record<string, any> to Record<string, unknown> in BookingHandlerInput type - Remove unused eventTypeId variable in getAttributeRoutingConfig function Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: revert to Record<string, any> with ESLint disable for BookingHandlerInput - Revert from Record<string, unknown> to Record<string, any> to maintain type compatibility - Add ESLint disable comment to suppress no-explicit-any warning - Maintains consistency with handleNewRecurringBooking.ts pattern Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: pass CRM record ID from booker state to handleNewBooking - Add crmRecordId field to booker store interface and initialization - Update mapBookingToMutationInput to include record ID from booker state - Modify handleNewBooking to extract record ID from bookingData parameter - Add crmRecordId to BookingCreateBody schema in Prisma layer - Follow existing pattern for CRM fields (teamMemberEmail, crmOwnerRecordType, crmAppSlug) - Ensures record ID flows: booker store → booking form → mapBookingToMutationInput → handleNewBooking This replaces the previous backend CRM service extraction approach with frontend booker state approach as requested by the user. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Pass crmRecordId as prop --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
236 lines
6.4 KiB
TypeScript
236 lines
6.4 KiB
TypeScript
import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types/types";
|
|
import { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
|
import { acrossQueryValueCompatiblity } from "@calcom/lib/raqb/raqbUtils";
|
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
|
import { getUsersAttributes } from "@calcom/lib/service/attribute/server/getAttributes";
|
|
import prisma from "@calcom/prisma";
|
|
import { AssignmentReasonEnum } from "@calcom/prisma/enums";
|
|
|
|
const { getAttributesQueryValue } = acrossQueryValueCompatiblity;
|
|
|
|
export enum RRReassignmentType {
|
|
ROUND_ROBIN = "round_robin",
|
|
MANUAL = "manual",
|
|
}
|
|
|
|
export default class AssignmentReasonRecorder {
|
|
/**
|
|
* We should use decorators to wrap the methods with withReporting
|
|
* but we can't don't have support for static methods in decorators
|
|
* so this is a workaround to wrap the methods with withReporting.
|
|
*/
|
|
static routingFormRoute = withReporting(
|
|
AssignmentReasonRecorder._routingFormRoute,
|
|
"AssignmentReasonRecorder.routingFormRoute"
|
|
);
|
|
static async _routingFormRoute({
|
|
bookingId,
|
|
routingFormResponseId,
|
|
organizerId,
|
|
teamId,
|
|
isRerouting,
|
|
reroutedByEmail,
|
|
}: {
|
|
bookingId: number;
|
|
routingFormResponseId: number;
|
|
organizerId: number;
|
|
teamId: number;
|
|
isRerouting: boolean;
|
|
reroutedByEmail?: string | null;
|
|
}) {
|
|
// Get the routing form data
|
|
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({
|
|
where: {
|
|
id: routingFormResponseId,
|
|
},
|
|
include: {
|
|
form: {
|
|
select: {
|
|
routes: true,
|
|
fields: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!routingFormResponse) return;
|
|
// Figure out which route was called
|
|
const { form } = routingFormResponse;
|
|
if (!form.routes || !form.fields) return;
|
|
|
|
const parsedRoutes = zodRoutes.safeParse(form.routes);
|
|
|
|
if (!parsedRoutes.success || !parsedRoutes.data) return;
|
|
|
|
const chosenRouteId = routingFormResponse.chosenRouteId;
|
|
|
|
const takenRoute = parsedRoutes.data.find((route) => route.id === chosenRouteId);
|
|
|
|
if (!takenRoute) return;
|
|
|
|
// Get a user's attributes
|
|
const usersAttributes = await getUsersAttributes({ userId: organizerId, teamId });
|
|
|
|
if (!("attributesQueryValue" in takenRoute)) return;
|
|
|
|
const formAttributesQuery = takenRoute.attributesQueryValue;
|
|
|
|
// Figure out the attributes associated with that route
|
|
const attributesQueryValue = getAttributesQueryValue({
|
|
attributesQueryValue: formAttributesQuery,
|
|
attributes: usersAttributes,
|
|
dynamicFieldValueOperands: {
|
|
fields: (form.fields as Fields) || [],
|
|
response: routingFormResponse.response as FormResponse,
|
|
},
|
|
});
|
|
|
|
if (!attributesQueryValue) return;
|
|
|
|
const attributesUsedToRoute = attributesQueryValue.children1;
|
|
|
|
if (!attributesUsedToRoute) return;
|
|
|
|
const attributeValues: string[] = [];
|
|
|
|
for (const attribute of Object.keys(attributesUsedToRoute)) {
|
|
const attributeToFilter = attributesUsedToRoute[attribute].properties;
|
|
|
|
if (!attributeToFilter) continue;
|
|
|
|
const userAttribute = usersAttributes.find((attribute) => attributeToFilter.field === attribute.id);
|
|
|
|
const attributeValue = attributeToFilter.value;
|
|
|
|
if (!userAttribute || !attributeValue || typeof attributeValue[0] === null) continue;
|
|
|
|
if (attributeValue && attributeValue[0]) {
|
|
const attributeValueString = (() => {
|
|
if (Array.isArray(attributeValue[0])) {
|
|
return attributeValue[0][0];
|
|
} else {
|
|
return attributeValue[0];
|
|
}
|
|
})();
|
|
|
|
attributeValues.push(`${userAttribute?.name}: ${attributeValueString}`);
|
|
}
|
|
}
|
|
|
|
const reasonEnum = isRerouting
|
|
? AssignmentReasonEnum.REROUTED
|
|
: AssignmentReasonEnum.ROUTING_FORM_ROUTING;
|
|
const reasonString = `${
|
|
isRerouting && reroutedByEmail ? `Rerouted by ${reroutedByEmail}` : ""
|
|
} ${attributeValues.join(", ")}`;
|
|
|
|
await prisma.assignmentReason.create({
|
|
data: {
|
|
bookingId: bookingId,
|
|
reasonEnum,
|
|
reasonString,
|
|
},
|
|
});
|
|
|
|
return {
|
|
reasonEnum,
|
|
reasonString,
|
|
};
|
|
}
|
|
|
|
// Separate method to handle rerouting
|
|
static CRMOwnership = withReporting(
|
|
AssignmentReasonRecorder._CRMOwnership,
|
|
"AssignmentReasonRecorder.CRMOwnership"
|
|
);
|
|
static async _CRMOwnership({
|
|
bookingId,
|
|
crmAppSlug,
|
|
teamMemberEmail,
|
|
recordType,
|
|
routingFormResponseId,
|
|
recordId,
|
|
}: {
|
|
bookingId: number;
|
|
crmAppSlug: string;
|
|
teamMemberEmail: string;
|
|
recordType: string;
|
|
routingFormResponseId: number;
|
|
recordId?: string;
|
|
}) {
|
|
const appAssignmentReasonHandler = (await import("./appAssignmentReasonHandler")).default;
|
|
const appHandler = appAssignmentReasonHandler[crmAppSlug];
|
|
if (!appHandler) return;
|
|
|
|
const crmRoutingReason = await appHandler({
|
|
recordType,
|
|
teamMemberEmail,
|
|
routingFormResponseId,
|
|
recordId,
|
|
});
|
|
|
|
if (!crmRoutingReason || !crmRoutingReason.assignmentReason) return;
|
|
|
|
const { reasonEnum, assignmentReason } = crmRoutingReason;
|
|
|
|
await prisma.assignmentReason.create({
|
|
data: {
|
|
bookingId,
|
|
reasonEnum,
|
|
reasonString: assignmentReason,
|
|
},
|
|
});
|
|
|
|
return {
|
|
reasonEnum,
|
|
reasonString: assignmentReason,
|
|
};
|
|
}
|
|
static roundRobinReassignment = withReporting(
|
|
AssignmentReasonRecorder._roundRobinReassignment,
|
|
"AssignmentReasonRecorder.roundRobinReassignment"
|
|
);
|
|
static async _roundRobinReassignment({
|
|
bookingId,
|
|
reassignById,
|
|
reassignReason,
|
|
reassignmentType,
|
|
}: {
|
|
bookingId: number;
|
|
reassignById: number;
|
|
reassignReason?: string;
|
|
reassignmentType: RRReassignmentType;
|
|
}) {
|
|
const reassignedBy = await prisma.user.findUnique({
|
|
where: {
|
|
id: reassignById,
|
|
},
|
|
select: {
|
|
username: true,
|
|
},
|
|
});
|
|
|
|
const reasonEnum =
|
|
reassignmentType === RRReassignmentType.MANUAL
|
|
? AssignmentReasonEnum.REASSIGNED
|
|
: AssignmentReasonEnum.RR_REASSIGNED;
|
|
|
|
const reasonString = `Reassigned by: ${reassignedBy?.username || "team member"}. ${
|
|
reassignReason ? `Reason: ${reassignReason}` : ""
|
|
}`;
|
|
|
|
await prisma.assignmentReason.create({
|
|
data: {
|
|
bookingId: bookingId,
|
|
reasonEnum,
|
|
reasonString,
|
|
},
|
|
});
|
|
|
|
return {
|
|
reasonEnum,
|
|
reasonString,
|
|
};
|
|
}
|
|
}
|