From b1d9e2b84c95b14be90de125ca4276328a5b6623 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Sat, 12 Oct 2024 06:02:27 -0400 Subject: [PATCH] feat: Salesforce choose how to save attendees & other options (#17009) * Refactor get SF user and create event * Add new Salesforce options * Pass app options to CrmService * Add record enum * Add creating new lead and contact under an account * Handle if the contact already exists but not connected to account * Create a lead if an account doesn't exist * Merge fix * Refactor - add generateCreateRecordBody * Type fix * Clean up * Add getAppOptions to other CRM services * Type fixes * Fix typo * feat: Salesforce round robin skip - choose which entity to search on (#17038) * Add option to search which entity for the owner * Add logic to search entity for skipping RR assignment * Type fixes * Disable license * Skip license check in component * Revert license changes --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- apps/web/public/static/locales/en/common.json | 9 + .../app-store/_utils/CRMRoundRobinSkip.ts | 4 +- packages/app-store/_utils/getCrm.ts | 6 +- packages/app-store/closecom/lib/CrmService.ts | 6 +- packages/app-store/hubspot/lib/CrmService.ts | 6 +- .../app-store/pipedrive-crm/lib/CrmService.ts | 8 +- .../components/EventTypeAppCardInterface.tsx | 136 +++++++++-- .../app-store/salesforce/lib/CrmService.ts | 215 ++++++++++++++++-- .../app-store/salesforce/lib/recordEnum.ts | 5 + packages/app-store/salesforce/zod.ts | 8 + .../app-store/zoho-bigin/lib/CrmService.ts | 6 +- packages/app-store/zohocrm/lib/CrmService.ts | 6 +- packages/core/EventManager.ts | 55 ++--- packages/core/crmManager/crmManager.ts | 19 +- packages/types/CrmService.d.ts | 13 +- 15 files changed, 406 insertions(+), 96 deletions(-) create mode 100644 packages/app-store/salesforce/lib/recordEnum.ts diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index ded33df650..f22c2c06d4 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2644,6 +2644,15 @@ "month_to_date": "month to date", "year_to_date": "year to date", "custom_range": "custom range", + "salesforce_create_record_as": "On booking, add events on and new attendees as:", + "salesforce_lead": "Lead", + "salesforce_contact_under_account": "Contact under an account", + "salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce", + "salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee", + "salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts", "mass_assign_attributes": "Mass assign attributes", + "salesforce_book_directly_with_attendee_owner": "If attendee exists in Salesforce, book directly with the owner", + "salesforce_check_owner_of": "Entity to check owner of to book directly", + "account": "Account", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/_utils/CRMRoundRobinSkip.ts b/packages/app-store/_utils/CRMRoundRobinSkip.ts index 902f6dcba4..bdd514c6fd 100644 --- a/packages/app-store/_utils/CRMRoundRobinSkip.ts +++ b/packages/app-store/_utils/CRMRoundRobinSkip.ts @@ -24,7 +24,7 @@ export async function getCRMContactOwnerForRRLeadSkip( if (!crm) return; - const contact = await crm.getContacts(bookerEmail, true); + const contact = await crm.getContacts({ emails: bookerEmail, forRoundRobinSkip: true }); if (!contact?.length) return; return contact[0].ownerEmail; } @@ -57,5 +57,5 @@ async function getCRMManagerWithRRLeadSkip(apps: z.infer = new (...args: Args) => I; -type CrmClass = Class; +type CrmClass = Class; const log = logger.getSubLogger({ prefix: ["CrmManager"] }); -export const getCrm = async (credential: CredentialPayload) => { +export const getCrm = async (credential: CredentialPayload, appOptions: any) => { if (!credential || !credential.key) return null; const { type: crmType } = credential; @@ -26,7 +26,7 @@ export const getCrm = async (credential: CredentialPayload) => { if (crmApp && "lib" in crmApp && "CrmService" in crmApp.lib) { const CrmService = crmApp.lib.CrmService as CrmClass; - return new CrmService(credential); + return new CrmService(credential, appOptions); } }; diff --git a/packages/app-store/closecom/lib/CrmService.ts b/packages/app-store/closecom/lib/CrmService.ts index 86a4ddc900..4dd17fa421 100644 --- a/packages/app-store/closecom/lib/CrmService.ts +++ b/packages/app-store/closecom/lib/CrmService.ts @@ -121,7 +121,7 @@ export default class CloseComCRMService implements CRM { await this.closeComDeleteCustomActivity(uid); } - async getContacts(emails: string | string[]): Promise { + async getContacts({ emails }: { emails: string | string[] }): Promise { const contactsQuery = await this.closeCom.contact.search({ emails: Array.isArray(emails) ? emails : [emails], }); @@ -177,4 +177,8 @@ export default class CloseComCRMService implements CRM { return contacts; } + + getAppOptions() { + console.log("No options implemented"); + } } diff --git a/packages/app-store/hubspot/lib/CrmService.ts b/packages/app-store/hubspot/lib/CrmService.ts index 7cbbe1f9a6..80847f0889 100644 --- a/packages/app-store/hubspot/lib/CrmService.ts +++ b/packages/app-store/hubspot/lib/CrmService.ts @@ -198,7 +198,7 @@ export default class HubspotCalendarService implements CRM { return await this.hubspotDeleteMeeting(uid); } - async getContacts(emails: string | string[]): Promise { + async getContacts({ emails }: { emails: string | string[] }): Promise { const auth = await this.auth; await auth.getToken(); @@ -269,4 +269,8 @@ export default class HubspotCalendarService implements CRM { }; }); } + + getAppOptions() { + console.log("No options implemented"); + } } diff --git a/packages/app-store/pipedrive-crm/lib/CrmService.ts b/packages/app-store/pipedrive-crm/lib/CrmService.ts index ee5dac16a0..b7e88d30f6 100644 --- a/packages/app-store/pipedrive-crm/lib/CrmService.ts +++ b/packages/app-store/pipedrive-crm/lib/CrmService.ts @@ -78,8 +78,8 @@ export default class PipedriveCrmService implements CRM { return results.map((result) => result.result); } - async getContacts(email: string | string[]): Promise { - const emailArray = Array.isArray(email) ? email : [email]; + async getContacts({ emails }: { emails: string | string[] }): Promise { + const emailArray = Array.isArray(emails) ? emails : [emails]; const result = emailArray.map(async (attendeeEmail) => { const headers = new Headers(); @@ -230,4 +230,8 @@ export default class PipedriveCrmService implements CRM { async listCalendars(_event?: CalendarEvent): Promise { return Promise.resolve([]); } + + getAppOptions() { + console.log("No options implemented"); + } } diff --git a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx index 1597bc4f57..ed232e6626 100644 --- a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx @@ -1,4 +1,5 @@ import { usePathname } from "next/navigation"; +import { useState } from "react"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; @@ -7,8 +8,9 @@ import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { SchedulingType } from "@calcom/prisma/enums"; -import { Switch, Alert } from "@calcom/ui"; +import { Switch, Alert, Select } from "@calcom/ui"; +import { SalesforceRecordEnum } from "../lib/recordEnum"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { @@ -17,9 +19,32 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const { enabled, updateEnabled } = useIsAppEnabled(app); const isRoundRobinLeadSkipEnabled = getAppData("roundRobinLeadSkip"); + const roundRobinSkipCheckRecordOn = + getAppData("roundRobinSkipCheckRecordOn") ?? SalesforceRecordEnum.CONTACT; const isSkipContactCreationEnabled = getAppData("skipContactCreation"); + const createLeadIfAccountNull = getAppData("createLeadIfAccountNull"); + const createNewContactUnderAccount = getAppData("createNewContactUnderAccount"); + const createEventOn = getAppData("createEventOn") ?? SalesforceRecordEnum.CONTACT; const { t } = useLocale(); + const recordOptions = [ + { label: t("contact"), value: SalesforceRecordEnum.CONTACT }, + { label: t("salesforce_lead"), value: SalesforceRecordEnum.LEAD }, + { label: t("salesforce_contact_under_account"), value: SalesforceRecordEnum.ACCOUNT }, + ]; + const [createEventOnSelectedOption, setCreateEventOnSelectedOption] = useState( + recordOptions.find((option) => option.value === createEventOn) ?? recordOptions[0] + ); + + const checkOwnerOptions = [ + { label: t("contact"), value: SalesforceRecordEnum.CONTACT }, + { label: t("salesforce_lead"), value: SalesforceRecordEnum.LEAD }, + { label: t("account"), value: SalesforceRecordEnum.ACCOUNT }, + ]; + const [checkOwnerSelectedOption, setCheckOwnerSelectedOption] = useState( + checkOwnerOptions.find((option) => option.value === roundRobinSkipCheckRecordOn) ?? checkOwnerOptions[0] + ); + return ( <> -
- { - setAppData("skipContactCreation", checked); - }} - /> -
- - {eventType.schedulingType === SchedulingType.ROUND_ROBIN ? ( -
- { - setAppData("roundRobinLeadSkip", checked); - if (checked) { - // temporary solution, enabled should always be already set - setAppData("enabled", checked); +
+ + { + if (e) { + setCheckOwnerSelectedOption(e); + setAppData("roundRobinSkipCheckRecordOn", e.value); + } + }} + /> +
+ ) : null} + +
+ ) : null} +
); }; diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 34e78fa671..034838ee97 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -15,6 +15,7 @@ import type { CRM, Contact, CrmEvent } from "@calcom/types/CrmService"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse"; import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse"; +import { SalesforceRecordEnum } from "./recordEnum"; type ExtendedTokenResponse = TokenResponse & { instance_url: string; @@ -55,11 +56,18 @@ export default class SalesforceCRMService implements CRM { private conn: Promise; private log: typeof logger; private calWarnings: string[] = []; + private appOptions: any; + private doNotCreateEvent = false; - constructor(credential: CredentialPayload) { + constructor(credential: CredentialPayload, appOptions: any) { this.integrationName = "salesforce_other_calendar"; this.conn = this.getClient(credential).then((c) => c); this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + this.appOptions = appOptions; + } + + public getAppOptions() { + return this.appOptions; } private getClient = async (credential: CredentialPayload) => { @@ -224,7 +232,10 @@ export default class SalesforceCRMService implements CRM { }); } - async createEvent(event: CalendarEvent, contacts: Contact[]): Promise { + async createEvent(event: CalendarEvent, contacts: Contact[]): Promise { + const skipEventCreation = this.getDoNotCreateEvent(); + if (skipEventCreation) return undefined; + const sfEvent = await this.salesforceCreateEvent(event, contacts); if (sfEvent.success) { return Promise.resolve({ @@ -265,17 +276,46 @@ export default class SalesforceCRMService implements CRM { } } - async getContacts(email: string | string[], includeOwner?: boolean) { + async getContacts({ + emails, + includeOwner, + forRoundRobinSkip, + }: { + emails: string | string[]; + includeOwner?: boolean; + forRoundRobinSkip?: boolean; + }) { const conn = await this.conn; - const emails = Array.isArray(email) ? email : [email]; - const soql = `SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('${emails.join("','")}')`; + const emailArray = Array.isArray(emails) ? emails : [emails]; + const appOptions = this.getAppOptions(); + const recordToSearch = + (forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ?? + SalesforceRecordEnum.CONTACT; + let soql: string; + if (recordToSearch === SalesforceRecordEnum.ACCOUNT) { + // For an account let's assume that the first email is the one we should be querying against + const attendeeEmail = emailArray[0]; + soql = `SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = '${attendeeEmail}' AND AccountId != null`; + + // If this is for a round robin skip then we need to return the account record + if (forRoundRobinSkip) { + const results = await conn.query(soql); + if (results.records.length) { + const contact = results.records[0] as { AccountId: string }; + soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${contact.AccountId}'`; + } + } + // If creating events on contacts or leads + } else { + soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join("','")}')`; + } const results = await conn.query(soql); if (!results || !results.records.length) return []; const records = results.records as ContactRecord[]; - if (includeOwner) { + if (includeOwner || forRoundRobinSkip) { const ownerIds: Set = new Set(); records.forEach((record) => { ownerIds.add(record.OwnerId); @@ -304,27 +344,160 @@ export default class SalesforceCRMService implements CRM { async createContacts(contactsToCreate: { email: string; name: string }[], organizerEmail?: string) { const conn = await this.conn; - - // See if the organizer exists in the CRM + const appOptions = this.getAppOptions(); + const createEventOn = appOptions.createEventOn ?? SalesforceRecordEnum.CONTACT; const organizerId = organizerEmail ? await this.getSalesforceUserIdFromEmail(organizerEmail) : undefined; - const createdContacts = await Promise.all( - contactsToCreate.map(async (attendee) => { - const [FirstName, LastName] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""]; - return await conn - .sobject("Contact") + const createdContacts: { id: string; email: string }[] = []; + + if (createEventOn === SalesforceRecordEnum.CONTACT || createEventOn === SalesforceRecordEnum.LEAD) { + // See if the organizer exists in the CRM + await Promise.all( + contactsToCreate.map(async (attendee) => { + return await conn + .sobject(createEventOn) + .create( + this.generateCreateRecordBody({ + attendee, + recordType: createEventOn, + organizerId, + }) + ) + .then((result) => { + if (result.success) { + createdContacts.push({ id: result.id, email: attendee.email }); + } + }); + }) + ); + } + + if (createEventOn === SalesforceRecordEnum.ACCOUNT) { + if (!appOptions.createNewContactUnderAccount && !appOptions.createLeadIfAccountNull) { + this.setDoNotCreateEvent(true); + return [{ id: "Do not create event", email: "placeholder" }]; + } + + // Base this off of the first contact + const attendee = contactsToCreate[0]; + + const emailDomain = attendee.email.split("@")[1]; + + const response = await conn.query( + `SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${emailDomain}' AND AccountId != null` + ); + + const accountId = this.getDominantAccountId(response.records as { AccountId: string }[]); + + if (accountId && appOptions.createNewContactUnderAccount) { + // First see if the contact already exists and connect it to the account + const userQuery = await conn.query(`SELECT Id, Email FROM Contact WHERE Email = '${attendee.email}'`); + if (userQuery.records.length) { + const contact = userQuery.records[0] as { Id: string; Email: string }; + await conn.sobject(SalesforceRecordEnum.CONTACT).update({ + // The first argument is the WHERE clause + Id: contact.Id, + AccountId: accountId, + }); + return [{ id: contact.Id, email: contact.Email }]; + } + + await conn + .sobject(SalesforceRecordEnum.CONTACT) .create({ - FirstName, - LastName: LastName || "-", - Email: attendee.email, - ...(organizerId && { OwnerId: organizerId }), + ...this.generateCreateRecordBody({ + attendee, + recordType: SalesforceRecordEnum.CONTACT, + organizerId, + }), + AccountId: accountId, }) .then((result) => { if (result.success) { - return { id: result.id, email: attendee.email }; + createdContacts.push({ id: result.id, email: attendee.email }); } }); - }) - ); - return createdContacts.filter((contact): contact is Contact => contact !== undefined); + } + + if (appOptions.createLeadIfAccountNull) { + await Promise.all( + contactsToCreate.map(async (attendee) => { + return await conn + .sobject(SalesforceRecordEnum.LEAD) + .create( + this.generateCreateRecordBody({ + attendee, + recordType: SalesforceRecordEnum.LEAD, + organizerId, + }) + ) + .then((result) => { + if (result.success) { + createdContacts.push({ id: result.id, email: attendee.email }); + } + }); + }) + ); + } + } + + return createdContacts; + } + + private setDoNotCreateEvent(boolean: boolean) { + this.doNotCreateEvent = boolean; + } + + private getDoNotCreateEvent() { + return this.doNotCreateEvent; + } + + private getDominantAccountId(contacts: { AccountId: string }[]) { + // To get the dominant AccountId we only need to iterate through half the array + const iterateLength = Math.ceil(contacts.length / 2); + // Store AccountId frequencies + const accountIdCounts: { [accountId: string]: number } = {}; + + for (const contact of contacts) { + const accountId = contact.AccountId; + accountIdCounts[accountId] = (accountIdCounts[accountId] || 0) + 1; + // If the number of AccountIds makes up 50% of the array length then return early + if (accountIdCounts[accountId] > iterateLength) return accountId; + } + + // Else figure out which AccountId occurs the most + let dominantAccountId; + let highestCount = 0; + + for (const accountId in accountIdCounts) { + if (accountIdCounts[accountId] > highestCount) { + highestCount = accountIdCounts[accountId]; + dominantAccountId = accountId; + } + } + + return dominantAccountId; + } + + private generateCreateRecordBody({ + attendee, + recordType, + organizerId, + }: { + attendee: { email: string; name: string }; + recordType: SalesforceRecordEnum; + organizerId?: string; + }) { + const [FirstName, LastName] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""]; + + // Assume that the first part of the email domain is the company title + const company = attendee.email.split("@")[1].split(".")[0]; + + return { + LastName: LastName || "-", + FirstName, + Email: attendee.email, + ...(organizerId && { OwnerId: organizerId }), + ...(recordType === SalesforceRecordEnum.LEAD && { Company: company }), + }; } } diff --git a/packages/app-store/salesforce/lib/recordEnum.ts b/packages/app-store/salesforce/lib/recordEnum.ts new file mode 100644 index 0000000000..b2b77eddec --- /dev/null +++ b/packages/app-store/salesforce/lib/recordEnum.ts @@ -0,0 +1,5 @@ +export enum SalesforceRecordEnum { + CONTACT = "Contact", + LEAD = "Lead", + ACCOUNT = "Account", +} diff --git a/packages/app-store/salesforce/zod.ts b/packages/app-store/salesforce/zod.ts index 0d0e11352f..274e3e820a 100644 --- a/packages/app-store/salesforce/zod.ts +++ b/packages/app-store/salesforce/zod.ts @@ -1,10 +1,18 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "../eventTypeAppCardZod"; +import { SalesforceRecordEnum } from "./lib/recordEnum"; export const appDataSchema = eventTypeAppCardZod.extend({ roundRobinLeadSkip: z.boolean().optional(), + roundRobinSkipCheckRecordOn: z + .nativeEnum(SalesforceRecordEnum) + .default(SalesforceRecordEnum.CONTACT) + .optional(), skipContactCreation: z.boolean().optional(), + createEventOn: z.nativeEnum(SalesforceRecordEnum).default(SalesforceRecordEnum.CONTACT).optional(), + createNewContactUnderAccount: z.boolean().optional(), + createLeadIfAccountNull: z.boolean().optional(), }); export const appKeysSchema = z.object({ diff --git a/packages/app-store/zoho-bigin/lib/CrmService.ts b/packages/app-store/zoho-bigin/lib/CrmService.ts index d556ad853e..05907a7475 100644 --- a/packages/app-store/zoho-bigin/lib/CrmService.ts +++ b/packages/app-store/zoho-bigin/lib/CrmService.ts @@ -155,7 +155,7 @@ export default class BiginCrmService implements CRM { /*** * Finds existing Zoho Bigin Contact record based on email address. Returns a list of contacts objects that matched. */ - async getContacts(emails: string | string[]) { + async getContacts({ emails }: { emails: string | string[] }) { const token = await this.auth.getToken(); const emailsArray = Array.isArray(emails) ? emails : [emails]; @@ -288,6 +288,10 @@ export default class BiginCrmService implements CRM { async listCalendars(_event?: CalendarEvent): Promise { return Promise.resolve([]); } + + getAppOptions() { + console.log("No options implemented"); + } } const toISO8601String = (date: Date) => { diff --git a/packages/app-store/zohocrm/lib/CrmService.ts b/packages/app-store/zohocrm/lib/CrmService.ts index 21929a6989..a1d498d06b 100644 --- a/packages/app-store/zohocrm/lib/CrmService.ts +++ b/packages/app-store/zohocrm/lib/CrmService.ts @@ -91,7 +91,7 @@ export default class ZohoCrmCrmService implements CRM { }); } - async getContacts(emails: string | string[]) { + async getContacts({ emails }: { emails: string | string[] }) { const auth = await this.auth; await auth.getToken(); const emailsArray = Array.isArray(emails) ? emails : [emails]; @@ -289,4 +289,8 @@ export default class ZohoCrmCrmService implements CRM { await auth.getToken(); return await this.deleteMeeting(uid); } + + getAppOptions() { + console.log("No options implemented"); + } } diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 40799a2c30..c8e6ed3f47 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -100,7 +100,7 @@ export default class EventManager { calendarCredentials: CredentialPayload[]; videoCredentials: CredentialPayload[]; crmCredentials: CredentialPayload[]; - appOptions: AppOptions; + appOptions?: z.infer; /** * Takes an array of credentials and initializes a new instance of the EventManager. @@ -132,7 +132,7 @@ export default class EventManager { (cred) => cred.type.endsWith("_crm") || cred.type.endsWith("_other_calendar") ); - this.appOptions = this.generateAppOptions(eventTypeAppMetadata); + this.appOptions = eventTypeAppMetadata; } /** @@ -969,29 +969,32 @@ export default class EventManager { const createdEvents = []; const uid = getUid(event); for (const credential of this.crmCredentials) { - const crm = new CrmManager(credential); + const currentAppOption = this.getAppOptionsFromEventMetadata(credential); + + const crm = new CrmManager(credential, currentAppOption); let success = true; - const skipContactCreation = this.appOptions.crm.skipContactCreation.includes(credential.appId || ""); - const createdEvent = await crm.createEvent(event, skipContactCreation).catch((error) => { + const createdEvent = await crm.createEvent(event, currentAppOption).catch((error) => { success = false; log.warn(`Error creating crm event for ${credential.type}`, error); }); - createdEvents.push({ - type: credential.type, - appName: credential.appId || "", - uid, - success, - createdEvent: { - id: createdEvent?.id || "", + if (createdEvent) { + createdEvents.push({ type: credential.type, + appName: credential.appId || "", + uid, + success, + createdEvent: { + id: createdEvent?.id || "", + type: credential.type, + credentialId: credential.id, + }, + id: createdEvent?.id || "", + originalEvent: event, credentialId: credential.id, - }, - id: createdEvent?.id || "", - originalEvent: event, - credentialId: credential.id, - }); + }); + } } return createdEvents; } @@ -1031,20 +1034,10 @@ export default class EventManager { } } - private generateAppOptions(eventTypeAppMetadata?: z.infer) { - const appOptions: AppOptions = { - crm: { - skipContactCreation: [], - }, - }; + private getAppOptionsFromEventMetadata(credential: CredentialPayload) { + if (!this.appOptions || !credential.appId) return {}; - if (eventTypeAppMetadata) { - for (const key in eventTypeAppMetadata) { - const app = eventTypeAppMetadata[key as keyof typeof eventTypeAppMetadata]; - if (app?.skipContactCreation) appOptions.crm.skipContactCreation.push(key); - } - } - - return appOptions; + if (credential.appId in this.appOptions) + return this.appOptions[credential.appId as keyof typeof this.appOptions]; } } diff --git a/packages/core/crmManager/crmManager.ts b/packages/core/crmManager/crmManager.ts index 23864fedb8..835142effe 100644 --- a/packages/core/crmManager/crmManager.ts +++ b/packages/core/crmManager/crmManager.ts @@ -8,13 +8,15 @@ const log = logger.getSubLogger({ prefix: ["CrmManager"] }); export default class CrmManager { crmService: CRM | null | undefined = null; credential: CredentialPayload; - constructor(credential: CredentialPayload) { + appOptions: any; + constructor(credential: CredentialPayload, appOptions?: any) { this.credential = credential; + this.appOptions = appOptions; } private async getCrmService(credential: CredentialPayload) { if (this.crmService) return this.crmService; - const crmService = await getCrm(credential); + const crmService = await getCrm(credential, this.appOptions); this.crmService = crmService; if (this.crmService === null) { @@ -25,10 +27,11 @@ export default class CrmManager { return crmService; } - public async createEvent(event: CalendarEvent, skipContactCreation?: boolean) { + public async createEvent(event: CalendarEvent, appOptions?: any) { const crmService = await this.getCrmService(this.credential); + const { skipContactCreation } = crmService?.getAppOptions(); // First see if the attendees already exist in the crm - let contacts = (await this.getContacts(event.attendees.map((a) => a.email))) || []; + let contacts = (await this.getContacts({ emails: event.attendees.map((a) => a.email) })) || []; // Ensure that all attendees are in the crm if (contacts.length == event.attendees.length) { return await crmService?.createEvent(event, contacts); @@ -54,9 +57,13 @@ export default class CrmManager { return await crmService?.deleteEvent(uid); } - public async getContacts(emailOrEmails: string | string[], includeOwner?: boolean) { + public async getContacts(params: { + emails: string | string[]; + includeOwner?: boolean; + forRoundRobinSkip?: boolean; + }) { const crmService = await this.getCrmService(this.credential); - const contacts = await crmService?.getContacts(emailOrEmails, includeOwner); + const contacts = await crmService?.getContacts(params); return contacts; } diff --git a/packages/types/CrmService.d.ts b/packages/types/CrmService.d.ts index e3bf894a48..029bc59a60 100644 --- a/packages/types/CrmService.d.ts +++ b/packages/types/CrmService.d.ts @@ -25,9 +25,18 @@ export interface CrmEvent { } export interface CRM { - createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise; + createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise; updateEvent: (uid: string, event: CalendarEvent) => Promise; deleteEvent: (uid: string) => Promise; - getContacts: (emails: string | string[], includeOwner?: boolean) => Promise; + getContacts: ({ + emails, + includeOwner, + forRoundRobinSkip, + }: { + emails: string | string[]; + includeOwner?: boolean; + forRoundRobinSkip?: boolean; + }) => Promise; createContacts: (contactsToCreate: ContactCreateInput[], organizerEmail?: string) => Promise; + getAppOptions: () => any; }