Skip to content

Commit 3784421

Browse files
joeauyeungdevin-ai-integration[bot]emrysal
authored
feat: include record IDs in Salesforce assignment reason strings (#22561)
* 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: [email protected] <[email protected]> * 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: [email protected] <[email protected]> * 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: [email protected] <[email protected]> * 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: [email protected] <[email protected]> * Pass crmRecordId as prop --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <[email protected]>
1 parent e7535b9 commit 3784421

File tree

22 files changed

+147
-36
lines changed

22 files changed

+147
-36
lines changed

apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,14 @@ export async function getCRMData(
122122
const crmContactOwnerEmail = query["cal.crmContactOwnerEmail"];
123123
const crmContactOwnerRecordType = query["cal.crmContactOwnerRecordType"];
124124
const crmAppSlugParam = query["cal.crmAppSlug"];
125+
const crmRecordIdParam = query["cal.crmRecordId"];
125126

126127
let teamMemberEmail = Array.isArray(crmContactOwnerEmail) ? crmContactOwnerEmail[0] : crmContactOwnerEmail;
127128
let crmOwnerRecordType = Array.isArray(crmContactOwnerRecordType)
128129
? crmContactOwnerRecordType[0]
129130
: crmContactOwnerRecordType;
130131
let crmAppSlug = Array.isArray(crmAppSlugParam) ? crmAppSlugParam[0] : crmAppSlugParam;
132+
let crmRecordId = Array.isArray(crmRecordIdParam) ? crmRecordIdParam[0] : crmRecordIdParam;
131133

132134
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
133135
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
@@ -137,6 +139,7 @@ export async function getCRMData(
137139
email,
138140
recordType,
139141
crmAppSlug: crmAppSlugQuery,
142+
recordId: crmRecordIdQuery,
140143
} = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
141144
query,
142145
eventData,
@@ -145,11 +148,13 @@ export async function getCRMData(
145148
teamMemberEmail = email ?? undefined;
146149
crmOwnerRecordType = recordType ?? undefined;
147150
crmAppSlug = crmAppSlugQuery ?? undefined;
151+
crmRecordId = crmRecordIdQuery ?? undefined;
148152
}
149153

150154
return {
151155
teamMemberEmail,
152156
crmOwnerRecordType,
153157
crmAppSlug,
158+
crmRecordId,
154159
};
155160
}

apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ function mockGetCRMContactOwnerForRRLeadSkip({
2929
}) {
3030
vi.mocked(getCRMContactOwnerForRRLeadSkip).mockImplementation((_bookerEmail, _eventMetadata) => {
3131
if (_bookerEmail === bookerEmail) {
32-
return Promise.resolve({ email: teamMemberEmail, recordType: null, crmAppSlug: null });
32+
return Promise.resolve({ email: teamMemberEmail, recordType: null, crmAppSlug: null, recordId: null });
3333
}
34-
return Promise.resolve({ email: null, recordType: null, crmAppSlug: null });
34+
return Promise.resolve({ email: null, recordType: null, crmAppSlug: null, recordId: null });
3535
});
3636
}
3737

@@ -45,9 +45,9 @@ function mockBookingFormHandler({
4545
vi.mocked(bookingFormHandlers.salesforce).mockImplementation(
4646
(_bookerEmail, _attributeRoutingConfig, _eventTypeId) => {
4747
if (_bookerEmail === bookerEmail) {
48-
return Promise.resolve({ email: teamMemberEmail, recordType: null });
48+
return Promise.resolve({ email: teamMemberEmail, recordType: null, recordId: null });
4949
}
50-
return Promise.resolve({ email: null, recordType: null });
50+
return Promise.resolve({ email: null, recordType: null, recordId: null });
5151
}
5252
);
5353
}

apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
9292
const crmContactOwnerEmail = query["cal.crmContactOwnerEmail"];
9393
const crmContactOwnerRecordType = query["cal.crmContactOwnerRecordType"];
9494
const crmAppSlugParam = query["cal.crmAppSlug"];
95+
const crmRecordIdParam = query["cal.crmRecordId"];
9596

9697
// Handle string[] type from query params
9798
let teamMemberEmail = Array.isArray(crmContactOwnerEmail) ? crmContactOwnerEmail[0] : crmContactOwnerEmail;
@@ -101,6 +102,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
101102
: crmContactOwnerRecordType;
102103

103104
let crmAppSlug = Array.isArray(crmAppSlugParam) ? crmAppSlugParam[0] : crmAppSlugParam;
105+
let crmRecordId = Array.isArray(crmRecordIdParam) ? crmRecordIdParam[0] : crmRecordIdParam;
104106

105107
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
106108
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
@@ -110,6 +112,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
110112
email,
111113
recordType,
112114
crmAppSlug: crmAppSlugQuery,
115+
recordId,
113116
} = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
114117
query,
115118
eventData,
@@ -118,6 +121,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
118121
teamMemberEmail = email ?? undefined;
119122
crmOwnerRecordType = recordType ?? undefined;
120123
crmAppSlug = crmAppSlugQuery ?? undefined;
124+
crmRecordId = recordId ?? undefined;
121125
}
122126

