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 Affinity CRM #499

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/api/scripts/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ CREATE TABLE connector_sets
fs_googledrive boolean NULL,
fs_sharepoint boolean NULL,
fs_onedrive boolean NULL,
crm_affinity boolean NULL,
CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set )
);

Expand Down
8 changes: 4 additions & 4 deletions packages/api/scripts/seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first
('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','[email protected]', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora');


INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, crm_microsoftdynamicssales, ecom_webflow, tcg_linear, ecom_shopify, ecom_woocommerce, ecom_amazon, ecom_squarespace, hris_gusto, fs_googledrive, fs_dropbox, fs_sharepoint, fs_onedrive) VALUES
('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE);
INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, crm_microsoftdynamicssales, ecom_webflow, tcg_linear, ecom_shopify, ecom_woocommerce, ecom_amazon, ecom_squarespace, hris_gusto, fs_googledrive, fs_dropbox, fs_sharepoint, fs_onedrive,crm_affinity) VALUES
('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE);

INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES
('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'),
Expand Down
29 changes: 23 additions & 6 deletions packages/api/src/@core/utils/types/original/original.crm.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@

import { AffinityCompanyInput, AffinityCompanyOutput } from '@crm/company/services/affinity/types';
import { AffinityDealInput, AffinityDealOutput } from '@crm/deal/services/affinity/types';
import { AffinityNoteInput, AffinityNoteOutput } from '@crm/note/services/affinity/types';
import { AffinityUserInput, AffinityUserOutput } from '@crm/user/services/affinity/types';
import { AffinityContactInput, AffinityContactOutput } from '@crm/contact/services/affinity/types';
import { MicrosoftdynamicssalesEngagementInput, MicrosoftdynamicssalesEngagementOutput } from '@crm/engagement/services/microsoftdynamicssales/types';

import { MicrosoftdynamicssalesTaskInput, MicrosoftdynamicssalesTaskOutput } from '@crm/task/services/microsoftdynamicssales/types';
Expand Down Expand Up @@ -152,6 +158,7 @@ import {
ZendeskUserInput,
ZendeskUserOutput,
} from '@ticketing/user/services/zendesk/types';

import { SalesforceContactInput, SalesforceContactOutput } from '@crm/contact/services/salesforce/types';
import { SalesforceDealInput, SalesforceDealOutput } from '@crm/deal/services/salesforce/types';
import { SalesforceCompanyInput, SalesforceCompanyOutput } from '@crm/company/services/salesforce/types';
Expand All @@ -163,28 +170,31 @@ import { SalesforceUserInput, SalesforceUserOutput } from '@crm/user/services/sa

/* contact */
export type OriginalContactInput =
| AffinityCompanyInput
| HubspotContactInput
| ZohoContactInput
| ZendeskContactInput
| PipedriveContactInput
| AttioContactInput
| CloseContactInput
| CloseContactInput
| MicrosoftdynamicssalesContactInput
| SalesforceContactInput;

/* deal */
export type OriginalDealInput =
| AffinityDealInput
| HubspotDealOutput
| ZohoDealOutput
| ZendeskDealOutput
| PipedriveDealOutput
| CloseDealOutput
| AttioDealInput
| AttioDealInput
| MicrosoftdynamicssalesDealInput
| SalesforceDealInput

/* company */
export type OriginalCompanyInput =
| AffinityCompanyInput
| HubspotCompanyOutput
| ZohoCompanyOutput
| ZendeskCompanyOutput
Expand All @@ -202,6 +212,7 @@ export type OriginalEngagementInput =

/* note */
export type OriginalNoteInput =
| AffinityNoteInput
| HubspotNoteInput
| ZohoNoteInput
| ZendeskNoteInput
Expand All @@ -216,7 +227,7 @@ export type OriginalTaskInput =
| ZendeskTaskInput
| PipedriveTaskInput
| CloseTaskInput
| AttioTaskInput | MicrosoftdynamicssalesTaskInput | SalesforceTaskInput;
| AttioTaskInput | MicrosoftdynamicssalesTaskInput | SalesforceTaskInput;

/* stage */
export type OriginalStageInput =
Expand All @@ -230,11 +241,12 @@ export type OriginalStageInput =

/* user */
export type OriginalUserInput =
| AffinityUserInput
| HubspotUserInput
| ZohoUserInput
| ZendeskUserInput
| PipedriveUserInput
| CloseUserOutput | MicrosoftdynamicssalesUserInput | SalesforceUserInput
| CloseUserOutput | MicrosoftdynamicssalesUserInput | SalesforceUserInput

export type CrmObjectInput =
| OriginalContactInput
Expand All @@ -249,6 +261,7 @@ export type CrmObjectInput =
/* OUTPUT */

export type OriginalContactOutput =
| AffinityContactInput
| HubspotContactOutput
| ZohoContactOutput
| ZendeskContactOutput
Expand All @@ -258,6 +271,7 @@ export type OriginalContactOutput =

/* deal */
export type OriginalDealOutput =
| AffinityDealOutput
| HubspotDealOutput
| ZohoDealOutput
| ZendeskDealOutput
Expand All @@ -267,6 +281,7 @@ export type OriginalDealOutput =

/* company */
export type OriginalCompanyOutput =
| AffinityCompanyOutput
| HubspotCompanyOutput
| ZohoCompanyOutput
| ZendeskCompanyOutput
Expand All @@ -284,12 +299,13 @@ export type OriginalEngagementOutput =

/* note */
export type OriginalNoteOutput =
| AffinityNoteOutput
| HubspotNoteOutput
| ZohoNoteOutput
| ZendeskNoteOutput
| PipedriveNoteOutput
| CloseNoteOutput
| AttioNoteOutput | MicrosoftdynamicssalesNoteOutput | SalesforceNoteOutput;
| AttioNoteOutput | MicrosoftdynamicssalesNoteOutput | SalesforceNoteOutput;

/* task */
export type OriginalTaskOutput =
Expand All @@ -312,12 +328,13 @@ export type OriginalStageOutput =

/* user */
export type OriginalUserOutput =
| AffinityUserOutput
| HubspotUserOutput
| ZohoUserOutput
| ZendeskUserOutput
| PipedriveUserOutput
| CloseUserInput
| AttioUserOutput | MicrosoftdynamicssalesUserOutput| SalesforceUserOutput;
| AttioUserOutput | MicrosoftdynamicssalesUserOutput | SalesforceUserOutput;

export type CrmObjectOutput =
| OriginalContactOutput
Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/crm/company/company.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { ZohoService } from './services/zoho';
import { ZohoCompanyMapper } from './services/zoho/mappers';
import { SyncService } from './sync/sync.service';
import { SalesforceCompanyMapper } from './services/salesforce/mappers';
import { AffinityCompanyMapper } from './services/affinity/mappers';
import { AffinityService } from './services/affinity';

@Module({
controllers: [CompanyController],
Expand All @@ -40,6 +42,7 @@ import { SalesforceCompanyMapper } from './services/salesforce/mappers';
SalesforceService,
AttioService,
CloseService,

/* PROVIDERS MAPPERS */
AttioCompanyMapper,
CloseCompanyMapper,
Expand All @@ -50,7 +53,9 @@ import { SalesforceCompanyMapper } from './services/salesforce/mappers';
ZohoCompanyMapper,
MicrosoftdynamicssalesService,
MicrosoftdynamicssalesCompanyMapper,
AffinityService,
AffinityCompanyMapper,
],
exports: [SyncService, ServiceRegistry, WebhookService],
})
export class CompanyModule {}
export class CompanyModule { }
97 changes: 97 additions & 0 deletions packages/api/src/crm/company/services/affinity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { CrmObject } from '@crm/@lib/@types';
import { PrismaService } from '@@core/@core-services/prisma/prisma.service';
import { LoggerService } from '@@core/@core-services/logger/logger.service';
import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors';
import { EncryptionService } from '@@core/@core-services/encryption/encryption.service';
import { ApiResponse } from '@@core/utils/types';
import { ICompanyService } from '@crm/company/types';
import { ServiceRegistry } from '../registry.service';
import { AffinityCompanyInput, AffinityCompanyOutput } from './types';
import { SyncParam } from '@@core/utils/types/interface';
import { OriginalCompanyOutput } from '@@core/utils/types/original/original.crm';

