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 3 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
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
3 changes: 3 additions & 0 deletions packages/api/src/@core/connections/connections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export class ConnectionsController {
`No Callback Params found for code, found ${code}`,
);

console.log("In connection controller - Mit")
this.logger.log("in connection controller - Mit")
Copy link
Contributor

Choose a reason for hiding this comment

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

Using both console.log and this.logger.log for logging the same message is redundant. It's recommended to stick with this.logger.log for consistency and to leverage the logging framework's capabilities, such as log levels and formatting.

- console.log("In connection controller - Mit")
- this.logger.log("in connection controller - Mit")
+ this.logger.log("In connection controller - Mit")

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("In connection controller - Mit")
this.logger.log("in connection controller - Mit")
this.logger.log("In connection controller - Mit")


const stateData = JSON.parse(decodeURIComponent(state));
const { projectId, linkedUserId, providerName, returnUrl } = stateData;
switch (getProviderVertical(providerName.toLowerCase())) {
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);

}

Check warning on line 110 in packages/api/src/@core/connections/crm/services/attio/attio.service.ts

View workflow job for this annotation

GitHub Actions / Build and Test (18.x)

'opts' is defined but never used
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 @@ -41,11 +41,15 @@
zohoLocation?: string,
) {
try {
this.logger.log("callback called")
console.log("callback called")
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the previous file, using both console.log and this.logger.log for logging the same message is redundant. Prefer using this.logger.log for consistency across the application.

- this.logger.log("callback called")
- console.log("callback called")
+ this.logger.log("Callback called")

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
this.logger.log("callback called")
console.log("callback called")
this.logger.log("Callback called")

if (!code) {
throw new NotFoundError(`no ${providerName} code found, found ${code}`);
}

const serviceName = providerName.toLowerCase();
this.logger.log("callback in connection service - Mit")

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

if (!service) {
Copy link
Contributor

Choose a reason for hiding this comment

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

📝 NOTE
This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [51-64]

The console.log statement here seems to be used for debugging purposes. 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("In callback2")

📝 NOTE
This review was outside the diff hunks, and no overlapping diff hunk was found. Original lines [109-109]

The variable data is assigned a value but never used. If the intention was to log or process this data, ensure that it's being utilized appropriately. Otherwise, consider removing this assignment to clean up the code.

Copy link
Contributor

Choose a reason for hiding this comment

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

📝 NOTE
This review was outside the diff hunks, and no overlapping diff hunk was found. Original lines [107-107]

The variable data is assigned a value but never used within the handleCRMTokensRefresh method. If the intention was to log or process this data, ensure that it is utilized appropriately. Otherwise, consider removing the assignment to avoid confusion and potential memory overhead.

Expand All @@ -57,6 +61,7 @@
code: code,
location: zohoLocation || null,
};
console.log("In callback2")
Copy link
Contributor

Choose a reason for hiding this comment

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

Using console.log for logging is not recommended in production code as it lacks the flexibility and control provided by more sophisticated logging frameworks like the one already used in this service (this.logger). Consider replacing console.log with this.logger.log for consistency and better log management.

- console.log("In callback2")
+ this.logger.log("In callback2")

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("In callback2")
this.logger.log("In callback2")

const data: Connection = await service.handleCallback(callbackOpts);
this.logger.log('data is ' + data);
const event = await this.prisma.events.create({
Expand Down Expand Up @@ -101,7 +106,7 @@
refreshToken: refresh_token,
account_url: account_url,
};
const data = await service.handleTokenRefresh(refreshOpts);

Check warning on line 109 in packages/api/src/@core/connections/crm/services/crm.connection.service.ts

View workflow job for this annotation

GitHub Actions / Build and Test (18.x)

'data' is assigned a value but never used
} catch (error) {
handleServiceError(error, this.logger);
}
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