655 lines
20 KiB
TypeScript
655 lines
20 KiB
TypeScript
import logger from "@calcom/lib/logger";
|
|
import { CalendarEvent } from "@calcom/types/Calendar";
|
|
|
|
export type CloseComLead = {
|
|
companyName?: string | null | undefined;
|
|
contactName?: string;
|
|
contactEmail?: string;
|
|
description?: string;
|
|
};
|
|
|
|
export type CloseComFieldOptions = [string, string, boolean, boolean][];
|
|
|
|
export type CloseComLeadCreateResult = {
|
|
status_id: string;
|
|
status_label: string;
|
|
display_name: string;
|
|
addresses: { [key: string]: any }[];
|
|
name: string;
|
|
contacts: { [key: string]: any }[];
|
|
[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: {
|
|
email: string;
|
|
type: string;
|
|
}[];
|
|
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: any;
|
|
date_updated: string;
|
|
user_name: string;
|
|
created_by_name: "Bruce Wayne";
|
|
id: string;
|
|
created_by: string;
|
|
status: string;
|
|
user_id: string;
|
|
users: any[];
|
|
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 || "";
|
|
|
|
/**
|
|
* 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.getChildLogger({ 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: { [key: string]: any };
|
|
}): Promise<{ data: { [key: string]: any }[] }> => {
|
|
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: { [key: string]: any } }): 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: { [key: string]: any } }): Promise<CloseComCustomContactFieldGet> => {
|
|
return this._get({ urlPath: "/custom_field/contact/", query });
|
|
},
|
|
},
|
|
shared: {
|
|
get: async ({ query }: { query: { [key: string]: any } }): 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?: { [key: string]: any } }) => {
|
|
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?: { [key: string]: any };
|
|
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(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();
|
|
});
|
|
};
|
|
|
|
public async getCloseComContactIds(
|
|
persons: { email: string; name: string | null }[],
|
|
leadFromCalComId?: string
|
|
): Promise<string[]> {
|
|
// Check if persons exist or to see if any should be created
|
|
const closeComContacts = await this.contact.search({
|
|
emails: persons.map((att) => att.email),
|
|
});
|
|
// NOTE: If contact is duplicated in Close.com we will get more results
|
|
// messing around with the expected number of contacts retrieved
|
|
if (closeComContacts.data.length < persons.length && leadFromCalComId) {
|
|
// Create missing contacts
|
|
const personsEmails = persons.map((att) => att.email);
|
|
// Existing contacts based on persons emails: contacts may have more
|
|
// than one email, we just need the one used by the event.
|
|
const existingContactsEmails = closeComContacts.data.flatMap((cont) =>
|
|
cont.emails.filter((em) => personsEmails.includes(em.email)).map((ems) => ems.email)
|
|
);
|
|
const nonExistingContacts = persons.filter((person) => !existingContactsEmails.includes(person.email));
|
|
const createdContacts = await Promise.all(
|
|
nonExistingContacts.map(
|
|
async (per) =>
|
|
await this.contact.create({
|
|
person: per,
|
|
leadId: leadFromCalComId,
|
|
})
|
|
)
|
|
);
|
|
if (createdContacts.length === nonExistingContacts.length) {
|
|
// All non existent contacts where created
|
|
return Promise.resolve(
|
|
closeComContacts.data.map((cont) => cont.id).concat(createdContacts.map((cont) => cont.id))
|
|
);
|
|
} else {
|
|
return Promise.reject("Some contacts were not possible to create in Close.com");
|
|
}
|
|
} else {
|
|
return Promise.resolve(closeComContacts.data.map((cont) => cont.id));
|
|
}
|
|
}
|
|
|
|
public async getCustomActivityTypeInstanceData(
|
|
event: CalendarEvent,
|
|
customFields: CloseComFieldOptions
|
|
): Promise<CloseComCustomActivityCreate> {
|
|
// Get Cal.com generic Lead
|
|
const leadFromCalComId = await this.getCloseComLeadId();
|
|
// Get Contacts ids
|
|
const contactsIds = await this.getCloseComContactIds(event.attendees, leadFromCalComId);
|
|
// Get Custom Activity Type id
|
|
const customActivityTypeAndFieldsIds = await this.getCloseComCustomActivityTypeFieldsIds(customFields);
|
|
// Prepare values for each Custom Activity Fields
|
|
const customActivityFieldsValues = [
|
|
contactsIds.length > 1 ? contactsIds.slice(1) : null, // Attendee
|
|
event.startTime, // Date & Time
|
|
event.attendees[0].timeZone, // Time Zone
|
|
contactsIds[0], // Organizer
|
|
event.additionalNotes ?? null, // Additional Notes
|
|
];
|
|
// Preparing Custom Activity Instance data for Close.com
|
|
return Object.assign(
|
|
{},
|
|
{
|
|
custom_activity_type_id: customActivityTypeAndFieldsIds.activityType,
|
|
lead_id: leadFromCalComId,
|
|
}, // This is to add each field as `"custom.FIELD_ID": "value"` in the object
|
|
...customActivityTypeAndFieldsIds.fields.map((fieldId: string, index: number) => {
|
|
return {
|
|
[`custom.${fieldId}`]: customActivityFieldsValues[index],
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
public async getCustomFieldsIds(
|
|
entity: keyof CloseCom["customField"],
|
|
customFields: CloseComFieldOptions,
|
|
custom_activity_type_id?: string
|
|
): Promise<string[]> {
|
|
// Get Custom Activity Fields
|
|
const allFields: CloseComCustomActivityFieldGet | CloseComCustomContactFieldGet = await this.customField[
|
|
entity
|
|
].get({
|
|
query: { _fields: ["name", "id"].concat(entity === "activity" ? ["custom_activity_type_id"] : []) },
|
|
});
|
|
let relevantFields: { [key: string]: any }[];
|
|
if (entity === "activity") {
|
|
relevantFields = (allFields as CloseComCustomActivityFieldGet).data.filter(
|
|
(fie) => fie.custom_activity_type_id === custom_activity_type_id
|
|
);
|
|
} else {
|
|
relevantFields = allFields.data as CloseComCustomActivityFieldGet["data"];
|
|
}
|
|
const customFieldsNames = relevantFields.map((fie) => fie.name);
|
|
const customFieldsExist = customFields.map((cusFie) => customFieldsNames.includes(cusFie[0]));
|
|
return await Promise.all(
|
|
customFieldsExist.map(async (exist, idx) => {
|
|
if (!exist && entity !== "shared") {
|
|
const [name, type, required, multiple] = customFields[idx];
|
|
let created: { [key: string]: any };
|
|
if (entity === "activity" && custom_activity_type_id) {
|
|
created = await this.customField[entity].create({
|
|
name,
|
|
type,
|
|
required,
|
|
accepts_multiple_values: multiple,
|
|
editable_with_roles: [],
|
|
custom_activity_type_id,
|
|
});
|
|
return created.id;
|
|
} else {
|
|
if (entity === "contact") {
|
|
created = await this.customField[entity].create({
|
|
name,
|
|
type,
|
|
required,
|
|
accepts_multiple_values: multiple,
|
|
editable_with_roles: [],
|
|
});
|
|
return created.id;
|
|
}
|
|
}
|
|
} else {
|
|
const index = customFieldsNames.findIndex((val) => val === customFields[idx][0]);
|
|
if (index >= 0) {
|
|
return relevantFields[index].id;
|
|
} else {
|
|
throw Error("Couldn't find the field index");
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public async getCloseComCustomActivityTypeFieldsIds(customFields: CloseComFieldOptions) {
|
|
// Check if Custom Activity Type exists
|
|
const customActivities = await this.customActivity.type.get();
|
|
const calComCustomActivity = customActivities.data.filter((act) => act.name === "Cal.com Activity");
|
|
if (calComCustomActivity.length > 0) {
|
|
// Cal.com Custom Activity type exist
|
|
// Get Custom Activity Type fields ids
|
|
const fields = await this.getCustomFieldsIds("activity", customFields, calComCustomActivity[0].id);
|
|
return {
|
|
activityType: calComCustomActivity[0].id,
|
|
fields,
|
|
};
|
|
} else {
|
|
// Cal.com Custom Activity type doesn't exist
|
|
// Create Custom Activity Type
|
|
const { id: activityType } = await this.customActivity.type.create({
|
|
name: "Cal.com Activity",
|
|
description: "Bookings in your Cal.com account",
|
|
});
|
|
// Create Custom Activity Fields
|
|
const fields = await Promise.all(
|
|
customFields.map(async ([name, type, required, multiple]) => {
|
|
const creation = await this.customField.activity.create({
|
|
custom_activity_type_id: activityType,
|
|
name,
|
|
type,
|
|
required,
|
|
accepts_multiple_values: multiple,
|
|
editable_with_roles: [],
|
|
});
|
|
return creation.id;
|
|
})
|
|
);
|
|
return {
|
|
activityType,
|
|
fields,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async getCloseComLeadId(
|
|
leadInfo: CloseComLead = {
|
|
companyName: "From Cal.com",
|
|
description: "Generic Lead for Contacts created by Cal.com",
|
|
}
|
|
): Promise<string> {
|
|
debugger;
|
|
const closeComLeadNames = await this.lead.list({ query: { _fields: ["name", "id"] } });
|
|
const searchLeadFromCalCom = closeComLeadNames.data.filter((lead) => lead.name === leadInfo.companyName);
|
|
if (searchLeadFromCalCom.length === 0) {
|
|
// No Lead exists, create it
|
|
const createdLeadFromCalCom = await this.lead.create(leadInfo);
|
|
return createdLeadFromCalCom.id;
|
|
} else {
|
|
return searchLeadFromCalCom[0].id;
|
|
}
|
|
}
|
|
}
|
|
|
|
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: [],
|
|
};
|
|
},
|
|
},
|
|
},
|
|
};
|