Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Attio CRM integration #320

Merged
merged 6 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ ZENDESK_SELL_CLIENT_SECRET=
# Freshsales
FRESHSALES_CLIENT_ID=
FRESHSALES_CLIENT_SECRET=
# Attio
ATTIO_CLIENT_ID=
ATTIO_CLIENT_SECRET=
# ================================================
# Ticketing
# ================================================
Expand All @@ -55,7 +58,8 @@ ZENDESK_TICKETING_CLIENT_SECRET=
# Must be set in the perspective of the end user browser

NEXT_PUBLIC_BACKEND_DOMAIN=http://localhost:3000 # https://api.panora.dev/
NEXT_PUBLIC_FRONTEND_DOMAIN=http://127.0.0.1:5173
NEXT_PUBLIC_FRONTEND_DOMAIN=http://localhost:81
NEXT_PUBLIC_ML_FRONTED_DOMAIN=http://localhost:81
NEXT_PUBLIC_POSTHOG_KEY=<ph_project_api_key>
NEXT_PUBLIC_POSTHOG_HOST=<ph_instance_address>
NEXT_PUBLIC_DISTRIBUTION="managed" #managed or self-host
Expand Down
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node-linker=hoisted
package-import-method=clone-or-copy
Binary file added apps/client-ts/public/providers/crm/attio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ services:
ENCRYPT_CRYPTO_SECRET_KEY: ${ENCRYPT_CRYPTO_SECRET_KEY}
HUBSPOT_CLIENT_ID: ${HUBSPOT_CLIENT_ID}
HUBSPOT_CLIENT_SECRET: ${HUBSPOT_CLIENT_SECRET}
ATTIO_CLIENT_ID: ${ATTIO_CLIENT_ID}
ATTIO_CLIENT_SECRET: ${ATTIO_CLIENT_SECRET}
ZOHOCRM_CLIENT_ID: ${ZOHOCRM_CLIENT_ID}
ZOHOCRM_CLIENT_SECRET: ${ZOHOCRM_CLIENT_SECRET}
PIPEDRIVE_CLIENT_ID: ${PIPEDRIVE_CLIENT_ID}
Expand Down Expand Up @@ -82,8 +84,8 @@ services:
dockerfile: ./apps/client-ts/Dockerfile.dev
context: ./
args:
VITE_BACKEND_DOMAIN: ${VITE_BACKEND_DOMAIN}
VITE_FRONTEND_DOMAIN: ${VITE_FRONTEND_DOMAIN}
VITE_BACKEND_DOMAIN: ${NEXT_PUBLIC_BACKEND_DOMAIN}
VITE_FRONTEND_DOMAIN: ${NEXT_PUBLIC_FRONTEND_DOMAIN}
environment:
NEXT_PUBLIC_STYTCH_SECRET: ${NEXT_PUBLIC_STYTCH_SECRET}
NEXT_PUBLIC_STYTCH_PROJECT_ID: ${NEXT_PUBLIC_STYTCH_PROJECT_ID}
Expand Down Expand Up @@ -111,6 +113,9 @@ services:
build:
dockerfile: ./apps/magic-link/Dockerfile.dev
context: ./
args:
VITE_BACKEND_DOMAIN: ${NEXT_PUBLIC_BACKEND_DOMAIN}
VITE_ML_FRONTEND_URL: ${NEXT_PUBLIC_ML_FRONTED_DOMAIN}
restart:
always
ports:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ services:
ENCRYPT_CRYPTO_SECRET_KEY: ${ENCRYPT_CRYPTO_SECRET_KEY}
HUBSPOT_CLIENT_ID: ${HUBSPOT_CLIENT_ID}
HUBSPOT_CLIENT_SECRET: ${HUBSPOT_CLIENT_SECRET}
ATTIO_CLIENT_ID: ${ATTIO_CLIENT_ID}
ATTIO_CLIENT_SECRET: ${ATTIO_CLIENT_SECRET}
ZOHOCRM_CLIENT_ID: ${ZOHOCRM_CLIENT_ID}
ZOHOCRM_CLIENT_SECRET: ${ZOHOCRM_CLIENT_SECRET}
PIPEDRIVE_CLIENT_ID: ${PIPEDRIVE_CLIENT_ID}
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ services:
ENCRYPT_CRYPTO_SECRET_KEY: ${ENCRYPT_CRYPTO_SECRET_KEY}
HUBSPOT_CLIENT_ID: ${HUBSPOT_CLIENT_ID}
HUBSPOT_CLIENT_SECRET: ${HUBSPOT_CLIENT_SECRET}
ATTIO_CLIENT_ID: ${ATTIO_CLIENT_ID}
ATTIO_CLIENT_SECRET: ${ATTIO_CLIENT_SECRET}
ZOHOCRM_CLIENT_ID: ${ZOHOCRM_CLIENT_ID}
ZOHOCRM_CLIENT_SECRET: ${ZOHOCRM_CLIENT_SECRET}
PIPEDRIVE_CLIENT_ID: ${PIPEDRIVE_CLIENT_ID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HubspotConnectionService } from './services/hubspot/hubspot.service';
import { ZohoConnectionService } from './services/zoho/zoho.service';
import { ZendeskConnectionService } from './services/zendesk/zendesk.service';
import { PipedriveConnectionService } from './services/pipedrive/pipedrive.service';
import { AttioConnectionService } from './services/attio/attio.service';

