diff --git a/packages/app-store/routing-forms/components/FormInputFields.tsx b/packages/app-store/routing-forms/components/FormInputFields.tsx index 00cb27ee56..f48aa913f2 100644 --- a/packages/app-store/routing-forms/components/FormInputFields.tsx +++ b/packages/app-store/routing-forms/components/FormInputFields.tsx @@ -70,6 +70,7 @@ export default function FormInputFields(props: FormInputFieldsProps) { ...response, [field.id]: { label: field.label, + identifier: field?.identifier, value: getFieldResponseForJsonLogic({ field, value }), }, }; diff --git a/packages/app-store/routing-forms/trpc/response.schema.ts b/packages/app-store/routing-forms/trpc/response.schema.ts index 861e721184..da4222d7f8 100644 --- a/packages/app-store/routing-forms/trpc/response.schema.ts +++ b/packages/app-store/routing-forms/trpc/response.schema.ts @@ -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())]), }) ), diff --git a/packages/app-store/routing-forms/types/types.d.ts b/packages/app-store/routing-forms/types/types.d.ts index 13da8303bb..2d6f2a5c04 100644 --- a/packages/app-store/routing-forms/types/types.d.ts +++ b/packages/app-store/routing-forms/types/types.d.ts @@ -22,6 +22,7 @@ export type FormResponse = Record< { value: number | string | string[]; label: string; + identifier?: string; } >; diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 578321d077..6e8c7af64e 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -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,