Files
calendar/packages/lib/CloseCom.ts
T
+1 625a7ec180 refactor: Introduce crmManager (#14272)
* fix timezone display on booking page to reflect event availability timezone

* migrate fetching event owner's schedule to server side

* migrate fetching event owner's schedule to server side

* fix e2e test errors

* Add WEBAPP_URL_FOR_OAUTH to salesforce auth

* In event manager constructor include "_crm" credentials as calendar creds

* Change crm apps to type to end with `_crm`

* Move sendgrid out of CRM

* Add zoho bigin to CRM apps

* When getting apps, use slug

* Add `crm` variants

* Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH`

* Refactor creating credentials

* Fix empty CRM page

* Use credentials with `_crm`

* Abstract getAppCategoryTitle

* Add integration.handler changes

* Init crmManager

* Change salesforce to CrmService

* Create crmManager

* Create contact on new event

* Create event

* Create new CRM reference

* - Fix create new contact for salesforce
- Add reschedule to crmManager

* Create deleteAllCRMEvents

* When searching for credential, look for current credentials in class

* On cancel, delete 3rd party events

* Add delete method

* Type fix

* Type fix

* Convert Close.com to CrmService

* Convert Close.com to CrmService

* Move hubspot to CrmService

* Convert Pipedrive to CrmService

* Rename classes to CrmService

* Move ZohoCrm to CrmService

* Move Bigin to CrmService

* Type return for CrmServices

* Fix type errors

* Close.com create leads and contacts

* Fix tests

* Type fix

* Zoho bug fixes

* Clean up

* Type fixes

* Remove apiDeletes

* Type fixes

* Specific typing

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* feat: Enable CRM apps on a per event type basis (#14450)

* Add Salesforce to be an event type app

* Handle new booking, only get enabled CRM credentials

* Abstract generating search params

* Add close.com to event type

* Clean up

* Move hubspot to event type

* Add pipedrive to event type

* Add zoho bigin to event type

* Add zoho crm to event type

* Remove console.log

* Add deleting CRM apps from event type

* Delete event type apps

* Fix deleting credentials

* Add CRM app data to event type metadata

* Backwards compatibility: add CRM credential if doesn't exist on event type

* Don't include user CRM credentials for backwards comp

* Backwards compatibility show CRM app is enabled and dirty field

* Clean up

* Type fixes

* Type fixes

* Type fix

* Type fix

* Remove console.log

* Test fix

* Upgrade embed-react vite version - dev

* Change build can't find error message

* Add back omni install prop

* Clean up

* Refactor `writeAppDataToEventType`

* Use eventType repository in writeAppDataToEventType

* Clean up old comments

* Add error logging

* createCRMEvents pass event uid as created event uid

* Use `getUid`

* Clean up props in create crm event

* Small changes to `crmManager`

* Fix zoho CRM

* refactor crmManager

* Undo vite config change

* Fix teamId query

* Fix bigin error

* Remove need for `writeAppDataToEventType`

* Add `getAllCredentials` test

* Add crmManager tests

* Type fixes

* Fix type errors

* Fix getAllCredentials test

* Fix tests

* Skip CRM manager tests for now

* feat: Skip RR Assignment if Contact Exists In Salesforce (#14556)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Update yarn.lock

* @zomars feedback - use new URL for state params

* fix: update hook to not produce enabled === undefined

* fix: update app card interfaces to use the new enabled from useIsAppEnabled

* fix: feedback for crm RR skip (#15160)

* code clean up

* fix type any

* test setup for RR lead skip

* code clean up

* simplify code

* type error

* finshed first test for RR lead skip

* add seconds test

* add test for handleNewBooking

* test if teamMember is set

* fix missing enabled key

* fix tests

* fix type error

* use setSystemTime instead of getDate

* remove nested if

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* fix type error

* fix: remove app metadata from all eventTypes on deleting the app

* fix: update hook to not produce enabled === undefined (default to false)

---------

Co-authored-by: Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com>
Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-05-31 13:54:52 +00:00

505 lines
14 KiB
TypeScript

import logger from "@calcom/lib/logger";
export type CloseComLead = {
companyName?: string | null | undefined;
contactName?: string;
contactEmail?: string;
description?: string;
id?: string | PromiseLike<string>;
};
export type CloseComFieldOptions = [string, string, boolean, boolean][];
type CloseComContactEmail = {
email: string;
type: string;
};
export type CloseComLeadCreateResult = {
status_id: string;
status_label: string;
display_name: string;
addresses: { [key: string]: string }[];
name: string;
contacts: {
id: string;
emails: CloseComContactEmail[];
}[];
[key: CloseComCustomActivityCustomField<string>]: string;
id: string;
};
export type CloseComStatus = {
id: string;
organization_id: string;
label: string;
};
export type CloseComCustomActivityTypeCreate = {
name: string;
description: string;
};
export type CloseComContactSearch = {
data: {
__object_type: "contact";
emails: CloseComContactEmail[];
id: string;
lead_id: string;
name: string;
}[];
cursor: null;
};
export type CloseComCustomActivityTypeGet = {
data: {
api_create_only: boolean;
created_by: string;
date_created: string;
date_updated: string;
description: string;
editable_with_roles: string[];
fields: CloseComCustomActivityFieldGet["data"][number][];
id: string;
name: string;
organization_id: string;
updated_by: string;
}[];
cursor: null;
};
export type CloseComCustomActivityFieldCreate = CloseComCustomContactFieldCreate & {
custom_activity_type_id: string;
};
export type CloseComCustomContactFieldGet = {
data: {
id: string;
name: string;
description: string;
type: string;
choices?: string[];
accepts_multiple_values: boolean;
editable_with_roles: string[];
date_created: string;
date_updated: string;
created_by: string;
updated_by: string;
organization_id: string;
}[];
};
export type CloseComCustomContactFieldCreate = {
name: string;
description?: string;
type: string;
required: boolean;
accepts_multiple_values: boolean;
editable_with_roles: string[];
choices?: string[];
};
export type CloseComCustomActivityFieldGet = {
data: {
id: string;
name: string;
description: string;
type: string;
choices?: string[];
accepts_multiple_values: boolean;
editable_with_roles: string[];
date_created: string;
date_updated: string;
created_by: string;
updated_by: string;
organization_id: string;
custom_activity_type_id: string;
}[];
};
export type CloseComCustomActivityCreate = {
custom_activity_type_id: string;
lead_id: string;
[key: CloseComCustomActivityCustomField<string>]: string;
};
export type typeCloseComCustomActivityGet = {
organization_id: string;
contact_id: string;
date_updated: string;
user_name: string;
created_by_name: "Bruce Wayne";
id: string;
created_by: string;
status: string;
user_id: string;
users: string[];
lead_id: string;
_type: string;
updated_by: string;
custom_activity_type_id: string;
date_created: string;
updated_by_name: string;
[key: CloseComCustomActivityCustomField<string>]: string;
};
type CloseComCustomActivityCustomField<T extends string> = `custom.${T}`;
const environmentApiKey = process.env.CLOSECOM_API_KEY || "";
type CloseComQuery = {
negate?: boolean;
queries?: CloseComQuery[] | CloseComCondition[];
object_type?: string;
related_object_type?: string;
related_query?: CloseComQuery;
this_object_type?: string;
type?: "object_type" | "has_related" | "and" | "or";
_fields?: string[];
};
type CloseComCondition = {
condition: {
mode: "full_words";
type: "text";
value: string;
};
field: {
field_name: string;
object_type: string;
type: "regular_field";
};
negate: boolean;
type: "field_condition";
};
export type CloseComCustomFieldCreateResponse = {
data: {
id: string;
name?: string;
type?: string;
required?: boolean;
accepts_multiple_values: boolean;
editable_with_roles: string[];
custom_activity_type_id?: string;
};
};
/**
* This class to instance communicating to Close.com APIs requires an API Key.
*
* You can either pass to the constructor an API Key or have one defined as an
* environment variable in case the communication to Close.com is just for
* one account only, not configurable by any user at any moment.
*/
export default class CloseCom {
private apiUrl = "https://api.close.com/api/v1";
private apiKey: string | undefined = undefined;
private log: typeof logger;
constructor(providedApiKey = "") {
this.log = logger.getSubLogger({ prefix: [`[[lib] close.com`] });
if (!providedApiKey && !environmentApiKey) throw Error("Close.com Api Key not present");
this.apiKey = providedApiKey || environmentApiKey;
}
public me = async () => {
return this._get({ urlPath: "/me/" });
};
public contact = {
search: async ({ emails }: { emails: string[] }): Promise<CloseComContactSearch> => {
return this._post({
urlPath: "/data/search/",
data: closeComQueries.contact.search(emails),
});
},
create: async (data: {
person: { name: string | null; email: string };
leadId: string;
}): Promise<CloseComContactSearch["data"][number]> => {
return this._post({ urlPath: "/contact/", data: closeComQueries.contact.create(data) });
},
update: async ({
contactId,
...data
}: {
person: { name: string; email: string };
contactId: string;
leadId?: string;
}): Promise<CloseComContactSearch["data"][number]> => {
return this._put({
urlPath: `/contact/${contactId}/`,
data: closeComQueries.contact.update(data),
});
},
delete: async (contactId: string) => {
return this._delete({
urlPath: `/contact/${contactId}/`,
});
},
};
public lead = {
list: async ({
query,
}: {
query: CloseComQuery;
}): Promise<{ data: { [key: string]: CloseComQuery }[] }> => {
return this._get({ urlPath: "/lead", query });
},
status: async () => {
return this._get({ urlPath: `/status/lead/` });
},
update: async (leadId: string, data: CloseComLead): Promise<CloseComLeadCreateResult> => {
return this._put({
urlPath: `/lead/${leadId}`,
data,
});
},
create: async (data: CloseComLead): Promise<CloseComLeadCreateResult> => {
return this._post({
urlPath: "/lead/",
data: closeComQueries.lead.create(data),
});
},
delete: async (leadId: string) => {
return this._delete({ urlPath: `/lead/${leadId}/` });
},
};
public customActivity = {
type: {
create: async (
data: CloseComCustomActivityTypeCreate
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
return this._post({
urlPath: "/custom_activity",
data: closeComQueries.customActivity.type.create(data),
});
},
get: async (): Promise<CloseComCustomActivityTypeGet> => {
return this._get({ urlPath: "/custom_activity" });
},
},
};
public customField = {
activity: {
create: async (
data: CloseComCustomActivityFieldCreate
): Promise<CloseComCustomActivityFieldGet["data"][number]> => {
return this._post({ urlPath: "/custom_field/activity/", data });
},
get: async ({ query }: { query: CloseComQuery }): Promise<CloseComCustomActivityFieldGet> => {
return this._get({ urlPath: "/custom_field/activity/", query });
},
},
contact: {
create: async (
data: CloseComCustomContactFieldCreate
): Promise<CloseComCustomContactFieldGet["data"][number]> => {
return this._post({ urlPath: "/custom_field/contact/", data });
},
get: async ({ query }: { query: CloseComQuery }): Promise<CloseComCustomContactFieldGet> => {
return this._get({ urlPath: "/custom_field/contact/", query });
},
},
shared: {
get: async ({ query }: { query: CloseComQuery }): Promise<CloseComCustomContactFieldGet> => {
return this._get({ urlPath: "/custom_field/shared/", query });
},
},
};
public activity = {
custom: {
create: async (
data: CloseComCustomActivityCreate
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
return this._post({ urlPath: "/activity/custom/", data });
},
delete: async (uuid: string) => {
return this._delete({ urlPath: `/activity/custom/${uuid}/` });
},
update: async (
uuid: string,
data: Partial<CloseComCustomActivityCreate>
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
return this._put({ urlPath: `/activity/custom/${uuid}/`, data });
},
},
};
private _get = async ({ urlPath, query }: { urlPath: string; query?: CloseComQuery }) => {
return await this._request({ urlPath, method: "get", query });
};
private _post = async ({ urlPath, data }: { urlPath: string; data: Record<string, unknown> }) => {
return this._request({ urlPath, method: "post", data });
};
private _put = async ({ urlPath, data }: { urlPath: string; data: Record<string, unknown> }) => {
return this._request({ urlPath, method: "put", data });
};
private _delete = async ({ urlPath }: { urlPath: string }) => {
return this._request({ urlPath, method: "delete" });
};
private _request = async ({
urlPath,
data,
method,
query,
}: {
urlPath: string;
method: string;
query?: CloseComQuery;
data?: Record<string, unknown>;
}) => {
this.log.debug(method, urlPath, query, data);
const credentials = Buffer.from(`${this.apiKey}:`).toString("base64");
const headers = {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
};
const queryString = query ? `?${new URLSearchParams(String(query)).toString()}` : "";
return await fetch(`${this.apiUrl}${urlPath}${queryString}`, {
headers,
method,
body: JSON.stringify(data),
}).then(async (response) => {
if (!response.ok) {
const message = `[Close.com app] An error has occured: ${response.status}`;
this.log.error(await response.json());
throw new Error(message);
}
return await response.json();
});
};
}
export const closeComQueries = {
contact: {
search(contactEmails: string[]) {
return {
limit: null,
_fields: {
contact: ["id", "name", "emails"],
},
query: {
negate: false,
queries: [
{
negate: false,
object_type: "contact",
type: "object_type",
},
{
negate: false,
queries: [
{
negate: false,
related_object_type: "contact_email",
related_query: {
negate: false,
queries: contactEmails.map((contactEmail) => ({
condition: {
mode: "full_words",
type: "text",
value: contactEmail,
},
field: {
field_name: "email",
object_type: "contact_email",
type: "regular_field",
},
negate: false,
type: "field_condition",
})),
type: "or",
},
this_object_type: "contact",
type: "has_related",
},
],
type: "and",
},
],
type: "and",
},
results_limit: null,
sort: [],
};
},
create(data: { person: { name: string | null; email: string }; leadId: string }) {
return {
lead_id: data.leadId,
name: data.person.name ?? data.person.email,
emails: [{ email: data.person.email, type: "office" }],
};
},
update({ person, leadId, ...rest }: { person: { name: string; email: string }; leadId?: string }) {
return {
...(leadId && { lead_id: leadId }),
name: person.name ?? person.email,
emails: [{ email: person.email, type: "office" }],
...rest,
};
},
},
lead: {
create({ companyName, contactEmail, contactName, description }: CloseComLead) {
return {
name: companyName,
...(description ? { description } : {}),
...(contactEmail && contactName
? {
contacts: [
{
name: contactName,
email: contactEmail,
emails: [
{
type: "office",
email: contactEmail,
},
],
},
],
}
: {}),
};
},
},
customActivity: {
type: {
create({ name, description }: CloseComCustomActivityTypeCreate) {
return {
name: name,
description: description,
api_create_only: false,
editable_with_roles: ["admin"],
};
},
},
},
customField: {
activity: {
create({
custom_activity_type_id,
name,
type,
required,
accepts_multiple_values,
}: CloseComCustomActivityFieldCreate) {
return {
custom_activity_type_id,
name,
type,
required,
accepts_multiple_values,
editable_with_roles: [],
};
},
},
},
};