@Injectable()
export class AffinityService implements ICompanyService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
) {
this.logger.setContext(
CrmObject.company.toUpperCase() + ':' + AffinityService.name,
);
this.registry.registerService('affinity', this);
}
Comment on lines +16 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

Refactor string concatenation to use template literals.

The existing comment and static analysis tool both suggest using template literals for setting the logger context. This change will improve readability and maintain consistency with modern JavaScript practices.

- this.logger.setContext(
-     CrmObject.company.toUpperCase() + ':' + AffinityService.name,
- );
+ this.logger.setContext(`${CrmObject.company.toUpperCase()}:${AffinityService.name}`);
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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Injectable()
export class AffinityService implements ICompanyService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
) {
this.logger.setContext(
CrmObject.company.toUpperCase() + ':' + AffinityService.name,
);
this.registry.registerService('affinity', this);
}
@Injectable()
export class AffinityService implements ICompanyService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
) {
this.logger.setContext(`${CrmObject.company.toUpperCase()}:${AffinityService.name}`);
this.registry.registerService('affinity', this);
}
Tools
Biome

[error] 25-25: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)

async addCompany(
companyData: AffinityCompanyInput,
linkedUserId: string,
): Promise<ApiResponse<AffinityCompanyOutput>> {
try {
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});

const resp = await axios.post(
`${connection.account_url}/organizations`,
JSON.stringify({
data: companyData,
}),
{
headers: {
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
},
);
return {
data: resp.data,
message: 'Affinity company created',
statusCode: 201,
};
} catch (error) {
throw error;
}
Comment on lines +29 to +63
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant catch block and approve API call logic.

The method correctly handles the API call to add a company. However, the catch block that only rethrows the error is redundant and can be removed for clarity.

