Skip to content

Commit

Permalink
feat: Salesforce - send routing form responses to Salesforce (calcom#…
Browse files Browse the repository at this point in the history
  • Loading branch information
joeauyeung authored Nov 28, 2024
1 parent be2f0f9 commit f2efd6d
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default function FormInputFields(props: FormInputFieldsProps) {
...response,
[field.id]: {
label: field.label,
identifier: field?.identifier,
value: getFieldResponseForJsonLogic({ field, value }),
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/routing-forms/trpc/response.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const ZResponseInputSchema = z.object({
response: z.record(
z.object({
label: z.string(),
identifier: z.string().optional(),
value: z.union([z.string(), z.number(), z.array(z.string())]),
})
),
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/routing-forms/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type FormResponse = Record<
{
value: number | string | string[];
label: string;
identifier?: string;
}
>;

Expand Down
111 changes: 84 additions & 27 deletions packages/app-store/salesforce/lib/CrmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import jsforce from "jsforce";
import { RRule } from "rrule";
import { z } from "zod";

import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
Expand Down Expand Up @@ -524,14 +525,14 @@ export default class SalesforceCRMService implements CRM {

for (const attendee of contactsToCreate) {
try {
const result = await conn.sobject(SalesforceRecordEnum.LEAD).create(
this.generateCreateRecordBody({
attendee,
recordType: SalesforceRecordEnum.LEAD,
organizerId,
calEventResponses,
})
);
const createBody = await this.generateCreateRecordBody({
attendee,
recordType: SalesforceRecordEnum.LEAD,
organizerId,
calEventResponses,
});

const result = await conn.sobject(SalesforceRecordEnum.LEAD).create(createBody);
if (result.success) {
createdContacts.push({ id: result.id, email: attendee.email });
}
Expand Down Expand Up @@ -688,15 +689,17 @@ export default class SalesforceCRMService implements CRM {
}) {
const conn = await this.conn;

const createBody = await this.generateCreateRecordBody({
attendee,
recordType: recordType,
organizerId,
calEventResponses,
});

return await conn
.sobject(recordType)
.create({
...this.generateCreateRecordBody({
attendee,
recordType: recordType,
organizerId,
calEventResponses,
}),
...createBody,
AccountId: accountId,
})
.then((result) => {
Expand All @@ -712,7 +715,7 @@ export default class SalesforceCRMService implements CRM {
});
}

private generateCreateRecordBody({
private async generateCreateRecordBody({
attendee,
recordType,
organizerId,
Expand All @@ -728,7 +731,8 @@ export default class SalesforceCRMService implements CRM {

// Assume that the first part of the email domain is the company title
const company =
this.getCompanyNameFromBookingResponse(calEventResponses) ?? attendee.email.split("@")[1].split(".")[0];
(await this.getCompanyNameFromBookingResponse(calEventResponses)) ??
attendee.email.split("@")[1].split(".")[0];
return {
LastName: LastName || "-",
FirstName,
Expand Down Expand Up @@ -878,11 +882,15 @@ export default class SalesforceCRMService implements CRM {
// Handle different field types
if (fieldConfig.fieldType === field.type) {
if (field.type === SalesforceFieldType.TEXT || field.type === SalesforceFieldType.PHONE) {
writeOnRecordBody[field.name] = this.getTextFieldValue({
const extractedText = await this.getTextFieldValue({
fieldValue: fieldConfig.value,
fieldLength: field.length,
calEventResponses,
bookingUid,
});
if (extractedText) {
writeOnRecordBody[field.name] = extractedText;
}
} else if (field.type === SalesforceFieldType.DATE) {
const dateValue = await this.getDateFieldValue(
fieldConfig.value,
Expand All @@ -899,28 +907,77 @@ export default class SalesforceCRMService implements CRM {

return writeOnRecordBody;
}
private getTextFieldValue({
private async getTextFieldValue({
fieldValue,
fieldLength,
calEventResponses,
bookingUid,
}: {
fieldValue: string;
fieldLength: number;
calEventResponses?: CalEventResponses | null;
bookingUid?: string | null;
}) {
let valueToWrite = fieldValue.substring(0, fieldLength);
if (!calEventResponses) return valueToWrite;
// If no {} then indicates we're passing a static value
if (!fieldValue.startsWith("{") && !fieldValue.endsWith("}")) return fieldValue;

// Check if we need to replace any values with values from the booking questions
const regexValueToReplace = /\{(.*?)\}/g;
valueToWrite = valueToWrite.replace(regexValueToReplace, (match, captured) => {
return calEventResponses[captured]?.value ? calEventResponses[captured].value.toString() : match;
});
let valueToWrite = fieldValue;

if (fieldValue.startsWith("{form:")) {
// Get routing from response
if (!bookingUid) return;
valueToWrite = await this.getTextValueFromRoutingFormResponse(fieldValue, bookingUid);
} else {
// Get the value from the booking response
if (!calEventResponses) return;
valueToWrite = this.getTextValueFromBookingResponse(fieldValue, calEventResponses);
}

// If a value wasn't found in the responses. Don't return the field name
if (valueToWrite === fieldValue) return;

// Trim incase the replacement values increased the length
return valueToWrite.substring(0, fieldLength);
}

private async getTextValueFromRoutingFormResponse(fieldValue: string, bookingUid: string) {
// Get the form response
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findFirst({
where: {
routedToBookingUid: bookingUid,
},
select: {
response: true,
},
});
if (!routingFormResponse) return fieldValue;
const response = routingFormResponse.response as FormResponse;
const regex = /\{form:(.*?)\}/;
const regexMatch = fieldValue.match(regex);
if (!regexMatch) return fieldValue;

const identifierField = regexMatch?.[1];
if (!identifierField) return fieldValue;

// Search for fieldValue, only handle raw text return for now
for (const fieldId of Object.keys(response)) {
const field = response[fieldId];

if (field?.identifier === identifierField) {
return field.value.toString();
}
}

return fieldValue;
}

private getTextValueFromBookingResponse(fieldValue: string, calEventResponses: CalEventResponses) {
const regexValueToReplace = /\{(.*?)\}/g;
return fieldValue.replace(regexValueToReplace, (match, captured) => {
return calEventResponses[captured]?.value ? calEventResponses[captured].value.toString() : match;
});
}

private async getDateFieldValue(
fieldValue: string,
startTime: string,
Expand Down Expand Up @@ -1052,7 +1109,7 @@ export default class SalesforceCRMService implements CRM {
}

/** Search the booking questions for the Company field value rather than relying on the email domain */
private getCompanyNameFromBookingResponse(calEventResponses?: CalEventResponses | null) {
private async getCompanyNameFromBookingResponse(calEventResponses?: CalEventResponses | null) {
const appOptions = this.getAppOptions();
const companyFieldName = "Company";
const defaultTextValueLength = 225;
Expand All @@ -1064,7 +1121,7 @@ export default class SalesforceCRMService implements CRM {
// Check that we're writing to the Company field
if (!(companyFieldName in onBookingWriteToRecordFields)) return;

const companyValue = this.getTextFieldValue({
const companyValue = await this.getTextFieldValue({
fieldValue: onBookingWriteToRecordFields[companyFieldName].value,
fieldLength: defaultTextValueLength,
calEventResponses,
Expand Down

0 comments on commit f2efd6d

Please sign in to comment.