Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat: implement close crm integration #364

Merged
merged 20 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
576d653
feat(close-crm-integration): implement OAuth integration and generate…
ashutoshdhande Oct 24, 2023
198dcc3
last day at HOME
ashutoshdhande Nov 2, 2023
f3c5604
feat(contact): implement CRUD operations
ashutoshdhande Nov 10, 2023
bc2309b
feat(contact-service): implement pagination in getContacts
ashutosh-revert Nov 14, 2023
c928652
fix(contact-service): resolve NaN issue in pagination for getContacts
ashutosh-revert Nov 14, 2023
03817fe
fix(contact-service): resolve name issue for closecrm
ashutosh-revert Nov 14, 2023
1066484
feat(contact-service): Implementation of update contact in close crm
ashutosh-revert Nov 14, 2023
8eb5bcd
feat(contact-service): Implement search endpoint and fix name field i…
ashutosh-revert Nov 15, 2023
4c7e3fd
Merge branch 'main' of https://github.com/ashutoshdhande/Revert into …
ashutosh-revert Nov 15, 2023
d3fe54c
docs(readme): add setup instructions for Close CRM
ashutosh-revert Nov 15, 2023
8d36e6a
feat: Implemented endpoints for note service
ashutosh-revert Nov 16, 2023
9366a6e
feat: Implemented endpoints for task service
ashutosh-revert Nov 16, 2023
b4e5784
feat: Implemented endpoints for user service
ashutosh-revert Nov 16, 2023
4f2b38e
feat: Implemented endpoints for events service
ashutosh-revert Nov 16, 2023
4f590e5
feat: Implemented endpoints for leads service
ashutosh-revert Nov 16, 2023
7202415
feat: Implemented proxy endpoint for close crm
ashutosh-revert Nov 17, 2023
b912764
feat: Implemented endpoints for deals service
ashutosh-revert Nov 20, 2023
c58776a
refactor: removed unnecessary comments
ashutosh-revert Nov 21, 2023
42e3d60
refactor: reverted to original state by removing local changes
ashutosh-revert Nov 21, 2023
9a19666
refactor: env.example updated
ashutosh-revert Nov 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fern/definition/common/associations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ types:
dealId:
type: optional<string>
docs: The id of the deal to associate with contact
leadId:
type: optional<string>
docs: The id of lead to associate with contact for close crm

examples:
- name: Associate hubspot contact with deal
value:
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_BOT_TOKEN=
LOOPS_ONBOARDING_TXN_ID=
LOOPS_API_KEY=
LOOPS_API_KEY=
CLOSECRM_CLIENT_ID=
CLOSECRM_CLIENT_SECRET=
10 changes: 10 additions & 0 deletions packages/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@ yarn workspace @revertdotdev/backend db-seed
- **Enter the client_id and client_secret you copied in the previous step**
- **(Optional) Copy the scopes (from the Pipedrive app) and add them to the revert dashboard to get granular control over the scope of your app**
- You can skip this step and use the default revert scopes and permissions

#### Connect to CLOSE CRM via Revert

