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>
This commit is contained in:
co-authored by
CarinaWolli
Shaik-Sirajuddin
Shaik-Sirajuddin
Shaik-Sirajuddin
sean-brydon
Omar López
sean-brydon
Carina Wollendorfer
Somay Chauhan
parent
5602f7bd95
commit
625a7ec180
@@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
key: {},
|
||||
teamId: Number(teamId),
|
||||
});
|
||||
|
||||
const tenantId = teamId ? teamId : userId;
|
||||
res.status(200).json({
|
||||
url: `https://oauth.pipedrive.com/oauth/authorize?client_id=${appKeys.client_id}&redirect_uri=https://app.revert.dev/oauth-callback/pipedrive&state={%22tenantId%22:%22${tenantId}%22,%22revertPublicToken%22:%22${process.env.REVERT_PUBLIC_TOKEN}%22}`,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||
app={app}
|
||||
teamId={eventType.team?.id || undefined}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
hideAppCardOptions
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppCard;
|
||||
@@ -7,6 +7,7 @@
|
||||
"url": "https://revert.dev",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"extendsFeature": "EventType",
|
||||
"publisher": "Revert.dev ",
|
||||
"email": "jatin@revert.dev",
|
||||
"description": "Founded in 2010, Pipedrive is an easy and effective sales CRM that drives small business growth.\r\rToday, Pipedrive is used by revenue teams at more than 100,000 companies worldwide. Pipedrive is headquartered in New York and has offices across Europe and the US.\r\rThe company is backed by majority holder Vista Equity Partners, Bessemer Venture Partners, Insight Partners, Atomico, and DTCP.\r\rLearn more at www.pipedrive.com.",
|
||||
|
||||
+20
-100
@@ -1,14 +1,13 @@
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
Person,
|
||||
} 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";
|
||||
|
||||
@@ -34,7 +33,7 @@ type ContactCreateResult = {
|
||||
};
|
||||
};
|
||||
|
||||
export default class PipedriveCalendarService implements Calendar {
|
||||
export default class PipedriveCrmService implements CRM {
|
||||
private log: typeof logger;
|
||||
private tenantId: string;
|
||||
private revertApiKey: string;
|
||||
@@ -46,8 +45,8 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
this.log = logger.getSubLogger({ prefix: [`[[lib] ${appConfig.slug}`] });
|
||||
}
|
||||
|
||||
private createContacts = async (attendees: Person[]) => {
|
||||
const result = attendees.map(async (attendee) => {
|
||||
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);
|
||||
@@ -74,17 +73,21 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
return await Promise.all(result);
|
||||
};
|
||||
|
||||
private contactSearch = async (event: CalendarEvent) => {
|
||||
const result = event.attendees.map(async (attendee) => {
|
||||
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: attendee.email });
|
||||
const bodyRaw = JSON.stringify({ searchCriteria: attendeeEmail });
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
@@ -100,8 +103,9 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
return { status: "error", results: [] };
|
||||
}
|
||||
});
|
||||
return await Promise.all(result);
|
||||
};
|
||||
const results = await Promise.all(result);
|
||||
return results[0].results;
|
||||
}
|
||||
|
||||
private getMeetingBody = (event: CalendarEvent): string => {
|
||||
return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
|
||||
@@ -111,7 +115,7 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
}`;
|
||||
};
|
||||
|
||||
private createPipedriveEvent = async (event: CalendarEvent, contacts: CalendarEvent["attendees"]) => {
|
||||
private createPipedriveEvent = async (event: CalendarEvent, contacts: Contact[]) => {
|
||||
const eventPayload = {
|
||||
subject: event.title,
|
||||
startDateTime: event.startTime,
|
||||
@@ -173,7 +177,7 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
return await fetch(`${this.revertApiUrl}crm/events/${uid}`, requestOptions);
|
||||
};
|
||||
|
||||
async handleEventCreation(event: CalendarEvent, contacts: CalendarEvent["attendees"]) {
|
||||
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 });
|
||||
@@ -190,92 +194,8 @@ export default class PipedriveCalendarService implements Calendar {
|
||||
return Promise.reject("Something went wrong when creating a meeting in PipedriveCRM");
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
let contacts = await this.contactSearch(event);
|
||||
contacts = contacts.filter((c) => c.results.length >= 1);
|
||||
if (contacts && contacts.length) {
|
||||
if (contacts.length === event.attendees.length) {
|
||||
// all contacts are in Pipedrive CRM already.
|
||||
this.log.debug("contact:search:all", { event, contacts: contacts });
|
||||
const existingPeople = contacts.map((c) => {
|
||||
return {
|
||||
id: Number(c.results[0].id),
|
||||
name: `${c.results[0].firstName} ${c.results[0].lastName}`,
|
||||
email: c.results[0].email,
|
||||
timeZone: event.attendees[0].timeZone,
|
||||
language: event.attendees[0].language,
|
||||
};
|
||||
});
|
||||
return await this.handleEventCreation(event, existingPeople);
|
||||
} else {
|
||||
// Some attendees don't exist in PipedriveCRM
|
||||
// Get the existing contacts' email to filter out
|
||||
this.log.debug("contact:search:notAll", { event, contacts });
|
||||
const existingContacts = contacts.map((contact) => contact.results[0].email);
|
||||
this.log.debug("contact:filter:existing", { existingContacts });
|
||||
// Get non existing contacts filtering out existing from attendees
|
||||
const nonExistingContacts: Person[] = event.attendees.filter(
|
||||
(attendee) => !existingContacts.includes(attendee.email)
|
||||
);
|
||||
this.log.debug("contact:filter:nonExisting", { nonExistingContacts });
|
||||
// Only create contacts in PipedriveCRM that were not present in the previous contact search
|
||||
const createdContacts = await this.createContacts(nonExistingContacts);
|
||||
this.log.debug("contact:created", { createdContacts });
|
||||
// Continue with event creation and association only when all contacts are present in Pipedrive
|
||||
if (createdContacts[0] && createdContacts[0].status === "ok") {
|
||||
this.log.debug("contact:creation:ok");
|
||||
const existingPeople = contacts.map((c) => {
|
||||
return {
|
||||
id: Number(c.results[0].id),
|
||||
name: c.results[0].name,
|
||||
email: c.results[0].email,
|
||||
timeZone: nonExistingContacts[0].timeZone,
|
||||
language: nonExistingContacts[0].language,
|
||||
};
|
||||
});
|
||||
const newlyCreatedPeople = createdContacts.map((c) => {
|
||||
return {
|
||||
id: Number(c.result.id),
|
||||
name: c.result.name,
|
||||
email: c.result.email,
|
||||
timeZone: nonExistingContacts[0].timeZone,
|
||||
language: nonExistingContacts[0].language,
|
||||
};
|
||||
});
|
||||
const allContacts = existingPeople.concat(newlyCreatedPeople);
|
||||
// ensure the order of attendees is maintained.
|
||||
allContacts.sort((a, b) => {
|
||||
const indexA = event.attendees.findIndex((c) => c.email === a.email);
|
||||
const indexB = event.attendees.findIndex((c) => c.email === b.email);
|
||||
return indexA - indexB;
|
||||
});
|
||||
return await this.handleEventCreation(event, allContacts);
|
||||
}
|
||||
return Promise.reject({
|
||||
calError: "Something went wrong when creating non-existing attendees in PipedriveCRM",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.log.debug("contact:search:none", { event, contacts });
|
||||
const createdContacts = await this.createContacts(event.attendees);
|
||||
this.log.debug("contact:created", { createdContacts });
|
||||
if (createdContacts[0] && createdContacts[0].status === "ok") {
|
||||
this.log.debug("contact:creation:ok");
|
||||
const newContacts = createdContacts.map((c) => {
|
||||
return {
|
||||
id: Number(c.result.id),
|
||||
name: c.result.name,
|
||||
email: c.result.email,
|
||||
timeZone: event.attendees[0].timeZone,
|
||||
language: event.attendees[0].language,
|
||||
};
|
||||
});
|
||||
return await this.handleEventCreation(event, newContacts);
|
||||
}
|
||||
}
|
||||
return Promise.reject({
|
||||
calError: "Something went wrong when searching/creating the attendees in PipedriveCRM",
|
||||
});
|
||||
async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<NewCalendarEventType> {
|
||||
return await this.handleEventCreation(event, contacts);
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
@@ -1 +1 @@
|
||||
export { default as CalendarService } from "./CalendarService";
|
||||
export { default as CrmService } from "./CrmService";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
client_id: z.string().min(1),
|
||||
client_secret: z.string().min(1),
|
||||
});
|
||||
|
||||
export const appDataSchema = z.object({});
|
||||
export const appDataSchema = eventTypeAppCardZod;
|
||||
|
||||
Reference in New Issue
Block a user