+1









Joe Au-Yeung
GitHub
Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Shaik-Sirajuddin
Shaik-Sirajuddin
sean-brydon
Omar López
CarinaWolli
sean-brydon
Carina Wollendorfer
Somay Chauhan
625a7ec180
* 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>
215 lines
7.8 KiB
TypeScript
215 lines
7.8 KiB
TypeScript
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
|
|
import type {
|
|
CloseComCustomActivityCreate,
|
|
CloseComCustomActivityFieldGet,
|
|
CloseComCustomContactFieldGet,
|
|
CloseComCustomFieldCreateResponse,
|
|
CloseComFieldOptions,
|
|
CloseComLead,
|
|
} from "./CloseCom";
|
|
import type CloseCom from "./CloseCom";
|
|
import { APP_NAME } from "./constants";
|
|
|
|
export async function getCloseComContactIds(
|
|
persons: { email: string; name: string | null }[],
|
|
closeCom: CloseCom,
|
|
leadFromCalComId?: string
|
|
): Promise<string[]> {
|
|
// Check if persons exist or to see if any should be created
|
|
const closeComContacts = await closeCom.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 closeCom.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));
|
|
}
|
|
}
|
|
|
|
export async function getCustomActivityTypeInstanceData(
|
|
event: CalendarEvent,
|
|
customFields: CloseComFieldOptions,
|
|
closeCom: CloseCom
|
|
): Promise<CloseComCustomActivityCreate> {
|
|
// Get Cal.com generic Lead
|
|
const leadFromCalComId = await getCloseComLeadId(closeCom);
|
|
// Get Contacts ids
|
|
const contactsIds = await getCloseComContactIds(event.attendees, closeCom, leadFromCalComId);
|
|
// Get Custom Activity Type id
|
|
const customActivityTypeAndFieldsIds = await getCloseComCustomActivityTypeFieldsIds(customFields, closeCom);
|
|
// 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],
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function getCustomFieldsIds(
|
|
entity: keyof CloseCom["customField"],
|
|
customFields: CloseComFieldOptions,
|
|
closeCom: CloseCom,
|
|
custom_activity_type_id?: string
|
|
): Promise<string[]> {
|
|
// Get Custom Activity Fields
|
|
const allFields: CloseComCustomActivityFieldGet | CloseComCustomContactFieldGet =
|
|
await closeCom.customField[entity].get({
|
|
query: { _fields: ["name", "id"].concat(entity === "activity" ? ["custom_activity_type_id"] : []) },
|
|
});
|
|
let relevantFields: { id: string; name: string }[];
|
|
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.flatMap(async (exist, idx) => {
|
|
if (!exist && entity !== "shared") {
|
|
const [name, type, required, multiple] = customFields[idx];
|
|
let created: CloseComCustomFieldCreateResponse["data"];
|
|
if (entity === "activity" && custom_activity_type_id) {
|
|
created = await closeCom.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 closeCom.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");
|
|
}
|
|
}
|
|
// Return an array with a single undefined value for the case where "exist" is true
|
|
return "";
|
|
})
|
|
).then((results) => results.filter((id) => id));
|
|
}
|
|
|
|
export async function getCloseComCustomActivityTypeFieldsIds(
|
|
customFields: CloseComFieldOptions,
|
|
closeCom: CloseCom
|
|
) {
|
|
// Check if Custom Activity Type exists
|
|
const customActivities = await closeCom.customActivity.type.get();
|
|
const calComCustomActivity = customActivities.data.filter((act) => act.name === `${APP_NAME} Activity`);
|
|
if (calComCustomActivity.length > 0) {
|
|
// Cal.com Custom Activity type exist
|
|
// Get Custom Activity Type fields ids
|
|
const fields = await getCustomFieldsIds("activity", customFields, closeCom, 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 closeCom.customActivity.type.create({
|
|
name: `${APP_NAME} Activity`,
|
|
description: `Bookings in your ${APP_NAME} account`,
|
|
});
|
|
// Create Custom Activity Fields
|
|
const fields = await Promise.all(
|
|
customFields.map(async ([name, type, required, multiple]) => {
|
|
const creation = await closeCom.customField.activity.create({
|
|
custom_activity_type_id: activityType,
|
|
name,
|
|
type,
|
|
required,
|
|
accepts_multiple_values: multiple,
|
|
editable_with_roles: [],
|
|
});
|
|
return creation.id;
|
|
})
|
|
);
|
|
return {
|
|
activityType,
|
|
fields,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function getCloseComLeadId(
|
|
closeCom: CloseCom,
|
|
leadInfo: CloseComLead = {
|
|
companyName: `From ${APP_NAME}`,
|
|
description: `Generic Lead for Contacts created by ${APP_NAME}`,
|
|
}
|
|
): Promise<string> {
|
|
// TODO: Check for leads against email rather than name
|
|
const closeComLeadNames = await closeCom.lead.list({ query: { _fields: ["name", "id"] } });
|
|
const searchLeadFromCalCom: CloseComLead[] = closeComLeadNames.data.filter(
|
|
(lead) => lead.name === leadInfo.companyName
|
|
);
|
|
if (searchLeadFromCalCom.length === 0) {
|
|
// No Lead exists, create it
|
|
const createdLeadFromCalCom = await closeCom.lead.create(leadInfo);
|
|
return createdLeadFromCalCom.id ?? "";
|
|
} else {
|
|
return searchLeadFromCalCom[0].id ?? "";
|
|
}
|
|
}
|