feat: Salesforce - send routing form responses to Salesforce (#17890)

This commit is contained in:
Joe Au-Yeung
2024-11-28 12:37:45 -07:00
committed by GitHub
parent be2f0f986e
commit f2efd6dc28
4 changed files with 87 additions and 27 deletions
@@ -70,6 +70,7 @@ export default function FormInputFields(props: FormInputFieldsProps) {
...response,
[field.id]: {
label: field.label,
identifier: field?.identifier,
value: getFieldResponseForJsonLogic({ field, value }),
},
};
@@ -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())]),
})
),
+1
View File
@@ -22,6 +22,7 @@ export type FormResponse = Record<
{
value: number | string | string[];
label: string;
identifier?: string;
}
>;
+84 -27
View File
@@ -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";
@@ -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 });
}
@@ -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) => {
@@ -712,7 +715,7 @@ export default class SalesforceCRMService implements CRM {
});
}
private generateCreateRecordBody({
private async generateCreateRecordBody({
attendee,
recordType,
organizerId,
@@ -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,
@@ -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,
@@ -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,
@@ -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;
@@ -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,