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 9cf5a0449..5b4fe47b7 100644 --- a/packages/api/src/@core/utils/types/original/original.crm.ts +++ b/packages/api/src/@core/utils/types/original/original.crm.ts @@ -16,8 +16,6 @@ import { ZohoCompanyOutput, FreshsalesEngagementInput, FreshsalesEngagementOutput, - FreshsalesLeadInput, - FreshsalesLeadOutput, FreshsalesNoteInput, FreshsalesNoteOutput, FreshsalesStageInput, @@ -27,32 +25,24 @@ import { HubspotDealOutput, HubspotEngagementInput, HubspotEngagementOutput, - HubspotLeadInput, - HubspotLeadOutput, HubspotStageInput, HubspotStageOutput, HubspotTaskInput, HubspotTaskOutput, PipedriveEngagementInput, PipedriveEngagementOutput, - PipedriveLeadInput, - PipedriveLeadOutput, PipedriveStageInput, PipedriveStageOutput, PipedriveTaskInput, PipedriveTaskOutput, ZendeskEngagementInput, ZendeskEngagementOutput, - ZendeskLeadInput, - ZendeskLeadOutput, ZendeskStageInput, ZendeskStageOutput, ZendeskTaskInput, ZendeskTaskOutput, ZohoEngagementInput, ZohoEngagementOutput, - ZohoLeadInput, - ZohoLeadOutput, ZohoNoteInput, ZohoNoteOutput, ZohoStageInput, @@ -145,13 +135,7 @@ export type OriginalStageInput = | ZendeskStageInput | PipedriveStageInput; -/* lead */ -export type OriginalLeadInput = - | FreshsalesLeadInput - | HubspotLeadInput - | ZohoLeadInput - | ZendeskLeadInput - | PipedriveLeadInput; +/* engagementType */ /* user */ export type OriginalUserInput = @@ -169,7 +153,6 @@ export type CrmObjectInput = | OriginalNoteInput | OriginalTaskInput | OriginalStageInput - | OriginalLeadInput | OriginalUserInput; /* OUTPUT */ @@ -229,13 +212,7 @@ export type OriginalStageOutput = | ZendeskStageOutput | PipedriveStageOutput; -/* lead */ -export type OriginalLeadOutput = - | FreshsalesLeadOutput - | HubspotLeadOutput - | ZohoLeadOutput - | ZendeskLeadOutput - | PipedriveLeadOutput; +/* engagementType */ /* user */ export type OriginalUserOutput = @@ -253,5 +230,4 @@ export type CrmObjectOutput = | OriginalNoteOutput | OriginalTaskOutput | OriginalStageOutput - | OriginalLeadOutput | OriginalUserOutput; diff --git a/packages/api/src/@core/utils/types/unfify.output.ts b/packages/api/src/@core/utils/types/unify.output.ts similarity index 100% rename from packages/api/src/@core/utils/types/unfify.output.ts rename to packages/api/src/@core/utils/types/unify.output.ts diff --git a/packages/api/src/@core/utils/unification/unify.ts b/packages/api/src/@core/utils/unification/unify.ts index 2a0576be4..4a1ba53c7 100644 --- a/packages/api/src/@core/utils/unification/unify.ts +++ b/packages/api/src/@core/utils/unification/unify.ts @@ -8,7 +8,7 @@ import { import { unifyCrm } from '@crm/@utils/@unification'; import { TicketingObject } from '@ticketing/@utils/@types'; import { unifyTicketing } from '@ticketing/@utils/@unification'; -import { UnifySourceType } from '../types/unfify.output'; +import { UnifySourceType } from '../types/unify.output'; /* to fetch data diff --git a/packages/api/src/crm/@utils/@types/index.ts b/packages/api/src/crm/@utils/@types/index.ts index 30f5b9d02..474c7d766 100644 --- a/packages/api/src/crm/@utils/@types/index.ts +++ b/packages/api/src/crm/@utils/@types/index.ts @@ -22,12 +22,7 @@ import { UnifiedEngagementInput, UnifiedEngagementOutput, } from '@crm/engagement/types/model.unified'; -import { ILeadService } from '@crm/lead/types'; -import { leadUnificationMapping } from '@crm/lead/types/mappingsTypes'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; + import { INoteService } from '@crm/note/types'; import { noteUnificationMapping } from '@crm/note/types/mappingsTypes'; import { @@ -62,6 +57,7 @@ export enum CrmObject { note = 'note', task = 'task', engagement = 'engagement', + engagementType = 'engagementType', stage = 'stage', user = 'user', } @@ -75,8 +71,6 @@ export type UnifiedCrm = | UnifiedDealOutput | UnifiedEngagementInput | UnifiedEngagementOutput - | UnifiedLeadInput - | UnifiedLeadOutput | UnifiedNoteInput | UnifiedNoteOutput | UnifiedStageInput @@ -91,7 +85,6 @@ export const unificationMapping = { [CrmObject.deal]: dealUnificationMapping, [CrmObject.company]: companyUnificationMapping, [CrmObject.engagement]: engagementUnificationMapping, - [CrmObject.lead]: leadUnificationMapping, [CrmObject.note]: noteUnificationMapping, [CrmObject.stage]: stageUnificationMapping, [CrmObject.task]: taskUnificationMapping, @@ -104,7 +97,6 @@ export type ICrmService = | IEngagementService | INoteService | IDealService - | ILeadService | ITaskService | IStageService | ICompanyService; @@ -144,13 +136,6 @@ export * from '../../deal/services/hubspot/types'; export * from '../../deal/services/zoho/types'; export * from '../../deal/services/pipedrive/types'; -/* lead */ -export * from '../../lead/services/freshsales/types'; -export * from '../../lead/services/zendesk/types'; -export * from '../../lead/services/hubspot/types'; -export * from '../../lead/services/zoho/types'; -export * from '../../lead/services/pipedrive/types'; - /* task */ export * from '../../task/services/freshsales/types'; export * from '../../task/services/zendesk/types'; @@ -172,6 +157,8 @@ export * from '../../company/services/hubspot/types'; export * from '../../company/services/zoho/types'; export * from '../../company/services/pipedrive/types'; +/* engagementType */ + export class Email { @ApiProperty({ description: 'The email address', @@ -237,12 +224,12 @@ export class Address { @ApiProperty({ description: 'The address type', }) - address_type: string; + address_type?: string; @ApiProperty({ description: 'The owner type of the address', }) - owner_type: string; + owner_type?: string; } export type NormalizedContactInfo = { diff --git a/packages/api/src/crm/@utils/@unification/index.ts b/packages/api/src/crm/@utils/@unification/index.ts index ff7a4366e..8e744892b 100644 --- a/packages/api/src/crm/@utils/@unification/index.ts +++ b/packages/api/src/crm/@utils/@unification/index.ts @@ -1,6 +1,6 @@ import { CrmObject, unificationMapping } from '@crm/@utils/@types'; import { Unified, UnifyReturnType } from '@@core/utils/types'; -import { UnifySourceType } from '@@core/utils/types/unfify.output'; +import { UnifySourceType } from '@@core/utils/types/unify.output'; import { CrmObjectInput } from '@@core/utils/types/original/original.crm'; export async function desunifyCrm({ diff --git a/packages/api/src/crm/company/sync/sync.service.ts b/packages/api/src/crm/company/sync/sync.service.ts index 662a9a9a7..7ac7e3917 100644 --- a/packages/api/src/crm/company/sync/sync.service.ts +++ b/packages/api/src/crm/company/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,10 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedCompanyOutput } from '../types/model.unified'; import { ICompanyService } from '../types'; +import { OriginalCompanyOutput } from '@@core/utils/types/original/original.crm'; +import { crm_companies as CrmCompany } from '@prisma/client'; +import { normalizeEmailsAndNumbers } from '@crm/contact/utils'; +import { normalizeAddresses } from '../utils'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +30,361 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncCompanies(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_companies table + //its role is to fetch all companies from providers 3rd parties and save the info inside our db + async syncCompanies() { + try { + this.logger.log(`Syncing companies....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncCompaniesForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncCompaniesForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} companies for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping companies syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'company', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: ICompanyService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncCompanies(linkedUserId, remoteProperties); + + const sourceObject: OriginalCompanyOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.company, + providerName: integrationId, + customFieldMappings, + })) as UnifiedCompanyOutput[]; + + //TODO + const companyIds = sourceObject.map((company) => + 'id' in company ? String(company.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const companies_data = await this.saveCompanysInDb( + linkedUserId, + unifiedObject, + companyIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.company.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + companies_data, + 'crm.company.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveCompanysInDb( + linkedUserId: string, + companies: UnifiedCompanyOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let companies_results: CrmCompany[] = []; + for (let i = 0; i < companies.length; i++) { + const company = companies[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingCompany = await this.prisma.crm_companies.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + include: { + crm_email_addresses: true, + crm_phone_numbers: true, + crm_addresses: true, + }, + }); + + const { normalizedEmails, normalizedPhones } = + normalizeEmailsAndNumbers( + company.email_addresses, + company.phone_numbers, + ); + + const normalizedAddresses = normalizeAddresses(company.addresses); + + let unique_crm_company_id: string; + + if (existingCompany) { + // Update the existing company + let data: any = { + modified_at: new Date(), + crm_email_addresses: { + update: normalizedEmails.map((email, index) => ({ + where: { + id_crm_email: + existingCompany.crm_email_addresses[index].id_crm_email, + }, + data: email, + })), + }, + crm_phone_numbers: { + update: normalizedPhones.map((phone, index) => ({ + where: { + id_crm_phone_number: + existingCompany.crm_phone_numbers[index] + .id_crm_phone_number, + }, + data: phone, + })), + }, + crm_addresses: { + update: normalizedAddresses.map((addy, index) => ({ + where: { + id_crm_address: + existingCompany.crm_addresses[index].id_crm_address, + }, + data: addy, + })), + }, + }; + if (company.name) { + data = { ...data, name: company.name }; + } + if (company.industry) { + data = { ...data, industry: company.industry }; + } + if (company.number_of_employees) { + data = { + ...data, + number_of_employees: company.number_of_employees, + }; + } + if (company.user_id) { + data = { ...data, id_crm_user: company.user_id }; + } + + const res = await this.prisma.crm_companies.update({ + where: { + id_crm_company: existingCompany.id_crm_company, + }, + data: data, + }); + unique_crm_company_id = res.id_crm_company; + companies_results = [...companies_results, res]; + } else { + // Create a new company + this.logger.log('company not exists'); + const uuid = uuidv4(); + let data: any = { + id_crm_company: uuid, + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + + if (company.name) { + data = { ...data, name: company.name }; + } + if (company.industry) { + data = { ...data, industry: company.industry }; + } + if (company.number_of_employees) { + data = { + ...data, + number_of_employees: company.number_of_employees, + }; + } + if (company.user_id) { + data = { ...data, id_crm_user: company.user_id }; + } + + if (normalizedEmails) { + data['crm_email_addresses'] = { + create: { ...normalizedEmails, id_crm_company: uuid }, + }; + } + + if (normalizedPhones) { + data['crm_phone_numbers'] = { + create: { ...normalizedPhones, id_crm_company: uuid }, + }; + } + + if (normalizedAddresses) { + data['crm_addresses'] = { + create: { ...normalizeAddresses, id_crm_company: uuid }, + }; + } + + const res = await this.prisma.crm_companies.create({ + data: data, + }); + unique_crm_company_id = res.id_crm_company; + companies_results = [...companies_results, res]; + } + + // check duplicate or existing values + if (company.field_mappings && company.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_company_id, + }, + }); + + for (const mapping of company.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_company_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_company_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return companies_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/contact/services/freshsales/mappers.ts b/packages/api/src/crm/contact/services/freshsales/mappers.ts index 6f0aed394..6d7594211 100644 --- a/packages/api/src/crm/contact/services/freshsales/mappers.ts +++ b/packages/api/src/crm/contact/services/freshsales/mappers.ts @@ -4,6 +4,7 @@ import { UnifiedContactOutput, } from '@crm/contact/types/model.unified'; import { + Address, FreshsalesContactInput, FreshsalesContactOutput, } from '@crm/@utils/@types'; @@ -56,12 +57,20 @@ export class FreshsalesContactMapper implements IContactMapper { phone_type: 'mobile', }); } + const address: Address = { + street_1: contact.address, + city: contact.city, + state: contact.state, + postal_code: contact.zipcode, + country: contact.country, + }; return { first_name: contact.first_name, last_name: contact.last_name, email_addresses, phone_numbers, + addresses: [address], }; } } diff --git a/packages/api/src/crm/contact/services/hubspot/mappers.ts b/packages/api/src/crm/contact/services/hubspot/mappers.ts index c42678ce1..42c71a5f4 100644 --- a/packages/api/src/crm/contact/services/hubspot/mappers.ts +++ b/packages/api/src/crm/contact/services/hubspot/mappers.ts @@ -1,4 +1,8 @@ -import { HubspotContactInput, HubspotContactOutput } from '@crm/@utils/@types'; +import { + Address, + HubspotContactInput, + HubspotContactOutput, +} from '@crm/@utils/@types'; import { UnifiedContactInput, UnifiedContactOutput, @@ -65,6 +69,14 @@ export class HubspotContactMapper implements IContactMapper { const field_mappings = customFieldMappings.map((mapping) => ({ [mapping.slug]: contact.properties[mapping.remote_id], })); + const address: Address = { + street_1: '', + city: '', + state: '', + postal_code: '', + country: '', + }; + return { first_name: contact.properties.firstname, last_name: contact.properties.lastname, @@ -78,6 +90,7 @@ export class HubspotContactMapper implements IContactMapper { { phone_number: '' /*contact.properties.*/, phone_type: 'primary' }, ], field_mappings, + addresses: [address], }; } } diff --git a/packages/api/src/crm/contact/services/pipedrive/mappers.ts b/packages/api/src/crm/contact/services/pipedrive/mappers.ts index 2dfdce0ba..9eb10ebae 100644 --- a/packages/api/src/crm/contact/services/pipedrive/mappers.ts +++ b/packages/api/src/crm/contact/services/pipedrive/mappers.ts @@ -1,4 +1,5 @@ import { + Address, PipedriveContactInput, PipedriveContactOutput, } from '@crm/@utils/@types'; @@ -74,6 +75,14 @@ export class PipedriveContactMapper implements IContactMapper { const field_mappings = customFieldMappings.map((mapping) => ({ [mapping.slug]: contact[mapping.remote_id], })); + const address: Address = { + street_1: '', + city: '', + state: '', + postal_code: '', + country: '', + }; + return { first_name: contact.first_name, last_name: contact.last_name, @@ -86,6 +95,7 @@ export class PipedriveContactMapper implements IContactMapper { phone_type: p.label ? p.label : '', })), // Map each phone number, field_mappings, + addresses: [address], }; } } diff --git a/packages/api/src/crm/contact/services/zendesk/mappers.ts b/packages/api/src/crm/contact/services/zendesk/mappers.ts index 728657a3c..9aeef2099 100644 --- a/packages/api/src/crm/contact/services/zendesk/mappers.ts +++ b/packages/api/src/crm/contact/services/zendesk/mappers.ts @@ -1,4 +1,8 @@ -import { ZendeskContactInput, ZendeskContactOutput } from '@crm/@utils/@types'; +import { + Address, + ZendeskContactInput, + ZendeskContactOutput, +} from '@crm/@utils/@types'; import { UnifiedContactInput, UnifiedContactOutput, @@ -23,6 +27,13 @@ export class ZendeskContactMapper implements IContactMapper { last_name: source.last_name, email: primaryEmail, phone: primaryPhone, + address: { + line1: source.addresses[0].street_1, + city: source.addresses[0].city, + state: source.addresses[0].state, + postal_code: source.addresses[0].postal_code, + country: source.addresses[0].country, + }, }; if (customFieldMappings && source.field_mappings) { @@ -84,12 +95,21 @@ export class ZendeskContactMapper implements IContactMapper { }); } + const address: Address = { + street_1: contact.address.line1, + city: contact.address.city, + state: contact.address.state, + postal_code: contact.address.postal_code, + country: contact.address.country, + }; + return { first_name: contact.first_name, last_name: contact.last_name, email_addresses, phone_numbers, field_mappings, + addresses: [address], }; } } diff --git a/packages/api/src/crm/contact/services/zoho/mappers.ts b/packages/api/src/crm/contact/services/zoho/mappers.ts index afbd8d34b..389b5d67c 100644 --- a/packages/api/src/crm/contact/services/zoho/mappers.ts +++ b/packages/api/src/crm/contact/services/zoho/mappers.ts @@ -1,4 +1,8 @@ -import { ZohoContactInput, ZohoContactOutput } from '@crm/@utils/@types'; +import { + Address, + ZohoContactInput, + ZohoContactOutput, +} from '@crm/@utils/@types'; import { UnifiedContactInput, UnifiedContactOutput, @@ -22,6 +26,11 @@ export class ZohoContactMapper implements IContactMapper { Last_Name: source.last_name, Email: primaryEmail, Phone: primaryPhone, + Mailing_Street: source.addresses[0].street_1, + Mailing_City: source.addresses[0].city, + Mailing_State: source.addresses[0].state, + Mailing_Zip: source.addresses[0].postal_code, + Mailing_Country: source.addresses[0].country, }; if (customFieldMappings && source.field_mappings) { @@ -100,12 +109,21 @@ export class ZohoContactMapper implements IContactMapper { }); } + const address: Address = { + street_1: contact.Mailing_Street, + city: contact.Mailing_City, + state: contact.Mailing_State, + postal_code: contact.Mailing_Zip, + country: contact.Mailing_Country, + }; + return { first_name: contact.First_Name ? contact.First_Name : '', last_name: contact.Last_Name ? contact.Last_Name : '', email_addresses, phone_numbers, field_mappings, + addresses: [address], }; } } diff --git a/packages/api/src/crm/contact/sync/sync.service.ts b/packages/api/src/crm/contact/sync/sync.service.ts index 8bc4eefe3..db83141e3 100644 --- a/packages/api/src/crm/contact/sync/sync.service.ts +++ b/packages/api/src/crm/contact/sync/sync.service.ts @@ -15,6 +15,7 @@ import { crm_contacts as CrmContact } from '@prisma/client'; import { IContactService } from '../types'; import { OriginalContactOutput } from '@@core/utils/types/original/original.crm'; import { ServiceRegistry } from '../services/registry.service'; +import { normalizeAddresses } from '@crm/company/utils'; @Injectable() export class SyncContactsService implements OnModuleInit { @@ -85,6 +86,96 @@ export class SyncContactsService implements OnModuleInit { } } + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncContactsForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} contacts for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping contacts syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // 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, + ); + + const service: IContactService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncContacts(linkedUserId, remoteProperties); + + const sourceObject: OriginalContactOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.contact, + providerName: integrationId, + customFieldMappings, + })) as UnifiedContactOutput[]; + + //TODO + const contactIds = sourceObject.map((contact) => + 'id' in contact + ? String(contact.id) + : 'contact_id' in contact + ? String(contact.contact_id) + : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const contacts_data = await this.saveContactsInDb( + linkedUserId, + unifiedObject, + contactIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.contact.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + contacts_data, + 'crm.contact.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + async saveContactsInDb( linkedUserId: string, contacts: UnifiedContactOutput[], @@ -108,7 +199,11 @@ export class SyncContactsService implements OnModuleInit { remote_platform: originSource, id_linked_user: linkedUserId, }, - include: { crm_email_addresses: true, crm_phone_numbers: true }, + include: { + crm_email_addresses: true, + crm_phone_numbers: true, + crm_addresses: true, + }, }); const { normalizedEmails, normalizedPhones } = @@ -117,48 +212,72 @@ export class SyncContactsService implements OnModuleInit { contact.phone_numbers, ); + const normalizedAddresses = normalizeAddresses(contact.addresses); + let unique_crm_contact_id: string; if (existingContact) { // Update the existing contact + let data: any = { + modified_at: new Date(), + crm_email_addresses: { + update: normalizedEmails.map((email, index) => ({ + where: { + id_crm_email: + existingContact.crm_email_addresses[index].id_crm_email, + }, + data: email, + })), + }, + crm_phone_numbers: { + update: normalizedPhones.map((phone, index) => ({ + where: { + id_crm_phone_number: + existingContact.crm_phone_numbers[index] + .id_crm_phone_number, + }, + data: phone, + })), + }, + crm_addresses: { + update: normalizedAddresses.map((addy, index) => ({ + where: { + id_crm_address: + existingContact.crm_addresses[index].id_crm_address, + }, + data: addy, + })), + }, + }; + + if (contact.first_name) { + data = { ...data, first_name: contact.first_name }; + } + if (contact.last_name) { + data = { ...data, last_name: contact.last_name }; + } + if (contact.user_id) { + data = { + ...data, + id_crm_user: contact.user_id, + }; + } + const res = await this.prisma.crm_contacts.update({ where: { id_crm_contact: existingContact.id_crm_contact, }, - data: { - first_name: contact.first_name ? contact.first_name : '', - last_name: contact.last_name ? contact.last_name : '', - modified_at: new Date(), - crm_email_addresses: { - update: normalizedEmails.map((email, index) => ({ - where: { - id_crm_email: - existingContact.crm_email_addresses[index].id_crm_email, - }, - data: email, - })), - }, - crm_phone_numbers: { - update: normalizedPhones.map((phone, index) => ({ - where: { - id_crm_phone_number: - existingContact.crm_phone_numbers[index] - .id_crm_phone_number, - }, - data: phone, - })), - }, - }, + data: data, }); - contacts_results = [...contacts_results, res]; + unique_crm_contact_id = res.id_crm_contact; + contacts_results = [...contacts_results, res]; } else { // Create a new contact this.logger.log('not existing contact ' + contact.first_name); - const data = { - id_crm_contact: uuidv4(), - first_name: contact.first_name ? contact.first_name : '', - last_name: contact.last_name ? contact.last_name : '', + const uuid = uuidv4(); + let data: any = { + id_crm_contact: uuid, created_at: new Date(), modified_at: new Date(), id_linked_user: linkedUserId, @@ -166,24 +285,42 @@ export class SyncContactsService implements OnModuleInit { remote_platform: originSource, }; + if (contact.first_name) { + data = { ...data, first_name: contact.first_name }; + } + if (contact.last_name) { + data = { ...data, last_name: contact.last_name }; + } + if (contact.user_id) { + data = { + ...data, + id_crm_user: contact.user_id, + }; + } + if (normalizedEmails) { data['crm_email_addresses'] = { - create: normalizedEmails, + create: { ...normalizedEmails, id_crm_contact: uuid }, }; } if (normalizedPhones) { data['crm_phone_numbers'] = { - create: normalizedPhones, + create: { ...normalizedPhones, id_crm_contact: uuid }, + }; + } + if (normalizedAddresses) { + data['crm_addresses'] = { + create: { ...normalizeAddresses, id_crm_contact: uuid }, }; } + const res = await this.prisma.crm_contacts.create({ data: data, }); - contacts_results = [...contacts_results, res]; unique_crm_contact_id = res.id_crm_contact; + contacts_results = [...contacts_results, res]; } - // check duplicate or existing values if (contact.field_mappings && contact.field_mappings.length > 0) { const entity = await this.prisma.entity.create({ @@ -248,94 +385,4 @@ export class SyncContactsService implements OnModuleInit { handleServiceError(error, this.logger); } } - - //todo: HANDLE DATA REMOVED FROM PROVIDER - async syncContactsForLinkedUser( - integrationId: string, - linkedUserId: string, - id_project: string, - ) { - try { - this.logger.log( - `Syncing ${integrationId} contacts for linkedUser ${linkedUserId}`, - ); - // check if linkedUser has a connection if not just stop sync - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: integrationId, - }, - }); - if (!connection) { - this.logger.warn( - `Skipping contacts syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, - ); - return; - } - // 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, - ); - - const service: IContactService = - this.serviceRegistry.getService(integrationId); - const resp: ApiResponse = - await service.syncContacts(linkedUserId, remoteProperties); - - const sourceObject: OriginalContactOutput[] = resp.data; - //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); - //unify the data according to the target obj wanted - const unifiedObject = (await unify({ - sourceObject, - targetType: CrmObject.contact, - providerName: integrationId, - customFieldMappings, - })) as UnifiedContactOutput[]; - - //TODO - const contactIds = sourceObject.map((contact) => - 'id' in contact - ? String(contact.id) - : 'contact_id' in contact - ? String(contact.contact_id) - : undefined, - ); - - //insert the data in the DB with the fieldMappings (value table) - const contacts_data = await this.saveContactsInDb( - linkedUserId, - unifiedObject, - contactIds, - integrationId, - sourceObject, - ); - const event = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'success', - type: 'crm.contact.pulled', - method: 'PULL', - url: '/pull', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - await this.webhook.handleWebhook( - contacts_data, - 'crm.contact.pulled', - id_project, - event.id_event, - ); - } catch (error) { - handleServiceError(error, this.logger); - } - } } diff --git a/packages/api/src/crm/crm.module.ts b/packages/api/src/crm/crm.module.ts index bd00ac021..e625a833f 100644 --- a/packages/api/src/crm/crm.module.ts +++ b/packages/api/src/crm/crm.module.ts @@ -1,38 +1,39 @@ import { Module } from '@nestjs/common'; import { ContactModule } from './contact/contact.module'; import { DealModule } from './deal/deal.module'; -import { LeadModule } from './lead/lead.module'; import { NoteModule } from './note/note.module'; import { EngagementModule } from './engagement/engagement.module'; import { StageModule } from './stage/stage.module'; import { TaskModule } from './task/task.module'; import { UserModule } from './user/user.module'; import { CompanyModule } from './company/company.module'; +import { EngagementTypeModule } from './engagementType/engagementType.module'; +import { EngagementTypeModule } from './engagement-type/engagement-type.module'; @Module({ imports: [ ContactModule, DealModule, - LeadModule, NoteModule, CompanyModule, EngagementModule, StageModule, TaskModule, UserModule, + EngagementTypeModule, ], providers: [], controllers: [], exports: [ ContactModule, DealModule, - LeadModule, NoteModule, CompanyModule, EngagementModule, StageModule, TaskModule, UserModule, + EngagementTypeModule, ], }) export class CrmModule {} diff --git a/packages/api/src/crm/deal/sync/sync.service.ts b/packages/api/src/crm/deal/sync/sync.service.ts index e6c4232a5..175866b4d 100644 --- a/packages/api/src/crm/deal/sync/sync.service.ts +++ b/packages/api/src/crm/deal/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedDealOutput } from '../types/model.unified'; import { IDealService } from '../types'; +import { OriginalDealOutput } from '@@core/utils/types/original/original.crm'; +import { crm_deals as CrmDeal } from '@prisma/client'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,302 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncDeals(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_deals table + //its role is to fetch all deals from providers 3rd parties and save the info inside our db + async syncDeals() { + try { + this.logger.log(`Syncing deals....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncDealsForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncDealsForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} deals for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping deals syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'deal', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IDealService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncDeals( + linkedUserId, + remoteProperties, + ); + + const sourceObject: OriginalDealOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.deal, + providerName: integrationId, + customFieldMappings, + })) as UnifiedDealOutput[]; + + //TODO + const dealIds = sourceObject.map((deal) => + 'id' in deal ? String(deal.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const deals_data = await this.saveDealsInDb( + linkedUserId, + unifiedObject, + dealIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.deal.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + deals_data, + 'crm.deal.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveDealsInDb( + linkedUserId: string, + deals: UnifiedDealOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let deals_results: CrmDeal[] = []; + for (let i = 0; i < deals.length; i++) { + const deal = deals[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingDeal = await this.prisma.crm_deals.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + }); + + let unique_crm_deal_id: string; + + if (existingDeal) { + // Update the existing deal + let data: any = { + modified_at: new Date(), + }; + if (deal.name) { + data = { ...data, body: deal.name }; + } + if (deal.description) { + data = { ...data, html_body: deal.description }; + } + if (deal.amount) { + data = { ...data, is_private: deal.amount }; + } + if (deal.user_id) { + data = { ...data, creator_type: deal.user_id }; + } + if (deal.stage_id) { + data = { ...data, creator_type: deal.stage_id }; + } + + const res = await this.prisma.crm_deals.update({ + where: { + id_crm_deal: existingDeal.id_crm_deal, + }, + data: data, + }); + unique_crm_deal_id = res.id_crm_deal; + deals_results = [...deals_results, res]; + } else { + // Create a new deal + this.logger.log('deal not exists'); + let data: any = { + id_crm_deal: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + + if (deal.name) { + data = { ...data, body: deal.name }; + } + if (deal.description) { + data = { ...data, html_body: deal.description }; + } + if (deal.amount) { + data = { ...data, is_private: deal.amount }; + } + if (deal.user_id) { + data = { ...data, creator_type: deal.user_id }; + } + if (deal.stage_id) { + data = { ...data, creator_type: deal.stage_id }; + } + const res = await this.prisma.crm_deals.create({ + data: data, + }); + unique_crm_deal_id = res.id_crm_deal; + deals_results = [...deals_results, res]; + } + + // check duplicate or existing values + if (deal.field_mappings && deal.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_deal_id, + }, + }); + + for (const mapping of deal.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_deal_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_deal_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return deals_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/engagement/sync/sync.service.ts b/packages/api/src/crm/engagement/sync/sync.service.ts index f7cbc7817..db7117fcf 100644 --- a/packages/api/src/crm/engagement/sync/sync.service.ts +++ b/packages/api/src/crm/engagement/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedEngagementOutput } from '../types/model.unified'; import { IEngagementService } from '../types'; +import { crm_engagements as CrmEngagement } from '@prisma/client'; +import { OriginalEngagementOutput } from '@@core/utils/types/original/original.crm'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,329 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncEngagements(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_engagements table + //its role is to fetch all engagements from providers 3rd parties and save the info inside our db + async syncEngagements() { + try { + this.logger.log(`Syncing engagements....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncEngagementsForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncEngagementsForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} engagements for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping engagements syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'engagement', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IEngagementService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.syncEngagements(linkedUserId, remoteProperties); + + const sourceObject: OriginalEngagementOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.engagement, + providerName: integrationId, + customFieldMappings, + })) as UnifiedEngagementOutput[]; + + //TODO + const engagementIds = sourceObject.map((engagement) => + 'id' in engagement ? String(engagement.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const engagements_data = await this.saveEngagementsInDb( + linkedUserId, + unifiedObject, + engagementIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.engagement.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + engagements_data, + 'crm.engagement.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveEngagementsInDb( + linkedUserId: string, + engagements: UnifiedEngagementOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let engagements_results: CrmEngagement[] = []; + for (let i = 0; i < engagements.length; i++) { + const engagement = engagements[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingEngagement = await this.prisma.crm_engagements.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + }); + + let unique_crm_engagement_id: string; + + if (existingEngagement) { + // Update the existing engagement + let data: any = { + modified_at: new Date(), + }; + + if (engagement.content) { + data = { ...data, content: engagement.content }; + } + if (engagement.direction) { + data = { ...data, direction: engagement.direction }; + } + if (engagement.subject) { + data = { ...data, subject: engagement.subject }; + } + if (engagement.start_at) { + data = { ...data, start_at: engagement.start_at }; + } + if (engagement.end_time) { + data = { ...data, end_time: engagement.end_time }; + } + if (engagement.engagement_type) { + data = { + ...data, + id_crm_engagement_type: engagement.engagement_type, + }; + } + if (engagement.company_id) { + data = { ...data, id_crm_company: engagement.company_id }; + } + + /*TODO: + if (engagement.contacts) { + data = { ...data, end_time: engagement.end_time }; + }*/ + const res = await this.prisma.crm_engagements.update({ + where: { + id_crm_engagement: existingEngagement.id_crm_engagement, + }, + data: data, + }); + unique_crm_engagement_id = res.id_crm_engagement; + engagements_results = [...engagements_results, res]; + } else { + // Create a new engagement + this.logger.log('engagement not exists'); + let data: any = { + id_crm_engagement: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + + if (engagement.content) { + data = { ...data, content: engagement.content }; + } + if (engagement.direction) { + data = { ...data, direction: engagement.direction }; + } + if (engagement.subject) { + data = { ...data, subject: engagement.subject }; + } + if (engagement.start_at) { + data = { ...data, start_at: engagement.start_at }; + } + if (engagement.end_time) { + data = { ...data, end_time: engagement.end_time }; + } + if (engagement.engagement_type) { + data = { + ...data, + id_crm_engagement_type: engagement.engagement_type, + }; + } + if (engagement.company_id) { + data = { ...data, id_crm_company: engagement.company_id }; + } + + /*TODO: + if (engagement.contacts) { + data = { ...data, end_time: engagement.end_time }; + }*/ + + const res = await this.prisma.crm_engagements.create({ + data: data, + }); + unique_crm_engagement_id = res.id_crm_engagement; + engagements_results = [...engagements_results, res]; + } + + // check duplicate or existing values + if (engagement.field_mappings && engagement.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_engagement_id, + }, + }); + + for (const mapping of engagement.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_engagement_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_engagement_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return engagements_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/lead/lead.controller.ts b/packages/api/src/crm/lead/lead.controller.ts deleted file mode 100644 index bba29480e..000000000 --- a/packages/api/src/crm/lead/lead.controller.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { - Controller, - Post, - Body, - Query, - Get, - Patch, - Param, - Headers, -} from '@nestjs/common'; -import { LoggerService } from '@@core/logger/logger.service'; -import { - ApiBody, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, - ApiHeader, -} from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; -import { LeadService } from './services/lead.service'; -import { UnifiedLeadInput, UnifiedLeadOutput } from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; - -@ApiTags('crm/lead') -@Controller('crm/lead') -export class LeadController { - private readonly connectionUtils = new ConnectionUtils(); - - constructor( - private readonly leadService: LeadService, - private logger: LoggerService, - ) { - this.logger.setContext(LeadController.name); - } - - @ApiOperation({ - operationId: 'getLeads', - summary: 'List a batch of Leads', - }) - @ApiHeader({ - name: 'connection_token', - required: true, - description: 'The connection token', - example: 'b008e199-eda9-4629-bd41-a01b6195864a', - }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Crm software.', - }) - @ApiCustomResponse(UnifiedLeadOutput) - //@UseGuards(ApiKeyAuthGuard) - @Get() - async getLeads( - @Headers('connection_token') connection_token: string, - @Query('remote_data') remote_data?: boolean, - ) { - try{ - const { linkedUserId, remoteSource } = - await this.connectionUtils.getConnectionMetadataFromConnectionToken( - connection_token, - ); - return this.leadService.getLeads( - remoteSource, - linkedUserId, - remote_data, - ); - }catch(error){ - throw new Error(error); - } - } - - @ApiOperation({ - operationId: 'getLead', - summary: 'Retrieve a Lead', - description: 'Retrieve a lead from any connected Crm software', - }) - @ApiParam({ - name: 'id', - required: true, - type: String, - description: 'id of the lead you want to retrieve.', - }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Crm software.', - }) - @ApiCustomResponse(UnifiedLeadOutput) - //@UseGuards(ApiKeyAuthGuard) - @Get(':id') - getLead( - @Param('id') id: string, - @Query('remote_data') remote_data?: boolean, - ) { - return this.leadService.getLead(id, remote_data); - } - - @ApiOperation({ - operationId: 'addLead', - summary: 'Create a Lead', - description: 'Create a lead in any supported Crm software', - }) - @ApiHeader({ - name: 'connection_token', - required: true, - description: 'The connection token', - example: 'b008e199-eda9-4629-bd41-a01b6195864a', - }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Crm software.', - }) - @ApiBody({ type: UnifiedLeadInput }) - @ApiCustomResponse(UnifiedLeadOutput) - //@UseGuards(ApiKeyAuthGuard) - @Post() - async addLead( - @Body() unifiedLeadData: UnifiedLeadInput, - @Headers('connection_token') connection_token: string, - @Query('remote_data') remote_data?: boolean, - ) { - try{ - const { linkedUserId, remoteSource } = - await this.connectionUtils.getConnectionMetadataFromConnectionToken( - connection_token, - ); - return this.leadService.addLead( - unifiedLeadData, - remoteSource, - linkedUserId, - remote_data, - ); - }catch(error){ - throw new Error(error); - } - } - - @ApiOperation({ - operationId: 'addLeads', - summary: 'Add a batch of Leads', - }) - @ApiHeader({ - name: 'connection_token', - required: true, - description: 'The connection token', - example: 'b008e199-eda9-4629-bd41-a01b6195864a', - }) - @ApiQuery({ - name: 'remote_data', - required: false, - type: Boolean, - description: - 'Set to true to include data from the original Crm software.', - }) - @ApiBody({ type: UnifiedLeadInput, isArray: true }) - @ApiCustomResponse(UnifiedLeadOutput) - //@UseGuards(ApiKeyAuthGuard) - @Post('batch') - async addLeads( - @Body() unfiedLeadData: UnifiedLeadInput[], - @Headers('connection_token') connection_token: string, - @Query('remote_data') remote_data?: boolean, - ) { - try{ - const { linkedUserId, remoteSource } = - await this.connectionUtils.getConnectionMetadataFromConnectionToken( - connection_token, - ); - return this.leadService.batchAddLeads( - unfiedLeadData, - remoteSource, - linkedUserId, - remote_data, - ); - }catch(error){ - throw new Error(error); - } - - } - - @ApiOperation({ - operationId: 'updateLead', - summary: 'Update a Lead', - }) - @ApiCustomResponse(UnifiedLeadOutput) - //@UseGuards(ApiKeyAuthGuard) - @Patch() - updateLead( - @Query('id') id: string, - @Body() updateLeadData: Partial, - ) { - return this.leadService.updateLead(id, updateLeadData); - } -} diff --git a/packages/api/src/crm/lead/lead.module.ts b/packages/api/src/crm/lead/lead.module.ts deleted file mode 100644 index 26d630094..000000000 --- a/packages/api/src/crm/lead/lead.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LeadController } from './lead.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/logger/logger.service'; -import { LeadService } from './services/lead.service'; -import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { FreshsalesService } from './services/freshsales'; -import { HubspotService } from './services/hubspot'; -import { PipedriveService } from './services/pipedrive'; -import { ZendeskService } from './services/zendesk'; -import { ZohoService } from './services/zoho'; - -@Module({ - imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), - ], - controllers: [LeadController], - providers: [ - LeadService, - PrismaService, - LoggerService, - SyncService, - WebhookService, - EncryptionService, - FieldMappingService, - ServiceRegistry, - /* PROVIDERS SERVICES */ - FreshsalesService, - ZendeskService, - ZohoService, - PipedriveService, - HubspotService, - ], - exports: [SyncService], -}) -export class LeadModule {} diff --git a/packages/api/src/crm/lead/services/freshsales/index.ts b/packages/api/src/crm/lead/services/freshsales/index.ts deleted file mode 100644 index 8323b1be9..000000000 --- a/packages/api/src/crm/lead/services/freshsales/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Injectable } from '@nestjs/common'; -import axios from 'axios'; -import { - CrmObject, - FreshsalesLeadInput, - FreshsalesLeadOutput, -} from '@crm/@utils/@types'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { LoggerService } from '@@core/logger/logger.service'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { ApiResponse } from '@@core/utils/types'; -import { ILeadService } from '@crm/lead/types'; -import { ServiceRegistry } from '../registry.service'; - -@Injectable() -export class FreshsalesService implements ILeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - CrmObject.lead.toUpperCase() + ':' + FreshsalesService.name, - ); - this.registry.registerService('freshsales', this); - } - - async addLead( - leadData: FreshsalesLeadInput, - linkedUserId: string, - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - }, - }); - const dataBody = { - lead: leadData, - }; - const resp = await axios.post( - 'https://domain.freshsales.io/api/leads', - JSON.stringify(dataBody), - { - headers: { - Authorization: `Token token=${this.cryptoService.decrypt( - connection.access_token, - )}`, - 'Content-Type': 'application/json', - }, - }, - ); - return { - data: resp.data, - message: 'Freshsales lead created', - statusCode: 201, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Freshsales', - CrmObject.lead, - ActionType.POST, - ); - } - } - - async syncLeads( - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.READ - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - }, - }); - const resp = await axios.get(`https://domain.freshsales.io/api/leads`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - return { - data: resp.data, - message: 'Freshsales leads retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Freshsales', - CrmObject.lead, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/crm/lead/services/freshsales/mappers.ts b/packages/api/src/crm/lead/services/freshsales/mappers.ts deleted file mode 100644 index cc2001e1e..000000000 --- a/packages/api/src/crm/lead/services/freshsales/mappers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ILeadMapper } from '@crm/lead/types'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; -import { FreshsalesLeadInput, FreshsalesLeadOutput } from '@crm/@utils/@types'; - -//TODO -export class FreshsalesLeadMapper implements ILeadMapper { - desunify(source: UnifiedLeadInput): FreshsalesLeadInput { - return; - } - - unify( - source: FreshsalesLeadOutput | FreshsalesLeadOutput[], - ): UnifiedLeadOutput | UnifiedLeadOutput[] { - // Handling single FreshsalesLeadOutput - if (!Array.isArray(source)) { - return this.mapSingleFreshsalesLeadToUnified(source); - } - - // Handling array of FreshsalesLeadOutput - return source.map((lead) => this.mapSingleFreshsalesLeadToUnified(lead)); - } - - private mapSingleFreshsalesLeadToUnified( - lead: FreshsalesLeadOutput, - ): UnifiedLeadOutput { - return; - } -} diff --git a/packages/api/src/crm/lead/services/freshsales/types.ts b/packages/api/src/crm/lead/services/freshsales/types.ts deleted file mode 100644 index 54ad5fdc8..000000000 --- a/packages/api/src/crm/lead/services/freshsales/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface FreshsalesLeadInput { - first_name: string; - last_name: string; - mobile_number: string | string[]; -} -export interface FreshsalesLeadOutput { - id: number; - first_name: string; - last_name: string; - display_name: string; - avatar: string | null; - job_title: string | null; - city: string | null; - state: string | null; - zipcode: string | null; - country: string | null; - email: string | null; - time_zone: string | null; - work_number: string | null; - mobile_number: string; - address: string | null; - last_seen: string | null; - lead_score: number; - last_contacted: string | null; - open_engagements_amount: string; - links: { - conversations: string; - activities: string; - }; - custom_field: Record; - updated_at: string; - keyword: string | null; - medium: string | null; - facebook: string | null; - twitter: string | null; - linkedin: string | null; -} diff --git a/packages/api/src/crm/lead/services/hubspot/index.ts b/packages/api/src/crm/lead/services/hubspot/index.ts deleted file mode 100644 index 0c06c60b9..000000000 --- a/packages/api/src/crm/lead/services/hubspot/index.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILeadService } from '@crm/lead/types'; -import { - CrmObject, - HubspotLeadInput, - HubspotLeadOutput, - commonHubspotProperties, -} from '@crm/@utils/@types'; -import axios from 'axios'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { LoggerService } from '@@core/logger/logger.service'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { ApiResponse } from '@@core/utils/types'; -import { ServiceRegistry } from '../registry.service'; - -@Injectable() -export class HubspotService implements ILeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - CrmObject.lead.toUpperCase() + ':' + HubspotService.name, - ); - this.registry.registerService('hubspot', this); - } - async addLead( - leadData: HubspotLeadInput, - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.write - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'hubspot', - }, - }); - const dataBody = { - properties: leadData, - }; - const resp = await axios.post( - `https://api.hubapi.com/crm/v3/objects/leads/`, - JSON.stringify(dataBody), - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - return { - data: resp.data, - message: 'Hubspot lead created', - statusCode: 201, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Hubspot', - CrmObject.lead, - ActionType.POST, - ); - } - } - - async syncLeads( - linkedUserId: string, - custom_properties?: string[], - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.READ - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'hubspot', - }, - }); - - const commonPropertyNames = Object.keys(commonHubspotProperties); - const allProperties = [...commonPropertyNames, ...custom_properties]; - const baseURL = 'https://api.hubapi.com/crm/v3/objects/leads/'; - - 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 ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - this.logger.log(`Synced hubspot leads !`); - - return { - data: resp.data.results, - message: 'Hubspot leads retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Hubspot', - CrmObject.lead, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/crm/lead/services/hubspot/mappers.ts b/packages/api/src/crm/lead/services/hubspot/mappers.ts deleted file mode 100644 index 7f42a9a1d..000000000 --- a/packages/api/src/crm/lead/services/hubspot/mappers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { HubspotLeadInput, HubspotLeadOutput } from '@crm/@utils/@types'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; -import { ILeadMapper } from '@crm/lead/types'; - -export class HubspotLeadMapper implements ILeadMapper { - desunify( - source: UnifiedLeadInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): HubspotLeadInput { - return; - } - - unify( - source: HubspotLeadOutput | HubspotLeadOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput | UnifiedLeadOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleLeadToUnified(source, customFieldMappings); - } - // Handling array of HubspotLeadOutput - return source.map((lead) => - this.mapSingleLeadToUnified(lead, customFieldMappings), - ); - } - - private mapSingleLeadToUnified( - lead: HubspotLeadOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput { - return; - } -} diff --git a/packages/api/src/crm/lead/services/hubspot/types.ts b/packages/api/src/crm/lead/services/hubspot/types.ts deleted file mode 100644 index f9c81ac66..000000000 --- a/packages/api/src/crm/lead/services/hubspot/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface HubspotLeadInput { - email?: string; - firstname?: string; - phone?: string; - lastname?: string; - city?: string; - country?: string; - zip?: string; - state?: string; - address?: string; - mobilephone?: string; - hubspot_owner_id?: string; - associatedcompanyid?: string; - fax?: string; - jobtitle?: string; - [key: string]: any; -} - -export interface HubspotLeadOutput { - id: string; -} diff --git a/packages/api/src/crm/lead/services/lead.service.ts b/packages/api/src/crm/lead/services/lead.service.ts deleted file mode 100644 index 697c17233..000000000 --- a/packages/api/src/crm/lead/services/lead.service.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { LoggerService } from '@@core/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { handleServiceError } from '@@core/utils/errors'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedLeadInput, UnifiedLeadOutput } from '../types/model.unified'; -import { desunify } from '@@core/utils/unification/desunify'; -import { CrmObject } from '@crm/@utils/@types'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { ServiceRegistry } from './registry.service'; -import { OriginalLeadOutput } from '@@core/utils/types/original/original.crm'; -import { unify } from '@@core/utils/unification/unify'; -import { ILeadService } from '../types'; - -@Injectable() -export class LeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, - ) { - this.logger.setContext(LeadService.name); - } - - async batchAddLeads( - unifiedLeadData: UnifiedLeadInput[], - integrationId: string, - linkedUserId: string, - remote_data?: boolean, - ): Promise { - try { - const responses = await Promise.all( - unifiedLeadData.map((unifiedData) => - this.addLead( - unifiedData, - integrationId.toLowerCase(), - linkedUserId, - remote_data, - ), - ), - ); - - return responses; - } catch (error) { - handleServiceError(error, this.logger); - } - } - - async addLead( - unifiedLeadData: UnifiedLeadInput, - integrationId: string, - linkedUserId: string, - remote_data?: boolean, - ): Promise { - /*try { - const linkedUser = await this.prisma.linked_users.findUnique({ - where: { - id_linked_user: linkedUserId, - }, - }); - - //CHECKS - if (!linkedUser) throw new Error('Linked User Not Found'); - const tick = unifiedLeadData.ticket_id; - //check if contact_id and account_id refer to real uuids - if (tick) { - const search = await this.prisma.tcg_tickets.findUnique({ - where: { - id_tcg_ticket: tick, - }, - }); - if (!search) - throw new Error('You inserted a ticket_id which does not exist'); - } - - const contact = unifiedLeadData.contact_id; - //check if contact_id and account_id refer to real uuids - if (contact) { - const search = await this.prisma.tcg_contacts.findUnique({ - where: { - id_tcg_contact: contact, - }, - }); - if (!search) - throw new Error('You inserted a contact_id which does not exist'); - } - const user = unifiedLeadData.user_id; - //check if contact_id and account_id refer to real uuids - if (user) { - const search = await this.prisma.tcg_users.findUnique({ - where: { - id_tcg_user: user, - }, - }); - if (!search) - throw new Error('You inserted a user_id which does not exist'); - } - - const attachmts = unifiedLeadData.attachments; - //CHEK IF attachments contains valid Attachment uuids - if (attachmts && attachmts.length > 0) { - attachmts.map(async (attachmt) => { - const search = await this.prisma.tcg_attachments.findUnique({ - where: { - id_tcg_attachment: attachmt, - }, - }); - if (!search) - throw new Error( - 'You inserted an attachment_id which does not exist', - ); - }); - } - - //desunify the data according to the target obj wanted - const desunifiedObject = await desunify({ - sourceObject: unifiedLeadData, - targetType: CrmObject.lead, - providerName: integrationId, - customFieldMappings: [], - }); - - const service: ILeadService = - this.serviceRegistry.getService(integrationId); - //get remote_id of the ticket so the lead is inserted successfully - const ticket = await this.prisma.tcg_tickets.findUnique({ - where: { - id_tcg_ticket: unifiedLeadData.ticket_id, - }, - select: { - remote_id: true, - }, - }); - if (!ticket) - throw new Error('ticket does not exist for the lead you try to create'); - const resp: ApiResponse = await service.addLead( - desunifiedObject, - linkedUserId, - ticket.remote_id, - ); - - //unify the data according to the target obj wanted - const unifiedObject = (await unify({ - sourceObject: [resp.data], - targetType: CrmObject.lead, - providerName: integrationId, - customFieldMappings: [], - })) as UnifiedLeadOutput[]; - - // add the lead inside our db - const source_lead = resp.data; - const target_lead = unifiedObject[0]; - const originId = 'id' in source_lead ? String(source_lead.id) : undefined; //TODO - - const existingLead = await this.prisma.tcg_leads.findFirst({ - where: { - remote_id: originId, - remote_platform: integrationId, - id_linked_user: linkedUserId, - }, - }); - - let unique_ticketing_lead_id: string; - const opts = - target_lead.creator_type === 'contact' - ? { - id_tcg_contact: unifiedLeadData.contact_id, - } - : target_lead.creator_type === 'user' - ? { - id_tcg_user: unifiedLeadData.user_id, - } - : {}; //case where nothing is passed for creator or a not authorized value; - - if (existingLead) { - // Update the existing lead - let data: any = { - id_tcg_ticket: unifiedLeadData.ticket_id, - modified_at: new Date(), - }; - if (target_lead.body) { - data = { ...data, body: target_lead.body }; - } - if (target_lead.html_body) { - data = { ...data, html_body: target_lead.html_body }; - } - if (target_lead.is_private) { - data = { ...data, is_private: target_lead.is_private }; - } - if (target_lead.creator_type) { - data = { ...data, creator_type: target_lead.creator_type }; - } - data = { ...data, ...opts }; - - const res = await this.prisma.tcg_leads.update({ - where: { - id_tcg_lead: existingLead.id_tcg_lead, - }, - data: data, - }); - unique_ticketing_lead_id = res.id_tcg_lead; - } else { - // Create a new lead - this.logger.log('lead not exists'); - let data: any = { - id_tcg_lead: uuidv4(), - created_at: new Date(), - modified_at: new Date(), - id_tcg_ticket: unifiedLeadData.ticket_id, - id_linked_user: linkedUserId, - remote_id: originId, - remote_platform: integrationId, - }; - - if (target_lead.body) { - data = { ...data, body: target_lead.body }; - } - if (target_lead.html_body) { - data = { ...data, html_body: target_lead.html_body }; - } - if (target_lead.is_private) { - data = { ...data, is_private: target_lead.is_private }; - } - if (target_lead.creator_type) { - data = { ...data, creator_type: target_lead.creator_type }; - } - data = { ...data, ...opts }; - - const res = await this.prisma.tcg_leads.create({ - data: data, - }); - unique_ticketing_lead_id = res.id_tcg_lead; - } - - //insert remote_data in db - await this.prisma.remote_data.upsert({ - where: { - ressource_owner_id: unique_ticketing_lead_id, - }, - create: { - id_remote_data: uuidv4(), - ressource_owner_id: unique_ticketing_lead_id, - format: 'json', - data: JSON.stringify(source_lead), - created_at: new Date(), - }, - update: { - data: JSON.stringify(source_lead), - created_at: new Date(), - }, - }); - - const result_lead = await this.getLead( - unique_ticketing_lead_id, - remote_data, - ); - - const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; - - const event = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: status_resp, - type: 'ticketing.lead.push', //sync, push or pull - method: 'POST', - url: '/ticketing/lead', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - await this.webhook.handleWebhook( - result_lead, - 'ticketing.lead.created', - linkedUser.id_project, - event.id_event, - ); - return result_lead; - } catch (error) { - handleServiceError(error, this.logger); - }*/ - return; - } - - async getLead( - id_leading_lead: string, - remote_data?: boolean, - ): Promise { - /*try { - const lead = await this.prisma.tcg_leads.findUnique({ - where: { - id_tcg_lead: id_leading_lead, - }, - }); - - // WE SHOULDNT HAVE FIELD MAPPINGS TO COMMENT - - // Fetch field mappings for the lead - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: lead.id_tcg_lead, - }, - }, - include: { - attribute: true, - }, - }); - - const fieldMappingsMap = new Map(); - - values.forEach((value) => { - fieldMappingsMap.set(value.attribute.slug, value.data); - }); - - // Convert the map to an array of objects - const field_mappings = Array.from(fieldMappingsMap, ([key, value]) => ({ - [key]: value, - })); - - // Transform to UnifiedLeadOutput format - const unifiedLead: UnifiedLeadOutput = { - id: lead.id_tcg_lead, - body: lead.body, - html_body: lead.html_body, - is_private: lead.is_private, - creator_type: lead.creator_type, - ticket_id: lead.id_tcg_ticket, - contact_id: lead.id_tcg_contact, // uuid of Contact object - user_id: lead.id_tcg_user, // uuid of User object - }; - - let res: UnifiedLeadOutput = { - ...unifiedLead, - }; - - if (remote_data) { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: lead.id_tcg_lead, - }, - }); - const remote_data = JSON.parse(resp.data); - - res = { - ...res, - remote_data: remote_data, - }; - } - - return res; - } catch (error) { - handleServiceError(error, this.logger); - }*/ - return; - } - - async getLeads( - integrationId: string, - linkedUserId: string, - remote_data?: boolean, - ): Promise { - /*try { - const leads = await this.prisma.tcg_leads.findMany({ - where: { - remote_platform: integrationId.toLowerCase(), - id_linked_user: linkedUserId, - }, - }); - - const unifiedLeads: UnifiedLeadOutput[] = await Promise.all( - leads.map(async (lead) => { - //WE SHOULDNT HAVE FIELD MAPPINGS FOR COMMENT - // Fetch field mappings for the ticket - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: lead.id_tcg_ticket, - }, - }, - include: { - attribute: true, - }, - }); - // Create a map to store unique field mappings - const fieldMappingsMap = new Map(); - - values.forEach((value) => { - fieldMappingsMap.set(value.attribute.slug, value.data); - }); - - // Convert the map to an array of objects - const field_mappings = Array.from( - fieldMappingsMap, - ([key, value]) => ({ [key]: value }), - ); - - // Transform to UnifiedLeadOutput format - return { - id: lead.id_tcg_lead, - body: lead.body, - html_body: lead.html_body, - is_private: lead.is_private, - creator_type: lead.creator_type, - ticket_id: lead.id_tcg_ticket, - contact_id: lead.id_tcg_contact, // uuid of Contact object - user_id: lead.id_tcg_user, // uuid of User object - }; - }), - ); - - let res: UnifiedLeadOutput[] = unifiedLeads; - - if (remote_data) { - const remote_array_data: UnifiedLeadOutput[] = await Promise.all( - res.map(async (lead) => { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: lead.id, - }, - }); - const remote_data = JSON.parse(resp.data); - return { ...lead, remote_data }; - }), - ); - res = remote_array_data; - } - - const event = await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'success', - type: 'ticketing.lead.pulled', - method: 'GET', - url: '/ticketing/lead', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - - return res; - } catch (error) { - handleServiceError(error, this.logger); - }*/ - return; - } -} diff --git a/packages/api/src/crm/lead/services/pipedrive/index.ts b/packages/api/src/crm/lead/services/pipedrive/index.ts deleted file mode 100644 index 89939b32c..000000000 --- a/packages/api/src/crm/lead/services/pipedrive/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILeadService } from '@crm/lead/types'; -import { - CrmObject, - PipedriveLeadInput, - PipedriveLeadOutput, -} from '@crm/@utils/@types'; -import axios from 'axios'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { LoggerService } from '@@core/logger/logger.service'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { ApiResponse } from '@@core/utils/types'; -import { ServiceRegistry } from '../registry.service'; - -@Injectable() -export class PipedriveService implements ILeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - CrmObject.lead.toUpperCase() + ':' + PipedriveService.name, - ); - this.registry.registerService('pipedrive', this); - } - - async addLead( - leadData: PipedriveLeadInput, - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.write - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'pipedrive', - }, - }); - const resp = await axios.post( - `https://api.pipedrive.com/v1/persons`, - JSON.stringify(leadData), - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - return { - data: resp.data.data, - message: 'Pipedrive lead created', - statusCode: 201, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Pipedrive', - CrmObject.lead, - ActionType.POST, - ); - } - return; - } - - async syncLeads( - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.READ - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'pipedrive', - }, - }); - const resp = await axios.get(`https://api.pipedrive.com/v1/persons`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - - return { - data: resp.data.data, - message: 'Pipedrive leads retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Pipedrive', - CrmObject.lead, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/crm/lead/services/pipedrive/mappers.ts b/packages/api/src/crm/lead/services/pipedrive/mappers.ts deleted file mode 100644 index 3b825c62e..000000000 --- a/packages/api/src/crm/lead/services/pipedrive/mappers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { PipedriveLeadInput, PipedriveLeadOutput } from '@crm/@utils/@types'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; -import { ILeadMapper } from '@crm/lead/types'; - -export class PipedriveLeadMapper implements ILeadMapper { - desunify( - source: UnifiedLeadInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): PipedriveLeadInput { - return; - } - - unify( - source: PipedriveLeadOutput | PipedriveLeadOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput | UnifiedLeadOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleLeadToUnified(source, customFieldMappings); - } - - // Handling array of HubspotLeadOutput - return source.map((lead) => - this.mapSingleLeadToUnified(lead, customFieldMappings), - ); - } - - private mapSingleLeadToUnified( - lead: PipedriveLeadOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput { - return; - } -} diff --git a/packages/api/src/crm/lead/services/pipedrive/types.ts b/packages/api/src/crm/lead/services/pipedrive/types.ts deleted file mode 100644 index 8cfd600e3..000000000 --- a/packages/api/src/crm/lead/services/pipedrive/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -export interface PipedriveLead { - id: string; - company_id: number; - owner_id: { - id: number; - name: string; - email: string; - has_pic: number; - pic_hash: string; - active_flag: boolean; - value: number; - }; - org_id: { - name: string; - people_count: number; - owner_id: number; - address: string; - active_flag: boolean; - cc_email: string; - value: number; - }; - name: string; - first_name: string; - last_name: string; - open_deals_count: number; - related_open_deals_count: number; - closed_deals_count: number; - related_closed_deals_count: number; - participant_open_deals_count: number; - participant_closed_deals_count: number; - email_messages_count: number; - activities_count: number; - done_activities_count: number; - undone_activities_count: number; - files_count: number; - notes_count: number; - followers_count: number; - won_deals_count: number; - related_won_deals_count: number; - lost_deals_count: number; - related_lost_deals_count: number; - active_flag: boolean; - phone: { value: string; primary: boolean; label: string }[]; - email: { value: string; primary: boolean; label: string }[]; - primary_email: string; - first_char: string; - update_time: Date; - add_time: Date; - visible_to: string; - marketing_status: string; - picture_id: { - item_type: string; - item_id: number; - active_flag: boolean; - add_time: string; - update_time: string; - added_by_user_id: number; - pictures: { - '128': string; - '512': string; - }; - value: number; - }; - next_activity_date: string; - next_activity_time: string; - next_activity_id: number; - last_activity_id: number; - last_activity_date: string; - last_incoming_mail_time: string; - last_outgoing_mail_time: string; - label: number; - org_name: string; - owner_name: string; - cc_email: string; - [key: string]: any; -} - -export type PipedriveLeadInput = Partial; -export type PipedriveLeadOutput = PipedriveLeadInput; diff --git a/packages/api/src/crm/lead/services/registry.service.ts b/packages/api/src/crm/lead/services/registry.service.ts deleted file mode 100644 index f256e4744..000000000 --- a/packages/api/src/crm/lead/services/registry.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILeadService } from '../types'; - -@Injectable() -export class ServiceRegistry { - private serviceMap: Map; - - constructor() { - this.serviceMap = new Map(); - } - - registerService(serviceKey: string, service: ILeadService) { - this.serviceMap.set(serviceKey, service); - } - - getService(integrationId: string): ILeadService { - const service = this.serviceMap.get(integrationId); - if (!service) { - throw new Error(); - } - return service; - } -} diff --git a/packages/api/src/crm/lead/services/zendesk/index.ts b/packages/api/src/crm/lead/services/zendesk/index.ts deleted file mode 100644 index 7f79d0ca4..000000000 --- a/packages/api/src/crm/lead/services/zendesk/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILeadService } from '@crm/lead/types'; -import { - CrmObject, - ZendeskLeadInput, - ZendeskLeadOutput, -} from '@crm/@utils/@types'; -import axios from 'axios'; -import { LoggerService } from '@@core/logger/logger.service'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { ApiResponse } from '@@core/utils/types'; -import { ServiceRegistry } from '../registry.service'; -@Injectable() -export class ZendeskService implements ILeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - CrmObject.lead.toUpperCase() + ':' + ZendeskService.name, - ); - this.registry.registerService('zendesk', this); - } - - async addLead( - leadData: ZendeskLeadInput, - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.write - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'zendesk', - }, - }); - const resp = await axios.post( - `https://api.getbase.com/v2/leads`, - { - data: leadData, - }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - - return { - data: resp.data.data, - message: 'Zendesk lead created', - statusCode: 201, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Zendesk', - CrmObject.lead, - ActionType.POST, - ); - } - return; - } - - async syncLeads( - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.READ - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'zendesk', - }, - }); - const resp = await axios.get(`https://api.getbase.com/v2/leads`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - const finalData = resp.data.items.map((item) => { - return item.data; - }); - this.logger.log(`Synced zendesk leads !`); - - return { - data: finalData, - message: 'Zendesk leads retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Zendesk', - CrmObject.lead, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/crm/lead/services/zendesk/mappers.ts b/packages/api/src/crm/lead/services/zendesk/mappers.ts deleted file mode 100644 index bdba4f5f3..000000000 --- a/packages/api/src/crm/lead/services/zendesk/mappers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ZendeskLeadInput, ZendeskLeadOutput } from '@crm/@utils/@types'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; -import { ILeadMapper } from '@crm/lead/types'; - -export class ZendeskLeadMapper implements ILeadMapper { - desunify( - source: UnifiedLeadInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): ZendeskLeadInput { - return; - } - - unify( - source: ZendeskLeadOutput | ZendeskLeadOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput | UnifiedLeadOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleLeadToUnified(source, customFieldMappings); - } - - // Handling array of HubspotLeadOutput - return source.map((lead) => - this.mapSingleLeadToUnified(lead, customFieldMappings), - ); - } - - private mapSingleLeadToUnified( - lead: ZendeskLeadOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput { - return; - } -} diff --git a/packages/api/src/crm/lead/services/zendesk/types.ts b/packages/api/src/crm/lead/services/zendesk/types.ts deleted file mode 100644 index e9d0f140a..000000000 --- a/packages/api/src/crm/lead/services/zendesk/types.ts +++ /dev/null @@ -1,91 +0,0 @@ -export interface ZendeskLead { - owner_id: number; - created_at: string; - description: string | null; - industry: string | null; - billing_address: string | null; - linkedin: string | null; - title: string; - contact_id: number; - skype: string | null; - twitter: string | null; - shipping_address: string | null; - id: number; - fax: string | null; - is_organization: boolean; - first_name: string; - email: string; - prospect_status: string; - website: string | null; - address: Address; - facebook: string | null; - mobile: string; - last_name: string; - tags: Tag[]; - custom_field_values: CustomFieldValue[]; - phone: string; - customer_status: string; - name: string; - creator_id: number; - meta: Meta; - custom_fields: Record; -} - -export type ZendeskLeadInput = Partial; -export type ZendeskLeadOutput = ZendeskLeadInput; - -type TagData = { - name: string; - resource_type: string; - id: number; -}; - -type TagMeta = { - type: string; -}; - -type Tag = { - data: TagData; - meta: TagMeta; -}; - -type PreviousEvent = { - title: string; -}; - -type Meta = { - event_id: string; - event_cause: string; - sequence: number; - event_time: string; - event_type: string; - previous: PreviousEvent; - type: string; -}; - -interface Address { - line1: string; - city: string; - postal_code: string; - state: string; - country: string; -} - -type CustomFieldData = { - name: string; - resource_type: string; - id: number; - type: string; -}; - -type CustomFieldMeta = { - type: string; -}; - -type CustomFieldValue = { - value: boolean; - custom_field: { - data: CustomFieldData; - meta: CustomFieldMeta; - }; -}; diff --git a/packages/api/src/crm/lead/services/zoho/index.ts b/packages/api/src/crm/lead/services/zoho/index.ts deleted file mode 100644 index 28762efa0..000000000 --- a/packages/api/src/crm/lead/services/zoho/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ILeadService } from '@crm/lead/types'; -import { CrmObject, ZohoLeadInput, ZohoLeadOutput } from '@crm/@utils/@types'; -import axios from 'axios'; -import { LoggerService } from '@@core/logger/logger.service'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { ActionType, handleServiceError } from '@@core/utils/errors'; -import { EncryptionService } from '@@core/encryption/encryption.service'; -import { ApiResponse } from '@@core/utils/types'; -import { ServiceRegistry } from '../registry.service'; - -@Injectable() -export class ZohoService implements ILeadService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - CrmObject.lead.toUpperCase() + ':' + ZohoService.name, - ); - this.registry.registerService('zoho', this); - } - - async addLead( - leadData: ZohoLeadInput, - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.write - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'zoho', - }, - }); - const resp = await axios.post( - `https://www.zohoapis.eu/crm/v3/Leads`, - { data: [leadData] }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Zoho-oauthtoken ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - //this.logger.log('zoho resp is ' + JSON.stringify(resp)); - return { - data: resp.data.data, - message: 'Zoho lead created', - statusCode: 201, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Zoho', - CrmObject.lead, - ActionType.POST, - ); - } - return; - } - - async syncLeads( - linkedUserId: string, - ): Promise> { - try { - //TODO: check required scope => crm.objects.leads.READ - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'zoho', - }, - }); - //TODO: handle fields - const fields = 'First_Name,Last_Name,Full_Name,Email,Phone'; - const resp = await axios.get( - `https://www.zohoapis.eu/crm/v3/Leads?fields=${fields}`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Zoho-oauthtoken ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - //this.logger.log('CONTACTS ZOHO ' + JSON.stringify(resp.data.data)); - this.logger.log(`Synced zoho leads !`); - return { - data: resp.data.data, - message: 'Zoho leads retrieved', - statusCode: 200, - }; - } catch (error) { - handleServiceError( - error, - this.logger, - 'Zoho', - CrmObject.lead, - ActionType.GET, - ); - } - } -} diff --git a/packages/api/src/crm/lead/services/zoho/mappers.ts b/packages/api/src/crm/lead/services/zoho/mappers.ts deleted file mode 100644 index 7cc59dbff..000000000 --- a/packages/api/src/crm/lead/services/zoho/mappers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ZohoLeadInput, ZohoLeadOutput } from '@crm/@utils/@types'; -import { - UnifiedLeadInput, - UnifiedLeadOutput, -} from '@crm/lead/types/model.unified'; -import { ILeadMapper } from '@crm/lead/types'; - -export class ZohoLeadMapper implements ILeadMapper { - desunify( - source: UnifiedLeadInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): ZohoLeadInput { - return; - } - - unify( - source: ZohoLeadOutput | ZohoLeadOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput | UnifiedLeadOutput[] { - if (!Array.isArray(source)) { - return this.mapSingleLeadToUnified(source, customFieldMappings); - } - - // Handling array of HubspotLeadOutput - return source.map((lead) => - this.mapSingleLeadToUnified(lead, customFieldMappings), - ); - } - - private mapSingleLeadToUnified( - lead: ZohoLeadOutput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput { - return; - } -} diff --git a/packages/api/src/crm/lead/services/zoho/types.ts b/packages/api/src/crm/lead/services/zoho/types.ts deleted file mode 100644 index f2827c9ab..000000000 --- a/packages/api/src/crm/lead/services/zoho/types.ts +++ /dev/null @@ -1,50 +0,0 @@ -export interface ZohoLead { - Owner: string; - Lead_Source: string; - First_Name: string; - Last_Name: string; - Full_Name: string; - Account_Name: string; - Email: string; - Title: string; - Department: string; - Phone: string; - Home_Phone: string; - Other_Phone: string; - Fax: string; - Mobile: string; - Date_of_Birth: Date; - Assistant: string; - Asst_Phone: string; - Email_Opt_Out: boolean; - Created_By: string; - Skype_ID: string; - Modified_By: string; - Created_Time: Date; - Modified_Time: Date; - Salutation: string; - Secondary_Email: string; - Last_Activity_Time: Date; - Twitter: string; - Reporting_To: string; - Unsubscribed_Mode: string; - Unsubscribed_Time: Date; - Last_Enriched_Time__s: Date; - Enrich_Status__s: string; - Mailing_Street: string; - Other_Street: string; - Mailing_City: string; - Other_City: string; - Mailing_State: string; - Other_State: string; - Mailing_Zip: string; - Other_Zip: string; - Mailing_Country: string; - Other_Country: string; - Description: string; - Record_Image: string; - [key: string]: any; -} - -export type ZohoLeadInput = Partial; -export type ZohoLeadOutput = ZohoLeadInput; diff --git a/packages/api/src/crm/lead/sync/sync.service.ts b/packages/api/src/crm/lead/sync/sync.service.ts deleted file mode 100644 index 91fec3ba1..000000000 --- a/packages/api/src/crm/lead/sync/sync.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { LoggerService } from '@@core/logger/logger.service'; -import { PrismaService } from '@@core/prisma/prisma.service'; -import { NotFoundError, handleServiceError } from '@@core/utils/errors'; -import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; -import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { ServiceRegistry } from '../services/registry.service'; -import { unify } from '@@core/utils/unification/unify'; -import { CrmObject } from '@crm/@utils/@types'; -import { WebhookService } from '@@core/webhook/webhook.service'; -import { UnifiedLeadOutput } from '../types/model.unified'; -import { ILeadService } from '../types'; - -@Injectable() -export class SyncService implements OnModuleInit { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private webhook: WebhookService, - private fieldMappingService: FieldMappingService, - private serviceRegistry: ServiceRegistry, - ) { - this.logger.setContext(SyncService.name); - } - - async onModuleInit() { - // Initialization logic - } - - // Additional methods and logic -} diff --git a/packages/api/src/crm/lead/types/index.ts b/packages/api/src/crm/lead/types/index.ts deleted file mode 100644 index 5a46e3094..000000000 --- a/packages/api/src/crm/lead/types/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedLeadInput, UnifiedLeadOutput } from './model.unified'; -import { OriginalLeadOutput } from '@@core/utils/types/original/original.crm'; -import { ApiResponse } from '@@core/utils/types'; - -export interface ILeadService { - addLead( - leadData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncLeads( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; -} - -export interface ILeadMapper { - desunify( - source: UnifiedLeadInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): DesunifyReturnType; - - unify( - source: OriginalLeadOutput | OriginalLeadOutput[], - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): UnifiedLeadOutput | UnifiedLeadOutput[]; -} diff --git a/packages/api/src/crm/lead/types/mappingsTypes.ts b/packages/api/src/crm/lead/types/mappingsTypes.ts deleted file mode 100644 index 017fd3957..000000000 --- a/packages/api/src/crm/lead/types/mappingsTypes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FreshsalesLeadMapper } from '../services/freshsales/mappers'; -import { HubspotLeadMapper } from '../services/hubspot/mappers'; -import { PipedriveLeadMapper } from '../services/pipedrive/mappers'; -import { ZendeskLeadMapper } from '../services/zendesk/mappers'; -import { ZohoLeadMapper } from '../services/zoho/mappers'; - -const hubspotLeadMapper = new HubspotLeadMapper(); -const zendeskLeadMapper = new ZendeskLeadMapper(); -const zohoLeadMapper = new ZohoLeadMapper(); -const pipedriveLeadMapper = new PipedriveLeadMapper(); -const freshSalesLeadMapper = new FreshsalesLeadMapper(); - -export const leadUnificationMapping = { - hubspot: { - unify: hubspotLeadMapper.unify, - desunify: hubspotLeadMapper.desunify, - }, - pipedrive: { - unify: pipedriveLeadMapper.unify, - desunify: pipedriveLeadMapper.desunify, - }, - zoho: { - unify: zohoLeadMapper.unify, - desunify: zohoLeadMapper.desunify, - }, - zendesk: { - unify: zendeskLeadMapper.unify, - desunify: zendeskLeadMapper.desunify, - }, - freshsales: { - unify: freshSalesLeadMapper.unify, - desunify: freshSalesLeadMapper.desunify, - }, -}; diff --git a/packages/api/src/crm/lead/types/model.unified.ts b/packages/api/src/crm/lead/types/model.unified.ts deleted file mode 100644 index 77ca70511..000000000 --- a/packages/api/src/crm/lead/types/model.unified.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class UnifiedLeadInput { - @ApiPropertyOptional({ - type: [{}], - description: - 'The custom field mappings of the lead between the remote 3rd party & Panora', - }) - field_mappings?: Record[]; -} - -export class UnifiedLeadOutput extends UnifiedLeadInput { - @ApiPropertyOptional({ description: 'The uuid of the lead' }) - id?: string; - - @ApiPropertyOptional({ - description: 'The id of the lead in the context of the Crm 3rd Party', - }) - remote_id?: string; - - @ApiPropertyOptional({ - type: [{}], - description: - 'The remote data of the lead in the context of the Crm 3rd Party', - }) - remote_data?: Record; -} diff --git a/packages/api/src/crm/lead/utils/index.ts b/packages/api/src/crm/lead/utils/index.ts deleted file mode 100644 index f849788c1..000000000 --- a/packages/api/src/crm/lead/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ diff --git a/packages/api/src/crm/note/sync/sync.service.ts b/packages/api/src/crm/note/sync/sync.service.ts index 04a2ebcf3..f0fa6a080 100644 --- a/packages/api/src/crm/note/sync/sync.service.ts +++ b/packages/api/src/crm/note/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedNoteOutput } from '../types/model.unified'; import { INoteService } from '../types'; +import { crm_notes as CrmNote } from '@prisma/client'; +import { OriginalNoteOutput } from '@@core/utils/types/original/original.crm'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,295 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncNotes(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_notes table + //its role is to fetch all notes from providers 3rd parties and save the info inside our db + async syncNotes() { + try { + this.logger.log(`Syncing notes....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncNotesForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncNotesForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} notes for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping notes syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'note', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: INoteService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncNotes( + linkedUserId, + remoteProperties, + ); + + const sourceObject: OriginalNoteOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.note, + providerName: integrationId, + customFieldMappings, + })) as UnifiedNoteOutput[]; + + //TODO + const noteIds = sourceObject.map((note) => + 'id' in note ? String(note.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const notes_data = await this.saveNotesInDb( + linkedUserId, + unifiedObject, + noteIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.note.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + notes_data, + 'crm.note.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveNotesInDb( + linkedUserId: string, + notes: UnifiedNoteOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let notes_results: CrmNote[] = []; + for (let i = 0; i < notes.length; i++) { + const note = notes[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingNote = await this.prisma.crm_notes.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + }); + + let unique_crm_note_id: string; + + if (existingNote) { + // Update the existing note + let data: any = { + modified_at: new Date(), + }; + if (note.content) { + data = { ...data, content: note.content }; + } + if (note.contact_id) { + data = { ...data, id_crm_contact: note.contact_id }; + } + if (note.company_id) { + data = { ...data, id_crm_company: note.company_id }; + } + if (note.deal_id) { + data = { ...data, id_crm_deal: note.deal_id }; + } + + const res = await this.prisma.crm_notes.update({ + where: { + id_crm_note: existingNote.id_crm_note, + }, + data: data, + }); + unique_crm_note_id = res.id_crm_note; + notes_results = [...notes_results, res]; + } else { + // Create a new note + this.logger.log('note not exists'); + let data: any = { + id_crm_note: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + if (note.content) { + data = { ...data, content: note.content }; + } + if (note.contact_id) { + data = { ...data, id_crm_contact: note.contact_id }; + } + if (note.company_id) { + data = { ...data, id_crm_company: note.company_id }; + } + if (note.deal_id) { + data = { ...data, id_crm_deal: note.deal_id }; + } + + const res = await this.prisma.crm_notes.create({ + data: data, + }); + unique_crm_note_id = res.id_crm_note; + notes_results = [...notes_results, res]; + } + // check duplicate or existing values + if (note.field_mappings && note.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_note_id, + }, + }); + + for (const mapping of note.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_note_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_note_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return notes_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/stage/sync/sync.service.ts b/packages/api/src/crm/stage/sync/sync.service.ts index af22ac200..f6bb0a473 100644 --- a/packages/api/src/crm/stage/sync/sync.service.ts +++ b/packages/api/src/crm/stage/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedStageOutput } from '../types/model.unified'; import { IStageService } from '../types'; +import { crm_deals_stages as CrmStage } from '@prisma/client'; +import { OriginalStageOutput } from '@@core/utils/types/original/original.crm'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,280 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncStages(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_stages table + //its role is to fetch all stages from providers 3rd parties and save the info inside our db + async syncStages() { + try { + this.logger.log(`Syncing stages....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedStages = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedStages.map(async (linkedStage) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncStagesForLinkedStage( + provider, + linkedStage.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncStagesForLinkedStage( + integrationId: string, + linkedStageId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} stages for linkedStage ${linkedStageId}`, + ); + // check if linkedStage has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedStageId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping stages syncing... No ${integrationId} connection was found for linked stage ${linkedStageId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedStageId, + 'stage', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IStageService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncStages( + linkedStageId, + remoteProperties, + ); + + const sourceObject: OriginalStageOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.stage, + providerName: integrationId, + customFieldMappings, + })) as UnifiedStageOutput[]; + + //TODO + const stageIds = sourceObject.map((stage) => + 'id' in stage ? String(stage.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const stages_data = await this.saveStagesInDb( + linkedStageId, + unifiedObject, + stageIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.stage.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedStageId, + }, + }); + await this.webhook.handleWebhook( + stages_data, + 'crm.stage.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveStagesInDb( + linkedStageId: string, + stages: UnifiedStageOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let stages_results: CrmStage[] = []; + for (let i = 0; i < stages.length; i++) { + const stage = stages[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingStage = await this.prisma.crm_deals_stages.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedStageId, + }, + }); + + let unique_crm_stage_id: string; + + if (existingStage) { + // Update the existing stage + let data: any = { + modified_at: new Date(), + }; + + if (stage.stage_name) { + data = { ...data, stage_name: stage.stage_name }; + } + + const res = await this.prisma.crm_deals_stages.update({ + where: { + id_crm_deals_stage: existingStage.id_crm_deals_stage, + }, + data: data, + }); + unique_crm_stage_id = res.id_crm_deals_stage; + stages_results = [...stages_results, res]; + } else { + // Create a new stage + this.logger.log('stage not exists'); + let data: any = { + id_crm_stage: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedStageId, + remote_id: originId, + remote_platform: originSource, + }; + + if (stage.stage_name) { + data = { ...data, stage_name: stage.stage_name }; + } + const res = await this.prisma.crm_deals_stages.create({ + data: data, + }); + unique_crm_stage_id = res.id_crm_deals_stage; + stages_results = [...stages_results, res]; + } + + //TODO: + // check duplicate or existing values + if (stage.field_mappings && stage.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_stage_id, + }, + }); + + for (const mapping of stage.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedStageId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_stage_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_stage_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return stages_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/task/sync/sync.service.ts b/packages/api/src/crm/task/sync/sync.service.ts index c14491e0d..25acade54 100644 --- a/packages/api/src/crm/task/sync/sync.service.ts +++ b/packages/api/src/crm/task/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,6 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedTaskOutput } from '../types/model.unified'; import { ITaskService } from '../types'; +import { crm_tasks as CrmTask } from '@prisma/client'; +import { OriginalTaskOutput } from '@@core/utils/types/original/original.crm'; @Injectable() export class SyncService implements OnModuleInit { @@ -26,8 +28,319 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncTasks(); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_tasks table + //its role is to fetch all tasks from providers 3rd parties and save the info inside our db + async syncTasks() { + try { + this.logger.log(`Syncing tasks....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncTasksForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncTasksForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} tasks for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping tasks syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'task', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: ITaskService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncTasks( + linkedUserId, + remoteProperties, + ); + + const sourceObject: OriginalTaskOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.task, + providerName: integrationId, + customFieldMappings, + })) as UnifiedTaskOutput[]; + + //TODO + const taskIds = sourceObject.map((task) => + 'id' in task ? String(task.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const tasks_data = await this.saveTasksInDb( + linkedUserId, + unifiedObject, + taskIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.task.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + tasks_data, + 'crm.task.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + async saveTasksInDb( + linkedUserId: string, + tasks: UnifiedTaskOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let tasks_results: CrmTask[] = []; + for (let i = 0; i < tasks.length; i++) { + const task = tasks[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingTask = await this.prisma.crm_tasks.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + }); + + let unique_crm_task_id: string; + + if (existingTask) { + // Update the existing task + let data: any = { + modified_at: new Date(), + }; + if (task.subject) { + data = { ...data, subject: task.subject }; + } + if (task.content) { + data = { ...data, content: task.content }; + } + if (task.status) { + data = { ...data, status: task.status }; + } + if (task.due_date) { + data = { ...data, due_date: task.due_date }; + } + if (task.finished_date) { + data = { ...data, finished_date: task.finished_date }; + } + if (task.deal_id) { + data = { ...data, id_crm_deal: task.deal_id }; + } + if (task.user_id) { + data = { ...data, id_crm_user: task.user_id }; + } + if (task.company_id) { + data = { ...data, id_crm_company: task.company_id }; + } + + const res = await this.prisma.crm_tasks.update({ + where: { + id_crm_task: existingTask.id_crm_task, + }, + data: data, + }); + unique_crm_task_id = res.id_crm_task; + tasks_results = [...tasks_results, res]; + } else { + // Create a new task + this.logger.log('task not exists'); + let data: any = { + id_crm_task: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + + if (task.subject) { + data = { ...data, subject: task.subject }; + } + if (task.content) { + data = { ...data, content: task.content }; + } + if (task.status) { + data = { ...data, status: task.status }; + } + if (task.due_date) { + data = { ...data, due_date: task.due_date }; + } + if (task.finished_date) { + data = { ...data, finished_date: task.finished_date }; + } + if (task.deal_id) { + data = { ...data, id_crm_deal: task.deal_id }; + } + if (task.user_id) { + data = { ...data, id_crm_user: task.user_id }; + } + if (task.company_id) { + data = { ...data, id_crm_company: task.company_id }; + } + const res = await this.prisma.crm_tasks.create({ + data: data, + }); + unique_crm_task_id = res.id_crm_task; + tasks_results = [...tasks_results, res]; + } + // check duplicate or existing values + if (task.field_mappings && task.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_task_id, + }, + }); + + for (const mapping of task.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_task_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_task_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return tasks_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/crm/user/sync/sync.service.ts b/packages/api/src/crm/user/sync/sync.service.ts index 20ee06917..08a628049 100644 --- a/packages/api/src/crm/user/sync/sync.service.ts +++ b/packages/api/src/crm/user/sync/sync.service.ts @@ -3,7 +3,7 @@ import { LoggerService } from '@@core/logger/logger.service'; import { PrismaService } from '@@core/prisma/prisma.service'; import { NotFoundError, handleServiceError } from '@@core/utils/errors'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CRM_PROVIDERS } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -12,7 +12,8 @@ import { CrmObject } from '@crm/@utils/@types'; import { WebhookService } from '@@core/webhook/webhook.service'; import { UnifiedUserOutput } from '../types/model.unified'; import { IUserService } from '../types'; - +import { crm_users as CrmUser } from '@prisma/client'; +import { OriginalUserOutput } from '@@core/utils/types/original/original.crm'; @Injectable() export class SyncService implements OnModuleInit { constructor( @@ -26,8 +27,285 @@ export class SyncService implements OnModuleInit { } async onModuleInit() { - // Initialization logic + try { + await this.syncUsers(); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + @Cron('*/20 * * * *') + //function used by sync worker which populate our crm_users table + //its role is to fetch all users from providers 3rd parties and save the info inside our db + async syncUsers() { + try { + this.logger.log(`Syncing users....`); + const defaultOrg = await this.prisma.organizations.findFirst({ + where: { + name: 'Acme Inc', + }, + }); + + const defaultProject = await this.prisma.projects.findFirst({ + where: { + id_organization: defaultOrg.id_organization, + name: 'Project 1', + }, + }); + const id_project = defaultProject.id_project; + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { + id_project: id_project, + }, + }); + linkedUsers.map(async (linkedUser) => { + try { + const providers = CRM_PROVIDERS.filter( + (provider) => provider !== 'zoho' && provider !== 'freshsales', + ); + for (const provider of providers) { + try { + await this.syncUsersForLinkedUser( + provider, + linkedUser.id_linked_user, + id_project, + ); + } catch (error) { + handleServiceError(error, this.logger); + } + } + } catch (error) { + handleServiceError(error, this.logger); + } + }); + } catch (error) { + handleServiceError(error, this.logger); + } + } + + //todo: HANDLE DATA REMOVED FROM PROVIDER + async syncUsersForLinkedUser( + integrationId: string, + linkedUserId: string, + id_project: string, + ) { + try { + this.logger.log( + `Syncing ${integrationId} users for linkedUser ${linkedUserId}`, + ); + // check if linkedUser has a connection if not just stop sync + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: integrationId, + }, + }); + if (!connection) { + this.logger.warn( + `Skipping users syncing... No ${integrationId} connection was found for linked user ${linkedUserId} `, + ); + return; + } + // get potential fieldMappings and extract the original properties name + const customFieldMappings = + await this.fieldMappingService.getCustomFieldMappings( + integrationId, + linkedUserId, + 'user', + ); + const remoteProperties: string[] = customFieldMappings.map( + (mapping) => mapping.remote_id, + ); + + const service: IUserService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.syncUsers( + linkedUserId, + remoteProperties, + ); + + const sourceObject: OriginalUserOutput[] = resp.data; + //this.logger.log('SOURCE OBJECT DATA = ' + JSON.stringify(sourceObject)); + //unify the data according to the target obj wanted + const unifiedObject = (await unify({ + sourceObject, + targetType: CrmObject.user, + providerName: integrationId, + customFieldMappings, + })) as UnifiedUserOutput[]; + + //TODO + const userIds = sourceObject.map((user) => + 'id' in user ? String(user.id) : undefined, + ); + + //insert the data in the DB with the fieldMappings (value table) + const users_data = await this.saveUsersInDb( + linkedUserId, + unifiedObject, + userIds, + integrationId, + sourceObject, + ); + const event = await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'crm.user.pulled', + method: 'PULL', + url: '/pull', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + await this.webhook.handleWebhook( + users_data, + 'crm.user.pulled', + id_project, + event.id_event, + ); + } catch (error) { + handleServiceError(error, this.logger); + } } - // Additional methods and logic + async saveUsersInDb( + linkedUserId: string, + users: UnifiedUserOutput[], + originIds: string[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + let users_results: CrmUser[] = []; + for (let i = 0; i < users.length; i++) { + const user = users[i]; + const originId = originIds[i]; + + if (!originId || originId == '') { + throw new NotFoundError(`Origin id not there, found ${originId}`); + } + + const existingUser = await this.prisma.crm_users.findFirst({ + where: { + remote_id: originId, + remote_platform: originSource, + id_linked_user: linkedUserId, + }, + }); + + let unique_crm_user_id: string; + + if (existingUser) { + // Update the existing user + let data: any = { + modified_at: new Date(), + }; + + if (user.email) { + data = { ...data, email: user.email }; + } + if (user.name) { + data = { ...data, name: user.name }; + } + + const res = await this.prisma.crm_users.update({ + where: { + id_crm_user: existingUser.id_crm_user, + }, + data: data, + }); + unique_crm_user_id = res.id_crm_user; + users_results = [...users_results, res]; + } else { + // Create a new user + this.logger.log('user not exists'); + let data: any = { + id_crm_user: uuidv4(), + created_at: new Date(), + modified_at: new Date(), + id_linked_user: linkedUserId, + remote_id: originId, + remote_platform: originSource, + }; + + if (user.email) { + data = { ...data, email: user.email }; + } + if (user.name) { + data = { ...data, name: user.name }; + } + const res = await this.prisma.crm_users.create({ + data: data, + }); + unique_crm_user_id = res.id_crm_user; + users_results = [...users_results, res]; + } + + // check duplicate or existing values + if (user.field_mappings && user.field_mappings.length > 0) { + const entity = await this.prisma.entity.create({ + data: { + id_entity: uuidv4(), + ressource_owner_id: unique_crm_user_id, + }, + }); + + for (const mapping of user.field_mappings) { + const attribute = await this.prisma.attribute.findFirst({ + where: { + slug: Object.keys(mapping)[0], + source: originSource, + id_consumer: linkedUserId, + }, + }); + + if (attribute) { + await this.prisma.value.create({ + data: { + id_value: uuidv4(), + data: Object.values(mapping)[0] + ? Object.values(mapping)[0] + : 'null', + attribute: { + connect: { + id_attribute: attribute.id_attribute, + }, + }, + entity: { + connect: { + id_entity: entity.id_entity, + }, + }, + }, + }); + } + } + } + + //insert remote_data in db + await this.prisma.remote_data.upsert({ + where: { + ressource_owner_id: unique_crm_user_id, + }, + create: { + id_remote_data: uuidv4(), + ressource_owner_id: unique_crm_user_id, + format: 'json', + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + update: { + data: JSON.stringify(remote_data[i]), + created_at: new Date(), + }, + }); + } + return users_results; + } catch (error) { + handleServiceError(error, this.logger); + } + } } diff --git a/packages/api/src/ticketing/@utils/@types/index.ts b/packages/api/src/ticketing/@utils/@types/index.ts index 9861956f8..d31ed4846 100644 --- a/packages/api/src/ticketing/@utils/@types/index.ts +++ b/packages/api/src/ticketing/@utils/@types/index.ts @@ -1,4 +1,3 @@ -import { contactUnificationMapping } from '@crm/contact/types/mappingsTypes'; import { contactUnificationMapping as contactTicketingUnificationMapping } from '@ticketing/contact/types/mappingsTypes'; import { IAccountService } from '@ticketing/account/types'; import { accountUnificationMapping } from '@ticketing/account/types/mappingsTypes'; @@ -96,7 +95,7 @@ export type ITicketingService = | ITeamService | ITagService; -//TODO; export everything +/*TODO: export all providers */ export * from '../../ticket/services/zendesk/types'; export * from '../../comment/services/zendesk/types'; export * from '../../user/services/zendesk/types'; diff --git a/packages/api/src/ticketing/@utils/@unification/index.ts b/packages/api/src/ticketing/@utils/@unification/index.ts index 49c3b2a61..d89670f84 100644 --- a/packages/api/src/ticketing/@utils/@unification/index.ts +++ b/packages/api/src/ticketing/@utils/@unification/index.ts @@ -1,6 +1,6 @@ import { Unified, UnifyReturnType } from '@@core/utils/types'; import { TicketingObjectInput } from '@@core/utils/types/original/original.ticketing'; -import { UnifySourceType } from '@@core/utils/types/unfify.output'; +import { UnifySourceType } from '@@core/utils/types/unify.output'; import { TicketingObject, unificationMapping } from '@ticketing/@utils/@types'; export async function desunifyTicketing({