+1









Joe Au-Yeung
GitHub
CarinaWolli
Shaik-Sirajuddin <sirajuddinshaik30gmail.com>
Shaik-Sirajuddin
Shaik-Sirajuddin
sean-brydon
Omar López
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>
181 lines
6.1 KiB
TypeScript
181 lines
6.1 KiB
TypeScript
import z from "zod";
|
|
|
|
import type { CloseComFieldOptions } from "@calcom/lib/CloseCom";
|
|
import CloseCom from "@calcom/lib/CloseCom";
|
|
import { getCustomActivityTypeInstanceData } from "@calcom/lib/CloseComeUtils";
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
import logger from "@calcom/lib/logger";
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
|
import type { CRM, ContactCreateInput, CrmEvent, Contact } from "@calcom/types/CrmService";
|
|
|
|
const apiKeySchema = z.object({
|
|
encrypted: z.string(),
|
|
});
|
|
|
|
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
|
|
|
// Cal.com Custom Activity Fields
|
|
const calComCustomActivityFields: CloseComFieldOptions = [
|
|
// Field name, field type, required?, multiple values?
|
|
["Attendees", "contact", false, true],
|
|
["Date & Time", "datetime", true, false],
|
|
["Time zone", "text", true, false],
|
|
["Organizer", "contact", true, false],
|
|
["Additional notes", "text", false, false],
|
|
];
|
|
|
|
/**
|
|
* Authentication
|
|
* Close.com requires Basic Auth for any request to their APIs, which is far from
|
|
* ideal considering that such a strategy requires generating an API Key by the
|
|
* user and input it in our system. A Setup page was created when trying to install
|
|
* Close.com App in order to instruct how to create such resource and to obtain it.
|
|
*
|
|
* Meeting creation
|
|
* Close.com does not expose a "Meeting" API, it may be available in the future.
|
|
*
|
|
* Per Close.com documentation (https://developer.close.com/resources/custom-activities):
|
|
* "To work with Custom Activities, you will need to create a Custom Activity Type and
|
|
* likely add one or more Activity Custom Fields to that type. Once the Custom Activity
|
|
* Type is defined, you can create Custom Activity instances of that type as you would
|
|
* any other activity."
|
|
*
|
|
* Contact creation
|
|
* Every contact in Close.com need to belong to a Lead. When creating a contact in
|
|
* Close.com as part of this integration, a new generic Lead will be created in order
|
|
* to assign every contact created by this process, and it is named "From Cal.com"
|
|
*/
|
|
export default class CloseComCRMService implements CRM {
|
|
private integrationName = "";
|
|
private closeCom: CloseCom;
|
|
private log: typeof logger;
|
|
|
|
constructor(credential: CredentialPayload) {
|
|
this.integrationName = "closecom_other_calendar";
|
|
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
|
|
|
const parsedCredentialKey = apiKeySchema.safeParse(credential.key);
|
|
|
|
let decrypted;
|
|
if (parsedCredentialKey.success) {
|
|
decrypted = symmetricDecrypt(parsedCredentialKey.data.encrypted, CALENDSO_ENCRYPTION_KEY);
|
|
const { api_key } = JSON.parse(decrypted);
|
|
this.closeCom = new CloseCom(api_key);
|
|
} else {
|
|
throw Error(
|
|
`No API Key found for userId ${credential.userId} and appId ${credential.appId}: ${parsedCredentialKey.error}`
|
|
);
|
|
}
|
|
}
|
|
|
|
closeComUpdateCustomActivity = async (uid: string, event: CalendarEvent) => {
|
|
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
|
event,
|
|
calComCustomActivityFields,
|
|
this.closeCom
|
|
);
|
|
// Create Custom Activity type instance
|
|
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
|
customActivityTypeInstanceData
|
|
);
|
|
return this.closeCom.activity.custom.update(uid, customActivityTypeInstance);
|
|
};
|
|
|
|
closeComDeleteCustomActivity = async (uid: string) => {
|
|
return this.closeCom.activity.custom.delete(uid);
|
|
};
|
|
|
|
async createEvent(event: CalendarEvent): Promise<CrmEvent> {
|
|
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
|
event,
|
|
calComCustomActivityFields,
|
|
this.closeCom
|
|
);
|
|
// Create Custom Activity type instance
|
|
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
|
customActivityTypeInstanceData
|
|
);
|
|
return Promise.resolve({
|
|
uid: customActivityTypeInstance.id,
|
|
id: customActivityTypeInstance.id,
|
|
type: this.integrationName,
|
|
password: "",
|
|
url: "",
|
|
additionalInfo: {
|
|
customActivityTypeInstanceData,
|
|
},
|
|
success: true,
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async updateEvent(uid: string, event: CalendarEvent): Promise<CrmEvent> {
|
|
const updatedEvent = await this.closeComUpdateCustomActivity(uid, event);
|
|
return {
|
|
id: updatedEvent.id,
|
|
};
|
|
}
|
|
|
|
async deleteEvent(uid: string): Promise<void> {
|
|
await this.closeComDeleteCustomActivity(uid);
|
|
}
|
|
|
|
async getContacts(emails: string | string[]): Promise<Contact[]> {
|
|
const contactsQuery = await this.closeCom.contact.search({
|
|
emails: Array.isArray(emails) ? emails : [emails],
|
|
});
|
|
|
|
return contactsQuery.data.map((contact) => {
|
|
return {
|
|
id: contact.id,
|
|
email: contact.emails[0].email,
|
|
name: contact.name,
|
|
};
|
|
});
|
|
}
|
|
|
|
async createContacts(contactsToCreate: ContactCreateInput[]): Promise<Contact[]> {
|
|
// In Close.com contacts need to be attached to a lead
|
|
// Assume all attendees in an event belong under a lead
|
|
|
|
const contacts = [];
|
|
|
|
// Create main lead
|
|
const lead = await this.closeCom.lead.create({
|
|
contactName: contactsToCreate[0].name,
|
|
contactEmail: contactsToCreate[0].email,
|
|
});
|
|
|
|
contacts.push({
|
|
id: lead.contacts[0].id,
|
|
email: lead.contacts[0].emails[0].email,
|
|
});
|
|
|
|
// Check if we need to crate more contacts under the lead
|
|
if (contactsToCreate.length > 1) {
|
|
const createContactPromise = [];
|
|
for (const contact of contactsToCreate) {
|
|
createContactPromise.push(
|
|
this.closeCom.contact.create({
|
|
leadId: lead.id,
|
|
person: {
|
|
email: contact.email,
|
|
name: contact.name,
|
|
},
|
|
})
|
|
);
|
|
const createdContacts = await Promise.all(createContactPromise);
|
|
for (const createdContact of createdContacts) {
|
|
contacts.push({
|
|
id: createdContact.id,
|
|
email: createdContact.emails[0].email,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return contacts;
|
|
}
|
|
}
|