-        } catch (error) {
-            throw error;
-        }
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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async addCompany(
companyData: AffinityCompanyInput,
linkedUserId: string,
): Promise<ApiResponse<AffinityCompanyOutput>> {
try {
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});
const resp = await axios.post(
`${connection.account_url}/organizations`,
JSON.stringify({
data: companyData,
}),
{
headers: {
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
},
);
return {
data: resp.data,
message: 'Affinity company created',
statusCode: 201,
};
} catch (error) {
throw error;
}
async addCompany(
companyData: AffinityCompanyInput,
linkedUserId: string,
): Promise<ApiResponse<AffinityCompanyOutput>> {
try {
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});
const resp = await axios.post(
`${connection.account_url}/organizations`,
JSON.stringify({
data: companyData,
}),
{
headers: {
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
},
);
return {
data: resp.data,
message: 'Affinity company created',
statusCode: 201,
};
}
Tools
Biome

[error] 62-62: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}

async sync(data: SyncParam): Promise<ApiResponse<AffinityCompanyOutput[]>> {
try {
const { linkedUserId } = data;

const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});
const resp = await axios.get(
`${connection.account_url}/organizations`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
},
},
);
return {
data: resp.data,
message: 'Affinity companies retrieved',
statusCode: 200,
};
} catch (error) {
throw error;
}
Comment on lines +66 to +95
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant catch block and approve API call logic.

The method correctly handles the API call to sync companies. However, the catch block that only rethrows the error is redundant and can be removed for clarity.

-        } catch (error) {
-            throw error;
-        }
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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async sync(data: SyncParam): Promise<ApiResponse<AffinityCompanyOutput[]>> {
try {
const { linkedUserId } = data;
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});
const resp = await axios.get(
`${connection.account_url}/organizations`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
},
},
);
return {
data: resp.data,
message: 'Affinity companies retrieved',
statusCode: 200,
};
} catch (error) {
throw error;
}
async sync(data: SyncParam): Promise<ApiResponse<AffinityCompanyOutput[]>> {
const { linkedUserId } = data;
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'affinity',
vertical: 'crm',
},
});
const resp = await axios.get(
`${connection.account_url}/organizations`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${this.cryptoService.decrypt(
connection.access_token,
)}`,
},
},
);
return {
data: resp.data,
message: 'Affinity companies retrieved',
statusCode: 200,
};
}
Tools
Biome

[error] 94-94: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}
}
96 changes: 96 additions & 0 deletions packages/api/src/crm/company/services/affinity/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AffinityCompanyInput, AffinityCompanyOutput } from './types';
import {
UnifiedCrmCompanyInput,
UnifiedCrmCompanyOutput,
} from '@crm/company/types/model.unified';
import { ICompanyMapper } from '@crm/company/types';
import { Utils } from '@crm/@lib/@utils';
import { Injectable } from '@nestjs/common';
import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry';
import { getCountryCode, getCountryName } from '@@core/utils/types';

@Injectable()
export class AffinityCompanyMapper implements ICompanyMapper {
constructor(private mappersRegistry: MappersRegistry, private utils: Utils) {
this.mappersRegistry.registerService('crm', 'company', 'affinity', this);
}
async desunify(
source: UnifiedCrmCompanyInput,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<AffinityCompanyInput> {
const result: AffinityCompanyInput = {
name: source.name
};

// Affinity company does not have attribute for email address
// Affinity Company doest not have direct mapping of number of employees

if (customFieldMappings && source.field_mappings) {
for (const [k, v] of Object.entries(source.field_mappings)) {
const mapping = customFieldMappings.find(
(mapping) => mapping.slug === k,
);
if (mapping) {
result[mapping.remote_id] = v;
}
}
}

return result;
}

async unify(
source: AffinityCompanyOutput | AffinityCompanyOutput[],
connectionId: string,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<UnifiedCrmCompanyOutput | UnifiedCrmCompanyOutput[]> {
if (!Array.isArray(source)) {
return this.mapSingleCompanyToUnified(
source,
connectionId,
customFieldMappings,
);
}
// Handling array of AffinityCompanyOutput
return Promise.all(
source.map((company) =>
this.mapSingleCompanyToUnified(
company,
connectionId,
customFieldMappings,
),
),
);
}

private async mapSingleCompanyToUnified(
company: AffinityCompanyOutput,
connectionId: string,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<UnifiedCrmCompanyOutput> {
const field_mappings: { [key: string]: any } = {};
if (customFieldMappings) {
for (const mapping of customFieldMappings) {
field_mappings[mapping.slug] = company[mapping.remote_id];
}
}

let opts: any = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Use const for unmodified variables.

The variable opts is never reassigned and should be declared with const to prevent reassignment and better communicate its intended immutability. This change aligns with best practices for immutability in JavaScript/TypeScript.

- let opts: any = {};
+ const opts: any = {};
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. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let opts: any = {};
const opts: any = {};
Tools
Biome

[error] 87-87: This let declares a variable that is only assigned once.

'opts' is never reassigned.

Safe fix: Use const instead.

(lint/style/useConst)


return {
remote_id: company.id,
name: company.name,
field_mappings,
...opts,
};
}
}
Loading
Loading