Files
cal.diy/packages/app-store/pipedrive-crm/lib/CrmService.ts
T
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

234 lines
7.5 KiB
TypeScript

import { getLocation } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import type {
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { ContactCreateInput, CRM, Contact } from "@calcom/types/CrmService";
import appConfig from "../config.json";
type ContactSearchResult = {
status: string;
results: Array<{
id: string;
email: string;
firstName: string;
lastName: string;
name: string;
}>;
};
type ContactCreateResult = {
status: string;
result: {
id: string;
email: string;
firstName: string;
lastName: string;
name: string;
};
};
export default class PipedriveCrmService implements CRM {
private log: typeof logger;
private tenantId: string;
private revertApiKey: string;
private revertApiUrl: string;
constructor(credential: CredentialPayload) {
this.revertApiKey = process.env.REVERT_API_KEY || "";
this.revertApiUrl = process.env.REVERT_API_URL || "https://api.revert.dev/";
this.tenantId = String(credential.teamId ? credential.teamId : credential.userId); // Question: Is this a reasonable assumption to be made? Get confirmation on the exact field to be used here.
this.log = logger.getSubLogger({ prefix: [`[[lib] ${appConfig.slug}`] });
}
async createContacts(contactsToCreate: ContactCreateInput[]): Promise<Contact[]> {
const result = contactsToCreate.map(async (attendee) => {
const headers = new Headers();
headers.append("x-revert-api-token", this.revertApiKey);
headers.append("x-revert-t-id", this.tenantId);
headers.append("Content-Type", "application/json");
const [firstname, lastname] = !!attendee.name ? attendee.name.split(" ") : [attendee.email, "-"];
const bodyRaw = JSON.stringify({
firstName: firstname,
lastName: lastname || "-",
email: attendee.email,
});
const requestOptions = {
method: "POST",
headers: headers,
body: bodyRaw,
};
try {
const response = await fetch(`${this.revertApiUrl}crm/contacts`, requestOptions);
const result = (await response.json()) as ContactCreateResult;
return result;
} catch (error) {
return Promise.reject(error);
}
});
const results = await Promise.all(result);
return results.map((result) => result.result);
}
async getContacts(email: string | string[]): Promise<Contact[]> {
const emailArray = Array.isArray(email) ? email : [email];
const result = emailArray.map(async (attendeeEmail) => {
const headers = new Headers();
headers.append("x-revert-api-token", this.revertApiKey);
headers.append("x-revert-t-id", this.tenantId);
headers.append("Content-Type", "application/json");
const bodyRaw = JSON.stringify({ searchCriteria: attendeeEmail });
const requestOptions = {
method: "POST",
headers: headers,
body: bodyRaw,
};
try {
const response = await fetch(`${this.revertApiUrl}crm/contacts/search`, requestOptions);
const result = (await response.json()) as ContactSearchResult;
return result;
} catch (error) {
return { status: "error", results: [] };
}
});
const results = await Promise.all(result);
return results[0].results;
}
private getMeetingBody = (event: CalendarEvent): string => {
return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
event.attendees[0].timeZone
}<br><br><b>${event.organizer.language.translate("share_additional_notes")}</b><br>${
event.additionalNotes || "-"
}`;
};
private createPipedriveEvent = async (event: CalendarEvent, contacts: Contact[]) => {
const eventPayload = {
subject: event.title,
startDateTime: event.startTime,
endDateTime: event.endTime,
description: this.getMeetingBody(event),
location: getLocation(event),
associations: {
contactId: String(contacts[0].id),
},
};
const headers = new Headers();
headers.append("x-revert-api-token", this.revertApiKey);
headers.append("x-revert-t-id", this.tenantId);
headers.append("Content-Type", "application/json");
const eventBody = JSON.stringify(eventPayload);
const requestOptions = {
method: "POST",
headers: headers,
body: eventBody,
};
return await fetch(`${this.revertApiUrl}crm/events`, requestOptions);
};
private updateMeeting = async (uid: string, event: CalendarEvent) => {
const eventPayload = {
subject: event.title,
startDateTime: event.startTime,
endDateTime: event.endTime,
description: this.getMeetingBody(event),
location: getLocation(event),
};
const headers = new Headers();
headers.append("x-revert-api-token", this.revertApiKey);
headers.append("x-revert-t-id", this.tenantId);
headers.append("Content-Type", "application/json");
const eventBody = JSON.stringify(eventPayload);
const requestOptions = {
method: "PATCH",
headers: headers,
body: eventBody,
};
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
};
private deleteMeeting = async (uid: string) => {
const headers = new Headers();
headers.append("x-revert-api-token", this.revertApiKey);
headers.append("x-revert-t-id", this.tenantId);
const requestOptions = {
method: "DELETE",
headers: headers,
};
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
};
async handleEventCreation(event: CalendarEvent, contacts: Contact[]) {
const meetingEvent = await (await this.createPipedriveEvent(event, contacts)).json();
if (meetingEvent && meetingEvent.status === "ok") {
this.log.debug("event:creation:ok", { meetingEvent });
return Promise.resolve({
uid: meetingEvent.result.id,
id: meetingEvent.result.id,
type: appConfig.slug,
password: "",
url: "",
additionalInfo: { contacts, meetingEvent },
});
}
this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
return Promise.reject("Something went wrong when creating a meeting in PipedriveCRM");
}
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<NewCalendarEventType> {
return await this.handleEventCreation(event, contacts);
}
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
const meetingEvent = await (await this.updateMeeting(uid, event)).json();
if (meetingEvent && meetingEvent.status === "ok") {
this.log.debug("event:updation:ok", { meetingEvent });
return Promise.resolve({
uid: meetingEvent.result.id,
id: meetingEvent.result.id,
type: appConfig.slug,
password: "",
url: "",
additionalInfo: { meetingEvent },
});
}
this.log.debug("meeting:updation:notOk", { meetingEvent, event });
return Promise.reject("Something went wrong when updating a meeting in PipedriveCRM");
}
async deleteEvent(uid: string): Promise<void> {
await this.deleteMeeting(uid);
}
async getAvailability(
_dateFrom: string,
_dateTo: string,
_selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
return Promise.resolve([]);
}
async listCalendars(_event?: CalendarEvent): Promise<IntegrationCalendar[]> {
return Promise.resolve([]);
}
}