@Module({
imports: [WebhookModule],
Expand All @@ -26,10 +27,11 @@ import { PipedriveConnectionService } from './services/pipedrive/pipedrive.servi
// PROVIDERS SERVICES
FreshsalesConnectionService,
HubspotConnectionService,
AttioConnectionService,
ZohoConnectionService,
ZendeskConnectionService,
PipedriveConnectionService,
],
exports: [CrmConnectionsService],
})
export class CrmConnectionModule {}
export class CrmConnectionModule { }
117 changes: 117 additions & 0 deletions packages/api/src/@core/connections/crm/services/attio/attio.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Injectable } from "@nestjs/common";
import {
AttioOAuthResponse,
CallbackParams,
ICrmConnectionService,
RefreshParams,
} from "../../types";
import { PrismaService } from '@@core/prisma/prisma.service';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { Action, handleServiceError } from '@@core/utils/errors';
import { EnvironmentService } from '@@core/environment/environment.service';
import { EncryptionService } from '@@core/encryption/encryption.service';
import { ServiceConnectionRegistry } from '../registry.service';
import { LoggerService } from '@@core/logger/logger.service';


@Injectable()
export class AttioConnectionService implements ICrmConnectionService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private env: EnvironmentService,
private cryptoService: EncryptionService,
private registry: ServiceConnectionRegistry
) {
this.logger.setContext(AttioConnectionService.name);
this.registry.registerService("attio", this);
}

