From 4eb60f1984930c6d3dfb6ee1f99f7a272b29b8ad Mon Sep 17 00:00:00 2001 From: nael Date: Mon, 27 Nov 2023 15:10:17 +0100 Subject: [PATCH] feat: big update custom fields --- .../dto/create-custom-field.dto.ts | 1 - .../field-mapping/field-mapping.controller.ts | 16 +++- .../field-mapping/field-mapping.service.ts | 36 ++++++++- .../utils/unification/crm/hubspot/index.ts | 24 +++++- .../crm/hubspot/mappers/contact.ts | 40 +++++++++- .../src/@core/utils/unification/crm/index.ts | 18 ++++- .../src/@core/utils/unification/desunify.ts | 12 ++- .../api/src/@core/utils/unification/unify.ts | 12 ++- .../api/src/crm/contact/contact.controller.ts | 5 ++ .../api/src/crm/contact/contact.module.ts | 2 + .../crm/contact/services/contact.service.ts | 75 ++++++++++++++++--- .../src/crm/contact/services/hubspot/index.ts | 26 +++++-- .../src/crm/contact/services/hubspot/types.ts | 12 +++ .../dashboard/components/main-nav.tsx | 8 +- 14 files changed, 246 insertions(+), 41 deletions(-) diff --git a/packages/api/src/@core/field-mapping/dto/create-custom-field.dto.ts b/packages/api/src/@core/field-mapping/dto/create-custom-field.dto.ts index d38c9fc9b..90fac2e15 100644 --- a/packages/api/src/@core/field-mapping/dto/create-custom-field.dto.ts +++ b/packages/api/src/@core/field-mapping/dto/create-custom-field.dto.ts @@ -14,5 +14,4 @@ export class MapFieldToProviderDto { source_custom_field_id: string; source_provider: string; linked_user_id: string; - data: any; } diff --git a/packages/api/src/@core/field-mapping/field-mapping.controller.ts b/packages/api/src/@core/field-mapping/field-mapping.controller.ts index 0025c622e..4a7b8e3c3 100644 --- a/packages/api/src/@core/field-mapping/field-mapping.controller.ts +++ b/packages/api/src/@core/field-mapping/field-mapping.controller.ts @@ -7,6 +7,9 @@ import { } from './dto/create-custom-field.dto'; import { StandardObject } from '../utils/types'; +interface StandardObjectDto { + standardObjectName: string; +} @Controller('field-mapping') export class FieldMappingController { constructor( @@ -17,17 +20,24 @@ export class FieldMappingController { } @Post('addObjectEntity') - addStandardObjectEntity(@Body() standardObjectName: string) { - return this.fieldMappingService.addStandardObjectEntity(standardObjectName); + addStandardObjectEntity(@Body() dto: StandardObjectDto) { + return this.fieldMappingService.addStandardObjectEntity( + dto.standardObjectName, + ); } @Get('getObjectEntity') - getStandardObjectEntity(@Query() standardObjectName: string) { + getStandardObjectEntity(@Query('object') standardObjectName: string) { return this.fieldMappingService.getEntityId( standardObjectName as StandardObject, ); } + @Get('entities') + getEntities() { + return this.fieldMappingService.getEntities(); + } + @Get('attribute') getAttributes() { return this.fieldMappingService.getAttributes(); diff --git a/packages/api/src/@core/field-mapping/field-mapping.service.ts b/packages/api/src/@core/field-mapping/field-mapping.service.ts index e5de26b61..bf5091770 100644 --- a/packages/api/src/@core/field-mapping/field-mapping.service.ts +++ b/packages/api/src/@core/field-mapping/field-mapping.service.ts @@ -23,6 +23,7 @@ export class FieldMappingService { ressource_owner_id: standardObjectName, }, }); + return entity; } async getAttributes() { @@ -33,8 +34,12 @@ export class FieldMappingService { return await this.prisma.value.findMany(); } + async getEntities() { + return await this.prisma.entity.findMany(); + } + // and then retrieve them by their name - async getEntityId(standardObject: StandardObject): Promise { + async getEntityId(standardObject: StandardObject) { const res = await this.prisma.entity.findFirst({ where: { ressource_owner_id: standardObject as string, @@ -43,9 +48,30 @@ export class FieldMappingService { return res.id_entity; } + async getCustomFieldMappings( + integrationId: string, + linkedUserId: string, + standard_object: string, + ) { + return await this.prisma.attribute.findMany({ + where: { + source: integrationId, + id_consumer: linkedUserId, + entity: { + ressource_owner_id: standard_object, + }, + }, + select: { + remote_id: true, + slug: true, + }, + }); + } + async defineTargetField(dto: DefineTargetFieldDto) { // Create a new attribute in your system representing the target field const id_entity = await this.getEntityId(dto.object_type_owner); + this.logger.log('id entity is ' + id_entity); const attribute = await this.prisma.attribute.create({ data: { id_attribute: uuidv4(), @@ -59,7 +85,7 @@ export class FieldMappingService { source: '', id_entity: id_entity, scope: 'user', // [user | org] wide - id_consumer: '', + id_consumer: '00000000-0000-0000-0000-000000000000', //default }, }); @@ -80,9 +106,11 @@ export class FieldMappingService { }, }); + return updatedAttribute; + //insert inside the table value - const valueInserted = await this.prisma.value.create({ + /*const valueInserted = await this.prisma.value.create({ data: { id_value: uuidv4(), data: dto.data, @@ -91,6 +119,6 @@ export class FieldMappingService { }, }); - return updatedAttribute; + return updatedAttribute;*/ } } diff --git a/packages/api/src/@core/utils/unification/crm/hubspot/index.ts b/packages/api/src/@core/utils/unification/crm/hubspot/index.ts index 0fbb9c452..99aa9a91b 100644 --- a/packages/api/src/@core/utils/unification/crm/hubspot/index.ts +++ b/packages/api/src/@core/utils/unification/crm/hubspot/index.ts @@ -6,13 +6,21 @@ import { UnifiedContactInput } from '@contact/types/model.unified'; export async function desunifyHubspot({ sourceObject, targetType_, + customFieldMappings, }: { sourceObject: T; targetType_: CrmObject; + customFieldMappings?: { + slug: string; + remote_id: string; + }[]; }): Promise { switch (targetType_) { case CrmObject.contact: - return mapToHubspotContact(sourceObject as UnifiedContactInput); + return mapToHubspotContact( + sourceObject as UnifiedContactInput, + customFieldMappings, + ); case CrmObject.deal: //return mapToHubspotDeal(sourceObject); case CrmObject.company: @@ -24,11 +32,23 @@ export async function desunifyHubspot({ } export async function unifyHubspot< T extends UnifySourceType | UnifySourceType[], ->({ sourceObject, targetType_ }: { sourceObject: T; targetType_: CrmObject }) { +>({ + sourceObject, + targetType_, + customFieldMappings, +}: { + sourceObject: T; + targetType_: CrmObject; + customFieldMappings?: { + slug: string; + remote_id: string; + }[]; +}) { switch (targetType_) { case CrmObject.contact: return mapToUnifiedContact( sourceObject as HubspotContactOutput | HubspotContactOutput[], + customFieldMappings, ); case CrmObject.deal: //return mapToHubspotDeal(sourceObject); diff --git a/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts b/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts index 182013225..0eb57d12e 100644 --- a/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts +++ b/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts @@ -6,33 +6,66 @@ import { export function mapToHubspotContact( source: UnifiedContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], ): HubspotContactInput { // Assuming 'email_addresses' array contains at least one email and 'phone_numbers' array contains at least one phone number const primaryEmail = source.email_addresses?.[0]?.email_address; const primaryPhone = source.phone_numbers?.[0]?.phone_number; - return { + const result: HubspotContactInput = { firstname: source.first_name, lastname: source.last_name, email: primaryEmail, phone: primaryPhone, }; + + if (customFieldMappings && source.field_mappings) { + for (const fieldMapping of source.field_mappings) { + for (const key in fieldMapping) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === key, + ); + if (mapping) { + result[mapping.remote_id] = fieldMapping[key]; + } + } + } + } + return result; } export function mapToUnifiedContact( source: HubspotContactOutput | HubspotContactOutput[], + customFieldMappings?: { + slug: string; + remote_id: string; + }[], ): UnifiedContactOutput | UnifiedContactOutput[] { if (!Array.isArray(source)) { return _mapSingleContact(source); } - + console.log('hhddhdhdhdh'); + console.log(JSON.stringify(customFieldMappings)); // Handling array of HubspotContactOutput - return source.map(_mapSingleContact); + return source.map((contact) => + _mapSingleContact(contact, customFieldMappings), + ); } function _mapSingleContact( contact: HubspotContactOutput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], ): UnifiedContactOutput { + //TODO: check for field mappings + const field_mappings = customFieldMappings.map((mapping) => ({ + [mapping.slug]: contact.properties[mapping.remote_id], + })); return { first_name: contact.properties.firstname, last_name: contact.properties.lastname, @@ -45,5 +78,6 @@ function _mapSingleContact( phone_numbers: [ { phone_number: '' /*contact.properties.*/, phone_type: 'primary' }, ], + field_mappings: field_mappings, }; } diff --git a/packages/api/src/@core/utils/unification/crm/index.ts b/packages/api/src/@core/utils/unification/crm/index.ts index 9e072a0d6..2b9f89675 100644 --- a/packages/api/src/@core/utils/unification/crm/index.ts +++ b/packages/api/src/@core/utils/unification/crm/index.ts @@ -15,14 +15,23 @@ export async function desunifyCrm({ sourceObject, targetType_, providerName, + customFieldMappings, }: { sourceObject: T; targetType_: CrmObject; providerName: string; + customFieldMappings?: { + slug: string; + remote_id: string; + }[]; }): Promise { switch (providerName) { case 'hubspot': - return desunifyHubspot({ sourceObject, targetType_ }); + return desunifyHubspot({ + sourceObject, + targetType_, + customFieldMappings, + }); case 'pipedrive': return desunifyPipedrive({ sourceObject, targetType_ }); case 'zoho': @@ -39,14 +48,19 @@ export async function unifyCrm({ sourceObject, targetType_, providerName, + customFieldMappings, }: { sourceObject: T; targetType_: CrmObject; providerName: string; + customFieldMappings?: { + slug: string; + remote_id: string; + }[]; }): Promise { switch (providerName) { case 'hubspot': - return unifyHubspot({ sourceObject, targetType_ }); + return unifyHubspot({ sourceObject, targetType_, customFieldMappings }); case 'pipedrive': return unifyPipedrive({ sourceObject, targetType_ }); case 'zoho': diff --git a/packages/api/src/@core/utils/unification/desunify.ts b/packages/api/src/@core/utils/unification/desunify.ts index cf55b1b40..9d50ab028 100644 --- a/packages/api/src/@core/utils/unification/desunify.ts +++ b/packages/api/src/@core/utils/unification/desunify.ts @@ -13,15 +13,25 @@ export async function desunify({ sourceObject, targetType, providerName, + customFieldMappings, }: { sourceObject: T; targetType: TargetObject; providerName: string; + customFieldMappings?: { + slug: string; + remote_id: string; + }[]; }): Promise { switch (getProviderVertical(providerName)) { case ProviderVertical.CRM: const targetType_ = targetType as CrmObject; - return desunifyCrm({ sourceObject, targetType_, providerName }); + return desunifyCrm({ + sourceObject, + targetType_, + providerName, + customFieldMappings, + }); case ProviderVertical.ATS: break; case ProviderVertical.Accounting: diff --git a/packages/api/src/@core/utils/unification/unify.ts b/packages/api/src/@core/utils/unification/unify.ts index 4e2105ff2..984efa099 100644 --- a/packages/api/src/@core/utils/unification/unify.ts +++ b/packages/api/src/@core/utils/unification/unify.ts @@ -13,16 +13,26 @@ export async function unify({ sourceObject, targetType, providerName, + customFieldMappings, }: { sourceObject: T; targetType: TargetObject; providerName: string; + customFieldMappings: { + slug: string; + remote_id: string; + }[]; }): Promise { if (sourceObject == null) return []; switch (getProviderVertical(providerName)) { case ProviderVertical.CRM: const targetType_ = targetType as CrmObject; - return unifyCrm({ sourceObject, targetType_, providerName }); + return unifyCrm({ + sourceObject, + targetType_, + providerName, + customFieldMappings, + }); case ProviderVertical.ATS: break; case ProviderVertical.Accounting: diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index f9da53fbd..8fe20907e 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -12,6 +12,11 @@ export class ContactController { this.logger.setContext(ContactController.name); } + @Get('properties') + getCustomProperties(@Query('linkedUserId') linkedUserId: string) { + return this.contactService.getCustomProperties(linkedUserId); + } + @Get() getContacts( @Query('integrationId') integrationId: string, diff --git a/packages/api/src/crm/contact/contact.module.ts b/packages/api/src/crm/contact/contact.module.ts index 7fe75a97e..24711ef0e 100644 --- a/packages/api/src/crm/contact/contact.module.ts +++ b/packages/api/src/crm/contact/contact.module.ts @@ -8,6 +8,7 @@ import { ZohoService } from './services/zoho'; import { PipedriveService } from './services/pipedrive'; import { HubspotService } from './services/hubspot'; import { LoggerService } from '@@core/logger/logger.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @Module({ controllers: [ContactController], @@ -20,6 +21,7 @@ import { LoggerService } from '@@core/logger/logger.service'; PipedriveService, HubspotService, LoggerService, + FieldMappingService, ], }) export class ContactModule {} diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index b4c6cba7f..cd80349b5 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -23,6 +23,10 @@ import { UnifiedContactOutput, } from '@contact/types/model.unified'; import { OriginalContactOutput } from '@@core/utils/types'; +import { ActionType, handleServiceError } from '@@core/utils/errors'; +import { decrypt } from '@@core/utils/crypto'; +import axios from 'axios'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; @Injectable() export class ContactService { @@ -34,6 +38,7 @@ export class ContactService { private zendesk: ZendeskService, private pipedrive: PipedriveService, private logger: LoggerService, + private fieldMappingService: FieldMappingService, ) { this.logger.setContext(ContactService.name); } @@ -108,6 +113,38 @@ export class ContactService { /* */ + async getCustomProperties(linkedUserId: string) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + }, + }); + const resp = await axios.get( + `https://api.hubapi.com/properties/v1/contacts/properties`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${decrypt(connection.access_token)}`, + }, + }, + ); + return { + data: resp.data, + message: 'Hubspot contact properties retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + CrmObject.contact, + ActionType.GET, + ); + } + } + async addContact( unifiedContactData: UnifiedContactInput, integrationId: string, @@ -122,6 +159,7 @@ export class ContactService { }, }); const job_id = job_resp_create.id_job; + //TODO: add field mappings data too await this.addContactToDb(unifiedContactData, job_id); const job_resp_update = await this.prisma.jobs.update({ where: { @@ -132,28 +170,25 @@ export class ContactService { }, }); - // TODO: check if for contact object and provider there is a field mapping // Retrieve custom field mappings - /*const customFieldMappings = + // get potential fieldMappings and extract the original properties name + const customFieldMappings = await this.fieldMappingService.getCustomFieldMappings( integrationId, linkedUserId, - );*/ - + 'contact', + ); let resp: ApiResponse; //desunify the data according to the target obj wanted const desunifiedObject = await desunify({ sourceObject: unifiedContactData, targetType: CrmObject.contact, providerName: integrationId, + customFieldMappings: unifiedContactData.field_mappings + ? customFieldMappings + : [], }); - //TODO - /*desunifiedObject = this.applyCustomFieldMappings( - desunifiedObject, - customFieldMappings, - );*/ - switch (integrationId) { case 'freshsales': resp = await this.freshsales.addContact( @@ -193,12 +228,12 @@ export class ContactService { default: break; } - //unify the data according to the target obj wanted const unifiedObject = (await unify({ sourceObject: [resp.data], targetType: CrmObject.contact, providerName: integrationId, + customFieldMappings: customFieldMappings, })) as UnifiedContactOutput[]; let res: ContactResponse = { @@ -223,6 +258,7 @@ export class ContactService { return { ...resp, data: res }; } + //TODO: insert data in the db (would be used as data lake and webooks syncs later) async getContacts( integrationId: string, linkedUserId: string, @@ -237,6 +273,17 @@ export class ContactService { }); const job_id = job_resp_create.id_job; + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'contact', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + let resp: ApiResponse; switch (integrationId) { case 'freshsales': @@ -252,7 +299,7 @@ export class ContactService { break; case 'hubspot': - resp = await this.hubspot.getContacts(linkedUserId); + resp = await this.hubspot.getContacts(linkedUserId, remoteProperties); break; case 'pipedrive': @@ -262,12 +309,16 @@ export class ContactService { default: break; } + + //TODO: insert the data in the DB with the fieldMappings (value table) + const sourceObject: OriginalContactOutput[] = resp.data; //unify the data according to the target obj wanted const unifiedObject = (await unify({ sourceObject, targetType: CrmObject.contact, providerName: integrationId, + customFieldMappings, })) as UnifiedContactOutput[]; let res: ContactResponse = { diff --git a/packages/api/src/crm/contact/services/hubspot/index.ts b/packages/api/src/crm/contact/services/hubspot/index.ts index c880a2178..505b92077 100644 --- a/packages/api/src/crm/contact/services/hubspot/index.ts +++ b/packages/api/src/crm/contact/services/hubspot/index.ts @@ -4,6 +4,7 @@ import { CrmObject, HubspotContactInput, HubspotContactOutput, + commonHubspotProperties, } from 'src/crm/@types'; import axios from 'axios'; import { PrismaService } from '@@core/prisma/prisma.service'; @@ -60,6 +61,7 @@ export class HubspotService { } async getContacts( linkedUserId: string, + custom_properties?: string[], ): Promise> { try { //TODO: check required scope => crm.objects.contacts.READ @@ -68,15 +70,23 @@ export class HubspotService { id_linked_user: linkedUserId, }, }); - const resp = await axios.get( - `https://api.hubapi.com/crm/v3/objects/contacts/`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${decrypt(connection.access_token)}`, - }, + + const commonPropertyNames = Object.keys(commonHubspotProperties); + const allProperties = [...commonPropertyNames, ...custom_properties]; + const baseURL = 'https://api.hubapi.com/crm/v3/objects/contacts/'; + + const queryString = allProperties + .map((prop) => `properties=${encodeURIComponent(prop)}`) + .join('&'); + + const url = `${baseURL}?${queryString}`; + + const resp = await axios.get(url, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${decrypt(connection.access_token)}`, }, - ); + }); return { data: resp.data.results, message: 'Hubspot contacts retrieved', diff --git a/packages/api/src/crm/contact/services/hubspot/types.ts b/packages/api/src/crm/contact/services/hubspot/types.ts index 1468ce0b5..7e9de18ef 100644 --- a/packages/api/src/crm/contact/services/hubspot/types.ts +++ b/packages/api/src/crm/contact/services/hubspot/types.ts @@ -13,6 +13,7 @@ export interface HubspotContactInput { associatedcompanyid?: string; fax?: string; jobtitle?: string; + [key: string]: any; } type HubspotPropertiesOuput = { @@ -22,6 +23,17 @@ type HubspotPropertiesOuput = { hs_object_id: string; lastmodifieddate: string; lastname: string; + [key: string]: string; +}; + +export const commonHubspotProperties = { + createdate: '', + email: '', + firstname: '', + hs_object_id: '', + lastmodifieddate: '', + lastname: '', + // Add any other common properties here }; export interface HubspotContactOutput { id: string; diff --git a/packages/webapp/src/components/dashboard/components/main-nav.tsx b/packages/webapp/src/components/dashboard/components/main-nav.tsx index e31560802..b56e1ae08 100644 --- a/packages/webapp/src/components/dashboard/components/main-nav.tsx +++ b/packages/webapp/src/components/dashboard/components/main-nav.tsx @@ -13,25 +13,25 @@ export function MainNav({ href="/examples/dashboard" className="text-sm font-medium transition-colors hover:text-primary" > - Overview + Dashboard - Customers + Linked Accounts - Products + Configuration - Settings + Docs )