From 381e22ae7e995370bc2dbc68e3f8f94c12f34f62 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 26 Feb 2025 22:21:36 +0530 Subject: [PATCH] test: Salesforce CRM - Add user facing tests and more logs (#19531) * Added user facing tests * fix tests * Fix ts error --- .../app-store/salesforce/lib/CrmService.ts | 26 +- .../__tests__/CrmService.integration.test.ts | 340 ++++++++++++++++++ .../lib/__tests__/salesforceMock.ts | 294 +++++++++++++++ .../lib/server/getTeamMemberEmailFromCrm.ts | 7 +- 4 files changed, 655 insertions(+), 12 deletions(-) create mode 100644 packages/app-store/salesforce/lib/__tests__/CrmService.integration.test.ts create mode 100644 packages/app-store/salesforce/lib/__tests__/salesforceMock.ts diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 3de19e886f..90210ac2ae 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -391,7 +391,7 @@ export default class SalesforceCRMService implements CRM { 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 })); + log.info("[recordToSearch=ACCOUNT] Searching contact for email", safeStringify({ attendeeEmail })); 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 @@ -454,25 +454,26 @@ export default class SalesforceCRMService implements CRM { !(await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0])); const includeAccountRecordType = forRoundRobinSkip && recordToSearch === SalesforceRecordEnum.ACCOUNT; - return records.map((record) => { + // Handle if Account is nested + const ownerEmail = + recordToSearch === SalesforceRecordEnum.ACCOUNT && + record?.attributes?.type !== SalesforceRecordEnum.ACCOUNT + ? record?.Account?.Owner?.Email + : record?.Owner?.Email; + 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, + ownerEmail: ownerEmail, }), }; }); } catch (error) { - log.error("Error in getContacts", safeStringify({ error })); + log.error("Error in getContacts", safeStringify(error)); return []; } } @@ -482,7 +483,7 @@ export default class SalesforceCRMService implements CRM { organizerEmail?: string, calEventResponses?: CalEventResponses | null ) { - const log = logger.getSubLogger({ prefix: [`[getContacts]`] }); + const log = logger.getSubLogger({ prefix: [`[createContacts]`] }); const conn = await this.conn; const appOptions = this.getAppOptions(); const createEventOn = appOptions.createEventOn ?? SalesforceRecordEnum.CONTACT; @@ -901,13 +902,15 @@ export default class SalesforceCRMService implements CRM { private async getAccountBasedOnEmailDomainOfContacts(email: string) { const conn = await this.conn; const emailDomain = email.split("@")[1]; - + const log = logger.getSubLogger({ prefix: [`[getAccountBasedOnEmailDomainOfContacts]:${email}`] }); + log.info("Querying first account matching email domain", safeStringify({ emailDomain })); // 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}%' LIMIT 1` ); if (accountQuery.records.length > 0) { + log.info("Found account matching email domain", safeStringify({ emailDomain })); return { ...(accountQuery.records[0] as { Id?: string; OwnerId?: string; Owner?: { Email?: string } }), Email: undefined, @@ -928,6 +931,7 @@ export default class SalesforceCRMService implements CRM { const dominantAccountId = this.getDominantAccountId(contacts); const contactUnderAccount = contacts.find((contact) => contact.AccountId === dominantAccountId); + log.info("Using dominant account's owner", safeStringify({ dominantAccountId })); return { Id: dominantAccountId, diff --git a/packages/app-store/salesforce/lib/__tests__/CrmService.integration.test.ts b/packages/app-store/salesforce/lib/__tests__/CrmService.integration.test.ts new file mode 100644 index 0000000000..0e98fc451d --- /dev/null +++ b/packages/app-store/salesforce/lib/__tests__/CrmService.integration.test.ts @@ -0,0 +1,340 @@ +/** + * Purpose of these tests is to ensure that we test from CrmService and salesforce interactions perspective purely + * It allows us to document the behaviour more from User perspective and also verify it in parallel + * + * It will also give us confidence to refactor as it doesn't care about internal functions. + * + * CrmService.test.ts could still focus on testing detailed edge cases as needed. + */ +import "../../../../../tests/libs/__mocks__/prisma"; + +import jsforce from "@jsforce/jsforce-node"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import SalesforceCRMService from "../CrmService"; +import { SalesforceRecordEnum } from "../enums"; +import { createSalesforceMock } from "./salesforceMock"; + +vi.mock("@calcom/lib/constants", () => { + return { + WEBAPP_URL: "https://app.cal.com", + APP_CREDENTIAL_SHARING_ENABLED: false, + IS_PRODUCTION: false, + }; +}); +// Mock dependencies +vi.mock("@jsforce/jsforce-node", () => { + return { + default: { + Connection: vi.fn().mockImplementation(() => ({})), + }, + }; +}); + +vi.mock("@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain", () => ({ + checkIfFreeEmailDomain: vi.fn().mockResolvedValue(false), +})); + +vi.mock("../getSalesforceAppKeys", () => ({ + getSalesforceAppKeys: vi.fn().mockResolvedValue({ + consumer_key: "test_consumer_key", + consumer_secret: "test_consumer_secret", + }), +})); + +// Helper to create mock credential +const createMockCredential = () => { + return { + id: 1, + key: { + access_token: "test_access_token", + refresh_token: "test_refresh_token", + instance_url: "https://test.salesforce.com", + }, + type: "salesforce_other_calendar", + user: { + email: "test@example.com", + }, + userId: 1, + teamId: 1, + appId: "test_app_id", + invalid: false, + }; +}; + +const salesforceSettingScenario = { + createOnLeadAndSearchOnAccount: () => { + return { + appOptions: { + createEventOn: SalesforceRecordEnum.LEAD, + createEventOnLeadCheckForContact: true, + roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT, + }, + credential: createMockCredential(), + }; + }, +}; + +describe("SalesforceCRMService", () => { + let salesforceMock: ReturnType; + + beforeEach(() => { + salesforceMock = createSalesforceMock(); + fetchMock.mockReturnValueOnce( + Promise.resolve( + new Response( + JSON.stringify({ + id: "abc", + issued_at: "123", + instance_url: "https://test.salesforce.com", + signature: "123", + access_token: "123", + scope: "123", + token_type: "123", + }), + { + status: 200, + } + ) + ) + ); + // Override jsforce mock with our custom mock + // @ts-expect-error - Not full implementation of jsforce.Connection + vi.mocked(jsforce.Connection).mockImplementation(() => ({ + ...salesforceMock.mockConnection, + version: "1.0", + loginUrl: "https://test.salesforce.com", + instanceUrl: "https://test.salesforce.com", + accessToken: "123", + })); + }); + + describe("createOnLeadAndSearchOnAccount", () => { + const { appOptions, credential } = salesforceSettingScenario.createOnLeadAndSearchOnAccount(); + describe("getContacts: forRoundRobinSkip=true", () => { + const forRoundRobinSkip = true; + it("(Lookup-1) should return contact's account owner's email as ownerEmail if the contact is found directly by email", async () => { + const crmService = new SalesforceCRMService(credential, appOptions); + const contactAccountOwnerEmail = "contact-account-owner@acme.com"; + const contactOwnerEmail = "contact-owner@acme.com"; + const lookingForEmail = "test@example.com"; + + const account = salesforceMock.addAccount({ + Id: "test-account-id", + Owner: { + Email: contactAccountOwnerEmail, + }, + Website: "https://anything", + }); + + salesforceMock.addContact({ + Email: lookingForEmail, + FirstName: "Test", + LastName: "User", + AccountId: account.Id, + Owner: { + Email: contactOwnerEmail, + }, + }); + + const contacts = await crmService.getContacts({ + emails: lookingForEmail, + forRoundRobinSkip, + }); + + expect(contacts).toHaveLength(1); + expect(contacts[0].ownerEmail).toBe(contactAccountOwnerEmail); + expect(contacts[0].email).toBe(lookingForEmail); + expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT); + expect(contacts[0].id).toBe(account.Id); + }); + + it("(Lookup-2) should fallback to account(and use the account owner email as ownerEmail) matched by emailDomain if the contact is not found(i.e Lookup-1 fails)", async () => { + const crmService = new SalesforceCRMService(credential, appOptions); + const contactAccountOwnerEmail = "contact-account-owner@example.com"; + const emailDomain = "example.com"; + const lookingForEmail = `test@${emailDomain}`; + const account = salesforceMock.addAccount({ + Id: "test-account-id", + Owner: { + Email: contactAccountOwnerEmail, + }, + Website: `https://${emailDomain}`, + }); + + const contacts = await crmService.getContacts({ + emails: lookingForEmail, + forRoundRobinSkip, + }); + + expect(contacts).toHaveLength(1); + expect(contacts[0].ownerEmail).toBe(contactAccountOwnerEmail); + // Account doesn't have email + expect(contacts[0].email).toBe(""); + expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT); + // FIXME: Seems like a bug as this should have been accountId but it is undefined + // expect(contacts[0].id).toBe(account.Id); + }); + + it("(Lookup-3) should fallback to account having most number of contacts matched by emailDomain when Lookup-1 and Lookup-2 fails", async () => { + const crmService = new SalesforceCRMService(credential, appOptions); + const account1OwnerEmail = "contact-account-owner1@example.com"; + const account2OwnerEmail = "contact-account-owner2@example.com"; + const emailDomain = "example.com"; + const lookingForEmail = `test@${emailDomain}`; + const account1 = salesforceMock.addAccount({ + Id: "test-account-id1", + Owner: { + Email: account1OwnerEmail, + }, + Website: `https://anything`, + }); + + const dominatingAccount = salesforceMock.addAccount({ + Id: "test-account-id2", + Owner: { + Email: account2OwnerEmail, + }, + Website: `https://anything`, + }); + + salesforceMock.addContact({ + Email: `some-other-contact_acc2_1@${emailDomain}`, + FirstName: "Test", + LastName: "User", + AccountId: dominatingAccount.Id, + }); + + salesforceMock.addContact({ + Email: `some-other-contact_acc2_2@${emailDomain}`, + FirstName: "Test", + LastName: "User", + AccountId: dominatingAccount.Id, + }); + + salesforceMock.addContact({ + Email: `some-other-contact_acc1_1@${emailDomain}`, + FirstName: "Test", + LastName: "User", + AccountId: account1.Id, + }); + + const contacts = await crmService.getContacts({ + emails: lookingForEmail, + forRoundRobinSkip, + }); + + expect(contacts).toHaveLength(1); + expect(contacts[0].ownerEmail).toBe(account2OwnerEmail); + // Account doesn't have email + expect(contacts[0].email).toBe(""); + expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT); + // FIXME: Seems like a bug as this should have been accountId but it is undefined + // expect(contacts[0].id).toBe(dominatingAccount.Id); + }); + }); + + describe("getContacts: forRoundRobinSkip=false i.e. how it is called when creating event", () => { + const forRoundRobinSkip = false; + it("should return lead object if the lead is found directly by email and contact is not found by that email", async () => { + const crmService = new SalesforceCRMService(credential, appOptions); + const contactAccountOwnerEmail = "contact-account-owner@acme.com"; + const contactOwnerEmail = "contact-owner@acme.com"; + const leadOwnerEmail = "lead-owner@acme.com"; + const lookingForEmail = "test1@example.com"; + const contactEmail = "test2@example.com"; + + const account = salesforceMock.addAccount({ + Id: "test-account-id", + Owner: { + Email: contactAccountOwnerEmail, + }, + Website: "https://anything", + }); + + const contact = salesforceMock.addContact({ + // Contact doesn't have the matching email + Email: contactEmail, + FirstName: "Test", + LastName: "User", + AccountId: account.Id, + Owner: { + Email: contactOwnerEmail, + }, + }); + + const lead = salesforceMock.addLead({ + // Lead has the matching email + Email: lookingForEmail, + FirstName: "Test", + LastName: "User", + Owner: { + Email: leadOwnerEmail, + }, + }); + + const contactsOrLeadsOrAccounts = await crmService.getContacts({ + emails: lookingForEmail, + forRoundRobinSkip, + }); + + expect(contactsOrLeadsOrAccounts).toHaveLength(1); + // Because we intentionally don't include owner data in this case + expect(contactsOrLeadsOrAccounts[0].ownerEmail).toBe(undefined); + expect(contactsOrLeadsOrAccounts[0].email).toBe(lookingForEmail); + expect(contactsOrLeadsOrAccounts[0].recordType).toBe(SalesforceRecordEnum.LEAD); + expect(contactsOrLeadsOrAccounts[0].id).toBe(lead.Id); + }); + + it("should return contact object(if there is one by that email) even if lead is found by that email", async () => { + const crmService = new SalesforceCRMService(credential, appOptions); + const contactAccountOwnerEmail = "contact-account-owner@acme.com"; + const contactOwnerEmail = "contact-owner@acme.com"; + const leadOwnerEmail = "lead-owner@acme.com"; + const lookingForEmail = "test1@example.com"; + const contactEmail = "test2@example.com"; + + const account = salesforceMock.addAccount({ + Id: "test-account-id", + Owner: { + Email: contactAccountOwnerEmail, + }, + Website: "https://anything", + }); + + const contact = salesforceMock.addContact({ + // Contact doesn't have the matching email + Email: lookingForEmail, + FirstName: "Test", + LastName: "User", + AccountId: account.Id, + Owner: { + Email: contactOwnerEmail, + }, + }); + + const lead = salesforceMock.addLead({ + // Lead has the matching email + Email: lookingForEmail, + FirstName: "Test", + LastName: "User", + Owner: { + Email: leadOwnerEmail, + }, + }); + + const contactsOrLeadsOrAccounts = await crmService.getContacts({ + emails: lookingForEmail, + forRoundRobinSkip, + }); + + expect(contactsOrLeadsOrAccounts).toHaveLength(1); + // Because we intentionally don't include owner data in this case + expect(contactsOrLeadsOrAccounts[0].ownerEmail).toBe(undefined); + expect(contactsOrLeadsOrAccounts[0].id).toBe(contact.Id); + expect(contactsOrLeadsOrAccounts[0].email).toBe(lookingForEmail); + expect(contactsOrLeadsOrAccounts[0].recordType).toBe(SalesforceRecordEnum.CONTACT); + }); + }); + }); +}); diff --git a/packages/app-store/salesforce/lib/__tests__/salesforceMock.ts b/packages/app-store/salesforce/lib/__tests__/salesforceMock.ts new file mode 100644 index 0000000000..63bcf63ce0 --- /dev/null +++ b/packages/app-store/salesforce/lib/__tests__/salesforceMock.ts @@ -0,0 +1,294 @@ +import { vi } from "vitest"; + +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; + +type Contact = { + Id?: string; + FirstName?: string; + LastName?: string; + Email?: string; + AccountId?: string; + Account?: Account; + Owner?: { + Email?: string; + }; + OwnerId?: string; +}; +type Lead = { + Id?: string; + FirstName?: string; + LastName?: string; + Email?: string; + AccountId?: string; + Account?: Account; + Owner?: { + Email?: string; + }; + OwnerId?: string; +}; +type Account = { Id?: string; OwnerId?: string; Owner?: { Email?: string }; Website?: string }; +type User = { Id?: string; IsActive?: boolean; Email?: string }; + +// Salesforce mock factory +export const createSalesforceMock = () => { + // Store for mock records + const records = { + contacts: [] as Contact[], + leads: [] as Lead[], + accounts: [] as Account[], + users: [] as User[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + events: [] as any[], + }; + + // Query parser and responder + const handleQuery = (query: string) => { + // Simple SOQL parser + const fromMatch = query.match(/FROM\s+(\w+)/i); + const whereMatch = query.match(/WHERE\s+(.+?)(?:\s+LIMIT|\s+ORDER|\s*$)/i); + const limitMatch = query.match(/LIMIT\s+(\d+)/i); + + if (!fromMatch) return { records: [] }; + + const objectType = fromMatch[1]; + const whereClause = whereMatch ? whereMatch[1] : null; + const limit = limitMatch ? parseInt(limitMatch[1]) : Infinity; + + let result = []; + + switch (objectType) { + case "Contact": + result = records.contacts; + break; + case "Lead": + result = records.leads; + break; + case "Account": + result = records.accounts; + break; + case "User": + result = records.users; + break; + case "Event": + result = records.events; + break; + default: + return { records: [] }; + } + + // Apply where clause filtering (basic implementation) + if (whereClause) { + if (whereClause.includes("Email =")) { + const emailMatch = whereClause.match(/Email\s*=\s*'([^']+)'/i); + if (emailMatch) { + const email = emailMatch[1]; + result = result.filter((r) => r.Email === email); + } + } + + if (whereClause.includes("Email IN")) { + const emailsMatch = whereClause.match(/Email IN \('([^']+)(?:','([^']+))*'\)/i); + if (emailsMatch) { + const emails = whereClause + .match(/Email IN \((.+)\)/i)?.[1] + ?.split("','") + ?.map((e) => e.replace(/'/g, "")); + result = result.filter((r) => emails?.includes(r.Email)); + } + } + + if (whereClause.includes("Email LIKE")) { + const emailLikeMatch = whereClause.match(/Email LIKE '%@([^']+)'/i); + if (emailLikeMatch) { + const domain = emailLikeMatch[1]; + result = result.filter((r) => r.Email && r.Email.endsWith(`@${domain}`)); + } + } + + if (whereClause.includes("Id =")) { + const idMatch = whereClause.match(/Id\s*=\s*'([^']+)'/i); + if (idMatch) { + const id = idMatch[1]; + result = result.filter((r) => r.Id === id); + } + } + + if (whereClause.includes("AccountId =")) { + const accountIdMatch = whereClause.match(/AccountId\s*=\s*'([^']+)'/i); + if (accountIdMatch) { + const accountId = accountIdMatch[1]; + result = result.filter((r) => r.AccountId === accountId); + } + } + + if (whereClause.includes("AccountId != null")) { + result = result.filter((r) => r.AccountId); + } + + if (whereClause.includes("Website LIKE")) { + const websiteLikeMatch = whereClause.match(/Website LIKE '%([^']+)%'/i); + if (websiteLikeMatch) { + const websitePart = websiteLikeMatch[1]; + result = result.filter((r) => r.Website && r.Website.includes(websitePart)); + } + } + + if (whereClause.includes("IsActive = true")) { + result = result.filter((r) => r.IsActive === true); + } + } + + // Apply limit + result = result.slice(0, limit); + + // Handle special cases with nested property selection + if (query.includes("Account.Owner.Email")) { + result = result.map((r) => { + if (r.AccountId) { + const account = records.accounts.find((a) => a.Id === r.AccountId); + if (account && account.OwnerId) { + const owner = records.users.find((u) => u.Id === account.OwnerId); + r.Account = { + Owner: { + Email: owner ? owner.Email : null, + }, + OwnerId: account.OwnerId, + }; + } + } + return r; + }); + } + + if (query.includes("Owner.Email")) { + result = result.map((r) => { + if (r.OwnerId) { + const owner = records.users.find((u) => u.Id === r.OwnerId); + r.Owner = { + Email: owner ? owner.Email : null, + }; + } + return r; + }); + } + + return { records: result }; + }; + + const getAccountById = (id: string) => { + return records.accounts.find((a) => a.Id === id); + }; + + // Add record helpers + const addContact = (contact: Contact) => { + const newContact = { + Id: contact.Id || `003${Math.random().toString().slice(2, 10)}`, + attributes: { type: "Contact" }, + ...contact, + }; + + if (newContact.AccountId) { + const account = getAccountById(newContact.AccountId); + if (account) { + newContact.Account = { + ...account, + }; + } else { + throw new Error(`Account with id ${newContact.AccountId} not found`); + } + } + + logger.debug("[SalesforceMock]Adding contact", safeStringify(newContact)); + records.contacts.push(newContact); + return newContact; + }; + + const addLead = (lead: Lead) => { + const newLead = { + Id: lead.Id || `00Q${Math.random().toString().slice(2, 10)}`, + attributes: { type: "Lead" }, + ...lead, + }; + if (newLead.AccountId) { + const account = getAccountById(newLead.AccountId); + if (account) { + newLead.Account = { + ...account, + }; + } else { + throw new Error(`Account with id ${newLead.AccountId} not found`); + } + } + logger.debug("[SalesforceMock] Adding lead", safeStringify(newLead)); + records.leads.push(newLead); + return newLead; + }; + + const addAccount = (account: Account) => { + const newAccount = { + Id: account.Id || `001${Math.random().toString().slice(2, 10)}`, + ...account, + attributes: { type: "Account" }, + }; + records.accounts.push(newAccount); + logger.debug("[SalesforceMock] Adding account", safeStringify(newAccount)); + return newAccount; + }; + + const addUser = (user: User) => { + const newUser = { + Id: user.Id || `005${Math.random().toString().slice(2, 10)}`, + IsActive: user.IsActive !== undefined ? user.IsActive : true, + attributes: { type: "User" }, + ...user, + }; + logger.debug("[SalesforceMock] Adding user", safeStringify(newUser)); + records.users.push(newUser); + return newUser; + }; + + // Mock connection interface + const mockConnection = { + query: vi.fn().mockImplementation(handleQuery), + sobject: vi.fn().mockReturnValue({ + create: vi.fn().mockImplementation(() => ({ success: true, id: `id_${Math.random()}` })), + update: vi.fn().mockImplementation(() => ({ success: true })), + delete: vi.fn().mockImplementation(() => ({ success: true })), + }), + describe: vi.fn().mockImplementation(() => { + return { + fields: [ + { name: "Id", type: "id" }, + { name: "Email", type: "email" }, + { name: "FirstName", type: "string" }, + { name: "LastName", type: "string" }, + { name: "Company", type: "string" }, + { name: "Website", type: "url" }, + { name: "OwnerId", type: "reference" }, + { name: "NoShow__c", type: "boolean", length: 0 }, + { name: "LastMeetingDate__c", type: "date" }, + ], + }; + }), + clear: () => { + records.contacts = []; + records.leads = []; + records.accounts = []; + records.users = []; + records.events = []; + mockConnection.query.mockClear(); + mockConnection.sobject.mockClear(); + mockConnection.describe.mockClear(); + }, + }; + + return { + mockConnection, + addContact, + addLead, + addAccount, + addUser, + getRecords: () => records, + }; +}; diff --git a/packages/lib/server/getTeamMemberEmailFromCrm.ts b/packages/lib/server/getTeamMemberEmailFromCrm.ts index 3972db7393..7ca3ac4837 100644 --- a/packages/lib/server/getTeamMemberEmailFromCrm.ts +++ b/packages/lib/server/getTeamMemberEmailFromCrm.ts @@ -123,7 +123,12 @@ async function getOwnerEmailFromCrm( if (!crmContactOwner?.email) return returnNullValue; log.info( "[getOwnerEmailFromCrm] crmContactOwner", - safeStringify({ contactOwnerEmail: crmContactOwner.email, email, eventTypeId: eventData.id }) + safeStringify({ + contactOwnerEmail: crmContactOwner.email, + recordType: crmContactOwner.recordType, + email, + eventTypeId: eventData.id, + }) ); // Determine if the contactOwner is a part of the event type const contactOwnerQuery = await findUserByEmailWhoIsAHostOfEventType({