123127
const organizationSettings = getOrganizationSEOSettings(team);
@@ -167,6 +171,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
167171
teamMemberEmail,
168172
crmOwnerRecordType,
169173
crmAppSlug,
174+
crmRecordId,
170175
isSEOIndexable: allowSEOIndexing,
171176
},
172177
};

apps/web/modules/team/type-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function Type({
3434
teamMemberEmail,
3535
crmOwnerRecordType,
3636
crmAppSlug,
37+
crmRecordId,
3738
isEmbed,
3839
useApiV2,
3940
}: PageProps) {
@@ -64,6 +65,7 @@ function Type({
6465
teamMemberEmail={teamMemberEmail}
6566
crmOwnerRecordType={crmOwnerRecordType}
6667
crmAppSlug={crmAppSlug}
68+
crmRecordId={crmRecordId}
6769
/>
6870
</main>
6971
</BookingPageErrorBoundary>

packages/app-store/_utils/CRMRoundRobinSkip.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
1010
export async function getCRMContactOwnerForRRLeadSkip(
1111
bookerEmail: string,
1212
eventTypeMetadata: Prisma.JsonValue
13-
): Promise<{ email: string | null; recordType: string | null; crmAppSlug: string | null }> {
14-
const nullReturnValue = { email: null, recordType: null, crmAppSlug: "" };
13+
): Promise<{
14+
email: string | null;
15+
recordType: string | null;
16+
crmAppSlug: string | null;
17+
recordId: string | null;
18+
}> {
19+
const nullReturnValue = { email: null, recordType: null, crmAppSlug: "", recordId: null };
1520
const parsedEventTypeMetadata = EventTypeMetaDataSchema.safeParse(eventTypeMetadata);
1621
if (!parsedEventTypeMetadata.success || !parsedEventTypeMetadata.data?.apps) return nullReturnValue;
1722

@@ -24,7 +29,12 @@ export async function getCRMContactOwnerForRRLeadSkip(
2429
const endTime = performance.now();
2530
logger.info(`Fetching from CRM took ${endTime - startTime}ms`);
2631
if (!contact?.length || !contact[0].ownerEmail) return nullReturnValue;
27-
return { email: contact[0].ownerEmail ?? null, recordType: contact[0].recordType ?? null, crmAppSlug };
32+
return {
33+
email: contact[0].ownerEmail ?? null,
34+
recordType: contact[0].recordType ?? null,
35+
crmAppSlug,
36+
recordId: contact[0].id ?? null,
37+
};
2838
}
2939

3040
async function getCRMManagerWithRRLeadSkip(apps: z.infer<typeof EventTypeAppMetadataSchema>) {

packages/app-store/routing-forms/appBookingFormHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type AppBookingFormHandler = (
55
attendeeEmail: string,
66
attributeRoutingConfig: AttributeRoutingConfig,
77
eventTypeId: number
8-
) => Promise<{ email: string | null; recordType: string | null }>;
8+
) => Promise<{ email: string | null; recordType: string | null; recordId: string | null }>;
99

1010
const appBookingFormHandler: Record<string, AppBookingFormHandler> = {
1111
salesforce: routingFormBookingFormHandler,

packages/app-store/routing-forms/lib/crmRouting/routerGetCrmContactOwnerEmail.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ export default async function routerGetCrmContactOwnerEmail({
4040
const eventTypeMetadata = eventType.metadata;
4141
if (!eventTypeMetadata) return null;
4242

43-
let contactOwner: { email: string | null; recordType: string | null; crmAppSlug: string | null } = {
43+
let contactOwner: {
44+
email: string | null;
45+
recordType: string | null;
46+
crmAppSlug: string | null;
47+
recordId: string | null;
48+
} = {
4449
email: null,
4550
recordType: null,
4651
crmAppSlug: null,
52+
recordId: null,
4753
};
4854
// Determine if there is a CRM option enabled in the chosen route
4955
for (const appSlug of enabledAppSlugs) {

packages/app-store/routing-forms/lib/handleResponse.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ describe("handleResponse", () => {
267267
268268
recordType: "contact",
269269
crmAppSlug: "hubspot",
270+
recordId: "123",
270271
});
271272
vi.mocked(findTeamMembersMatchingAttributeLogic).mockResolvedValue({
272273
teamMembersMatchingAttributeLogic: [{ userId: 123, result: "MATCH" as any }],

packages/app-store/routing-forms/lib/handleResponse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const _handleResponse = async ({
9696
let crmContactOwnerEmail: string | null = null;
9797
let crmContactOwnerRecordType: string | null = null;
9898
let crmAppSlug: string | null = null;
99+
let crmRecordId: string | null = null;
99100
let timeTaken: Record<string, number | null> = {};
100101
if (chosenRoute) {
101102
if (isRouter(chosenRoute)) {
@@ -119,6 +120,7 @@ const _handleResponse = async ({
119120
crmContactOwnerEmail = contactOwnerQuery?.email ?? null;
120121
crmContactOwnerRecordType = contactOwnerQuery?.recordType ?? null;
121122
crmAppSlug = contactOwnerQuery?.crmAppSlug ?? null;
123+
crmRecordId = contactOwnerQuery?.recordId ?? null;
122124
})(),
123125
(async () => {
124126
const teamMembersMatchingAttributeLogicWithResult =
@@ -212,6 +214,7 @@ const _handleResponse = async ({
212214
crmContactOwnerEmail,
213215
crmContactOwnerRecordType,
214216
crmAppSlug,
217+
crmRecordId,
215218
attributeRoutingConfig: chosenRoute
216219
? "attributeRoutingConfig" in chosenRoute
217220
? chosenRoute.attributeRoutingConfig

packages/app-store/salesforce/lib/assignmentReasonHandler.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,52 @@ export async function assignmentReasonHandler({
88
recordType,
99
teamMemberEmail,
1010
routingFormResponseId,
11+
recordId,
1112
}: {
1213
recordType: string;
1314
teamMemberEmail: string;
1415
routingFormResponseId: number;
16+
recordId?: string;
1517
}) {
1618
const returnObject = { reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT };
1719

1820
switch (recordType) {
1921
case SalesforceRecordEnum.CONTACT:
20-
return { ...returnObject, assignmentReason: `Salesforce contact owner: ${teamMemberEmail}` };
22+
return {
23+
...returnObject,
24+
assignmentReason: `Salesforce contact owner: ${teamMemberEmail}${
25+
recordId ? ` (Contact ID: ${recordId})` : ""
26+
}`,
27+
};
2128
case SalesforceRecordEnum.LEAD:
22-
return { ...returnObject, assignmentReason: `Salesforce lead owner: ${teamMemberEmail}` };
29+
return {
30+
...returnObject,
31+
assignmentReason: `Salesforce lead owner: ${teamMemberEmail}${
32+
recordId ? ` (Lead ID: ${recordId})` : ""
33+
}`,
34+
};
2335
case SalesforceRecordEnum.ACCOUNT:
24-
return { ...returnObject, assignmentReason: `Salesforce account owner: ${teamMemberEmail}` };
36+
return {
37+
...returnObject,
38+
assignmentReason: `Salesforce account owner: ${teamMemberEmail}${
39+
recordId ? ` (Account ID: ${recordId})` : ""
40+
}`,
41+
};
2542
case RoutingReasons.ACCOUNT_LOOKUP_FIELD:
26-
const assignmentReason = await handleAccountLookupFieldReason(routingFormResponseId, teamMemberEmail);
43+
const assignmentReason = await handleAccountLookupFieldReason(
44+
routingFormResponseId,
45+
teamMemberEmail,
46+
recordId
47+
);
2748
return { ...returnObject, assignmentReason };
2849
}
2950
}
3051

31-
async function handleAccountLookupFieldReason(routingFormResponseId: number, teamMemberEmail: string) {
52+
async function handleAccountLookupFieldReason(
53+
routingFormResponseId: number,
54+
teamMemberEmail: string,
55+
recordId?: string
56+
) {
3257
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({
3358
where: {
3459
id: routingFormResponseId,
@@ -67,6 +92,8 @@ async function handleAccountLookupFieldReason(routingFormResponseId: number, tea
6792
const accountLookupFieldName = salesforceConfig?.rrSKipToAccountLookupFieldName;
6893

6994
return accountLookupFieldName
70-
? `Salesforce account lookup field: ${accountLookupFieldName} - ${teamMemberEmail}`
95+
? `Salesforce account lookup field: ${accountLookupFieldName} - ${teamMemberEmail}${
96+
recordId ? ` (Account ID: ${recordId})` : ""
97+
}`
7198
: undefined;
7299
}

0 commit comments

Comments
 (0)