From a22fd0c1513e3c6a406015bfc95efa71cc337ce1 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:14:49 -0500 Subject: [PATCH] chore: Salesforce - optimize `getContact` method [CAL-4940] (#18289) Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .../getTeamMemberEmailFromCrm.test.ts | 11 +- .../team/[slug]/[type]/getServerSideProps.tsx | 1 + .../salesforce/lib/CrmService.test.ts | 94 ++++----- .../app-store/salesforce/lib/CrmService.ts | 192 ++++++++---------- 4 files changed, 129 insertions(+), 169 deletions(-) diff --git a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts index a1337a6c04..e5f67ae1c1 100644 --- a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts +++ b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts @@ -166,6 +166,13 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => { schedulingType: SchedulingType.ROUND_ROBIN, metadata: null, length: 30, + hosts: [ + { + user: { + email: "owner@example.com", + }, + }, + ], }; it("should return null when email is not provided in query", async () => { @@ -235,7 +242,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => { }); it("should return null when CRM owner is not part of event type", async () => { - const ownerEmail = "owner@example.com"; + const ownerEmail = "ownerNotInEventType@example.com"; const bookerEmail = "booker@example.com"; mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail: ownerEmail }); @@ -331,7 +338,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => { it("should return null when when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided but the returned email isn't an event member", async () => { const bookerEmail = "booker@example.com"; - const teamMemberEmail = "owner@example.com"; + const teamMemberEmail = "ownerNotInEventType@example.com"; mockBookingFormHandler({ bookerEmail, teamMemberEmail }); const SOME_OTHER_EVENT_ID = 200; await createHostForEvent({ diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index 4d1af41981..84b8ab0ed9 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -92,6 +92,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => select: { name: true, username: true, + email: true, }, }, }, diff --git a/packages/app-store/salesforce/lib/CrmService.test.ts b/packages/app-store/salesforce/lib/CrmService.test.ts index c1a9afc1fa..560059bcef 100644 --- a/packages/app-store/salesforce/lib/CrmService.test.ts +++ b/packages/app-store/salesforce/lib/CrmService.test.ts @@ -18,6 +18,9 @@ const contactQueryResponse = { Email: "test@example.com", OwnerId: "owner001", attributes: { type: "Contact" }, + Owner: { + Email: "owner@example.com", + }, }, ], }; @@ -30,6 +33,12 @@ const contactUnderAccountQueryResponse = { OwnerId: "owner001", AccountId: "acc001", attributes: { type: "Contact" }, + Account: { + attributes: { type: "Account" }, + Owner: { + Email: "owner@example.com", + }, + }, }, ], }; @@ -41,6 +50,9 @@ const leadQueryResponse = { Email: "test@example.com", OwnerId: "owner001", attributes: { type: "Lead" }, + Owner: { + Email: "owner@example.com", + }, }, ], }; @@ -114,7 +126,7 @@ describe("SalesforceCRMService", () => { expect(result).toEqual([]); expect(mockConnection.query).toHaveBeenCalledWith( - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); @@ -147,7 +159,7 @@ describe("SalesforceCRMService", () => { // Verify the exact query made expect(querySpy).toHaveBeenCalledWith( - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); @@ -190,7 +202,7 @@ describe("SalesforceCRMService", () => { ]); expect(querySpy).toHaveBeenCalledWith( - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test1@example.com','test2@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test1@example.com','test2@example.com')" ); }); @@ -207,19 +219,16 @@ describe("SalesforceCRMService", () => { Email: "test@example.com", // OwnerId: "owner001", attributes: { type: "Contact" }, + Owner: { + Email: "owner@example.com", + }, }, ], }; - const ownerQueryResponse = { - records: [], - }; - // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy - .mockResolvedValueOnce(contactQueryResponse) // Contact query - .mockResolvedValueOnce(ownerQueryResponse); // Owner query + querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query const result = await service.getContacts({ emails: "test@example.com", @@ -231,14 +240,14 @@ describe("SalesforceCRMService", () => { id: "001", email: "test@example.com", ownerId: undefined, - ownerEmail: undefined, + ownerEmail: "owner@example.com", recordType: "Contact", }, ]); expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); @@ -265,7 +274,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); it("when createEventOn is lead", async () => { @@ -290,7 +299,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Lead WHERE Email IN ('test@example.com')" + "SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('test@example.com')" ); }); it("when createEventOn is contact under an account", async () => { @@ -315,7 +324,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" + "SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" ); }); }); @@ -328,9 +337,7 @@ describe("SalesforceCRMService", () => { // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy - .mockResolvedValueOnce(contactQueryResponse) // Contact query - .mockResolvedValueOnce(ownerQueryResponse); // Owner query + querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query const result = await service.getContacts({ emails: "test@example.com", @@ -349,11 +356,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" - ); - expect(querySpy).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); it("when createEventOn is lead", async () => { @@ -363,7 +366,7 @@ describe("SalesforceCRMService", () => { // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy.mockResolvedValueOnce(leadQueryResponse).mockResolvedValueOnce(ownerQueryResponse); + querySpy.mockResolvedValueOnce(leadQueryResponse); const result = await service.getContacts({ emails: "test@example.com", @@ -382,11 +385,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Lead WHERE Email IN ('test@example.com')" - ); - expect(querySpy).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('test@example.com')" ); }); it("when createEventOn is contact under an account", async () => { @@ -396,9 +395,7 @@ describe("SalesforceCRMService", () => { // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy - .mockResolvedValueOnce(contactUnderAccountQueryResponse) - .mockResolvedValueOnce(ownerQueryResponse); + querySpy.mockResolvedValueOnce(contactUnderAccountQueryResponse); const result = await service.getContacts({ emails: "test@example.com", @@ -411,17 +408,13 @@ describe("SalesforceCRMService", () => { email: "test@example.com", ownerId: "owner001", ownerEmail: "owner@example.com", - recordType: "Account", + recordType: "Contact", }, ]); expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" - ); - expect(querySpy).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" ); }); }); @@ -435,9 +428,7 @@ describe("SalesforceCRMService", () => { // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy - .mockResolvedValueOnce(contactQueryResponse) // Contact query - .mockResolvedValueOnce(ownerQueryResponse); // Owner query + querySpy.mockResolvedValueOnce(contactQueryResponse); // Contact query const result = await service.getContacts({ emails: "test@example.com", @@ -456,11 +447,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')" - ); - expect(querySpy).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, Owner.Email FROM Contact WHERE Email IN ('test@example.com')" ); }); it("checking against leads", async () => { @@ -470,7 +457,7 @@ describe("SalesforceCRMService", () => { // Setup query spy with specific responses const querySpy = vi.spyOn(mockConnection, "query"); - querySpy.mockResolvedValueOnce(leadQueryResponse).mockResolvedValueOnce(ownerQueryResponse); + querySpy.mockResolvedValueOnce(leadQueryResponse); const result = await service.getContacts({ emails: "test@example.com", @@ -489,11 +476,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId FROM Lead WHERE Email IN ('test@example.com')" - ); - expect(querySpy).toHaveBeenNthCalledWith( - 2, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, Owner.Email FROM Lead WHERE Email IN ('test@example.com')" ); }); it("checking against contacts under an account", async () => { @@ -523,12 +506,7 @@ describe("SalesforceCRMService", () => { expect(querySpy).toHaveBeenNthCalledWith( 1, - "SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" - ); - expect(querySpy).toHaveBeenNthCalledWith(2, "SELECT Id, OwnerId FROM Account WHERE Id = 'acc001'"); - expect(querySpy).toHaveBeenNthCalledWith( - 3, - expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'") + "SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = 'test@example.com' AND AccountId != null" ); }); }); diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 46253f6644..6b6259a571 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -44,9 +44,9 @@ const sfApiErrors = { }; type ContactRecord = { - Id: string; - Email: string; - OwnerId: string; + Id?: string; + Email?: string; + OwnerId?: string; [key: string]: any; }; @@ -348,7 +348,6 @@ export default class SalesforceCRMService implements CRM { SalesforceRecordEnum.CONTACT; let soql: string; - let accountOwnerId = ""; log.info( "Getting contacts for emails", @@ -366,58 +365,31 @@ export default class SalesforceCRMService implements CRM { // For an account let's assume that the first email is the one we should be querying against const attendeeEmail = emailArray[0]; log.info("Searching account for email", safeStringify({ attendeeEmail })); - - 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); - log.info("Account contact search results", safeStringify({ resultCount: results.records.length })); - - if (results.records?.length) { - const contact = results.records[0] as { AccountId?: string }; - if (contact?.AccountId) { - soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${contact.AccountId}'`; - } - } else { - // If we can't find the exact contact, then we need to search for an account where the contacts share the same email domain - const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendeeEmail); - if (accountId) { - soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${accountId}'`; - } - } - } - // If creating events on contacts or leads + soql = `SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${attendeeEmail}' AND AccountId != null`; } else { // Handle Contact/Lead record types - soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join( + soql = `SELECT Id, Email, OwnerId, Owner.Email FROM ${recordToSearch} WHERE Email IN ('${emailArray.join( "','" )}')`; } const results = await conn.query(soql); - log.info("Query results", safeStringify({ recordCount: results.records?.length })); - // If we're checking against the contact, the ownerId should take precedence - if (recordToSearch === SalesforceRecordEnum.ACCOUNT && results.records?.length) { - const account = results.records[0] as ContactRecord; - if (account?.OwnerId) { - accountOwnerId = account.OwnerId; - } - } + log.info("Query results", safeStringify({ recordCount: results.records?.length })); let records: ContactRecord[] = []; // If falling back to contacts, check for the contact before returning the leads or empty array if ( appOptions.createEventOn === SalesforceRecordEnum.LEAD && - appOptions.createEventOnLeadCheckForContact + appOptions.createEventOnLeadCheckForContact && + !forRoundRobinSkip ) { // Get any matching contacts const contactSearch = await conn.query( - `SELECT Id, Email, OwnerId FROM ${SalesforceRecordEnum.CONTACT} WHERE Email IN ('${emailArray.join( - "','" - )}')` + `SELECT Id, Email, OwnerId, Owner.Email FROM ${ + SalesforceRecordEnum.CONTACT + } WHERE Email IN ('${emailArray.join("','")}')` ); if (contactSearch?.records?.length > 0) { @@ -436,84 +408,42 @@ export default class SalesforceCRMService implements CRM { records = results.records as ContactRecord[]; } + if (recordToSearch === SalesforceRecordEnum.ACCOUNT && forRoundRobinSkip && !results.records.length) { + const attendeeEmail = emailArray[0]; + // If we can't find the exact contact, then we need to search for an account where the contacts share the same email domain + const account = await this.getAccountBasedOnEmailDomainOfContacts(attendeeEmail); + if (account) { + records = [account]; + } + } + if (!records.length) { log.info("No records found"); return []; } - // Handle owner information - if ( + const includeOwnerData = (includeOwner || forRoundRobinSkip) && - !(await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0])) - ) { - const ownerIds: Set = new Set(); + !(await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0])); - if (accountOwnerId) { - ownerIds.add(accountOwnerId); - } else { - records.forEach((record) => { - if (record?.OwnerId) { - ownerIds.add(record.OwnerId); - } - }); - } + const includeAccountRecordType = forRoundRobinSkip && recordToSearch === SalesforceRecordEnum.ACCOUNT; - const ownersQuery = - ownerIds.size > 0 - ? await Promise.all( - Array.from(ownerIds).map(async (ownerId) => { - const result = await this.getSalesforceUserFromUserId(ownerId); - return result; - }) - ) - : []; - - // Filter out any undefined results and ensure records exist - const validOwnersQuery = ownersQuery.filter((query): query is jsforce.QueryResult => { - const firstRecord = query?.records?.[0]; - if (!firstRecord) return false; - - return ( - typeof firstRecord === "object" && "Email" in firstRecord && typeof firstRecord.Email === "string" - ); - }); - - return records - .map((record) => { - if (!record?.Id) { - log.warn("Invalid record data", { record }); - return null; - } - - const ownerEmail = accountOwnerId - ? validOwnersQuery[0]?.records[0]?.Email - : validOwnersQuery.find((user) => user.records[0]?.Id === record.OwnerId)?.records[0]?.Email; - - return { - id: record.Id, - email: record.Email, - ownerId: record.OwnerId, - ownerEmail, - recordType: accountOwnerId ? SalesforceRecordEnum.ACCOUNT : record.attributes?.type, - }; - }) - .filter((record): record is NonNullable => record !== null); - } - - return records - .map((record) => { - if (!record?.Id || !record?.Email || !record?.attributes?.type) { - log.warn("Invalid record data for basic mapping", { record }); - return null; - } - - return { - id: record.Id, - email: record.Email, - recordType: record.attributes.type, - }; - }) - .filter((record): record is NonNullable => record !== null); + return records.map((record) => { + return { + id: includeAccountRecordType ? record?.AccountId : record?.Id || "", + email: record?.Email || "", + recordType: includeAccountRecordType ? SalesforceRecordEnum.ACCOUNT : record?.attributes?.type, + ...(includeOwnerData && { + ownerId: record?.OwnerId, + // Handle if Account is nested + ownerEmail: + recordToSearch === SalesforceRecordEnum.ACCOUNT && + record?.attributes?.type !== SalesforceRecordEnum.ACCOUNT + ? record?.Account?.Owner?.Email + : record?.Owner?.Email, + }), + }; + }); } catch (error) { log.error("Error in getContacts", safeStringify({ error })); return []; @@ -917,6 +847,50 @@ export default class SalesforceCRMService implements CRM { return this.getDominantAccountId(response.records as { AccountId: string }[]); } + private async getAccountBasedOnEmailDomainOfContacts(email: string) { + const conn = await this.conn; + const emailDomain = email.split("@")[1]; + + // First check if an account has the same website as the email domain of the attendee + const accountQuery = await conn.query( + `SELECT Id, OwnerId, Owner.Email FROM Account WHERE Website LIKE '%${emailDomain}%'` + ); + + if (accountQuery.records.length > 0) { + return { + ...(accountQuery.records[0] as { Id?: string; OwnerId?: string; Owner?: { Email?: string } }), + Email: undefined, + }; + } + + // Fallback to querying which account the majority of contacts are under + const contactQuery = await conn.query( + `SELECT Id, Email, AccountId, Account.OwnerId, Account.Owner.Email FROM Contact WHERE Email LIKE '%@${emailDomain}' AND AccountId != null` + ); + + const contacts = contactQuery?.records as { + AccountId: string; + Account: { OwnerId?: string; Owner: { Email: string } }; + }[]; + if (!contacts) return; + + const dominantAccountId = this.getDominantAccountId(contacts); + + const contactUnderAccount = contacts.find((contact) => contact.AccountId === dominantAccountId); + + return { + Id: dominantAccountId, + Email: undefined, + OwnerId: contactUnderAccount?.Account?.OwnerId, + Owner: { + Email: contactUnderAccount?.Account?.Owner?.Email, + }, + attributes: { + type: SalesforceRecordEnum.ACCOUNT, + }, + }; + } + private setFallbackToContact(boolean: boolean) { this.fallbackToContact = boolean; }