- Open [CLOSE CRM](https://www.close.com/) and sigin or create an account
- Left Pane Setting > Developer > OAuth Apps > create apps . More details mentioned [here](https://help.close.com/docs/create-oauth-apps)
- **Get your client_id and client_secret**:
- Select your app
- Copy Client ID and Client Secret
- **Login to your revert dashboard**
- **Enter the Client ID and Client Secret you copied in the previous step and click submit**
2 changes: 2 additions & 0 deletions packages/backend/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const config = {
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
LOOPS_ONBOARDING_TXN_ID: process.env.LOOPS_ONBOARDING_TXN_ID,
LOOPS_API_KEY: process.env.LOOPS_API_KEY,
CLOSECRM_CLIENT_ID: process.env.CLOSECRM_CLIENT_ID!,
CLOSECRM_CLIENT_SECRET: process.env.CLOSECRM_CLIENT_SECRET!,
};

export default config;
12 changes: 11 additions & 1 deletion packages/backend/constants/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TP_ID } from '@prisma/client';

export type CRM_TP_ID = 'zohocrm' | 'sfdc' | 'pipedrive' | 'hubspot';
export type CRM_TP_ID = 'zohocrm' | 'sfdc' | 'pipedrive' | 'hubspot' | 'closecrm';
// export type CHAT_TP_ID = 'slack';

export const DEFAULT_SCOPE = {
Expand Down Expand Up @@ -38,6 +38,7 @@ export const DEFAULT_SCOPE = {
[TP_ID.sfdc]: [], // https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_tokens_scopes.htm&type=5
[TP_ID.pipedrive]: [],
[TP_ID.slack]: ['users:read', 'users.profile:read'],
[TP_ID.closecrm]: [],
};

export const mapIntegrationIdToIntegrationName = {
Expand All @@ -46,6 +47,7 @@ export const mapIntegrationIdToIntegrationName = {
[TP_ID.sfdc]: 'Salesforce',
[TP_ID.zohocrm]: 'Zoho',
[TP_ID.slack]: 'Slack',
[TP_ID.closecrm]: 'Close',
};

export const rootSchemaMappingId = 'revertRootSchemaMapping';
Expand All @@ -67,47 +69,55 @@ export const objectNameMapping: Record<string, Record<CRM_TP_ID, string | undefi
[TP_ID.pipedrive]: 'organization',
[TP_ID.sfdc]: 'Account',
[TP_ID.zohocrm]: 'Accounts',
[TP_ID.closecrm]: 'organization',
},
[StandardObjects.contact]: {
[TP_ID.hubspot]: 'contacts',
[TP_ID.pipedrive]: 'person',
[TP_ID.sfdc]: 'Contact',
[TP_ID.zohocrm]: 'Contacts',
[TP_ID.closecrm]: 'contact',
},
[StandardObjects.deal]: {
[TP_ID.hubspot]: 'deals',
[TP_ID.pipedrive]: 'deal',
[TP_ID.sfdc]: 'Opportunity',
[TP_ID.zohocrm]: 'Deals',
[TP_ID.closecrm]: 'opportunity',
},
[StandardObjects.event]: {
[TP_ID.hubspot]: 'meetings',
[TP_ID.pipedrive]: 'activity',
[TP_ID.sfdc]: 'Event',
[TP_ID.zohocrm]: 'Events',
[TP_ID.closecrm]: '', // @TODO add equivalent cause there are activity, meeting and events options close crm api
},
[StandardObjects.lead]: {
[TP_ID.hubspot]: 'contacts',
[TP_ID.pipedrive]: 'lead',
[TP_ID.sfdc]: 'Lead',
[TP_ID.zohocrm]: 'Leads',
[TP_ID.closecrm]: 'lead',
},
[StandardObjects.note]: {
[TP_ID.hubspot]: 'notes',
[TP_ID.pipedrive]: 'note',
[TP_ID.sfdc]: 'Note',
[TP_ID.zohocrm]: 'Notes',
[TP_ID.closecrm]: 'note',
},
[StandardObjects.task]: {
[TP_ID.hubspot]: 'tasks',
[TP_ID.pipedrive]: 'activity',
[TP_ID.sfdc]: 'Task',
[TP_ID.zohocrm]: 'Tasks',
[TP_ID.closecrm]: 'task',
},
[StandardObjects.user]: {
[TP_ID.hubspot]: 'users',
[TP_ID.pipedrive]: undefined,
[TP_ID.sfdc]: 'User',
[TP_ID.zohocrm]: 'users',
[TP_ID.closecrm]: 'user',
},
};
27 changes: 27 additions & 0 deletions packages/backend/helpers/crm/closecrm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default function handleCloseCRMDisunify<T extends Record<string, any>>({
obj,
transformedObj,
}: {
obj: T;
transformedObj: any;
}) {
if (obj.firstName && obj.lastName) {
transformedObj['name'] = `${obj.firstName} ${obj.lastName}`;
} else if (obj.firstName) {
transformedObj['name'] = `${obj.firstName}`;
} else if (obj.lastName) {
transformedObj['name'] = `${obj.lastName}`;
}

['phones', 'emails'].forEach((key) => {
if (transformedObj[key] && transformedObj[key].constructor === Object) {
transformedObj[key] = Object.values(transformedObj[key]);
}
});

if (obj.associations) {
transformedObj['lead_id'] = obj.associations.leadId;
}

return transformedObj;
}
4 changes: 4 additions & 0 deletions packages/backend/helpers/crm/transform/disunify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { transformModelToFieldMapping } from '.';
import { handleHubspotDisunify, handlePipedriveDisunify, handleSfdcDisunify, handleZohoDisunify } from '..';
import { postprocessDisUnifyObject } from './preprocess';
import { flattenObj } from '../../../helpers/flattenObj';
import handleCloseCRMDisunify from '../closecrm';

export async function disunifyObject<T extends Record<string, any>>({
obj,
Expand Down Expand Up @@ -40,5 +41,8 @@ export async function disunifyObject<T extends Record<string, any>>({
case TP_ID.sfdc: {
return handleSfdcDisunify({ obj, objType, transformedObj: processedObj });
}
case TP_ID.closecrm: {
return handleCloseCRMDisunify({ obj, transformedObj: processedObj });
}
}
}
43 changes: 43 additions & 0 deletions packages/backend/helpers/crm/transform/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,40 @@ export const preprocessUnifyObject = <T extends Record<string, any>>({
};
},
},
[TP_ID.closecrm]: {
[StandardObjects.contact]: (obj: T) => {
if (obj.name) {
const names = obj.name.split(' ');
const modifiedObj = {
...obj,
firstName: names[0],
lastName: names[1],
};
return modifiedObj;
}
return { ...obj };
},
[StandardObjects.lead]: (obj: T) => {
if (obj.name) {
const names = obj.name.split(' ');
const modifiedObj = {
...obj,
firstName: names[0],
lastName: names[1],
};
return modifiedObj;
}
return { ...obj };
},
[StandardObjects.deal]: (obj: T) => {
return {
...obj,
isWon: obj['status_type'] === 'won' ? true : false,
confidence: obj['confidence'] ? Number((parseInt(obj['confidence']) / 100).toFixed(4)) : undefined,
value: obj['value'] ? Number(obj['value']) / 100 : undefined,
};
},
},
};
const transformFn = (preprocessMap[tpId] || {})[objType];
return transformFn ? transformFn(obj) : obj;
Expand Down Expand Up @@ -124,6 +158,15 @@ export const postprocessDisUnifyObject = <T extends Record<string, any>>({
};
},
},
[TP_ID.closecrm]: {
[StandardObjects.deal]: (obj: T) => {
return {
...obj,
confidence: obj.confidence ? obj.confidence * 100 : undefined,
value: obj.value ? obj.value * 100 : undefined,
};
},
},
};
const transformFn = (preprocessMap[tpId] || {})[objType];
return transformFn ? transformFn(obj) : obj;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ export const transformModelToFieldMapping = async ({
where: { object: objType, schema_mapping_id: !!tenantSchemaMappingId ? tenantSchemaMappingId : undefined },
include: { fieldMappings: { where: { source_tp_id: tpId } } },
});

