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>
This commit is contained in:
Joe Au-YeungandUdit Takkar authored and GitHub committed 2024-10-12 10:02:27 +00:00
1 parent 47b9e8a8ac
commit b1d9e2b84c
15 files changed
+406 -96

No files matched your search

@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -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<typeof EventTypeAppMeta
},
});
if (!crmCredential) return;
return new CrmManager(crmCredential);
return new CrmManager(crmCredential, crmRoundRobinLeadSkip);
}
+3 -3
View File
@@ -6,10 +6,10 @@ import appStore from "..";
type Class<I, Args extends any[] = any[]> = new (...args: Args) => I;
type CrmClass = Class<CRM, [CredentialPayload]>;
type CrmClass = Class<CRM, [CredentialPayload, any]>;
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);
}
};
@@ -121,7 +121,7 @@ export default class CloseComCRMService implements CRM {
await this.closeComDeleteCustomActivity(uid);
}
async getContacts(emails: string | string[]): Promise<Contact[]> {
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
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");
}
}
+5 -1
View File
@@ -198,7 +198,7 @@ export default class HubspotCalendarService implements CRM {
return await this.hubspotDeleteMeeting(uid);
}
async getContacts(emails: string | string[]): Promise<Contact[]> {
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
const auth = await this.auth;
await auth.getToken();
@@ -269,4 +269,8 @@ export default class HubspotCalendarService implements CRM {
};
});
}
getAppOptions() {
console.log("No options implemented");
}
}
@@ -78,8 +78,8 @@ export default class PipedriveCrmService implements CRM {
return results.map((result) => result.result);
}
async getContacts(email: string | string[]): Promise<Contact[]> {
const emailArray = Array.isArray(email) ? email : [email];
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
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<IntegrationCalendar[]> {
return Promise.resolve([]);
}
getAppOptions() {
console.log("No options implemented");
}
}
@@ -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<typeof appDataSchema>();
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 (
<AppCard
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
@@ -31,34 +56,95 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
switchChecked={enabled}
hideSettingsIcon>
<>
<div>
<Switch
label={t("skip_contact_creation", { appName: "Salesforce" })}
labelOnLeading
checked={isSkipContactCreationEnabled}
onCheckedChange={(checked) => {
setAppData("skipContactCreation", checked);
}}
/>
</div>
</>
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
<div className="mt-4">
<Switch
label={t("skip_rr_assignment_label")}
labelOnLeading
checked={isRoundRobinLeadSkipEnabled}
onCheckedChange={(checked) => {
setAppData("roundRobinLeadSkip", checked);
if (checked) {
// temporary solution, enabled should always be already set
setAppData("enabled", checked);
<div className="mb-4 ml-2">
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
{t("salesforce_create_record_as")}
</label>
<Select
className="mt-2 w-60"
options={recordOptions}
value={createEventOnSelectedOption}
onChange={(e) => {
if (e) {
setCreateEventOnSelectedOption(e);
setAppData("createEventOn", e.value);
}
}}
/>
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
</div>
) : null}
{createEventOnSelectedOption.value === SalesforceRecordEnum.CONTACT ? (
<div>
<Switch
label={t("skip_contact_creation", { appName: "Salesforce" })}
labelOnLeading
checked={isSkipContactCreationEnabled}
onCheckedChange={(checked) => {
setAppData("skipContactCreation", checked);
}}
/>
</div>
) : null}
{createEventOnSelectedOption.value === SalesforceRecordEnum.ACCOUNT ? (
<>
<div className="mb-4">
<Switch
label={t("salesforce_create_new_contact_under_account")}
labelOnLeading
checked={createNewContactUnderAccount}
onCheckedChange={(checked) => {
setAppData("createNewContactUnderAccount", checked);
}}
/>
</div>
<div>
<Switch
label={t("salesforce_if_account_does_not_exist")}
labelOnLeading
checked={createLeadIfAccountNull}
onCheckedChange={(checked) => {
setAppData("createLeadIfAccountNull", checked);
}}
/>
</div>
</>
) : null}
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
<div className="mt-4">
<Switch
label={t("salesforce_book_directly_with_attendee_owner")}
labelOnLeading
checked={isRoundRobinLeadSkipEnabled}
onCheckedChange={(checked) => {
setAppData("roundRobinLeadSkip", checked);
if (checked) {
// temporary solution, enabled should always be already set
setAppData("enabled", checked);
}
}}
/>
{isRoundRobinLeadSkipEnabled ? (
<div className="my-4 ml-2">
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
{t("salesforce_check_owner_of")}
</label>
<Select
className="mt-2 w-60"
options={checkOwnerOptions}
value={checkOwnerSelectedOption}
onChange={(e) => {
if (e) {
setCheckOwnerSelectedOption(e);
setAppData("roundRobinSkipCheckRecordOn", e.value);
}
}}
/>
</div>
) : null}
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
</div>
) : null}
</>
</AppCard>
);
};
+194 -21
View File
@@ -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<jsforce.Connection>;
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<CrmEvent> {
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<CrmEvent | undefined> {
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<string> = 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 }),
};
}
}
@@ -0,0 +1,5 @@
export enum SalesforceRecordEnum {
CONTACT = "Contact",
LEAD = "Lead",
ACCOUNT = "Account",
}
+8
View File
@@ -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({
@@ -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<IntegrationCalendar[]> {
return Promise.resolve([]);
}
getAppOptions() {
console.log("No options implemented");
}
}
const toISO8601String = (date: Date) => {
+5 -1
View File
@@ -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");
}
}
+24 -31
View File
@@ -100,7 +100,7 @@ export default class EventManager {
calendarCredentials: CredentialPayload[];
videoCredentials: CredentialPayload[];
crmCredentials: CredentialPayload[];
appOptions: AppOptions;
appOptions?: z.infer<typeof EventTypeAppMetadataSchema>;
/**
* 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<typeof EventTypeAppMetadataSchema>) {
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];
}
}
+13 -6
View File
@@ -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;
}
+11 -2
View File
@@ -25,9 +25,18 @@ export interface CrmEvent {
}
export interface CRM {
createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise<CrmEvent>;
createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise<CrmEvent | undefined>;
updateEvent: (uid: string, event: CalendarEvent) => Promise<CrmEvent>;
deleteEvent: (uid: string) => Promise<void>;
getContacts: (emails: string | string[], includeOwner?: boolean) => Promise<Contact[]>;
getContacts: ({
emails,
includeOwner,
forRoundRobinSkip,
}: {
emails: string | string[];
includeOwner?: boolean;
forRoundRobinSkip?: boolean;
}) => Promise<Contact[]>;
createContacts: (contactsToCreate: ContactCreateInput[], organizerEmail?: string) => Promise<Contact[]>;
getAppOptions: () => any;
}