diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 43efca349..dfc584060 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -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 ) ); diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index dc087a70d..45705aff7 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -2,10 +2,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first ('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$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'), diff --git a/packages/api/src/@core/utils/types/original/original.crm.ts b/packages/api/src/@core/utils/types/original/original.crm.ts index de6342ab0..081f68356 100644 --- a/packages/api/src/@core/utils/types/original/original.crm.ts +++ b/packages/api/src/@core/utils/types/original/original.crm.ts @@ -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'; @@ -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'; @@ -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 @@ -202,6 +212,7 @@ export type OriginalEngagementInput = /* note */ export type OriginalNoteInput = + | AffinityNoteInput | HubspotNoteInput | ZohoNoteInput | ZendeskNoteInput @@ -216,7 +227,7 @@ export type OriginalTaskInput = | ZendeskTaskInput | PipedriveTaskInput | CloseTaskInput - | AttioTaskInput | MicrosoftdynamicssalesTaskInput | SalesforceTaskInput; + | AttioTaskInput | MicrosoftdynamicssalesTaskInput | SalesforceTaskInput; /* stage */ export type OriginalStageInput = @@ -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 @@ -249,6 +261,7 @@ export type CrmObjectInput = /* OUTPUT */ export type OriginalContactOutput = + | AffinityContactInput | HubspotContactOutput | ZohoContactOutput | ZendeskContactOutput @@ -258,6 +271,7 @@ export type OriginalContactOutput = /* deal */ export type OriginalDealOutput = + | AffinityDealOutput | HubspotDealOutput | ZohoDealOutput | ZendeskDealOutput @@ -267,6 +281,7 @@ export type OriginalDealOutput = /* company */ export type OriginalCompanyOutput = + | AffinityCompanyOutput | HubspotCompanyOutput | ZohoCompanyOutput | ZendeskCompanyOutput @@ -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 = @@ -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 diff --git a/packages/api/src/crm/company/company.module.ts b/packages/api/src/crm/company/company.module.ts index dd3e52f4e..2f46c8c6b 100644 --- a/packages/api/src/crm/company/company.module.ts +++ b/packages/api/src/crm/company/company.module.ts @@ -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], @@ -40,6 +42,7 @@ import { SalesforceCompanyMapper } from './services/salesforce/mappers'; SalesforceService, AttioService, CloseService, + /* PROVIDERS MAPPERS */ AttioCompanyMapper, CloseCompanyMapper, @@ -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 { } diff --git a/packages/api/src/crm/company/services/affinity/index.ts b/packages/api/src/crm/company/services/affinity/index.ts new file mode 100644 index 000000000..c0ab83a49 --- /dev/null +++ b/packages/api/src/crm/company/services/affinity/index.ts @@ -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); + } + async addCompany( + companyData: AffinityCompanyInput, + linkedUserId: string, + ): Promise> { + 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 sync(data: SyncParam): Promise> { + 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; + } + } +} diff --git a/packages/api/src/crm/company/services/affinity/mappers.ts b/packages/api/src/crm/company/services/affinity/mappers.ts new file mode 100644 index 000000000..6a55052b0 --- /dev/null +++ b/packages/api/src/crm/company/services/affinity/mappers.ts @@ -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 { + 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 { + 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 { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = company[mapping.remote_id]; + } + } + + let opts: any = {}; + + return { + remote_id: company.id, + name: company.name, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/company/services/affinity/types.ts b/packages/api/src/crm/company/services/affinity/types.ts new file mode 100644 index 000000000..eeb82063e --- /dev/null +++ b/packages/api/src/crm/company/services/affinity/types.ts @@ -0,0 +1,55 @@ +interface AffinityCompany { + id: number + name: string + domain: string + domains: string[] + global: boolean + person_ids: number[] + opportunity_ids: number[] + list_entries: ListEntry[] + interaction_dates: InteractionDates + interactions: Interactions +} + +interface ListEntry { + id: number + list_id: number + creator_id: number + entity_id: number + created_at: string +} + +interface InteractionDates { + first_email_date: string + last_email_date: string + last_event_date: string + last_chat_message_date: string + last_interaction_date: string + next_event_date: string + first_event_date: string +} + +interface Interactions { + first_email: InteractionsType + last_email: InteractionsType + last_event: InteractionsType + last_chat_message: InteractionsType + last_interaction: InteractionsType + next_event: InteractionsType + first_event: InteractionsType +} + +interface InteractionsType { + date: string + person_ids: number[] +} + +export type AffinityCompanyInput = { + name: string, + domain?: string, + person_ids?: number[] +} + +export type AffinityCompanyOutput = Partial + + diff --git a/packages/api/src/crm/contact/contact.module.ts b/packages/api/src/crm/contact/contact.module.ts index e7587c37f..4cbff87f3 100644 --- a/packages/api/src/crm/contact/contact.module.ts +++ b/packages/api/src/crm/contact/contact.module.ts @@ -23,6 +23,8 @@ import { ZohoService } from './services/zoho'; import { ZohoContactMapper } from './services/zoho/mappers'; import { SyncService } from './sync/sync.service'; import { SalesforceContactMapper } from './services/salesforce/mappers'; +import { AffinityContactMapper } from './services/affinity/mappers'; +import { AffinityService } from './services/affinity'; @Module({ controllers: [ContactController], @@ -52,7 +54,9 @@ import { SalesforceContactMapper } from './services/salesforce/mappers'; ZohoContactMapper, MicrosoftdynamicssalesService, MicrosoftdynamicssalesContactMapper, + AffinityService, + AffinityContactMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) -export class ContactModule {} +export class ContactModule { } diff --git a/packages/api/src/crm/contact/services/affinity/index.ts b/packages/api/src/crm/contact/services/affinity/index.ts new file mode 100644 index 000000000..b299d001b --- /dev/null +++ b/packages/api/src/crm/contact/services/affinity/index.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@nestjs/common'; +import { IContactService } from '@crm/contact/types'; +import { CrmObject } from '@crm/@lib/@types'; +import axios from 'axios'; +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 { ServiceRegistry } from '../registry.service'; +import { AffinityContactInput, AffinityContactOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class AffinityService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + AffinityService.name, + ); + this.registry.registerService('affinity', this); + } + + async addContact( + contactData: AffinityContactInput, + linkedUserId: string, + ): Promise> { + 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}/persons`, + JSON.stringify({ + data: contactData, + }), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'affinity contact created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + 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}/persons`, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp.data, + message: 'Affinity contacts retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/contact/services/affinity/mappers.ts b/packages/api/src/crm/contact/services/affinity/mappers.ts new file mode 100644 index 000000000..02d36a439 --- /dev/null +++ b/packages/api/src/crm/contact/services/affinity/mappers.ts @@ -0,0 +1,110 @@ +import { + UnifiedCrmContactInput, + UnifiedCrmContactOutput, +} from '@crm/contact/types/model.unified'; +import { IContactMapper } from '@crm/contact/types'; +import { AffinityContactInput, AffinityContactOutput } from './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 AffinityContactMapper implements IContactMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'contact', 'affinity', this); + } + + async desunify( + source: UnifiedCrmContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + // Assuming 'email_addresses' and 'phone_numbers' arrays contain at least one entry + + + const result: AffinityContactInput = { + first_name: source.first_name, + last_name: source.last_name, + emails: source.email_addresses?.map((e) => e.email_address), + }; + + 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: AffinityContactOutput | AffinityContactOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleContactToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + // Handling array of HubspotContactOutput + return Promise.all( + source.map((contact) => + this.mapSingleContactToUnified( + contact, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleContactToUnified( + contact: AffinityContactOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = contact[mapping.remote_id]; + } + } + + const opts: any = {}; + + return { + remote_id: contact.id, + first_name: contact.first_name, + last_name: contact.last_name, + // user_id: contact.values.created_by[0]?.referenced_actor_id, + email_addresses: contact.emails?.map((e) => ({ + email_address: e, + email_address_type: '', + })), // Map each email + // phone_numbers: contact.values.phone_numbers?.map((p) => ({ + // phone_number: p.original_phone_number, + // phone_type: p.attribute_type ? p.attribute_type : '', + // })), // Map each phone number, + field_mappings, + // addresses: [address], + ...opts, + }; + } +} diff --git a/packages/api/src/crm/contact/services/affinity/types.ts b/packages/api/src/crm/contact/services/affinity/types.ts new file mode 100644 index 000000000..35171fbce --- /dev/null +++ b/packages/api/src/crm/contact/services/affinity/types.ts @@ -0,0 +1,57 @@ +interface AffinityContact { + id: number + type: number + first_name: string + last_name: string + primary_email: string + emails: string[] + organization_ids: number[] + opportunity_ids: number[] + current_organization_ids: number[] + list_entries: ListEntry[] + interaction_dates: InteractionDates + interactions: Interactions +} + +export type AffinityContactInput = { + first_name: string, + last_name: string, + emails: string[], + organization_ids?: number[] +} + +interface ListEntry { + id: number + list_id: number + creator_id: number + entity_id: number + created_at: string +} + +interface InteractionDates { + first_email_date: string + last_email_date: string + last_event_date: string + last_chat_message_date: string + last_interaction_date: string + next_event_date: string + first_event_date: string +} + +interface Interactions { + first_email: InteractionsType + last_email: InteractionsType + last_event: InteractionsType + last_chat_message: InteractionsType + last_interaction: InteractionsType + next_event: InteractionsType + first_event: InteractionsType +} + +interface InteractionsType { + date: string + person_ids: number[] +} + +export type AffinityContactOutput = Partial; + diff --git a/packages/api/src/crm/deal/deal.module.ts b/packages/api/src/crm/deal/deal.module.ts index 4b4e35c08..7605c4f81 100644 --- a/packages/api/src/crm/deal/deal.module.ts +++ b/packages/api/src/crm/deal/deal.module.ts @@ -22,6 +22,8 @@ import { ZohoService } from './services/zoho'; import { ZohoDealMapper } from './services/zoho/mappers'; import { SyncService } from './sync/sync.service'; import { SalesforceDealMapper } from './services/salesforce/mappers'; +import { AffinityDealMapper } from './services/affinity/mappers'; +import { AffinityService } from './services/affinity'; @Module({ controllers: [DealController], @@ -50,7 +52,9 @@ import { SalesforceDealMapper } from './services/salesforce/mappers'; CloseDealMapper, MicrosoftdynamicssalesService, MicrosoftdynamicssalesDealMapper, + AffinityService, + AffinityDealMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) -export class DealModule {} +export class DealModule { } diff --git a/packages/api/src/crm/deal/services/affinity/index.ts b/packages/api/src/crm/deal/services/affinity/index.ts new file mode 100644 index 000000000..488407b88 --- /dev/null +++ b/packages/api/src/crm/deal/services/affinity/index.ts @@ -0,0 +1,115 @@ +/* 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 { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { IDealService } from '@crm/deal/types'; +import { ServiceRegistry } from '../registry.service'; +import { AffinityDealInput, AffinityDealOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalDealOutput } from '@@core/utils/types/original/original.crm'; + +@Injectable() +export class AffinityService implements IDealService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.deal.toUpperCase() + ':' + AffinityService.name, + ); + this.registry.registerService('affinity', this); + } + async addDeal( + dealData: AffinityDealInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'affinity', + vertical: 'crm', + }, + }); + + const list_resp = await axios.get( + `${connection.account_url}/lists`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + for (const list of list_resp.data) { + if (list.type === 8) { + dealData.list_id = list.id; + } + + } + + const resp = await axios.post( + `${connection.account_url}/opportunities`, + JSON.stringify(dealData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp?.data, + message: 'Affinity deal created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + 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}/opportunities`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced affinity deals !`); + + return { + data: resp?.data?.data, + message: 'Affinity deals retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/deal/services/affinity/mappers.ts b/packages/api/src/crm/deal/services/affinity/mappers.ts new file mode 100644 index 000000000..0ca68cd28 --- /dev/null +++ b/packages/api/src/crm/deal/services/affinity/mappers.ts @@ -0,0 +1,133 @@ +import { Injectable } from '@nestjs/common'; +import { + UnifiedCrmDealInput, + UnifiedCrmDealOutput, +} from '@crm/deal/types/model.unified'; +import { IDealMapper } from '@crm/deal/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { AffinityDealInput, AffinityDealOutput } from './types'; + +@Injectable() +export class AffinityDealMapper implements IDealMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'deal', 'affinity', this); + } + + async desunify( + source: UnifiedCrmDealInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + + let opts: any = {}; + + if (source.company_id) { + const organization_id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + if (organization_id) { + opts = { + ...opts, + organization_ids: [organization_id] + } + } + } + + const result: AffinityDealInput = { + name: source.name, + list_id: 21, + ...opts + }; + + 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: AffinityDealOutput | AffinityDealOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleDealToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of AffinityDealOutput + return Promise.all( + source.map((deal) => + this.mapSingleDealToUnified(deal, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleDealToUnified( + deal: AffinityDealOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = deal[mapping.remote_id]; + } + } + + let opts: any = {} + + if (deal.list_entries) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(deal.list_entries[0].creator_id), + 'affinity' + ); + if (user_id) { + opts = { + ...opts, + user_id + } + } + } + + if (deal.organization_ids && deal.organization_ids.length > 0) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + String(deal.organization_ids[0]), + 'affinity' + ); + + if (company_id) { + opts = { + ...opts, + company_id + } + } + } + + return { + remote_id: String(deal.id), + name: deal.name, + description: '', // Placeholder if there's no direct mapping + amount: 0, + field_mappings, + ...opts + }; + } +} diff --git a/packages/api/src/crm/deal/services/affinity/types.ts b/packages/api/src/crm/deal/services/affinity/types.ts new file mode 100644 index 000000000..ba0bd1bd6 --- /dev/null +++ b/packages/api/src/crm/deal/services/affinity/types.ts @@ -0,0 +1,26 @@ +interface AffinityDeal { + id: number + name: string + person_ids: number[] + organization_ids: number[] + list_entries: ListEntry[] +} + +interface ListEntry { + id: number + creator_id: number + list_id: number + entity_id: number + entity_type: number + created_at: string +} + +export type AffinityDealInput = { + name: string, + list_id: number, + person_ids?: number[], + organization_ids?: number[] +} + +export type AffinityDealOutput = Partial + diff --git a/packages/api/src/crm/note/note.module.ts b/packages/api/src/crm/note/note.module.ts index e06593223..a06e842bc 100644 --- a/packages/api/src/crm/note/note.module.ts +++ b/packages/api/src/crm/note/note.module.ts @@ -22,6 +22,8 @@ import { ZohoService } from './services/zoho'; import { ZohoNoteMapper } from './services/zoho/mappers'; import { SyncService } from './sync/sync.service'; import { SalesforceNoteMapper } from './services/salesforce/mappers'; +import { AffinityNoteMapper } from './services/affinity/mappers'; +import { AffinityService } from './services/affinity'; @Module({ controllers: [NoteController], providers: [ @@ -49,7 +51,9 @@ import { SalesforceNoteMapper } from './services/salesforce/mappers'; CloseNoteMapper, MicrosoftdynamicssalesService, MicrosoftdynamicssalesNoteMapper, + AffinityService, + AffinityNoteMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) -export class NoteModule {} +export class NoteModule { } diff --git a/packages/api/src/crm/note/services/affinity/index.ts b/packages/api/src/crm/note/services/affinity/index.ts new file mode 100644 index 000000000..e10b1e12e --- /dev/null +++ b/packages/api/src/crm/note/services/affinity/index.ts @@ -0,0 +1,93 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { INoteService } from '@crm/note/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { AffinityNoteInput, AffinityNoteOutput } from './types'; + +@Injectable() +export class AffinityService implements INoteService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.note.toUpperCase() + ':' + AffinityService.name, + ); + this.registry.registerService('affinity', this); + } + + async addNote( + noteData: AffinityNoteInput, + linkedUserId: string, + ): Promise> { + 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}/notes`, + JSON.stringify(noteData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp?.data, + message: 'Affinity note created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + 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}/notes`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced affinity notes !`); + return { + data: resp?.data, + message: 'Affinity notes retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/note/services/affinity/mappers.ts b/packages/api/src/crm/note/services/affinity/mappers.ts new file mode 100644 index 000000000..d36752c7f --- /dev/null +++ b/packages/api/src/crm/note/services/affinity/mappers.ts @@ -0,0 +1,185 @@ +import { AffinityNoteInput, AffinityNoteOutput } from './types'; +import { + UnifiedCrmNoteInput, + UnifiedCrmNoteOutput, +} from '@crm/note/types/model.unified'; +import { INoteMapper } from '@crm/note/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AffinityNoteMapper implements INoteMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'note', 'affinity', this); + } + async desunify( + source: UnifiedCrmNoteInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + let opts: any = {}; + + if (source.user_id) { + const creator_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (creator_id) { + opts = { + ...opts, + creator_id + } + } + } + + if (source.company_id) { + const organization_id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + if (organization_id) { + opts = { + ...opts, + organization_ids: [organization_id] + } + } + } + + if (source.contact_id) { + const person_id = await this.utils.getRemoteIdFromContactUuid(source.contact_id); + if (person_id) { + opts = { + ...opts, + person_ids: [person_id] + } + } + } + + if (source.deal_id) { + const opportunity_id = await this.utils.getRemoteIdFromDealUuid(source.deal_id); + if (opportunity_id) { + opts = { + ...opts, + opportunity_ids: [opportunity_id] + } + } + } + + const result: AffinityNoteInput = { + content: source.content, + ...opts + }; + + 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: AffinityNoteOutput | AffinityNoteOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleNoteToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + return Promise.all( + source.map((note) => + this.mapSingleNoteToUnified(note, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleNoteToUnified( + note: AffinityNoteOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = note[mapping.remote_id]; + } + } + + let opts: any = {}; + + if (note.creator_id) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(note.creator_id), + 'affinity' + ); + if (user_id) { + opts = { + ...opts, + user_id + } + } + } + + if (note.organization_ids && note.organization_ids.length > 0) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + String(note.organization_ids[0]), + 'affinity' + ); + if (company_id) { + opts = { + ...opts, + company_id + } + } + } + + if (note.person_ids && note.person_ids.length > 0) { + const contact_id = await this.utils.getContactUuidFromRemoteId( + String(note.person_ids[0]), + 'affinity' + ); + if (contact_id) { + opts = { + ...opts, + contact_id + } + } + } + + if (note.opportunity_ids && note.opportunity_ids.length > 0) { + const deal_id = await this.utils.getDealUuidFromRemoteId( + String(note.opportunity_ids[0]), + 'affinity' + ); + if (deal_id) { + opts = { + ...opts, + deal_id + } + } + } + + + + return { + remote_id: note.id, + content: note.content, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/note/services/affinity/types.ts b/packages/api/src/crm/note/services/affinity/types.ts new file mode 100644 index 000000000..1b5fdfb32 --- /dev/null +++ b/packages/api/src/crm/note/services/affinity/types.ts @@ -0,0 +1,33 @@ +interface AffinityNote { + id: number + creator_id: number + person_ids: number[] + associated_person_ids: number[] + interaction_person_ids: number[] + interaction_id: number + interaction_type: number + is_meeting: boolean + mentioned_person_ids: number[] + organization_ids: number[] + opportunity_ids: number[] + parent_id: any + content: string + type: number + created_at: string + updated_at: string +} + +export type AffinityNoteInput = { + content: string, + person_ids?: number[], + organization_ids?: number[], + opportunity_ids?: number[], + type?: number, + parent_id?: number, + creator_id?: number, + created_at?: string +} + +export type AffinityNoteOutput = Partial + + diff --git a/packages/api/src/crm/user/services/affinity/index.ts b/packages/api/src/crm/user/services/affinity/index.ts new file mode 100644 index 000000000..a150e6b92 --- /dev/null +++ b/packages/api/src/crm/user/services/affinity/index.ts @@ -0,0 +1,61 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { IUserService } from '@crm/user/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { AffinityUserOutput } from './types'; + +@Injectable() +export class AffinityService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.user.toUpperCase() + ':' + AffinityService.name, + ); + this.registry.registerService('affinity', this); + } + + async sync(data: SyncParam): Promise> { + 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}/auth/whoami`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + this.logger.log(`Synced close users !`); + + return { + data: resp?.data, + message: 'Affinity users retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/user/services/affinity/mappers.ts b/packages/api/src/crm/user/services/affinity/mappers.ts new file mode 100644 index 000000000..d9a3aaa43 --- /dev/null +++ b/packages/api/src/crm/user/services/affinity/mappers.ts @@ -0,0 +1,70 @@ +import { AffinityUserOutput } from './types'; +import { + UnifiedCrmUserInput, + UnifiedCrmUserOutput, +} from '@crm/user/types/model.unified'; +import { IUserMapper } from '@crm/user/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@crm/@lib/@utils'; + +@Injectable() +export class AffinityUserMapper implements IUserMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'user', 'affinity', this); + } + desunify( + source: UnifiedCrmUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ) { + return; + } + + unify( + source: AffinityUserOutput | AffinityUserOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleUserToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of AffinityUserOutput + return Promise.all( + source.map((user) => + this.mapSingleUserToUnified(user, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleUserToUnified( + user: AffinityUserOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = user[mapping.remote_id]; + } + } + return { + remote_id: String(user.user.id), + name: `${user.user?.firstName} ${user.user?.lastName}`, + email: user.user?.email, + field_mappings, + }; + } +} diff --git a/packages/api/src/crm/user/services/affinity/types.ts b/packages/api/src/crm/user/services/affinity/types.ts new file mode 100644 index 000000000..d5051de99 --- /dev/null +++ b/packages/api/src/crm/user/services/affinity/types.ts @@ -0,0 +1,27 @@ +interface AffinityUser { + tenant: Tenant + user: User + grant: Grant +} + +interface Tenant { + id: number + name: string + subdomain: string +} + +interface User { + id: number + firstName: string + lastName: string + email: string +} + +interface Grant { + type: string + scope: string + createdAt: string +} + +export type AffinityUserOutput = Partial; +export type AffinityUserInput = null; diff --git a/packages/api/src/crm/user/user.module.ts b/packages/api/src/crm/user/user.module.ts index 87972a2cf..56a7af5e4 100644 --- a/packages/api/src/crm/user/user.module.ts +++ b/packages/api/src/crm/user/user.module.ts @@ -1,7 +1,9 @@ +import { AffinityUserMapper } from './services/affinity/mappers'; +import { AffinityService } from './services/affinity'; + import { MicrosoftdynamicssalesUserMapper } from './services/microsoftdynamicssales/mappers'; import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { Utils } from '@crm/@lib/@utils'; @@ -51,6 +53,8 @@ import { SalesforceUserMapper } from './services/salesforce/mappers'; CloseUserMapper, SalesforceUserMapper, MicrosoftdynamicssalesUserMapper, + AffinityService, + AffinityUserMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/shared/src/connectors/enum.ts b/packages/shared/src/connectors/enum.ts index b15cc0453..0f96aed78 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -6,6 +6,7 @@ export enum CrmConnectors { ATTIO = 'attio', CLOSE = 'close', MICROSOFTDYNAMICSSALES = 'microsoftdynamicssales', + AFFINITY = 'affinity', } export enum EcommerceConnectors { diff --git a/packages/shared/src/connectors/index.ts b/packages/shared/src/connectors/index.ts index 92b7df549..b06c1f805 100644 --- a/packages/shared/src/connectors/index.ts +++ b/packages/shared/src/connectors/index.ts @@ -1,4 +1,4 @@ -export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio', 'close', 'microsoftdynamicssales']; +export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio', 'close', 'microsoftdynamicssales', 'affinity']; export const HRIS_PROVIDERS = []; export const ATS_PROVIDERS = ['ashby']; export const ACCOUNTING_PROVIDERS = [];