feat(app-store): add zohocrm app to app-store (#7182)
* Add zoho-app to the app-store * Update packages/app-store/zohocrm/api/_getAdd.ts Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> * 1. Remove redundant check for `defaultHandler` 2. Restore yarn.lock` * update images * update README with zoho integration * Fix dirname * Fix types * Fix lastname bug * Fix timezone issue * Fix eslint warning for unused args * Revert yarn.lock --------- Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>pull/7745/head
parent
19b1f8a05f
commit
ec4228ab6a
|
@ -15,6 +15,7 @@
|
||||||
# - LARK
|
# - LARK
|
||||||
# - WEB3
|
# - WEB3
|
||||||
# - SALESFORCE
|
# - SALESFORCE
|
||||||
|
# - ZOHOCRM
|
||||||
|
|
||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
|
@ -109,4 +110,10 @@ INFURA_API_KEY=""
|
||||||
# Used for the Salesforce (Sales Cloud) app
|
# Used for the Salesforce (Sales Cloud) app
|
||||||
SALESFORCE_CONSUMER_KEY=""
|
SALESFORCE_CONSUMER_KEY=""
|
||||||
SALESFORCE_CONSUMER_SECRET=""
|
SALESFORCE_CONSUMER_SECRET=""
|
||||||
|
|
||||||
|
# - ZOHOCRM
|
||||||
|
# Used for the Zoho CRM integration
|
||||||
|
ZOHOCRM_CLIENT_ID=""
|
||||||
|
ZOHOCRM_CLIENT_SECRET=""
|
||||||
|
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
|
|
13
README.md
13
README.md
|
@ -451,6 +451,19 @@ following
|
||||||
9. Click the "Save" button at the bottom footer.
|
9. Click the "Save" button at the bottom footer.
|
||||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||||
|
|
||||||
|
### Obtaining ZohoCRM Client ID and Secret
|
||||||
|
|
||||||
|
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||||
|
2. From within the API console page, go to "Applications".
|
||||||
|
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||||
|
4. Fill in any information you want in the "Client Details" tab
|
||||||
|
5. Go to tab "Client Secret" tab.
|
||||||
|
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||||
|
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
|
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||||
|
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||||
|
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const getCalendar = (credential: CredentialPayload | null): Calendar | nu
|
||||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
log.info("calendarApp", calendarApp.lib.CalendarService);
|
||||||
const CalendarService = calendarApp.lib.CalendarService;
|
const CalendarService = calendarApp.lib.CalendarService;
|
||||||
return new CalendarService(credential);
|
return new CalendarService(credential);
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,6 +56,7 @@ import whereby_config_json from "./whereby/config.json";
|
||||||
import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata";
|
import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata";
|
||||||
import wordpress_config_json from "./wordpress/config.json";
|
import wordpress_config_json from "./wordpress/config.json";
|
||||||
import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
|
import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
|
||||||
|
import zohocrm_config_json from "./zohocrm/config.json";
|
||||||
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
|
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
|
||||||
|
|
||||||
export const appStoreMetadata = {
|
export const appStoreMetadata = {
|
||||||
|
@ -113,5 +114,6 @@ export const appStoreMetadata = {
|
||||||
wipemycalother: wipemycalother__metadata_ts,
|
wipemycalother: wipemycalother__metadata_ts,
|
||||||
wordpress: wordpress_config_json,
|
wordpress: wordpress_config_json,
|
||||||
zapier: zapier__metadata_ts,
|
zapier: zapier__metadata_ts,
|
||||||
|
zohocrm: zohocrm_config_json,
|
||||||
zoomvideo: zoomvideo__metadata_ts,
|
zoomvideo: zoomvideo__metadata_ts,
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,5 +56,6 @@ export const apiHandlers = {
|
||||||
wipemycalother: import("./wipemycalother/api"),
|
wipemycalother: import("./wipemycalother/api"),
|
||||||
wordpress: import("./wordpress/api"),
|
wordpress: import("./wordpress/api"),
|
||||||
zapier: import("./zapier/api"),
|
zapier: import("./zapier/api"),
|
||||||
|
zohocrm: import("./zohocrm/api"),
|
||||||
zoomvideo: import("./zoomvideo/api"),
|
zoomvideo: import("./zoomvideo/api"),
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ import * as tandemvideo from "./tandemvideo";
|
||||||
import * as vital from "./vital";
|
import * as vital from "./vital";
|
||||||
import * as wipemycalother from "./wipemycalother";
|
import * as wipemycalother from "./wipemycalother";
|
||||||
import * as zapier from "./zapier";
|
import * as zapier from "./zapier";
|
||||||
|
import * as zohocrm from "./zohocrm";
|
||||||
import * as zoomvideo from "./zoomvideo";
|
import * as zoomvideo from "./zoomvideo";
|
||||||
|
|
||||||
const appStore = {
|
const appStore = {
|
||||||
|
@ -42,6 +43,7 @@ const appStore = {
|
||||||
office365video,
|
office365video,
|
||||||
plausible,
|
plausible,
|
||||||
salesforce,
|
salesforce,
|
||||||
|
zohocrm,
|
||||||
sendgrid,
|
sendgrid,
|
||||||
stripepayment,
|
stripepayment,
|
||||||
tandemvideo,
|
tandemvideo,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
items:
|
||||||
|
- 1.png
|
||||||
|
---
|
||||||
|
|
||||||
|
{DESCRIPTION}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const appKeys = await getAppKeysFromSlug("zohocrm");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "zohocrm client id missing." });
|
||||||
|
|
||||||
|
const redirectUri = WEBAPP_URL + "/api/integrations/zohocrm/callback";
|
||||||
|
const url = `https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.modules.ALL,ZohoCRM.users.READ,AaaServer.profile.READ&client_id=${client_id}&response_type=code&access_type=offline&redirect_uri=${redirectUri}`;
|
||||||
|
res.status(200).json({ url });
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: import("./_getAdd"),
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import qs from "qs";
|
||||||
|
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
|
||||||
|
let client_id = "";
|
||||||
|
let client_secret = "";
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { code } = req.query;
|
||||||
|
if (code === undefined && typeof code !== "string") {
|
||||||
|
res.status(400).json({ message: "`code` must be a string" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session?.user?.id) {
|
||||||
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appKeys = await getAppKeysFromSlug("zohocrm");
|
||||||
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
|
if (!client_id) return res.status(400).json({ message: "Zoho Crm consumer key missing." });
|
||||||
|
if (!client_secret) return res.status(400).json({ message: "Zoho Crm consumer secret missing." });
|
||||||
|
|
||||||
|
const url = `${req.query["accounts-server"]}/oauth/v2/token`;
|
||||||
|
const redirectUri = WEBAPP_URL + "/api/integrations/zohocrm/callback";
|
||||||
|
const formData = {
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
client_id: client_id,
|
||||||
|
client_secret: client_secret,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
code: code,
|
||||||
|
};
|
||||||
|
const zohoCrmTokenInfo = await axios({
|
||||||
|
method: "post",
|
||||||
|
url: url,
|
||||||
|
data: qs.stringify(formData),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// set expiry date as offset from current time.
|
||||||
|
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||||
|
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
|
||||||
|
|
||||||
|
await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: "zohocrm_other_calendar",
|
||||||
|
key: zohoCrmTokenInfo.data as any,
|
||||||
|
userId: req.session.user.id,
|
||||||
|
appId: "zohocrm",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = decodeOAuthState(req);
|
||||||
|
res.redirect(
|
||||||
|
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "other", slug: "zohocrm" })
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as add } from "./add";
|
||||||
|
export { default as callback } from "./callback";
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||||
|
"name": "ZohoCRM",
|
||||||
|
"slug": "zohocrm",
|
||||||
|
"type": "zohocrm_other_calendar",
|
||||||
|
"imageSrc": "icon.png",
|
||||||
|
"logo": "icon.png",
|
||||||
|
"url": "https://cal.com/apps/zohocrm",
|
||||||
|
"variant": "other",
|
||||||
|
"categories": ["other"],
|
||||||
|
"publisher": "Cal.com ",
|
||||||
|
"email": "help@cal.com",
|
||||||
|
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day",
|
||||||
|
"isTemplate": false,
|
||||||
|
"__createdUsingCli": true,
|
||||||
|
"__template": "basic"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * as api from "./api";
|
||||||
|
export * as lib from "./lib";
|
|
@ -0,0 +1,329 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import qs from "qs";
|
||||||
|
|
||||||
|
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type {
|
||||||
|
Calendar,
|
||||||
|
CalendarEvent,
|
||||||
|
EventBusyDate,
|
||||||
|
IntegrationCalendar,
|
||||||
|
NewCalendarEventType,
|
||||||
|
Person,
|
||||||
|
} from "@calcom/types/Calendar";
|
||||||
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||||
|
|
||||||
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
|
export type ZohoToken = {
|
||||||
|
scope: string;
|
||||||
|
api_domain: string;
|
||||||
|
expires_in: number;
|
||||||
|
expiryDate: number;
|
||||||
|
token_type: string;
|
||||||
|
access_token: string;
|
||||||
|
accountServer: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ZohoContact = {
|
||||||
|
Email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts to the date Format as required by zoho: 2020-08-02T15:30:00+05:30
|
||||||
|
* https://www.zoho.com/crm/developer/docs/api/v2/events-response.html
|
||||||
|
*/
|
||||||
|
const toISO8601String = (date: Date) => {
|
||||||
|
const tzo = -date.getTimezoneOffset(),
|
||||||
|
dif = tzo >= 0 ? "+" : "-",
|
||||||
|
pad = function (num: number) {
|
||||||
|
return (num < 10 ? "0" : "") + num;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
"-" +
|
||||||
|
pad(date.getMonth() + 1) +
|
||||||
|
"-" +
|
||||||
|
pad(date.getDate()) +
|
||||||
|
"T" +
|
||||||
|
pad(date.getHours()) +
|
||||||
|
":" +
|
||||||
|
pad(date.getMinutes()) +
|
||||||
|
":" +
|
||||||
|
pad(date.getSeconds()) +
|
||||||
|
dif +
|
||||||
|
pad(Math.floor(Math.abs(tzo) / 60)) +
|
||||||
|
":" +
|
||||||
|
pad(Math.abs(tzo) % 60)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default class ZohoCrmCalendarService implements Calendar {
|
||||||
|
private integrationName = "";
|
||||||
|
private auth: Promise<{ getToken: () => Promise<void> }>;
|
||||||
|
private log: typeof logger;
|
||||||
|
private client_id = "";
|
||||||
|
private client_secret = "";
|
||||||
|
private accessToken = "";
|
||||||
|
|
||||||
|
constructor(credential: CredentialPayload) {
|
||||||
|
this.integrationName = "zohocrm_other_calendar";
|
||||||
|
this.auth = this.zohoCrmAuth(credential).then((r) => r);
|
||||||
|
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContacts = async (attendees: Person[]) => {
|
||||||
|
const contacts = attendees.map((attendee) => {
|
||||||
|
const [firstname, lastname] = !!attendee.name ? attendee.name.split(" ") : [attendee.email, "-"];
|
||||||
|
return {
|
||||||
|
First_Name: firstname,
|
||||||
|
Last_Name: lastname || "-",
|
||||||
|
Email: attendee.email,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return axios({
|
||||||
|
method: "post",
|
||||||
|
url: `https://www.zohoapis.com/crm/v3/Contacts`,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
authorization: `Zoho-oauthtoken ${this.accessToken}`,
|
||||||
|
},
|
||||||
|
data: JSON.stringify({ data: contacts }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private contactSearch = async (event: CalendarEvent) => {
|
||||||
|
const searchCriteria =
|
||||||
|
"(" + event.attendees.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`).join("or") + ")";
|
||||||
|
|
||||||
|
return await axios({
|
||||||
|
method: "get",
|
||||||
|
url: `https://www.zohoapis.com/crm/v3/Contacts/search?criteria=${searchCriteria}`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Zoho-oauthtoken ${this.accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => data.data)
|
||||||
|
.catch((e) => this.log.error(e, e.response?.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 createZohoEvent = async (event: CalendarEvent) => {
|
||||||
|
const zohoEvent = {
|
||||||
|
Event_Title: event.title,
|
||||||
|
Start_DateTime: toISO8601String(new Date(event.startTime)),
|
||||||
|
End_DateTime: toISO8601String(new Date(event.endTime)),
|
||||||
|
Description: this.getMeetingBody(event),
|
||||||
|
Venue: getLocation(event),
|
||||||
|
};
|
||||||
|
|
||||||
|
return axios({
|
||||||
|
method: "post",
|
||||||
|
url: `https://www.zohoapis.com/crm/v3/Events`,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
authorization: `Zoho-oauthtoken ${this.accessToken}`,
|
||||||
|
},
|
||||||
|
data: JSON.stringify({ data: [zohoEvent] }),
|
||||||
|
})
|
||||||
|
.then((data) => data.data)
|
||||||
|
.catch((e) => this.log.error(e, e.response?.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateMeeting = async (uid: string, event: CalendarEvent) => {
|
||||||
|
const zohoEvent = {
|
||||||
|
id: uid,
|
||||||
|
Event_Title: event.title,
|
||||||
|
Start_DateTime: toISO8601String(new Date(event.startTime)),
|
||||||
|
End_DateTime: toISO8601String(new Date(event.endTime)),
|
||||||
|
Description: this.getMeetingBody(event),
|
||||||
|
Venue: getLocation(event),
|
||||||
|
};
|
||||||
|
return axios({
|
||||||
|
method: "put",
|
||||||
|
url: `https://www.zohoapis.com/crm/v3/Events`,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
authorization: `Zoho-oauthtoken ${this.accessToken}`,
|
||||||
|
},
|
||||||
|
data: JSON.stringify({ data: [zohoEvent] }),
|
||||||
|
})
|
||||||
|
.then((data) => data.data)
|
||||||
|
.catch((e) => this.log.error(e, e.response?.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
private deleteMeeting = async (uid: string) => {
|
||||||
|
return axios({
|
||||||
|
method: "delete",
|
||||||
|
url: `https://www.zohoapis.com/crm/v3/Events?ids=${uid}`,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
authorization: `Zoho-oauthtoken ${this.accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => data.data)
|
||||||
|
.catch((e) => this.log.error(e, e.response?.data));
|
||||||
|
};
|
||||||
|
|
||||||
|
private zohoCrmAuth = async (credential: CredentialPayload) => {
|
||||||
|
const appKeys = await getAppKeysFromSlug("zohocrm");
|
||||||
|
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
|
||||||
|
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
|
||||||
|
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Zoho CRM client_id missing." });
|
||||||
|
if (!this.client_secret)
|
||||||
|
throw new HttpError({ statusCode: 400, message: "Zoho CRM client_secret missing." });
|
||||||
|
const credentialKey = credential.key as unknown as ZohoToken;
|
||||||
|
const isTokenValid = (token: ZohoToken) => {
|
||||||
|
const isValid = token && token.access_token && token.expiryDate && token.expiryDate < Date.now();
|
||||||
|
if (isValid) {
|
||||||
|
this.accessToken = token.access_token;
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshAccessToken = async (credentialKey: ZohoToken) => {
|
||||||
|
try {
|
||||||
|
const url = `${credentialKey.accountServer}/oauth/v2/token`;
|
||||||
|
const formData = {
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_id: this.client_id,
|
||||||
|
client_secret: this.client_secret,
|
||||||
|
refresh_token: credentialKey.refresh_token,
|
||||||
|
};
|
||||||
|
const zohoCrmTokenInfo = await axios({
|
||||||
|
method: "post",
|
||||||
|
url: url,
|
||||||
|
data: qs.stringify(formData),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!zohoCrmTokenInfo.data.error) {
|
||||||
|
// set expiry date as offset from current time.
|
||||||
|
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||||
|
|
||||||
|
await prisma.credential.update({
|
||||||
|
where: {
|
||||||
|
id: credential.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
key: {
|
||||||
|
...(zohoCrmTokenInfo.data as ZohoToken),
|
||||||
|
refresh_token: credentialKey.refresh_token,
|
||||||
|
accountServer: credentialKey.accountServer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.accessToken = zohoCrmTokenInfo.data.access_token;
|
||||||
|
this.log.debug("Fetched token", this.accessToken);
|
||||||
|
} else {
|
||||||
|
this.log.error(zohoCrmTokenInfo.data);
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.log.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getToken: () => (isTokenValid(credentialKey) ? Promise.resolve() : refreshAccessToken(credentialKey)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async handleEventCreation(event: CalendarEvent, contacts: CalendarEvent["attendees"]) {
|
||||||
|
const meetingEvent = await this.createZohoEvent(event);
|
||||||
|
if (meetingEvent.data && meetingEvent.data.length && meetingEvent.data[0].status === "success") {
|
||||||
|
this.log.debug("event:creation:ok", { meetingEvent });
|
||||||
|
return Promise.resolve({
|
||||||
|
uid: meetingEvent.data[0].details.id,
|
||||||
|
id: meetingEvent.data[0].details.id,
|
||||||
|
type: this.integrationName,
|
||||||
|
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 ZohoCRM");
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
|
const auth = await this.auth;
|
||||||
|
await auth.getToken();
|
||||||
|
const contacts = await this.contactSearch(event);
|
||||||
|
if (contacts.data && contacts.data.length) {
|
||||||
|
if (contacts.data.length === event.attendees.length) {
|
||||||
|
// all contacts are in Zoho CRM already.
|
||||||
|
this.log.debug("contact:search:all", { event, contacts: contacts.data });
|
||||||
|
return await this.handleEventCreation(event, event.attendees);
|
||||||
|
} else {
|
||||||
|
// Some attendees don't exist in ZohoCRM
|
||||||
|
// Get the existing contacts' email to filter out
|
||||||
|
this.log.debug("contact:search:notAll", { event, contacts });
|
||||||
|
const existingContacts = contacts.data.map((contact: ZohoContact) => contact.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 ZohoCRM that were not present in the previous contact search
|
||||||
|
const createContacts = await this.createContacts(nonExistingContacts);
|
||||||
|
this.log.debug("contact:created", { createContacts });
|
||||||
|
// Continue with event creation and association only when all contacts are present in Zoho
|
||||||
|
if (createContacts.data?.data[0].status === "success") {
|
||||||
|
this.log.debug("contact:creation:ok");
|
||||||
|
return await this.handleEventCreation(event, nonExistingContacts.concat(contacts));
|
||||||
|
}
|
||||||
|
return Promise.reject({
|
||||||
|
calError: "Something went wrong when creating non-existing attendees in ZohoCRM",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.log.debug("contact:search:none", { event, contacts });
|
||||||
|
const createContacts = await this.createContacts(event.attendees);
|
||||||
|
this.log.debug("contact:created", { createContacts });
|
||||||
|
if (createContacts.data?.data[0].status === "success") {
|
||||||
|
this.log.debug("contact:creation:ok");
|
||||||
|
return await this.handleEventCreation(event, event.attendees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject({
|
||||||
|
calError: "Something went wrong when searching/creating the attendees in ZohoCRM",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
|
const auth = await this.auth;
|
||||||
|
await auth.getToken();
|
||||||
|
return await this.updateMeeting(uid, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEvent(uid: string): Promise<void> {
|
||||||
|
const auth = await this.auth;
|
||||||
|
await auth.getToken();
|
||||||
|
return 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([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as CalendarService } from "./CalendarService";
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "@calcom/zohocrm",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@calcom/lib": "*",
|
||||||
|
"@calcom/prisma": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/types": "*"
|
||||||
|
},
|
||||||
|
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day"
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 273 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
|
@ -203,6 +203,12 @@
|
||||||
"categories": ["video"],
|
"categories": ["video"],
|
||||||
"slug": "facetime",
|
"slug": "facetime",
|
||||||
"type": "facetime_video",
|
"type": "facetime_video",
|
||||||
|
"isTemplate": false},
|
||||||
|
{
|
||||||
|
"dirName": "zohocrm",
|
||||||
|
"categories": ["other"],
|
||||||
|
"slug": "zohocrm",
|
||||||
|
"type": "zohocrm_other_calendar",
|
||||||
"isTemplate": false
|
"isTemplate": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import dotEnv from "dotenv";
|
import dotEnv from "dotenv";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
@ -245,6 +245,12 @@ export default async function main() {
|
||||||
consumer_secret: process.env.SALESFORCE_CONSUMER_SECRET,
|
consumer_secret: process.env.SALESFORCE_CONSUMER_SECRET,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (process.env.ZOHOCRM_CLIENT_ID && process.env.ZOHOCRM_CLIENT_SECRET) {
|
||||||
|
await createApp("zohocrm", "zohocrm", ["other"], "zohocrm_other_calendar", {
|
||||||
|
client_id: process.env.ZOHOCRM_CLIENT_ID,
|
||||||
|
client_secret: process.env.ZOHOCRM_CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
|
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
|
||||||
if (process.env.GIPHY_API_KEY) {
|
if (process.env.GIPHY_API_KEY) {
|
||||||
await createApp("giphy", "giphy", ["other"], "giphy_other", {
|
await createApp("giphy", "giphy", ["other"], "giphy_other", {
|
||||||
|
|
|
@ -246,6 +246,8 @@
|
||||||
"$RAILWAY_STATIC_URL",
|
"$RAILWAY_STATIC_URL",
|
||||||
"$SALESFORCE_CONSUMER_KEY",
|
"$SALESFORCE_CONSUMER_KEY",
|
||||||
"$SALESFORCE_CONSUMER_SECRET",
|
"$SALESFORCE_CONSUMER_SECRET",
|
||||||
|
"$ZOHOCRM_CLIENT_ID",
|
||||||
|
"$ZOHOCRM_CLIENT_SECRET",
|
||||||
"$SAML_ADMINS",
|
"$SAML_ADMINS",
|
||||||
"$SAML_DATABASE_URL",
|
"$SAML_DATABASE_URL",
|
||||||
"$SAML_CLIENT_SECRET_VERIFIER",
|
"$SAML_CLIENT_SECRET_VERIFIER",
|
||||||
|
|
Loading…
Reference in New Issue