async handleCallback(opts: CallbackParams) {
try {
console.log("Linked User iD : <MMMMKIIT")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console.log statement here appears to be a debugging statement. It's recommended to remove it or replace it with this.logger.log if you intend to keep it for logging purposes in production.

- console.log("Linked User iD : <MMMMKIIT")

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
console.log("Linked User iD : <MMMMKIIT")

const { linkedUserId, projectId, code } = opts;
this.logger.log(
'linkeduserid is ' + linkedUserId + ' inside callback attio',
);
const isNotUnique = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'attio',
},
});
if (isNotUnique) return;
//reconstruct the redirect URI that was passed in the frontend it must be the same
const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`;
const formData = new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.env.getAttioAuth().CLIENT_ID,
client_secret: this.env.getAttioAuth().CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
code: code,
});

const res = await axios.post(
'https://app.attio.com/oauth/token',
formData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);

const data: AttioOAuthResponse = res.data;

// Saving the token of customer inside db
let db_res;
const connection_token = uuidv4();

if (isNotUnique) {
// Update existing connection
db_res = await this.prisma.connections.update({
where: {
id_connection: isNotUnique.id_connection,
},
data: {
access_token: this.cryptoService.encrypt(data.access_token),
status: 'valid',
created_at: new Date(),
},
});
} else {
// Create new connection
db_res = await this.prisma.connections.create({
data: {
id_connection: uuidv4(),
connection_token: connection_token,
provider_slug: 'attio',
token_type: 'oauth',
access_token: this.cryptoService.encrypt(data.access_token),
status: 'valid',
created_at: new Date(),
projects: {
connect: { id_project: projectId },
},
linked_users: {
connect: { id_linked_user: linkedUserId },
},
},
});
}
this.logger.log('Successfully added tokens inside DB ' + db_res);
return db_res;


} catch (error) {
handleServiceError(error, this.logger, 'attio', Action.oauthCallback);

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opts parameter in the handleTokenRefresh method is defined but never used. Since Attio does not require token refreshes as noted in the comment, you can remove the opts parameter to avoid confusion and align with the method's purpose.

- async handleTokenRefresh(opts: RefreshParams) {
+ async handleTokenRefresh() {

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
}
async handleTokenRefresh() {

}

// It is not required for Attio as it does not provide refresh_token
async handleTokenRefresh(opts: RefreshParams) {
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class CrmConnectionsService {
}

const serviceName = providerName.toLowerCase();

const service = this.serviceRegistry.getService(serviceName);

if (!service) {
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/@core/connections/crm/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export interface HubspotOAuthResponse {
access_token: string;
expires_in: number;
}

export interface AttioOAuthResponse {
access_token: string,
token_type: string;
}
export interface ZohoOAuthResponse {
access_token: string;
refresh_token: string;
Expand Down
10 changes: 9 additions & 1 deletion packages/api/src/@core/environment/environment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type RateLimit = {

@Injectable()
export class EnvironmentService {
constructor(private configService: ConfigService) {}
constructor(private configService: ConfigService) { }

getEnvMode(): string {
return this.configService.get<string>('ENV');
Expand Down Expand Up @@ -46,6 +46,14 @@ export class EnvironmentService {
CLIENT_SECRET: this.configService.get<string>('HUBSPOT_CLIENT_SECRET'),
};
}

getAttioAuth(): OAuth {
return {
CLIENT_ID: this.configService.get<string>('ATTIO_CLIENT_ID'),
CLIENT_SECRET: this.configService.get<string>('ATTIO_CLIENT_SECRET'),
}
}

getZohoSecret(): OAuth {
return {
CLIENT_ID: this.configService.get<string>('ZOHO_CLIENT_ID'),
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/@core/utils/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type StandardObject = TargetObject;
export const domains = {
CRM: {
hubspot: 'https://api.hubapi.com',
attio: 'https://developers.attio.com',
zoho: 'https://www.zohoapis.eu/crm/v3',
zendesk: 'https://api.getbase.com/v2',
freshsales: '',
Expand All @@ -43,6 +44,7 @@ export const customPropertiesUrls = {
hubspot: `${domains['CRM']['hubspot']}/properties/v1/contacts/properties`,
zoho: `${domains['CRM']['zoho']}/settings/fields?module=Contact`,
zendesk: `${domains['CRM']['zendesk']}/contact/custom_fields`,
attio: `${domains['CRM']['attio']}/docs/standard-objects-people`,
freshsales: `${domains['CRM']['freshsales']}`, //TODO
pipedrive: `${domains['CRM']['pipedrive']}/v1/personFields`,
},
Expand All @@ -67,6 +69,7 @@ export enum CrmProviders {
HUBSPOT = 'hubspot',
PIPEDRIVE = 'pipedrive',
FRESHSALES = 'freshsales',
ATTIO = 'attio',
}

export enum AccountingProviders {
Expand All @@ -85,6 +88,7 @@ export const CRM_PROVIDERS = [
'hubspot',
'pipedrive',
'freshsales',
'attio',
];

export const HRIS_PROVIDERS = [''];
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/crm/@utils/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export * from '../../contact/services/zendesk/types';
export * from '../../contact/services/hubspot/types';
export * from '../../contact/services/zoho/types';
export * from '../../contact/services/pipedrive/types';
export * from '../../contact/services/attio/types'

/* user */
export * from '../../user/services/freshsales/types';
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/crm/contact/contact.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ContactController } from './contact.controller';
import { PrismaService } from '@@core/prisma/prisma.service';
import { FreshsalesService } from './services/freshsales';
import { ZendeskService } from './services/zendesk';
import { AttioService } from './services/attio'
import { ZohoService } from './services/zoho';
import { PipedriveService } from './services/pipedrive';
import { HubspotService } from './services/hubspot';
Expand Down Expand Up @@ -33,11 +34,12 @@ import { ServiceRegistry } from './services/registry.service';
ServiceRegistry,
/* PROVIDERS SERVICES */
FreshsalesService,
AttioService,
ZendeskService,
ZohoService,
PipedriveService,
HubspotService,
],
exports: [SyncContactsService],
})
export class ContactModule {}
export class ContactModule { }
Loading
Loading