const rootSchema = await prisma.schemas.findFirst({
where: { object: objType, schema_mapping_id: rootSchemaMappingId },
include: { fieldMappings: { where: { source_tp_id: tpId } } },
});

let crmObj: Record<string, string> = {};
Object.keys(unifiedObj).forEach((key) => {
const tenantFieldMapping = connectionSchema?.fieldMappings?.find(
Expand Down
16 changes: 14 additions & 2 deletions packages/backend/helpers/crm/transform/unify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,31 @@ export async function unifyObject<T extends Record<string, any>, K>({
tenantSchemaMappingId,
accountFieldMappingConfig,
});
const unifiedObject = {
const unifiedObject: {
additional: any;
associations: any | undefined;
} = {
...transformedObject,
additional: {
...(transformedObject.additional || {}),
},
associations: {},
};

// Map additional fields
Object.keys(obj).forEach((key) => {
if (!(key in unifiedObject) && key !== 'properties') {
unifiedObject['additional'][key] = obj[key];
if (unifiedObject.additional.lead_id || unifiedObject.additional.organization_id) {
unifiedObject['associations']['leadId'] = unifiedObject.additional.lead_id;
unifiedObject['associations']['companyId'] = unifiedObject.additional.organization_id;
}
}
});

// Check if associations object is empty and set it to undefined
if (Object.keys(unifiedObject.associations || {}).length === 0) {
unifiedObject.associations = undefined;
}

return unifiedObject as K;
}
6 changes: 5 additions & 1 deletion packages/backend/helpers/tenantIdMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { xprisma } from '../prisma/client';
import { logInfo, logError } from './logger';

const revertTenantMiddleware = () => async (req: Request, res: Response, next: () => any) => {
const { 'x-revert-t-id': tenantId, 'x-revert-api-token': token, 'x-revert-t-token': tenantSecretToken } = req.headers;
const {
'x-revert-t-id': tenantId,
'x-revert-api-token': token,
'x-revert-t-token': tenantSecretToken,
} = req.headers;
if (tenantSecretToken && !token) {
return next();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "TP_ID" ADD VALUE 'closecrm';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "apps" ADD COLUMN "app_bot_token" TEXT;

-- AlterTable
ALTER TABLE "connections" ADD COLUMN "app_bot_token" TEXT;
12 changes: 12 additions & 0 deletions packages/backend/prisma/migrations/20231108093347_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Warnings:

- You are about to drop the column `app_bot_token` on the `apps` table. All the data in the column will be lost.
- You are about to drop the column `app_bot_token` on the `connections` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "apps" DROP COLUMN "app_bot_token";

-- AlterTable
ALTER TABLE "connections" DROP COLUMN "app_bot_token";
1 change: 1 addition & 0 deletions packages/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum TP_ID {
zohocrm
sfdc
pipedrive
closecrm
slack
}

Expand Down
Loading
Loading