refactor: getContacts (#18137)

This commit is contained in:
Udit Takkar
2024-12-16 10:17:59 -05:00
committed by GitHub
parent 0466b7ebd6
commit 423b17a819
2 changed files with 453 additions and 97 deletions
@@ -0,0 +1,265 @@
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { describe, expect, it, beforeEach, vi } from "vitest";
import type { CredentialPayload } from "@calcom/types/Credential";
import SalesforceCRMService from "./CrmService";
import { SalesforceRecordEnum } from "./enums";
describe("SalesforceCRMService", () => {
let service: SalesforceCRMService;
let mockConnection: { query: any };
setupAndTeardown();
beforeEach(() => {
mockConnection = {
query: vi.fn(),
};
const mockCredential: CredentialPayload = {
id: 1,
type: "salesforce_crm",
key: {
access_token: "mock_token",
refresh_token: "mock_refresh",
instance_url: "https://mock.salesforce.com",
},
userId: 1,
appId: "salesforce",
teamId: null,
invalid: false,
user: {
email: "test-user@example.com",
},
};
service = new SalesforceCRMService(mockCredential, {}, true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
service.conn = Promise.resolve(mockConnection);
});
describe("getContacts", () => {
it("should return empty array when no records found", async () => {
// Setup mock response
mockConnection.query.mockResolvedValueOnce({ records: [] });
const result = await service.getContacts({ emails: "test@example.com" });
expect(result).toEqual([]);
expect(mockConnection.query).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')"
);
});
it("should handle single email string input", async () => {
// Setup mock response with descriptive name
const contactQueryResponse = {
records: [
{
Id: "001",
Email: "test@example.com",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
],
};
// Setup query spy
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactQueryResponse);
const result = await service.getContacts({ emails: "test@example.com" });
expect(result).toEqual([
{
id: "001",
email: "test@example.com",
recordType: "Contact",
},
]);
// Verify the exact query made
expect(querySpy).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')"
);
});
it("should handle array of emails input", async () => {
const contactsQueryResponse = {
records: [
{
Id: "001",
Email: "test1@example.com",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
{
Id: "002",
Email: "test2@example.com",
OwnerId: "owner002",
attributes: { type: "Contact" },
},
],
};
const querySpy = vi.spyOn(mockConnection, "query");
querySpy.mockResolvedValueOnce(contactsQueryResponse);
const result = await service.getContacts({
emails: ["test1@example.com", "test2@example.com"],
});
expect(result).toEqual([
{
id: "001",
email: "test1@example.com",
recordType: "Contact",
},
{
id: "002",
email: "test2@example.com",
recordType: "Contact",
},
]);
expect(querySpy).toHaveBeenCalledWith(
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test1@example.com','test2@example.com')"
);
});
it("should include owner information when includeOwner is true", async () => {
const contactQueryResponse = {
records: [
{
Id: "001",
Email: "test@example.com",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
],
};
const ownerQueryResponse = {
records: [
{
Id: "owner001",
Email: "owner@example.com",
Name: "Test Owner",
},
],
};
// Setup query spy with specific responses
const querySpy = vi.spyOn(mockConnection, "query");
querySpy
.mockResolvedValueOnce(contactQueryResponse) // Contact query
.mockResolvedValueOnce(ownerQueryResponse); // Owner query
const result = await service.getContacts({
emails: "test@example.com",
includeOwner: true,
});
expect(result).toEqual([
{
id: "001",
email: "test@example.com",
ownerId: "owner001",
ownerEmail: "owner@example.com",
recordType: "Contact",
},
]);
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'")
);
});
it("should handle account record type with round robin skip", async () => {
service = new SalesforceCRMService(
{} as CredentialPayload,
{
createEventOn: SalesforceRecordEnum.ACCOUNT,
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT,
},
true
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
service.conn = Promise.resolve(mockConnection);
const initialContactQueryResponse = {
records: [
{
Id: "001",
Email: "test@example.com",
AccountId: "acc001",
OwnerId: "owner001",
attributes: { type: "Contact" },
},
],
};
const accountQueryResponse = {
records: [
{
Id: "acc001",
OwnerId: "owner001",
Email: "test@example.com",
attributes: { type: "Account" },
},
],
};
const ownerQueryResponse = {
records: [
{
Id: "owner001",
Email: "owner@example.com",
Name: "Test Owner",
},
],
};
const querySpy = vi.spyOn(mockConnection, "query");
querySpy
.mockResolvedValueOnce(initialContactQueryResponse)
.mockResolvedValueOnce(accountQueryResponse)
.mockResolvedValueOnce(ownerQueryResponse);
const result = await service.getContacts({
emails: "test@example.com",
forRoundRobinSkip: true,
includeOwner: true,
});
expect(result).toEqual([
{
id: "acc001",
email: "test@example.com",
ownerId: "owner001",
ownerEmail: "owner@example.com",
recordType: "Account",
},
]);
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'")
);
});
});
});
+188 -97
View File
@@ -7,6 +7,7 @@ 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 logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type { CalendarEvent, CalEventResponses } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
@@ -62,16 +63,18 @@ const salesforceTokenSchema = z.object({
export default class SalesforceCRMService implements CRM {
private integrationName = "";
private conn: Promise<jsforce.Connection>;
private conn!: Promise<jsforce.Connection>;
private log: typeof logger;
private calWarnings: string[] = [];
private appOptions: any;
private doNotCreateEvent = false;
private fallbackToContact = false;
constructor(credential: CredentialPayload, appOptions: any) {
constructor(credential: CredentialPayload, appOptions: any, testMode = false) {
this.integrationName = "salesforce_other_calendar";
this.conn = this.getClient(credential).then((c) => c);
if (!testMode) {
this.conn = this.getClient(credential).then((c) => c);
}
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
this.appOptions = appOptions;
}
@@ -323,108 +326,196 @@ export default class SalesforceCRMService implements CRM {
includeOwner?: boolean;
forRoundRobinSkip?: boolean;
}) {
const conn = await this.conn;
const emailArray = Array.isArray(emails) ? emails : [emails];
const appOptions = this.getAppOptions();
const recordToSearch =
(forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ??
SalesforceRecordEnum.CONTACT;
let soql: string;
let accountOwnerId = "";
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`;
const log = logger.getSubLogger({ prefix: [`[getContacts]:${emails}`] });
try {
const conn = await this.conn;
const emailArray = Array.isArray(emails) ? emails : [emails];
// 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 };
if (contact) {
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
} else {
soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join("','")}')`;
}
const results = await conn.query(soql);
const appOptions = this.getAppOptions();
const recordToSearch =
(forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ??
SalesforceRecordEnum.CONTACT;
// 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;
accountOwnerId = account.OwnerId;
}
let soql: string;
let accountOwnerId = "";
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
) {
// Get any matching contacts
const contactSearch = await conn.query(
`SELECT Id, Email, OwnerId FROM ${SalesforceRecordEnum.CONTACT} WHERE Email IN ('${emailArray.join(
"','"
)}')`
log.info(
"Getting contacts for emails",
safeStringify({
emailArray,
includeOwner,
forRoundRobinSkip,
recordToSearch,
appOptions,
})
);
if (contactSearch && contactSearch.records.length > 0) {
records = contactSearch.records as ContactRecord[];
this.setFallbackToContact(true);
// Handle Account record type
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];
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
} else {
// Handle Contact/Lead record types
soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join(
"','"
)}')`;
}
} else if (!results || !results.records.length) {
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;
}
}
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
) {
// Get any matching contacts
const contactSearch = await conn.query(
`SELECT Id, Email, OwnerId FROM ${SalesforceRecordEnum.CONTACT} WHERE Email IN ('${emailArray.join(
"','"
)}')`
);
if (contactSearch?.records?.length > 0) {
records = contactSearch.records as ContactRecord[];
this.setFallbackToContact(true);
log.info(
"Found matching contacts, falling back to contact",
safeStringify({
contactCount: records.length,
})
);
}
}
if (!records.length && results?.records?.length) {
records = results.records as ContactRecord[];
}
if (!records.length) {
log.info("No records found");
return [];
}
// Handle owner information
if (includeOwner || forRoundRobinSkip) {
const ownerIds: Set<string> = new Set();
if (accountOwnerId) {
ownerIds.add(accountOwnerId);
} else {
records.forEach((record) => {
if (record?.OwnerId) {
ownerIds.add(record.OwnerId);
}
});
}
if (ownerIds.size === 0) {
log.warn("No owner IDs found for records");
return [];
}
const ownersQuery = 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<ContactRecord> => {
const firstRecord = query?.records?.[0];
if (!firstRecord) return false;
return (
typeof firstRecord === "object" && "Email" in firstRecord && typeof firstRecord.Email === "string"
);
});
if (validOwnersQuery.length === 0) {
log.warn("No valid owner records found");
return [];
}
return records
.map((record) => {
if (!record?.Id || !record?.OwnerId) {
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;
if (!ownerEmail) {
log.warn("Could not find owner email", { recordId: record.Id, ownerId: record.OwnerId });
return null;
}
return {
id: record.Id,
email: record.Email,
ownerId: record.OwnerId,
ownerEmail,
recordType: accountOwnerId ? SalesforceRecordEnum.ACCOUNT : record.attributes?.type,
};
})
.filter((record): record is NonNullable<typeof record> => 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<typeof record> => record !== null);
} catch (error) {
log.error("Error in getContacts", safeStringify({ error }));
return [];
}
if (!records.length) records = results.records as ContactRecord[];
if (includeOwner || forRoundRobinSkip) {
const ownerIds: Set<string> = new Set();
if (accountOwnerId) {
ownerIds.add(accountOwnerId);
} else {
records.forEach((record) => {
ownerIds.add(record.OwnerId);
});
}
const ownersQuery = (await Promise.all(
Array.from(ownerIds).map(async (ownerId) => {
return await this.getSalesforceUserFromUserId(ownerId);
})
)) as { records: ContactRecord[] }[];
const contactsWithOwners = records.map((record) => {
const ownerEmail = accountOwnerId
? ownersQuery[0].records[0].Email
: ownersQuery.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,
};
});
return contactsWithOwners;
}
return records
? records.map((record) => ({
id: record.Id,
email: record.Email,
recordType: record.attributes.type,
}))
: [];
}
async createContacts(