From cc1f8ef6ffd0a566d642ba4f67821c5bdd104931 Mon Sep 17 00:00:00 2001 From: mit-27 Date: Mon, 26 Aug 2024 01:31:26 -0600 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Add=20Microsoft=20Dynamics=2036?= =?UTF-8?q?5=20Sales=20CRM=20{Company,=20Contact,=20Deal,=20Note,=20Task,?= =?UTF-8?q?=20User}=20objects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/scripts/init.sql | 3 +- packages/api/scripts/seed.sql | 8 +- .../utils/types/original/original.crm.ts | 36 +- .../api/src/crm/company/company.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 97 ++++++ .../microsoftdynamicssales/mappers.ts | 309 ++++++++++++++++++ .../services/microsoftdynamicssales/types.ts | 153 +++++++++ .../api/src/crm/contact/contact.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 99 ++++++ .../microsoftdynamicssales/mappers.ts | 235 +++++++++++++ .../services/microsoftdynamicssales/types.ts | 228 +++++++++++++ packages/api/src/crm/deal/deal.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 96 ++++++ .../microsoftdynamicssales/mappers.ts | 130 ++++++++ .../services/microsoftdynamicssales/types.ts | 125 +++++++ packages/api/src/crm/note/note.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 93 ++++++ .../microsoftdynamicssales/mappers.ts | 151 +++++++++ .../services/microsoftdynamicssales/types.ts | 32 ++ .../services/microsoftdynamicssales/index.ts | 94 ++++++ .../microsoftdynamicssales/mappers.ts | 165 ++++++++++ .../services/microsoftdynamicssales/types.ts | 70 ++++ packages/api/src/crm/task/task.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 67 ++++ .../microsoftdynamicssales/mappers.ts | 71 ++++ .../services/microsoftdynamicssales/types.ts | 123 +++++++ packages/api/src/crm/user/user.module.ts | 4 + packages/shared/src/authUrl.ts | 4 +- packages/shared/src/connectors/enum.ts | 3 +- packages/shared/src/connectors/index.ts | 2 +- packages/shared/src/connectors/metadata.ts | 2 +- 31 files changed, 2398 insertions(+), 22 deletions(-) create mode 100644 packages/api/src/crm/company/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/company/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/company/services/microsoftdynamicssales/types.ts create mode 100644 packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/contact/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/contact/services/microsoftdynamicssales/types.ts create mode 100644 packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/deal/services/microsoftdynamicssales/types.ts create mode 100644 packages/api/src/crm/note/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/note/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/note/services/microsoftdynamicssales/types.ts create mode 100644 packages/api/src/crm/task/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/task/services/microsoftdynamicssales/types.ts create mode 100644 packages/api/src/crm/user/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/user/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/user/services/microsoftdynamicssales/types.ts diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 71a8c80ec..4aa51a322 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -549,7 +549,8 @@ CREATE TABLE connector_sets ecom_amazon boolean NULL, ecom_squarespace boolean NULL, ats_ashby boolean NULL, - CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) + crm_microsoftdynamicssales boolean NULL, +CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) ); diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index 233841fa2..9acc8bff4 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -1,10 +1,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES ('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora'); -INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby) VALUES - ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); +INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, crm_microsoftdynamicssales) VALUES + ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'), diff --git a/packages/api/src/@core/utils/types/original/original.crm.ts b/packages/api/src/@core/utils/types/original/original.crm.ts index aaeae0263..2e64529d6 100644 --- a/packages/api/src/@core/utils/types/original/original.crm.ts +++ b/packages/api/src/@core/utils/types/original/original.crm.ts @@ -1,3 +1,15 @@ +import { MicrosoftdynamicssalesTaskInput, MicrosoftdynamicssalesTaskOutput } from '@crm/task/services/microsoftdynamicssales/types'; + +import { MicrosoftdynamicssalesNoteInput, MicrosoftdynamicssalesNoteOutput } from '@crm/note/services/microsoftdynamicssales/types'; + +import { MicrosoftdynamicssalesDealInput, MicrosoftdynamicssalesDealOutput } from '@crm/deal/services/microsoftdynamicssales/types'; + +import { MicrosoftdynamicssalesContactInput, MicrosoftdynamicssalesContactOutput } from '@crm/contact/services/microsoftdynamicssales/types'; + +import { MicrosoftdynamicssalesCompanyInput, MicrosoftdynamicssalesCompanyOutput } from '@crm/company/services/microsoftdynamicssales/types'; + +import { MicrosoftdynamicssalesUserInput, MicrosoftdynamicssalesUserOutput } from '@crm/user/services/microsoftdynamicssales/types'; + import { AttioCompanyOutput } from '@crm/company/services/attio/types'; import { HubspotCompanyOutput } from '@crm/company/services/hubspot/types'; import { PipedriveCompanyOutput } from '@crm/company/services/pipedrive/types'; @@ -148,7 +160,7 @@ export type OriginalContactInput = | ZendeskContactInput | PipedriveContactInput | AttioContactInput - | CloseContactInput; + | CloseContactInput | MicrosoftdynamicssalesContactInput; /* deal */ export type OriginalDealInput = @@ -157,7 +169,7 @@ export type OriginalDealInput = | ZendeskDealOutput | PipedriveDealOutput | CloseDealOutput - | AttioDealInput; + | AttioDealInput | MicrosoftdynamicssalesDealInput; /* company */ export type OriginalCompanyInput = @@ -166,7 +178,7 @@ export type OriginalCompanyInput = | ZendeskCompanyOutput | PipedriveCompanyOutput | AttioCompanyOutput - | CloseCompanyOutput; + | CloseCompanyOutput | MicrosoftdynamicssalesCompanyInput; /* engagement */ export type OriginalEngagementInput = @@ -183,7 +195,7 @@ export type OriginalNoteInput = | ZendeskNoteInput | PipedriveNoteInput | CloseNoteInput - | AttioNoteInput; + | AttioNoteInput | MicrosoftdynamicssalesNoteInput; /* task */ export type OriginalTaskInput = @@ -192,7 +204,7 @@ export type OriginalTaskInput = | ZendeskTaskInput | PipedriveTaskInput | CloseTaskInput - | AttioTaskInput; + | AttioTaskInput | MicrosoftdynamicssalesTaskInput; /* stage */ export type OriginalStageInput = @@ -210,7 +222,7 @@ export type OriginalUserInput = | ZohoUserInput | ZendeskUserInput | PipedriveUserInput - | CloseUserOutput; + | CloseUserOutput | MicrosoftdynamicssalesUserInput; export type CrmObjectInput = | OriginalContactInput @@ -230,7 +242,7 @@ export type OriginalContactOutput = | ZendeskContactOutput | PipedriveContactOutput | AttioContactOutput - | CloseContactOutput; + | CloseContactOutput | MicrosoftdynamicssalesContactOutput; /* deal */ export type OriginalDealOutput = @@ -239,7 +251,7 @@ export type OriginalDealOutput = | ZendeskDealOutput | PipedriveDealOutput | CloseDealOutput - | AttioDealOutput; + | AttioDealOutput | MicrosoftdynamicssalesDealOutput; /* company */ export type OriginalCompanyOutput = @@ -248,7 +260,7 @@ export type OriginalCompanyOutput = | ZendeskCompanyOutput | PipedriveCompanyOutput | AttioCompanyOutput - | CloseCompanyOutput; + | CloseCompanyOutput | MicrosoftdynamicssalesCompanyOutput; /* engagement */ export type OriginalEngagementOutput = @@ -265,7 +277,7 @@ export type OriginalNoteOutput = | ZendeskNoteOutput | PipedriveNoteOutput | CloseNoteOutput - | AttioNoteOutput; + | AttioNoteOutput | MicrosoftdynamicssalesNoteOutput; /* task */ export type OriginalTaskOutput = @@ -274,7 +286,7 @@ export type OriginalTaskOutput = | ZendeskTaskOutput | PipedriveTaskOutput | CloseTaskOutput - | AttioTaskOutput; + | AttioTaskOutput | MicrosoftdynamicssalesTaskOutput; /* stage */ export type OriginalStageOutput = @@ -293,7 +305,7 @@ export type OriginalUserOutput = | ZendeskUserOutput | PipedriveUserOutput | CloseUserInput - | AttioUserOutput; + | AttioUserOutput | MicrosoftdynamicssalesUserOutput; export type CrmObjectOutput = | OriginalContactOutput diff --git a/packages/api/src/crm/company/company.module.ts b/packages/api/src/crm/company/company.module.ts index a77852e29..e822f855d 100644 --- a/packages/api/src/crm/company/company.module.ts +++ b/packages/api/src/crm/company/company.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesCompanyMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; @@ -52,6 +54,8 @@ import { SyncService } from './sync/sync.service'; PipedriveCompanyMapper, ZendeskCompanyMapper, ZohoCompanyMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesCompanyMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..b04664a29 --- /dev/null +++ b/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { CrmObject } from '@crm/@lib/@types'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { ICompanyService } from '@crm/company/types'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesCompanyInput, MicrosoftdynamicssalesCompanyOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalCompanyOutput } from '@@core/utils/types/original/original.crm'; + +@Injectable() +export class MicrosoftdynamicssalesService implements ICompanyService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.company.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + async addCompany( + companyData: MicrosoftdynamicssalesCompanyInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const resp = await axios.post( + `${connection.account_url}/api/data/v9.2/accounts`, + JSON.stringify({ + data: companyData, + }), + { + headers: { + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + 'Content-Type': 'application/json', + }, + }, + ); + return { + data: resp.data.value, + message: 'Microsoftdynamicssales company created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/accounts`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data.value, + message: 'Microsoftdynamicssales companies retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/company/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/company/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..3e4db043a --- /dev/null +++ b/packages/api/src/crm/company/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,309 @@ +import { MicrosoftdynamicssalesCompanyInput, MicrosoftdynamicssalesCompanyOutput } from './types'; +import { + UnifiedCrmCompanyInput, + UnifiedCrmCompanyOutput, +} from '@crm/company/types/model.unified'; +import { ICompanyMapper } from '@crm/company/types'; +import { Utils } from '@crm/@lib/@utils'; +import { Injectable } from '@nestjs/common'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { getCountryCode, getCountryName } from '@@core/utils/types'; + +@Injectable() +export class MicrosoftdynamicssalesCompanyMapper implements ICompanyMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'company', 'microsoftdynamicssales', this); + } + async desunify( + source: UnifiedCrmCompanyInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesCompanyInput = { + name: source.name, + } + + const industryToCodeJSON = { + "accounting": 1, + "agriculture and non-petrol natural resource extraction": 2, + "broadcasting printing and publishing": 3, + "brokers": 4, + "building supply retail": 5, + "business services": 6, + "consulting": 7, + "consumer services": 8, + "design, direction and creative management": 9, + "distributors, dispatchers and processors": 10, + "doctor's offices and clinics": 11, + "durable manufacturing": 12, + "eating and drinking places": 13, + "entertainment retail": 14, + "equipment rental and leasing": 15, + "financial": 16, + "food and tobacco processing": 17, + "inbound capital intensive processing": 18, + "inbound repair and services": 19, + "insurance": 20, + "legal services": 21, + "non-durable merchandise retail": 22, + "outbound consumer service": 23, + "petrochemical extraction and distribution": 24, + "service retail": 25, + "sig affiliations": 26, + "social services": 27, + "special outbound trade contractors": 28, + "specialty realty": 29, + "transportation": 30, + "utility creation and distribution": 31, + "vehicle retail": 32, + "wholesale": 33 + } + + if (source.industry) { + result.industrycode = industryToCodeJSON[source.industry.toLowerCase()]; + } + + if (source.number_of_employees) { + result.numberofemployees = source.number_of_employees; + } + + if (source.email_addresses[0]) { + result.emailaddress1 = source.email_addresses[0]?.email_address; + } + if (source.email_addresses[1]) { + result.emailaddress2 = source.email_addresses[1]?.email_address; + } + if (source.email_addresses[2]) { + result.emailaddress2 = source.email_addresses[2]?.email_address; + } + if (source.addresses[0]) { + result.address1_line1 = source.addresses[0].street_1; + result.address1_city = source.addresses[0].city; + result.address1_country = source.addresses[0].country; + result.address1_stateorprovince = source.addresses[0].state; + result.address1_postalcode = source.addresses[0].postal_code; + } + if (source.addresses[1]) { + result.address2_line1 = source.addresses[1].street_1; + result.address2_city = source.addresses[1].city; + result.address2_country = source.addresses[1].country; + result.address2_stateorprovince = source.addresses[1].state; + result.address2_postalcode = source.addresses[1].postal_code; + } + + if (source.phone_numbers[0]) { + result.telephone1 = source.phone_numbers[0].phone_number; + } + if (source.phone_numbers[1]) { + result.telephone2 = source.phone_numbers[1].phone_number; + } + if (source.phone_numbers[2]) { + result.telephone3 = source.phone_numbers[2].phone_number; + } + + + + + // Cannot set read only property _createdby_value + // if (source.user_id) { + // const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + // if (owner_id) { + // result._createdby_value = source.user_id; + // } + // } + + // Microsoftdynamicssales company does not have attribute for email address + // Microsoftdynamicssales Company doest not have direct mapping of number of employees + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + async unify( + source: MicrosoftdynamicssalesCompanyOutput | MicrosoftdynamicssalesCompanyOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleCompanyToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesCompanyOutput + return Promise.all( + source.map((company) => + this.mapSingleCompanyToUnified( + company, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCompanyToUnified( + company: MicrosoftdynamicssalesCompanyOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = company[mapping.remote_id]; + } + } + + let opts: any = { + addresses: [], + email_addresses: [], + phone_numbers: [] + }; + + const industryJSON = { + 1: "Accounting", + 2: "Agriculture and Non-petrol Natural Resource Extraction", + 3: "Broadcasting Printing and Publishing", + 4: "Brokers", + 5: "Building Supply Retail", + 6: "Business Services", + 7: "Consulting", + 8: "Consumer Services", + 9: "Design, Direction and Creative Management", + 10: "Distributors, Dispatchers and Processors", + 11: "Doctor's Offices and Clinics", + 12: "Durable Manufacturing", + 13: "Eating and Drinking Places", + 14: "Entertainment Retail", + 15: "Equipment Rental and Leasing", + 16: "Financial", + 17: "Food and Tobacco Processing", + 18: "Inbound Capital Intensive Processing", + 19: "Inbound Repair and Services", + 20: "Insurance", + 21: "Legal Services", + 22: "Non-Durable Merchandise Retail", + 23: "Outbound Consumer Service", + 24: "Petrochemical Extraction and Distribution", + 25: "Service Retail", + 26: "SIG Affiliations", + 27: "Social Services", + 28: "Special Outbound Trade Contractors", + 29: "Specialty Realty", + 30: "Transportation", + 31: "Utility Creation and Distribution", + 32: "Vehicle Retail", + 33: "Wholesale" + } + + let address: any = {}; + + if (company.address1_line1) { + address = { + street_1: company.address1_line1 ?? '', + city: company.address1_city ?? '', + state: company.address1_stateorprovince ?? '', + postal_code: company.address2_postalcode ?? '', + country: company.address1_country ?? '', + address_type: 'WORK', + owner_type: 'COMPANY', + } + opts.addresses.push(address); + } + if (company.address2_line1) { + address = { + street_1: company.address2_line1 ?? '', + city: company.address2_city ?? '', + state: company.address2_stateorprovince ?? '', + postal_code: company.address2_postalcode ?? '', + country: company.address2_country ?? '', + address_type: 'WORK', + owner_type: 'COMPANY', + } + opts.addresses.push(address); + } + + + if (company.emailaddress1) { + opts.email_addresses.push({ + email_address: company.emailaddress1, + email_address_type: "WORK" + }) + } + if (company.emailaddress2) { + opts.email_addresses.push({ + email_address: company.emailaddress2, + email_address_type: "WORK" + }) + } + if (company.emailaddress2) { + opts.email_addresses.push({ + email_address: company.emailaddress3, + email_address_type: "WORK" + }) + } + if (company.telephone1) { + opts.phone_numbers.push({ + phone_number: company.telephone1, + phone_type: "MOBILE" + }); + } + if (company.telephone2) { + opts.phone_numbers.push({ + phone_number: company.telephone2, + phone_type: "MOBILE" + }); + } + if (company.telephone3) { + opts.phone_numbers.push({ + phone_number: company.telephone3, + phone_type: "MOBILE" + }); + } + + + + if (company._createdby_value) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + company._createdby_value, + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + return { + remote_id: company.accountid, + remote_data: company, + name: company.name, + industry: + company.industrycode ? industryJSON[company.industrycode] : null, + number_of_employees: company.numberofemployees ?? null, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/company/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/company/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..96232c05f --- /dev/null +++ b/packages/api/src/crm/company/services/microsoftdynamicssales/types.ts @@ -0,0 +1,153 @@ +interface MicrosoftdynamicssalesCompany { + accountcategorycode: number; // Edm.Int32 + accountclassificationcode: number; // Edm.Int32 + accountid: string; // Edm.Guid + accountnumber: string; // Edm.String + accountratingcode: number; // Edm.Int32 + address1_addressid: string; // Edm.Guid + address1_addresstypecode: number; // Edm.Int32 + address1_city: string; // Edm.String + address1_composite: string; // Edm.String + address1_country: string; // Edm.String + address1_county: string; // Edm.String + address1_fax: string; // Edm.String + address1_freighttermscode: number; // Edm.Int32 + address1_latitude: number; // Edm.Double + address1_line1: string; // Edm.String + address1_line2: string; // Edm.String + address1_line3: string; // Edm.String + address1_longitude: number; // Edm.Double + address1_name: string; // Edm.String + address1_postalcode: string; // Edm.String + address1_postofficebox: string; // Edm.String + address1_primarycontactname: string; // Edm.String + address1_shippingmethodcode: number; // Edm.Int32 + address1_stateorprovince: string; // Edm.String + address1_telephone1: string; // Edm.String + address1_telephone2: string; // Edm.String + address1_telephone3: string; // Edm.String + address1_upszone: string; // Edm.String + address1_utcoffset: number; // Edm.Int32 + address2_addressid: string; // Edm.Guid + address2_addresstypecode: number; // Edm.Int32 + address2_city: string; // Edm.String + address2_composite: string; // Edm.String + address2_country: string; // Edm.String + address2_county: string; // Edm.String + address2_fax: string; // Edm.String + address2_freighttermscode: number; // Edm.Int32 + address2_latitude: number; // Edm.Double + address2_line1: string; // Edm.String + address2_line2: string; // Edm.String + address2_line3: string; // Edm.String + address2_longitude: number; // Edm.Double + address2_name: string; // Edm.String + address2_postalcode: string; // Edm.String + address2_postofficebox: string; // Edm.String + address2_primarycontactname: string; // Edm.String + address2_shippingmethodcode: number; // Edm.Int32 + address2_stateorprovince: string; // Edm.String + address2_telephone1: string; // Edm.String + address2_telephone2: string; // Edm.String + address2_telephone3: string; // Edm.String + address2_upszone: string; // Edm.String + address2_utcoffset: number; // Edm.Int32 + adx_createdbyipaddress: string; // Edm.String + adx_createdbyusername: string; // Edm.String + adx_modifiedbyipaddress: string; // Edm.String + adx_modifiedbyusername: string; // Edm.String + aging30: number; // Edm.Decimal + aging30_base: number; // Edm.Decimal + aging60: number; // Edm.Decimal + aging60_base: number; // Edm.Decimal + aging90: number; // Edm.Decimal + aging90_base: number; // Edm.Decimal + businesstypecode: number; // Edm.Int32 + createdon: string; // Edm.DateTimeOffset + creditlimit: number; // Edm.Decimal + creditlimit_base: number; // Edm.Decimal + creditonhold: boolean; // Edm.Boolean + customersizecode: number; // Edm.Int32 + customertypecode: number; // Edm.Int32 + description: string; // Edm.String + donotbulkemail: boolean; // Edm.Boolean + donotbulkpostalmail: boolean; // Edm.Boolean + donotemail: boolean; // Edm.Boolean + donotfax: boolean; // Edm.Boolean + donotphone: boolean; // Edm.Boolean + donotpostalmail: boolean; // Edm.Boolean + donotsendmm: boolean; // Edm.Boolean + emailaddress1: string; // Edm.String + emailaddress2: string; // Edm.String + emailaddress3: string; // Edm.String + entityimage: Uint8Array; // Edm.Binary + entityimage_timestamp: number; // Edm.Int64 + entityimage_url: string; // Edm.String + entityimageid: string; // Edm.Guid + exchangerate: number; // Edm.Decimal + fax: string; // Edm.String + followemail: boolean; // Edm.Boolean + ftpsiteurl: string; // Edm.String + importsequencenumber: number; // Edm.Int32 + industrycode: number; // Edm.Int32 + lastonholdtime: string; // Edm.DateTimeOffset + lastusedincampaign: string; // Edm.DateTimeOffset + marketcap: number; // Edm.Decimal + marketcap_base: number; // Edm.Decimal + marketingonly: boolean; // Edm.Boolean + merged: boolean; // Edm.Boolean + modifiedon: string; // Edm.DateTimeOffset + name: string; // Edm.String + numberofemployees: number; // Edm.Int32 + onholdtime: number; // Edm.Int32 + overriddencreatedon: string; // Edm.DateTimeOffset + ownershipcode: number; // Edm.Int32 + participatesinworkflow: boolean; // Edm.Boolean + paymenttermscode: number; // Edm.Int32 + preferredappointmentdaycode: number; // Edm.Int32 + preferredappointmenttimecode: number; // Edm.Int32 + preferredcontactmethodcode: number; // Edm.Int32 + primarysatoriid: string; // Edm.String + primarytwitterid: string; // Edm.String + processid: string; // Edm.Guid + revenue: number; // Edm.Decimal + revenue_base: number; // Edm.Decimal + sharesoutstanding: number; // Edm.Int32 + shippingmethodcode: number; // Edm.Int32 + sic: string; // Edm.String + stageid: string; // Edm.Guid + statecode: number; // Edm.Int32 + statuscode: number; // Edm.Int32 + stockexchange: string; // Edm.String + telephone1: string; // Edm.String + telephone2: string; // Edm.String + telephone3: string; // Edm.String + territorycode: number; // Edm.Int32 + tickersymbol: string; // Edm.String + timezoneruleversionnumber: number; // Edm.Int32 + transactioncurrencyid: string; // Edm.Guid + traversedpath: string; // Edm.String + utcconversiontimezonecode: number; // Edm.Int32 + versionnumber: number; // Edm.Int64 + websiteurl: string; // Edm.String + yominame: string; // Edm.String + _createdby_value: string; + _createdonbehalfby_value: string; + _masterid_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _msa_managingpartnerid_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _parentaccountid_value: string; + _preferredsystemuserid_value: string; + _primarycontactid_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _transactioncurrencyid_value: string; +} + +export type MicrosoftdynamicssalesCompanyInput = Partial; +export type MicrosoftdynamicssalesCompanyOutput = MicrosoftdynamicssalesCompanyInput; \ No newline at end of file diff --git a/packages/api/src/crm/contact/contact.module.ts b/packages/api/src/crm/contact/contact.module.ts index 9cda6805c..56e51789a 100644 --- a/packages/api/src/crm/contact/contact.module.ts +++ b/packages/api/src/crm/contact/contact.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesContactMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; @@ -45,6 +47,8 @@ import { SyncService } from './sync/sync.service'; PipedriveContactMapper, ZendeskContactMapper, ZohoContactMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesContactMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..f585e6966 --- /dev/null +++ b/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@nestjs/common'; +import { IContactService } from '@crm/contact/types'; +import { CrmObject } from '@crm/@lib/@types'; +import axios from 'axios'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesContactInput, MicrosoftdynamicssalesContactOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class MicrosoftdynamicssalesService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + + async addContact( + contactData: MicrosoftdynamicssalesContactInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const resp = await axios.post( + `${connection.account_url}/api/data/v9.2/contacts`, + JSON.stringify({ + data: contactData, + }), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data.value, + message: 'microsoftdynamicssales contact created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/contacts`, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp.data.value, + message: 'Microsoftdynamicssales contacts retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/contact/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/contact/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..d9c8acdce --- /dev/null +++ b/packages/api/src/crm/contact/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,235 @@ +import { + UnifiedCrmContactInput, + UnifiedCrmContactOutput, +} from '@crm/contact/types/model.unified'; +import { IContactMapper } from '@crm/contact/types'; +import { MicrosoftdynamicssalesContactInput, MicrosoftdynamicssalesContactOutput } from './types'; +import { Utils } from '@crm/@lib/@utils'; +import { Injectable } from '@nestjs/common'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; + +@Injectable() +export class MicrosoftdynamicssalesContactMapper implements IContactMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'contact', 'microsoftdynamicssales', this); + } + + async desunify( + source: UnifiedCrmContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + + + const result: MicrosoftdynamicssalesContactInput = { + firstname: source.first_name, + lastname: source.last_name, + fullname: `${source.first_name} ${source.last_name}`, + }; + if (source.email_addresses[0]) { + result.emailaddress1 = source.email_addresses[0]?.email_address; + } + if (source.email_addresses[1]) { + result.emailaddress2 = source.email_addresses[1]?.email_address; + } + if (source.email_addresses[2]) { + result.emailaddress2 = source.email_addresses[2]?.email_address; + } + if (source.addresses[0]) { + result.address1_line1 = source.addresses[0].street_1; + result.address1_city = source.addresses[0].city; + result.address1_country = source.addresses[0].country; + result.address1_stateorprovince = source.addresses[0].state; + result.address1_postalcode = source.addresses[0].postal_code; + } + if (source.addresses[1]) { + result.address2_line1 = source.addresses[1].street_1; + result.address2_city = source.addresses[1].city; + result.address2_country = source.addresses[1].country; + result.address2_stateorprovince = source.addresses[1].state; + result.address2_postalcode = source.addresses[1].postal_code; + } + if (source.addresses[2]) { + result.address3_line1 = source.addresses[2].street_1; + result.address3_city = source.addresses[2].city; + result.address3_country = source.addresses[2].country; + result.address3_stateorprovince = source.addresses[2].state; + result.address3_postalcode = source.addresses[2].postal_code; + } + + if (source.phone_numbers[0]) { + result.telephone1 = source.phone_numbers[0].phone_number; + } + if (source.phone_numbers[1]) { + result.telephone2 = source.phone_numbers[1].phone_number; + } + if (source.phone_numbers[2]) { + result.telephone3 = source.phone_numbers[2].phone_number; + } + + + + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + return result; + } + + async unify( + source: MicrosoftdynamicssalesContactOutput | MicrosoftdynamicssalesContactOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleContactToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + // Handling array of HubspotContactOutput + return Promise.all( + source.map((contact) => + this.mapSingleContactToUnified( + contact, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleContactToUnified( + contact: MicrosoftdynamicssalesContactOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = contact[mapping.remote_id]; + } + } + + let opts: any = { + addresses: [], + email_addresses: [], + phone_numbers: [] + }; + + let address: any = {}; + + if (contact.address1_line1) { + address = { + street_1: contact.address1_line1 ?? '', + city: contact.address1_city ?? '', + state: contact.address1_stateorprovince ?? '', + postal_code: contact.address2_postalcode ?? '', + country: contact.address1_country ?? '', + address_type: 'PERSONAL', + owner_type: 'CONTACT', + } + opts.addresses.push(address); + } + if (contact.address2_line1) { + address = { + street_1: contact.address2_line1 ?? '', + city: contact.address2_city ?? '', + state: contact.address2_stateorprovince ?? '', + postal_code: contact.address2_postalcode ?? '', + country: contact.address2_country ?? '', + address_type: 'PERSONAL', + owner_type: 'CONTACT', + } + opts.addresses.push(address); + } + if (contact.address3_line1) { + address = { + street_1: contact.address3_line1 ?? '', + city: contact.address3_city ?? '', + state: contact.address3_stateorprovince ?? '', + postal_code: contact.address3_postalcode ?? '', + country: contact.address3_country ?? '', + address_type: 'PERSONAL', + owner_type: 'CONTACT', + } + opts.addresses.push(address); + } + + if (contact.emailaddress1) { + opts.email_addresses.push({ + email_address: contact.emailaddress1, + email_address_type: "PERSONAL" + }) + } + if (contact.emailaddress2) { + opts.email_addresses.push({ + email_address: contact.emailaddress2, + email_address_type: "PERSONAL" + }) + } + if (contact.emailaddress2) { + opts.email_addresses.push({ + email_address: contact.emailaddress3, + email_address_type: "PERSONAL" + }) + } + if (contact.telephone1) { + opts.phone_numbers.push({ + phone_number: contact.telephone1, + phone_type: "MOBILE" + }); + } + if (contact.telephone2) { + opts.phone_numbers.push({ + phone_number: contact.telephone2, + phone_type: "MOBILE" + }); + } + if (contact.telephone3) { + opts.phone_numbers.push({ + phone_number: contact.telephone3, + phone_type: "MOBILE" + }); + } + + if (contact._createdby_value) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + contact._createdby_value, + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + return { + remote_id: contact.contactid, + remote_data: contact, + first_name: contact.firstname ?? '', + last_name: contact.lastname ?? '', + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/contact/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/contact/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..9bb1a30f9 --- /dev/null +++ b/packages/api/src/crm/contact/services/microsoftdynamicssales/types.ts @@ -0,0 +1,228 @@ +interface MicrosoftdynamicssalesContact { + accountrolecode: number; + address1_addressid: string; + address1_addresstypecode: number; + address1_city: string; + address1_composite: string; + address1_country: string; + address1_county: string; + address1_fax: string; + address1_freighttermscode: number; + address1_latitude: number; + address1_line1: string; + address1_line2: string; + address1_line3: string; + address1_longitude: number; + address1_name: string; + address1_postalcode: string; + address1_postofficebox: string; + address1_primarycontactname: string; + address1_shippingmethodcode: number; + address1_stateorprovince: string; + address1_telephone1: string; + address1_telephone2: string; + address1_telephone3: string; + address1_upszone: string; + address1_utcoffset: number; + address2_addressid: string; + address2_addresstypecode: number; + address2_city: string; + address2_composite: string; + address2_country: string; + address2_county: string; + address2_fax: string; + address2_freighttermscode: number; + address2_latitude: number; + address2_line1: string; + address2_line2: string; + address2_line3: string; + address2_longitude: number; + address2_name: string; + address2_postalcode: string; + address2_postofficebox: string; + address2_primarycontactname: string; + address2_shippingmethodcode: number; + address2_stateorprovince: string; + address2_telephone1: string; + address2_telephone2: string; + address2_telephone3: string; + address2_upszone: string; + address2_utcoffset: number; + address3_addressid: string; + address3_addresstypecode: number; + address3_city: string; + address3_composite: string; + address3_country: string; + address3_county: string; + address3_fax: string; + address3_freighttermscode: number; + address3_latitude: number; + address3_line1: string; + address3_line2: string; + address3_line3: string; + address3_longitude: number; + address3_name: string; + address3_postalcode: string; + address3_postofficebox: string; + address3_primarycontactname: string; + address3_shippingmethodcode: number; + address3_stateorprovince: string; + address3_telephone1: string; + address3_telephone2: string; + address3_telephone3: string; + address3_upszone: string; + address3_utcoffset: number; + adx_confirmremovepassword: boolean; + adx_createdbyipaddress: string; + adx_createdbyusername: string; + adx_identity_accessfailedcount: number; + adx_identity_emailaddress1confirmed: boolean; + adx_identity_lastsuccessfullogin: string; + adx_identity_locallogindisabled: boolean; + adx_identity_lockoutenabled: boolean; + adx_identity_lockoutenddate: string; + adx_identity_logonenabled: boolean; + adx_identity_mobilephoneconfirmed: boolean; + adx_identity_newpassword: string; + adx_identity_passwordhash: string; + adx_identity_securitystamp: string; + adx_identity_twofactorenabled: boolean; + adx_identity_username: string; + adx_modifiedbyipaddress: string; + adx_modifiedbyusername: string; + adx_organizationname: string; + adx_preferredlcid: number; + adx_profilealert: boolean; + adx_profilealertdate: string; + adx_profilealertinstructions: string; + adx_profileisanonymous: boolean; + adx_profilelastactivity: string; + adx_profilemodifiedon: string; + adx_publicprofilecopy: string; + adx_timezone: number; + aging30: number; + aging30_base: number; + aging60: number; + aging60_base: number; + aging90: number; + aging90_base: number; + anniversary: string; + annualincome: number; + annualincome_base: number; + assistantname: string; + assistantphone: string; + birthdate: string; + business2: string; + callback: string; + childrensnames: string; + company: string; + contactid: string; + createdon: string; + creditlimit: number; + creditlimit_base: number; + creditonhold: boolean; + customersizecode: number; + customertypecode: number; + department: string; + description: string; + donotbulkemail: boolean; + donotbulkpostalmail: boolean; + donotemail: boolean; + donotfax: boolean; + donotphone: boolean; + donotpostalmail: boolean; + donotsendmm: boolean; + educationcode: number; + emailaddress1: string; + emailaddress2: string; + emailaddress3: string; + employeeid: string; + entityimage: string; + entityimage_timestamp: number; + entityimage_url: string; + entityimageid: string; + exchangerate: number; + externaluseridentifier: string; + familystatuscode: number; + fax: string; + firstname: string; + followemail: boolean; + ftpsiteurl: string; + fullname: string; + gendercode: number; + governmentid: string; + haschildrencode: number; + home2: string; + importsequencenumber: number; + isbackofficecustomer: boolean; + jobtitle: string; + lastname: string; + lastonholdtime: string; + lastusedincampaign: string; + leadsourcecode: number; + managername: string; + managerphone: string; + marketingonly: boolean; + merged: boolean; + middlename: string; + mobilephone: string; + modifiedon: string; + msdyn_disablewebtracking: boolean; + msdyn_isminor: boolean; + msdyn_isminorwithparentalconsent: boolean; + msdyn_portaltermsagreementdate: string; + mspp_userpreferredlcid: number; + nickname: string; + numberofchildren: number; + onholdtime: number; + overriddencreatedon: string; + pager: string; + participatesinworkflow: boolean; + paymenttermscode: number; + preferredappointmentdaycode: number; + preferredappointmenttimecode: number; + preferredcontactmethodcode: number; + processid: string; + salutation: string; + shippingmethodcode: number; + spousesname: string; + stageid: string; + statecode: number; + statuscode: number; + subscriptionid: string; + suffix: string; + telephone1: string; + telephone2: string; + telephone3: string; + territorycode: number; + timespentbymeonemailandmeetings: string; + timezoneruleversionnumber: number; + traversedpath: string; + utcconversiontimezonecode: number; + versionnumber: number; + websiteurl: string; + yomifirstname: string; + yomifullname: string; + yomilastname: string; + yomimiddlename: string; + _createdby_value: string; + _createdonbehalfby_value: string; + _masterid_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _msa_managingpartnerid_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _parentcustomerid_value: string; + _preferredsystemuserid_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _transactioncurrencyid_value: string; + + +} + +export type MicrosoftdynamicssalesContactInput = Partial; +export type MicrosoftdynamicssalesContactOutput = MicrosoftdynamicssalesContactInput; \ No newline at end of file diff --git a/packages/api/src/crm/deal/deal.module.ts b/packages/api/src/crm/deal/deal.module.ts index d27a59782..334d7367b 100644 --- a/packages/api/src/crm/deal/deal.module.ts +++ b/packages/api/src/crm/deal/deal.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesDealMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; @@ -43,6 +45,8 @@ import { SyncService } from './sync/sync.service'; HubspotDealMapper, AttioDealMapper, CloseDealMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesDealMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..12979121b --- /dev/null +++ b/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { CrmObject } from '@crm/@lib/@types'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { IDealService } from '@crm/deal/types'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesDealInput, MicrosoftdynamicssalesDealOutput } from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class MicrosoftdynamicssalesService implements IDealService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.deal.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + async addDeal( + dealData: MicrosoftdynamicssalesDealInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const resp = await axios.post( + `${connection.account_url}/api/data/v9.2/opportunities`, + JSON.stringify({ + data: dealData, + }), + { + headers: { + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + 'Content-Type': 'application/json', + }, + }, + ); + return { + data: resp.data.value, + message: 'Microsoftdynamicssales deal created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/opportunities`, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data.value, + message: 'Microsoftdynamicssales deals retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..b8e64523d --- /dev/null +++ b/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,130 @@ +import { Injectable } from '@nestjs/common'; +import { + UnifiedCrmDealInput, + UnifiedCrmDealOutput, +} from '@crm/deal/types/model.unified'; +import { IDealMapper } from '@crm/deal/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { MicrosoftdynamicssalesDealInput, MicrosoftdynamicssalesDealOutput } from './types'; + +@Injectable() +export class MicrosoftdynamicssalesDealMapper implements IDealMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'deal', 'microsoftdynamicssales', this); + } + + async desunify( + source: UnifiedCrmDealInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesDealInput = { + name: source.name, + description: source.description, + totalamount: source.amount + } + + + + + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + result["parentaccountid@odata.bind"] = `/accounts(${id})`; + } + + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + async unify( + source: MicrosoftdynamicssalesDealOutput | MicrosoftdynamicssalesDealOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleDealToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesDealOutput + return Promise.all( + source.map((deal) => + this.mapSingleDealToUnified(deal, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleDealToUnified( + deal: MicrosoftdynamicssalesDealOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = deal[mapping.remote_id]; + } + } + + let opts: any = {}; + + if (deal._ownerid_value) { + const user_id = await this.utils.getUserUuidFromRemoteId( + deal._ownerid_value, + connectionId + ); + if (user_id) { + opts = { + ...opts, + user_id: user_id + } + } + } + + if (deal._accountid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + deal._accountid_value, + connectionId + ); + if (company_id) { + opts = { + ...opts, + company_id: company_id + } + } + } + + + return { + remote_id: deal.opportunityid, + remote_data: deal, + name: deal.name ?? '', + description: deal.description ?? '', + amount: deal.totalamount ?? 0, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/deal/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/deal/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..363c7ef79 --- /dev/null +++ b/packages/api/src/crm/deal/services/microsoftdynamicssales/types.ts @@ -0,0 +1,125 @@ +interface MicrosoftdynamicssalesDeal { + msdyn_opportunityscore: any, + totalamount_base: number, + pursuitdecision: boolean, + msdyn_scorehistory: any, + _msdyn_predictivescoreid_value: string, + actualvalue_base: any, + totallineitemdiscountamount: number, + totalamount: number, + identifypursuitteam: boolean, + statuscode: number, + msdyn_opportunityscoretrend: any, + totaldiscountamount_base: number, + _createdby_value: string, + name: string, + _owninguser_value: string, + _parentcontactid_value: string, + _msdyn_segmentid_value: any, + totaltax: number, + budgetstatus: any, + _accountid_value: any, + closeprobability: number, + finaldecisiondate: string, + _modifiedby_value: string, + _slainvokedid_value: any, + msdyn_similaropportunities: any, + msdyn_opportunitygrade: any, + quotecomments: any, + timeline: any, + qualificationcomments: any, + customerneed: string, + estimatedclosedate: string, + estimatedvalue: number, + identifycustomercontacts: boolean, + _contactid_value: any, + presentproposal: boolean, + captureproposalfeedback: boolean, + _campaignid_value: any, + estimatedvalue_base: number, + salesstagecode: number, + msdyn_forecastcategory: number, + stageid: any, + lastonholdtime: any, + need: any, + purchaseprocess: number, + sendthankyounote: boolean, + _parentaccountid_value: string, + totalamountlessfreight: number, + _modifiedonbehalfby_value: string, + _ownerid_value: string, + stepid: any, + developproposal: boolean, + freightamount_base: any, + description: string, + processid: any, + skippricecalculation: number, + discountamount_base: any, + onholdtime: any, + schedulefollowup_qualify: any, + importsequencenumber: any, + modifiedon: string, + totaltax_base: number, + opportunityratingcode: number, + utcconversiontimezonecode: any, + totallineitemdiscountamount_base: number, + salesstage: any, + discountamount: any, + traversedpath: any, + _createdonbehalfby_value: any, + _originatingleadid_value: any, + stepname: string, + customerpainpoints: any, + identifycompetitors: boolean, + emailaddress: string, + budgetamount: number, + msdyn_gdproptout: boolean, + totallineitemamount: number, + _owningteam_value: any, + _owningbusinessunit_value: string, + decisionmaker: boolean, + opportunityid: string, + totaldiscountamount: number, + evaluatefit: boolean, + _msdyn_opportunitykpiid_value: string, + proposedsolution: string, + scheduleproposalmeeting: any, + presentfinalproposal: boolean, + statecode: number, + isrevenuesystemcalculated: boolean, + participatesinworkflow: boolean, + exchangerate: number, + timespentbymeonemailandmeetings: any, + purchasetimeframe: number, + filedebrief: boolean, + teamsfollowed: any, + _customerid_value: string, + totalamountlessfreight_base: number, + _pricelevelid_value: string, + timezoneruleversionnumber: number, + overriddencreatedon: any, + prioritycode: number, + initialcommunication: any, + _transactioncurrencyid_value: string, + schedulefollowup_prospect: any, + currentsituation: string, + totallineitemamount_base: number, + msdyn_scorereasons: any, + budgetamount_base: number, + freightamount: any, + actualclosedate: any, + actualvalue: any, + discountpercentage: any, + confirminterest: boolean, + completeinternalreview: boolean, + completefinalproposal: boolean, + _slaid_value: any, + resolvefeedback: boolean, + createdon: string, + pricingerrorcode: number, + versionnumber: number, + [key: string]: any +} + +export type MicrosoftdynamicssalesDealInput = Partial; +export type MicrosoftdynamicssalesDealOutput = MicrosoftdynamicssalesDealInput; \ No newline at end of file diff --git a/packages/api/src/crm/note/note.module.ts b/packages/api/src/crm/note/note.module.ts index 0191db40a..280f0f69e 100644 --- a/packages/api/src/crm/note/note.module.ts +++ b/packages/api/src/crm/note/note.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesNoteMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; @@ -42,6 +44,8 @@ import { SyncService } from './sync/sync.service'; AttioNoteMapper, HubspotNoteMapper, CloseNoteMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesNoteMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..8a17ca8d7 --- /dev/null +++ b/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts @@ -0,0 +1,93 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { INoteService } from '@crm/note/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesNoteInput, MicrosoftdynamicssalesNoteOutput } from './types'; + +@Injectable() +export class MicrosoftdynamicssalesService implements INoteService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.note.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + + async addNote( + noteData: MicrosoftdynamicssalesNoteInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + const resp = await axios.post( + `${connection.account_url}/api/data/v9.2/annotations`, + JSON.stringify(noteData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp?.data.value, + message: 'Microsoftdynamicssales note created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const baseURL = `${connection.account_url}/api/data/v9.2/annotations`; + + const resp = await axios.get(baseURL, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced microsoftdynamicssales notes !`); + return { + data: resp?.data?.value, + message: 'Microsoftdynamicssales notes retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/note/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/note/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..6052d8f14 --- /dev/null +++ b/packages/api/src/crm/note/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,151 @@ +import { MicrosoftdynamicssalesNoteInput, MicrosoftdynamicssalesNoteOutput } from './types'; +import { + UnifiedCrmNoteInput, + UnifiedCrmNoteOutput, +} from '@crm/note/types/model.unified'; +import { INoteMapper } from '@crm/note/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MicrosoftdynamicssalesNoteMapper implements INoteMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'note', 'microsoftdynamicssales', this); + } + async desunify( + source: UnifiedCrmNoteInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + //get the parent id of the object tied to the note + let opts: any = {}; + + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + opts = { + ...opts, + "objectid_account@odata.bind": `/accounts(${id})` + } + } + if (source.contact_id) { + const id = await this.utils.getRemoteIdFromContactUuid(source.contact_id); + opts = { + ...opts, + "objectid_contact@odata.bind": `/contacts(${id})` + } + } + + // Do not think there is option to add opportunity + + + + const result: MicrosoftdynamicssalesNoteInput = { + notetext: source.content, + ...opts + }; + + return result; + } + + async unify( + source: MicrosoftdynamicssalesNoteOutput | MicrosoftdynamicssalesNoteOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleNoteToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + return Promise.all( + source.map((note) => + this.mapSingleNoteToUnified(note, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleNoteToUnified( + note: MicrosoftdynamicssalesNoteOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = note[mapping.remote_id]; + } + } + + let opts: any = {}; + + if (note._ownerid_value) { + const user_id = await this.utils.getUserUuidFromRemoteId( + note._ownerid_value, + connectionId + ); + if (user_id) { + opts = { + ...opts, + user_id: user_id + } + } + } + + // This could only contain either contact, comapany or deal id + if (note._objectid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + note._objectid_value, + connectionId + ); + if (company_id) { + opts = { + ...opts, + company_id: company_id + } + } + + const contact_id = await this.utils.getContactUuidFromRemoteId( + note._objectid_value, + connectionId + ); + if (contact_id) { + opts = { + ...opts, + contact_id: contact_id + } + } + + const deal_id = await this.utils.getDealUuidFromRemoteId( + note._objectid_value, + connectionId + ); + if (deal_id) { + opts = { + ...opts, + deal_id: deal_id + } + } + + } + + return { + remote_id: note.annotationid, + remote_data: note, + content: note.notetext ?? '', + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/note/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/note/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..fcdff526a --- /dev/null +++ b/packages/api/src/crm/note/services/microsoftdynamicssales/types.ts @@ -0,0 +1,32 @@ +interface MicrosoftdynamicssalesNote { + annotationid: string; + createdon: string; + documentbody: string; + documentbody_binary: string; + filename: string; + filesize: number; + importsequencenumber: number; + isdocument: boolean; + langid: string; + mimetype: string; + modifiedon: string; + notetext: string; + objecttypecode: string; + overriddencreatedon: string; + prefix: string; + stepid: string; + subject: string; + versionnumber: number; + _createdby_value: string; + _createdonbehalfby_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _objectid_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string +} + +export type MicrosoftdynamicssalesNoteInput = Partial; +export type MicrosoftdynamicssalesNoteOutput = MicrosoftdynamicssalesNoteInput; \ No newline at end of file diff --git a/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..b536fc781 --- /dev/null +++ b/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts @@ -0,0 +1,94 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { ITaskService } from '@crm/task/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesTaskInput, MicrosoftdynamicssalesTaskOutput } from './types'; + +@Injectable() +export class MicrosoftdynamicssalesService implements ITaskService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.task.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + + async addTask( + taskData: MicrosoftdynamicssalesTaskInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + const resp = await axios.post( + `${connection.account_url}/api/data/v9.2/tasks`, + JSON.stringify(taskData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + return { + data: resp?.data?.value, + message: 'Microsoftdynamicssales task created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const baseURL = `${connection.account_url}/api/data/v9.2/tasks`; + + const resp = await axios.get(baseURL, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced microsoftdynamicssales tasks !`); + return { + data: resp?.data?.value, + message: 'Microsoftdynamicssales tasks retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..794a2e2fd --- /dev/null +++ b/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,165 @@ +import { MicrosoftdynamicssalesTaskInput, MicrosoftdynamicssalesTaskOutput } from './types'; +import { + UnifiedCrmTaskInput, + UnifiedCrmTaskOutput, +} from '@crm/task/types/model.unified'; +import { ITaskMapper } from '@crm/task/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MicrosoftdynamicssalesTaskMapper implements ITaskMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'task', 'microsoftdynamicssales', this); + } + + async desunify( + source: UnifiedCrmTaskInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesTaskInput = { + subject: source.subject, + description: source.content + }; + + if (source.status) { + result.statecode = source.status === "COMPLETED" ? 1 : 0; + } + + if (source.due_date) { + result.scheduledend = source.due_date.toUTCString(); + } + + if (source.finished_date) { + result.actualend = source.finished_date.toUTCString(); + } + + // User have to provide either company_id or deal_id which will be associated with current task + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + result["regardingobjectid_account@odata.bind"] = `/accounts(${id})`; + } + + else if (source.deal_id) { + const id = await this.utils.getRemoteIdFromDealUuid(source.deal_id); + result["regardingobjectid_opportunity@odata.bind"] = `/opportunities(${id})`; + } + + return result; + } + + async unify( + source: MicrosoftdynamicssalesTaskOutput | MicrosoftdynamicssalesTaskOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleTaskToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + return Promise.all( + source.map((task) => + this.mapSingleTaskToUnified(task, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleTaskToUnified( + task: MicrosoftdynamicssalesTaskOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = task[mapping.remote_id]; + } + } + + let opts: any = {}; + + if (task.statecode) { + opts = { + ...opts, + status: task.statecode === 1 ? "COMPLETED" : "PENDING" + } + } + + if (task.scheduledend) { + opts = { + ...opts, + due_date: new Date(task.scheduledend) + } + } + + if (task.actualend) { + opts = { + ...opts, + finished_date: new Date(task.actualend) + } + } + + + // Here the task either associated to deal or company + if (task._regardingobjectid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + task._regardingobjectid_value, + connectionId, + ); + + if (company_id) { + opts = { + ...opts, + company_id: company_id, + } + } + + const deal_id = await this.utils.getDealUuidFromRemoteId( + task._regardingobjectid_value, + connectionId, + ) + + if (deal_id) { + opts = { + ...opts, + deal_id: deal_id, + } + } + } + + if (task._createdby_value) { + const user_id = await this.utils.getUserUuidFromRemoteId( + task._createdby_value, + connectionId + ) + + opts = { + ...opts, + user_id: user_id + } + } + + return { + remote_id: task.activityid, + subject: task.subject ?? '', + remote_data: task, + content: task.description ?? '', + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/task/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/task/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..374c8208e --- /dev/null +++ b/packages/api/src/crm/task/services/microsoftdynamicssales/types.ts @@ -0,0 +1,70 @@ +interface MicrosoftdynamicssalesTask { + activityadditionalparams: string; + activityid: string; + activitytypecode: string; + actualdurationminutes: number; + actualend: string; + actualstart: string; + category: string; + community: number; + createdon: string; + crmtaskassigneduniqueid: string; + deliverylastattemptedon: string; + deliveryprioritycode: number; + description: string; + descriptionblobid: string; + descriptionblobid_name: string; + exchangeitemid: string; + exchangerate: number; + exchangeweblink: string; + formattedscheduledend: string; + formattedscheduledstart: string; + importsequencenumber: number; + instancetypecode: number; + isbilled: boolean; + ismapiprivate: boolean; + isregularactivity: boolean; + isworkflowcreated: boolean; + lastonholdtime: string; + leftvoicemail: boolean; + modifiedon: string; + onholdtime: number; + overriddencreatedon: string; + percentcomplete: number; + postponeactivityprocessinguntil: string; + prioritycode: number; + processid: string; + scheduleddurationminutes: number; + scheduledend: string; + scheduledstart: string; + senton: string; + seriesid: string; + sortdate: string; + stageid: string; + statecode: number; + statuscode: number; + subcategory: string; + subject: string; + subscriptionid: string; + timezoneruleversionnumber: number; + traversedpath: string; + utcconversiontimezonecode: number; + versionnumber: number; + _createdby_value: string; + _createdonbehalfby_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _regardingobjectid_value: string; + _sendermailboxid_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _transactioncurrencyid_value: string; + [key: string]: any; +} + +export type MicrosoftdynamicssalesTaskInput = Partial; +export type MicrosoftdynamicssalesTaskOutput = MicrosoftdynamicssalesTaskInput; \ No newline at end of file diff --git a/packages/api/src/crm/task/task.module.ts b/packages/api/src/crm/task/task.module.ts index c790a2e36..63800e06a 100644 --- a/packages/api/src/crm/task/task.module.ts +++ b/packages/api/src/crm/task/task.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesTaskMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @@ -43,6 +45,8 @@ import { TaskController } from './task.controller'; HubspotTaskMapper, CloseTaskMapper, AttioTaskMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesTaskMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/user/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/user/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..83b1170b6 --- /dev/null +++ b/packages/api/src/crm/user/services/microsoftdynamicssales/index.ts @@ -0,0 +1,67 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { IUserService } from '@crm/user/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { MicrosoftdynamicssalesUserOutput } from './types'; + +@Injectable() +export class MicrosoftdynamicssalesService implements IUserService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.user.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + this.logger.log("==========="); + this.logger.log(this.cryptoService.decrypt( + connection.access_token, + )); + this.logger.log("==========="); + + + const baseURL = `${connection.account_url}/api/data/v9.2/systemusers`; + const resp = await axios.get(baseURL, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + this.logger.log(`Synced microsoftdynamicssales users ! : ${JSON.stringify(resp.data.value)}`); + + return { + data: resp.data.value, + message: 'Microsoftdynamicssales users retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/user/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/user/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..fa88aa44b --- /dev/null +++ b/packages/api/src/crm/user/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,71 @@ +import { MicrosoftdynamicssalesUserOutput } from './types'; +import { + UnifiedCrmUserInput, + UnifiedCrmUserOutput, +} from '@crm/user/types/model.unified'; +import { IUserMapper } from '@crm/user/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { Utils } from '@crm/@lib/@utils'; + +@Injectable() +export class MicrosoftdynamicssalesUserMapper implements IUserMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'user', 'microsoftdynamicssales', this); + } + desunify( + source: UnifiedCrmUserInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ) { + return; + } + + unify( + source: MicrosoftdynamicssalesUserOutput | MicrosoftdynamicssalesUserOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleUserToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesUserOutput + return Promise.all( + source.map((user) => + this.mapSingleUserToUnified(user, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleUserToUnified( + user: MicrosoftdynamicssalesUserOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = user[mapping.remote_id]; + } + } + return { + remote_id: user.ownerid, + remote_data: user, + name: `${user.firstname} ${user.lastname}`, + email: user.personalemailaddress, + field_mappings, + }; + } +} diff --git a/packages/api/src/crm/user/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/user/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..94437e339 --- /dev/null +++ b/packages/api/src/crm/user/services/microsoftdynamicssales/types.ts @@ -0,0 +1,123 @@ +interface MicrosoftdynamicssalesUser { + accessmode: number; // Edm.Int32 + address1_addressid: string; // Edm.Guid + address1_addresstypecode: number; // Edm.Int32 + address1_city: string; // Edm.String + address1_composite: string; // Edm.String + address1_country: string; // Edm.String + address1_county: string; // Edm.String + address1_fax: string; // Edm.String + address1_latitude: number; // Edm.Double + address1_line1: string; // Edm.String + address1_line2: string; // Edm.String + address1_line3: string; // Edm.String + address1_longitude: number; // Edm.Double + address1_name: string; // Edm.String + address1_postalcode: string; // Edm.String + address1_postofficebox: string; // Edm.String + address1_shippingmethodcode: number; // Edm.Int32 + address1_stateorprovince: string; // Edm.String + address1_telephone1: string; // Edm.String + address1_telephone2: string; // Edm.String + address1_telephone3: string; // Edm.String + address1_upszone: string; // Edm.String + address1_utcoffset: number; // Edm.Int32 + address2_addressid: string; // Edm.Guid + address2_addresstypecode: number; // Edm.Int32 + address2_city: string; // Edm.String + address2_composite: string; // Edm.String + address2_country: string; // Edm.String + address2_county: string; // Edm.String + address2_fax: string; // Edm.String + address2_latitude: number; // Edm.Double + address2_line1: string; // Edm.String + address2_line2: string; // Edm.String + address2_line3: string; // Edm.String + address2_longitude: number; // Edm.Double + address2_name: string; // Edm.String + address2_postalcode: string; // Edm.String + address2_postofficebox: string; // Edm.String + address2_shippingmethodcode: number; // Edm.Int32 + address2_stateorprovince: string; // Edm.String + address2_telephone1: string; // Edm.String + address2_telephone2: string; // Edm.String + address2_telephone3: string; // Edm.String + address2_upszone: string; // Edm.String + address2_utcoffset: number; // Edm.Int32 + applicationid: string; // Edm.Guid + applicationiduri: string; // Edm.String + azureactivedirectoryobjectid: string; // Edm.Guid + azuredeletedon: string; // Edm.DateTimeOffset + azurestate: number; // Edm.Int32 + caltype: number; // Edm.Int32 + createdon: string; // Edm.DateTimeOffset + defaultfilterspopulated: boolean; // Edm.Boolean + defaultodbfoldername: string; // Edm.String + deletedstate: number; // Edm.Int32 + disabledreason: string; // Edm.String + displayinserviceviews: boolean; // Edm.Boolean + domainname: string; // Edm.String + emailrouteraccessapproval: number; // Edm.Int32 + employeeid: string; // Edm.String + entityimage: string; // Edm.Binary + entityimage_timestamp: number; // Edm.Int64 + entityimage_url: string; // Edm.String + entityimageid: string; // Edm.Guid + exchangerate: number; // Edm.Decimal + firstname: string; // Edm.String + fullname: string; // Edm.String + governmentid: string; // Edm.String + homephone: string; // Edm.String + identityid: number; // Edm.Int32 + importsequencenumber: number; // Edm.Int32 + incomingemaildeliverymethod: number; // Edm.Int32 + internalemailaddress: string; // Edm.String + invitestatuscode: number; // Edm.Int32 + isdisabled: boolean; // Edm.Boolean + isemailaddressapprovedbyo365admin: boolean; // Edm.Boolean + isintegrationuser: boolean; // Edm.Boolean + islicensed: boolean; // Edm.Boolean + issyncwithdirectory: boolean; // Edm.Boolean + jobtitle: string; // Edm.String + lastname: string; // Edm.String + middlename: string; // Edm.String + mobilealertemail: string; // Edm.String + mobilephone: string; // Edm.String + modifiedon: string; // Edm.DateTimeOffset + nickname: string; // Edm.String + organizationid: string; // Edm.Guid + outgoingemaildeliverymethod: number; // Edm.Int32 + overriddencreatedon: string; // Edm.DateTimeOffset + ownerid: string; // Edm.Guid + passporthi: number; // Edm.Int32 + passportlo: number; // Edm.Int32 + personalemailaddress: string; // Edm.String + photourl: string; // Edm.String + preferredaddresscode: number; // Edm.Int32 + preferredemailcode: number; // Edm.Int32 + preferredphonecode: number; // Edm.Int32 + processid: string; // Edm.Guid + salutation: string; // Edm.String + setupuser: boolean; // Edm.Boolean + sharepointemailaddress: string; // Edm.String + skills: string; // Edm.String + stageid: string; // Edm.Guid + systemuserid: string; // Edm.Guid + timezoneruleversionnumber: number; // Edm.Int32 + title: string; // Edm.String + traversedpath: string; // Edm.String + userlicensetype: number; // Edm.Int32 + userpuid: string; // Edm.String + utcconversiontimezonecode: number; // Edm.Int32 + versionnumber: number; // Edm.Int64 + windowsliveid: string; // Edm.String + yammeremailaddress: string; // Edm.String + yammeruserid: string; // Edm.String + yomifirstname: string; // Edm.String + yomifullname: string; // Edm.String + yomilastname: string; // Edm.String + yomimiddlename: string; // Edm.String +} + +export type MicrosoftdynamicssalesUserInput = Partial; +export type MicrosoftdynamicssalesUserOutput = MicrosoftdynamicssalesUserInput; \ No newline at end of file diff --git a/packages/api/src/crm/user/user.module.ts b/packages/api/src/crm/user/user.module.ts index 0957b622f..9ca93bd02 100644 --- a/packages/api/src/crm/user/user.module.ts +++ b/packages/api/src/crm/user/user.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesUserMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @@ -43,6 +45,8 @@ import { UserController } from './user.controller'; HubspotUserMapper, AttioUserMapper, CloseUserMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesUserMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/shared/src/authUrl.ts b/packages/shared/src/authUrl.ts index 5a7ae7d8a..04f048dad 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -126,13 +126,13 @@ const handleOAuth2Url = async ({ ...dyn }: HandleOAuth2Url) => { const type = providerToType(providerName, vertical, authStrategy); - + const response = await fetch(`${apiUrl}/connection_strategies/getCredentials?projectId=${projectId}&type=${type}`); const data = await response.json() as OAuth2AuthData; const clientId = data.CLIENT_ID; if (!clientId) throw new ReferenceError(`No client id for type ${type}`); - + const scopes = data.SCOPE; const { urls: { authBaseUrl: baseUrl } } = config; if (!baseUrl) throw new ReferenceError(`No authBaseUrl found for type ${type}`); diff --git a/packages/shared/src/connectors/enum.ts b/packages/shared/src/connectors/enum.ts index a66547391..0679963b8 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -4,7 +4,8 @@ export enum CrmConnectors { HUBSPOT = 'hubspot', PIPEDRIVE = 'pipedrive', ATTIO = 'attio', - CLOSE = 'close' + CLOSE = 'close', + MICROSOFTDYNAMICSSALES = 'microsoftdynamicssales' } export enum EcommerceConnectors { diff --git a/packages/shared/src/connectors/index.ts b/packages/shared/src/connectors/index.ts index 105ca557a..c99c194d7 100644 --- a/packages/shared/src/connectors/index.ts +++ b/packages/shared/src/connectors/index.ts @@ -1,4 +1,4 @@ -export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio', 'close']; +export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio', 'close', 'microsoftdynamicssales']; export const HRIS_PROVIDERS = []; export const ATS_PROVIDERS = ['ashby']; export const ACCOUNTING_PROVIDERS = []; diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 873590f9c..3d2251a14 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -3090,4 +3090,4 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, } -}; +}; \ No newline at end of file From baf95fe27e971432f1816a41b05dfe2cc5bdca60 Mon Sep 17 00:00:00 2001 From: mit-27 Date: Wed, 28 Aug 2024 21:40:17 -0600 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20Tested=20all=20objects=20and=20?= =?UTF-8?q?fix=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/microsoftdynamicssales/index.ts | 24 ++++++++++++---- .../services/microsoftdynamicssales/index.ts | 23 +++++++++++---- .../services/microsoftdynamicssales/index.ts | 28 +++++++++++++++---- .../microsoftdynamicssales/mappers.ts | 6 ++-- .../services/microsoftdynamicssales/index.ts | 20 +++++++++++-- .../services/microsoftdynamicssales/index.ts | 18 ++++++++++-- .../microsoftdynamicssales/mappers.ts | 4 +-- packages/shared/src/connectors/metadata.ts | 2 +- 8 files changed, 100 insertions(+), 25 deletions(-) diff --git a/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts index b04664a29..4ff25e07d 100644 --- a/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts +++ b/packages/api/src/crm/company/services/microsoftdynamicssales/index.ts @@ -39,11 +39,9 @@ export class MicrosoftdynamicssalesService implements ICompanyService { }, }); - const resp = await axios.post( + const respToPost = await axios.post( `${connection.account_url}/api/data/v9.2/accounts`, - JSON.stringify({ - data: companyData, - }), + JSON.stringify(companyData), { headers: { Authorization: `Bearer ${this.cryptoService.decrypt( @@ -53,8 +51,24 @@ export class MicrosoftdynamicssalesService implements ICompanyService { }, }, ); + + const postCompanyId = respToPost.headers['location'].split("/").pop(); + // console.log(res.headers['location'].split('(')[1].split(')')[0]) + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postCompanyId}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { - data: resp.data.value, + data: resp.data, message: 'Microsoftdynamicssales company created', statusCode: 201, }; diff --git a/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts index f585e6966..caeeb9891 100644 --- a/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts +++ b/packages/api/src/crm/contact/services/microsoftdynamicssales/index.ts @@ -38,11 +38,9 @@ export class MicrosoftdynamicssalesService implements IContactService { }, }); - const resp = await axios.post( + const respToPost = await axios.post( `${connection.account_url}/api/data/v9.2/contacts`, - JSON.stringify({ - data: contactData, - }), + JSON.stringify(contactData), { headers: { 'Content-Type': 'application/json', @@ -52,8 +50,23 @@ export class MicrosoftdynamicssalesService implements IContactService { }, }, ); + + const postContactId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postContactId}`, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); return { - data: resp.data.value, + data: resp.data, message: 'microsoftdynamicssales contact created', statusCode: 201, }; diff --git a/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts index 12979121b..fd93e9d74 100644 --- a/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts +++ b/packages/api/src/crm/deal/services/microsoftdynamicssales/index.ts @@ -37,11 +37,9 @@ export class MicrosoftdynamicssalesService implements IDealService { }, }); - const resp = await axios.post( + const respToPost = await axios.post( `${connection.account_url}/api/data/v9.2/opportunities`, - JSON.stringify({ - data: dealData, - }), + JSON.stringify(dealData), { headers: { Authorization: `Bearer ${this.cryptoService.decrypt( @@ -51,8 +49,28 @@ export class MicrosoftdynamicssalesService implements IDealService { }, }, ); + + const postDealId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postDealId}`, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + + + + return { - data: resp.data.value, + data: resp.data, message: 'Microsoftdynamicssales deal created', statusCode: 201, }; diff --git a/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts index b8e64523d..a1ca99212 100644 --- a/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts +++ b/packages/api/src/crm/deal/services/microsoftdynamicssales/mappers.ts @@ -24,7 +24,7 @@ export class MicrosoftdynamicssalesDealMapper implements IDealMapper { const result: MicrosoftdynamicssalesDealInput = { name: source.name, description: source.description, - totalamount: source.amount + budgetamount: source.amount } @@ -103,9 +103,9 @@ export class MicrosoftdynamicssalesDealMapper implements IDealMapper { } } - if (deal._accountid_value) { + if (deal._customerid_value) { const company_id = await this.utils.getCompanyUuidFromRemoteId( - deal._accountid_value, + deal._customerid_value, connectionId ); if (company_id) { diff --git a/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts index 8a17ca8d7..475fba1e5 100644 --- a/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts +++ b/packages/api/src/crm/note/services/microsoftdynamicssales/index.ts @@ -36,7 +36,7 @@ export class MicrosoftdynamicssalesService implements INoteService { vertical: 'crm', }, }); - const resp = await axios.post( + const respToPost = await axios.post( `${connection.account_url}/api/data/v9.2/annotations`, JSON.stringify(noteData), { @@ -48,8 +48,24 @@ export class MicrosoftdynamicssalesService implements INoteService { }, }, ); + + const postNoteId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postNoteId}` + , { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { - data: resp?.data.value, + data: resp?.data, message: 'Microsoftdynamicssales note created', statusCode: 201, }; diff --git a/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts index b536fc781..3888854b6 100644 --- a/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts +++ b/packages/api/src/crm/task/services/microsoftdynamicssales/index.ts @@ -36,7 +36,7 @@ export class MicrosoftdynamicssalesService implements ITaskService { vertical: 'crm', }, }); - const resp = await axios.post( + const respToPost = await axios.post( `${connection.account_url}/api/data/v9.2/tasks`, JSON.stringify(taskData), { @@ -49,8 +49,22 @@ export class MicrosoftdynamicssalesService implements ITaskService { }, ); + const postTaskId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postTaskId}`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { - data: resp?.data?.value, + data: resp?.data, message: 'Microsoftdynamicssales task created', statusCode: 201, }; diff --git a/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts index 794a2e2fd..28020d540 100644 --- a/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts +++ b/packages/api/src/crm/task/services/microsoftdynamicssales/mappers.ts @@ -31,11 +31,11 @@ export class MicrosoftdynamicssalesTaskMapper implements ITaskMapper { } if (source.due_date) { - result.scheduledend = source.due_date.toUTCString(); + result.scheduledend = new Date(source.due_date).toUTCString(); } if (source.finished_date) { - result.actualend = source.finished_date.toUTCString(); + result.actualend = new Date(source.finished_date).toUTCString(); } // User have to provide either company_id or deal_id which will be associated with current task diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index b31075d44..b5baeafac 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -3090,4 +3090,4 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, } -}; \ No newline at end of file +}; From 87324457e3616f4181ffa62c8f42e18a885774c1 Mon Sep 17 00:00:00 2001 From: mit-27 Date: Fri, 30 Aug 2024 20:21:09 -0600 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Add=20engagement=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/magic-link/src/lib/ProviderModal.tsx | 2 +- .../utils/types/original/original.crm.ts | 6 +- .../src/crm/engagement/engagement.module.ts | 4 + .../services/microsoftdynamicssales/index.ts | 361 ++++++++++++ .../microsoftdynamicssales/mappers.ts | 518 ++++++++++++++++++ .../services/microsoftdynamicssales/types.ts | 276 ++++++++++ 6 files changed, 1164 insertions(+), 3 deletions(-) create mode 100644 packages/api/src/crm/engagement/services/microsoftdynamicssales/index.ts create mode 100644 packages/api/src/crm/engagement/services/microsoftdynamicssales/mappers.ts create mode 100644 packages/api/src/crm/engagement/services/microsoftdynamicssales/types.ts diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index e08a2238c..f7bc182c0 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -22,7 +22,7 @@ interface IBasicAuthFormData { } const domainFormats: { [key: string]: string } = { - microsoftdynamicssales: 'YOURORGNAME.api.crm12.dynamics.com', + microsoftdynamicssales: 'YOURORGNAME.api.crm3.dynamics.com', bigcommerce: 'If your api domain is https://api.bigcommerce.com/stores/eubckcvkzg/v3 then store_hash is eubckcvkzg', }; 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 2e64529d6..61503dad2 100644 --- a/packages/api/src/@core/utils/types/original/original.crm.ts +++ b/packages/api/src/@core/utils/types/original/original.crm.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesEngagementInput, MicrosoftdynamicssalesEngagementOutput } from '@crm/engagement/services/microsoftdynamicssales/types'; + import { MicrosoftdynamicssalesTaskInput, MicrosoftdynamicssalesTaskOutput } from '@crm/task/services/microsoftdynamicssales/types'; import { MicrosoftdynamicssalesNoteInput, MicrosoftdynamicssalesNoteOutput } from '@crm/note/services/microsoftdynamicssales/types'; @@ -186,7 +188,7 @@ export type OriginalEngagementInput = | ZohoEngagementInput | ZendeskEngagementInput | PipedriveEngagementInput - | CloseEngagementInput; + | CloseEngagementInput | MicrosoftdynamicssalesEngagementInput; /* note */ export type OriginalNoteInput = @@ -268,7 +270,7 @@ export type OriginalEngagementOutput = | ZohoEngagementOutput | ZendeskEngagementOutput | PipedriveEngagementOutput - | CloseEngagementOutput; + | CloseEngagementOutput | MicrosoftdynamicssalesEngagementOutput; /* note */ export type OriginalNoteOutput = diff --git a/packages/api/src/crm/engagement/engagement.module.ts b/packages/api/src/crm/engagement/engagement.module.ts index 4da6c55b5..bc2c9b92c 100644 --- a/packages/api/src/crm/engagement/engagement.module.ts +++ b/packages/api/src/crm/engagement/engagement.module.ts @@ -1,3 +1,5 @@ +import { MicrosoftdynamicssalesEngagementMapper } from './services/microsoftdynamicssales/mappers'; +import { MicrosoftdynamicssalesService } from './services/microsoftdynamicssales'; import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; @@ -48,6 +50,8 @@ import { SyncService } from './sync/sync.service'; PipedriveEngagementMapper, HubspotEngagementMapper, CloseEngagementMapper, + MicrosoftdynamicssalesService, + MicrosoftdynamicssalesEngagementMapper, ], exports: [SyncService, ServiceRegistry, WebhookService], }) diff --git a/packages/api/src/crm/engagement/services/microsoftdynamicssales/index.ts b/packages/api/src/crm/engagement/services/microsoftdynamicssales/index.ts new file mode 100644 index 000000000..27c89a3a6 --- /dev/null +++ b/packages/api/src/crm/engagement/services/microsoftdynamicssales/index.ts @@ -0,0 +1,361 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { CrmObject } from '@crm/@lib/@types'; +import { IEngagementService } from '@crm/engagement/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { + MicrosoftdynamicssalesEngagementAppointmentInput, + MicrosoftdynamicssalesEngagementAppointmentOutput, + MicrosoftdynamicssalesEngagementCallInput, + MicrosoftdynamicssalesEngagementCallOutput, + MicrosoftdynamicssalesEngagementEmailInput, + MicrosoftdynamicssalesEngagementEmailOutput, + MicrosoftdynamicssalesEngagementInput, + MicrosoftdynamicssalesEngagementOutput +} from './types'; + +@Injectable() +export class MicrosoftdynamicssalesService implements IEngagementService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.engagement.toUpperCase() + ':' + MicrosoftdynamicssalesService.name, + ); + this.registry.registerService('microsoftdynamicssales', this); + } + async addEngagement( + engagementData: MicrosoftdynamicssalesEngagementInput, + linkedUserId: string, + engagement_type: string, + ): Promise> { + try { + switch (engagement_type) { + case 'CALL': + return this.addCall( + engagementData as MicrosoftdynamicssalesEngagementCallInput, + linkedUserId, + ); + case 'MEETING': + return this.addMeeting( + engagementData as MicrosoftdynamicssalesEngagementAppointmentInput, + linkedUserId, + ); + case 'EMAIL': + return this.addEmail( + engagementData as MicrosoftdynamicssalesEngagementEmailInput, + linkedUserId, + ); + default: + break; + } + } catch (error) { + throw error; + } + } + + private async addCall( + engagementData: MicrosoftdynamicssalesEngagementCallInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const respToPost = await axios.post( + `${connection.account_url}/api/data/v9.2/phonecalls`, + JSON.stringify(engagementData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + const postCallId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postCallId}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + return { + data: resp.data, + message: 'Microsoftdynamicssales call created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + private async addMeeting( + engagementData: MicrosoftdynamicssalesEngagementAppointmentInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const respToPost = await axios.post( + `${connection.account_url}/api/data/v9.2/appointments`, + JSON.stringify(engagementData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + const postAppointmentId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postAppointmentId}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { + data: resp.data, + message: 'Microsoftdynamicssales meeting created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + private async addEmail( + engagementData: MicrosoftdynamicssalesEngagementEmailInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + const respToPost = await axios.post( + `${connection.account_url}/api/data/v9.2/emails`, + JSON.stringify(engagementData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + const postEmailId = respToPost.headers['location'].split("/").pop(); + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/${postEmailId}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + + return { + data: resp.data, + message: 'Microsoftdynamicssales email created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, custom_properties, engagement_type } = data; + + switch (engagement_type as string) { + case 'CALL': + return this.syncCalls(linkedUserId, custom_properties); + case 'MEETING': + return this.syncMeetings(linkedUserId, custom_properties); + case 'EMAIL': + return this.syncEmails(linkedUserId, custom_properties); + default: + break; + } + } catch (error) { + throw error; + } + } + + private async syncCalls(linkedUserId: string, custom_properties?: string[]) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + // const commonPropertyNames = Object.keys(commonCallMicrosoftdynamicssaleProperties); + // const allProperties = [...commonPropertyNames, ...custom_properties]; + // const baseURL = 'https://api.hubapi.com/crm/v3/objects/calls'; + + // const queryString = allProperties + // .map((prop) => `properties=${encodeURIComponent(prop)}`) + // .join('&'); + + // const url = `${baseURL}?${queryString}`; + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/phonecalls`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced microsoftdynamicssales engagements calls !`); + + return { + data: resp.data.value, + message: 'Microsoftdynamicssales engagements calls retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } + + private async syncMeetings( + linkedUserId: string, + custom_properties?: string[], + ) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + // const commonPropertyNames = Object.keys(commonMeetingMicrosoftdynamicssaleProperties); + // const allProperties = [...commonPropertyNames, ...custom_properties]; + // const baseURL = 'https://api.hubapi.com/crm/v3/objects/meetings'; + + // const queryString = allProperties + // .map((prop) => `properties=${encodeURIComponent(prop)}`) + // .join('&'); + + // const url = `${baseURL}?${queryString}`; + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/appointments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced microsoftdynamicssales engagements meetings !`); + + return { + data: resp.data.value, + message: 'Microsoftdynamicssales engagements meetings retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } + + private async syncEmails(linkedUserId: string, custom_properties?: string[]) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdynamicssales', + vertical: 'crm', + }, + }); + + // const commonPropertyNames = Object.keys(commonEmailMicrosoftdynamicssaleProperties); + // const allProperties = [...commonPropertyNames, ...custom_properties]; + // const baseURL = 'https://api.hubapi.com/crm/v3/objects/emails'; + + // const queryString = allProperties + // .map((prop) => `properties=${encodeURIComponent(prop)}`) + // .join('&'); + + // const url = `${baseURL}?${queryString}`; + + const resp = await axios.get( + `${connection.account_url}/api/data/v9.2/emails`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced microsoftdynamicssales engagements emails !`); + + return { + data: resp.data.value, + message: 'Microsoftdynamicssales engagements emails retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/engagement/services/microsoftdynamicssales/mappers.ts b/packages/api/src/crm/engagement/services/microsoftdynamicssales/mappers.ts new file mode 100644 index 000000000..95333a57b --- /dev/null +++ b/packages/api/src/crm/engagement/services/microsoftdynamicssales/mappers.ts @@ -0,0 +1,518 @@ +import { + MicrosoftdynamicssalesEngagementAppointmentInput, + MicrosoftdynamicssalesEngagementAppointmentOutput, + MicrosoftdynamicssalesEngagementCallInput, + MicrosoftdynamicssalesEngagementCallOutput, + MicrosoftdynamicssalesEngagementEmailInput, + MicrosoftdynamicssalesEngagementEmailOutput, + MicrosoftdynamicssalesEngagementInput, + MicrosoftdynamicssalesEngagementOutput +} from './types'; +import { + UnifiedCrmEngagementInput, + UnifiedCrmEngagementOutput, +} from '@crm/engagement/types/model.unified'; +import { IEngagementMapper } from '@crm/engagement/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MicrosoftdynamicssalesEngagementMapper implements IEngagementMapper { + constructor(private mappersRegistry: MappersRegistry, private utils: Utils) { + this.mappersRegistry.registerService('crm', 'engagement', 'microsoftdynamicssales', this); + } + + async desunify( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const type = source.type; + switch (type) { + case 'CALL': + return await this.desunifyCall(source, customFieldMappings); + case 'MEETING': + return await this.desunifyMeeting(source, customFieldMappings); + case 'EMAIL': + return await this.desunifyEmail(source, customFieldMappings); + default: + break; + } + return; + } + + private async desunifyCall( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesEngagementCallInput = { + description: source.content ?? '', + subject: source.subject ?? '', + }; + + if (source.start_at) { + result.actualstart = new Date(source.start_at).toUTCString(); + } + + if (source.end_time) { + result.actualend = new Date(source.end_time).toUTCString(); + } + + if (source.direction) { + result.directioncode = source.direction === 'INBOUND' ? false : true; + + } + + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + result["regardingobjectid_account@odata.bind"] = `/accounts(${id})`; + } + + // Only assigning first contact from contacts + if (source.contacts && source.contacts.length > 0) { + const id = await this.utils.getRemoteIdFromContactUuid(source.contacts[0]); + result["regardingobjectid_contact@odata.bind"] = `/contacts(${id})`; + } + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + private async desunifyMeeting( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesEngagementAppointmentInput = { + description: source.content ?? '', + subject: source.subject ?? '', + }; + + if (source.start_at) { + result.actualstart = new Date(source.start_at).toUTCString(); + } + + if (source.end_time) { + result.actualend = new Date(source.end_time).toUTCString(); + } + + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + result["regardingobjectid_account@odata.bind"] = `/accounts(${id})`; + } + + // Only assigning first contact from contacts + if (source.contacts && source.contacts.length > 0) { + const id = await this.utils.getRemoteIdFromContactUuid(source.contacts[0]); + result["regardingobjectid_contact@odata.bind"] = `/contacts(${id})`; + } + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + private async desunifyEmail( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: MicrosoftdynamicssalesEngagementEmailInput = { + description: source.content ?? '', + subject: source.subject ?? '', + }; + + if (source.start_at) { + result.actualstart = new Date(source.start_at).toUTCString(); + } + + if (source.end_time) { + result.actualend = new Date(source.end_time).toUTCString(); + } + + if (source.direction) { + result.directioncode = source.direction === 'INBOUND' ? false : true; + + } + + if (source.company_id) { + const id = await this.utils.getRemoteIdFromCompanyUuid(source.company_id); + result["regardingobjectid_account@odata.bind"] = `/accounts(${id})`; + } + + // Only assigning first contact from contacts + if (source.contacts && source.contacts.length > 0) { + const id = await this.utils.getRemoteIdFromContactUuid(source.contacts[0]); + result["regardingobjectid_contact@odata.bind"] = `/contacts(${id})`; + } + + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + async unify( + source: MicrosoftdynamicssalesEngagementOutput | MicrosoftdynamicssalesEngagementOutput[], + engagement_type: string, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + switch (engagement_type) { + case 'CALL': + return await this.unifyCall( + source as MicrosoftdynamicssalesEngagementCallOutput | MicrosoftdynamicssalesEngagementCallOutput[], + connectionId, + customFieldMappings, + ); + case 'MEETING': + return await this.unifyMeeting( + source as + | MicrosoftdynamicssalesEngagementAppointmentOutput + | MicrosoftdynamicssalesEngagementAppointmentOutput[], + connectionId, + customFieldMappings, + ); + case 'EMAIL': + return await this.unifyEmail( + source as + | MicrosoftdynamicssalesEngagementEmailOutput + | MicrosoftdynamicssalesEngagementEmailOutput[], + connectionId, + customFieldMappings, + ); + default: + break; + } + } + + private async unifyCall( + source: MicrosoftdynamicssalesEngagementCallOutput | MicrosoftdynamicssalesEngagementCallOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ) { + if (!Array.isArray(source)) { + return this.mapSingleEngagementCallToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesEngagementOutput + return Promise.all( + source.map((engagement) => + this.mapSingleEngagementCallToUnified( + engagement, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async unifyMeeting( + source: MicrosoftdynamicssalesEngagementAppointmentOutput | MicrosoftdynamicssalesEngagementAppointmentOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ) { + if (!Array.isArray(source)) { + return this.mapSingleEngagementMeetingToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesEngagementOutput + return Promise.all( + source.map((engagement) => + this.mapSingleEngagementMeetingToUnified( + engagement, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async unifyEmail( + source: MicrosoftdynamicssalesEngagementEmailOutput | MicrosoftdynamicssalesEngagementEmailOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ) { + if (!Array.isArray(source)) { + return this.mapSingleEngagementEmailToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of MicrosoftdynamicssalesEngagementOutput + return Promise.all( + source.map((engagement) => + this.mapSingleEngagementEmailToUnified( + engagement, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEngagementCallToUnified( + engagement: MicrosoftdynamicssalesEngagementCallOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = engagement[mapping.remote_id]; + } + } + + let opts: any = {}; + if (engagement._ownerid_value) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + engagement._ownerid_value, + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + if (engagement._regardingobjectid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (company_id) { + opts = { + ...opts, + company_id: company_id + } + } + + const contact_id = await this.utils.getContactUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (contact_id) { + opts = { + ...opts, + contacts: [contact_id] + } + } + } + + + + return { + remote_data: engagement, + remote_id: engagement.activityid, + content: engagement.description ?? '', + subject: engagement.subject ?? '', + start_at: engagement.actualstart ? new Date(engagement.actualstart) : null, + end_time: engagement.actualend ? new Date(engagement.actualend) : null, + type: 'CALL', + direction: engagement.directioncode ? engagement.directioncode ? "OUTBOUND" : "INBOUND" : null, + field_mappings, + ...opts, + }; + } + + private async mapSingleEngagementMeetingToUnified( + engagement: MicrosoftdynamicssalesEngagementAppointmentOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = engagement[mapping.remote_id]; + } + } + + let opts: any = {}; + if (engagement._ownerid_value) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + engagement._ownerid_value, + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + if (engagement._regardingobjectid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (company_id) { + opts = { + ...opts, + company_id: company_id + } + } + + const contact_id = await this.utils.getContactUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (contact_id) { + opts = { + ...opts, + contacts: [contact_id] + } + } + } + + return { + remote_data: engagement, + remote_id: engagement.activityid, + content: engagement.description ?? '', + subject: engagement.subject ?? '', + start_at: engagement.actualstart ? new Date(engagement.actualstart) : null, + end_time: engagement.actualend ? new Date(engagement.actualend) : null, + type: 'MEETING', + field_mappings, + ...opts, + }; + } + + private async mapSingleEngagementEmailToUnified( + engagement: MicrosoftdynamicssalesEngagementEmailOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = engagement[mapping.remote_id]; + } + } + + let opts: any = {}; + if (engagement._ownerid_value) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + engagement._ownerid_value, + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + if (engagement._regardingobjectid_value) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (company_id) { + opts = { + ...opts, + company_id: company_id + } + } + + const contact_id = await this.utils.getContactUuidFromRemoteId( + engagement._regardingobjectid_value, + connectionId + ); + + if (contact_id) { + opts = { + ...opts, + contacts: [contact_id] + } + } + } + + return { + remote_id: engagement.activityid, + remote_data: engagement, + content: engagement.description ?? '', + start_at: engagement.actualstart ? new Date(engagement.actualstart) : null, + end_time: engagement.actualend ? new Date(engagement.actualend) : null, + subject: engagement.subject ?? '', + type: 'EMAIL', + direction: engagement.directioncode ? engagement.directioncode ? "OUTBOUND" : "INBOUND" : null, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/engagement/services/microsoftdynamicssales/types.ts b/packages/api/src/crm/engagement/services/microsoftdynamicssales/types.ts new file mode 100644 index 000000000..92a7f8f97 --- /dev/null +++ b/packages/api/src/crm/engagement/services/microsoftdynamicssales/types.ts @@ -0,0 +1,276 @@ +interface MicrosoftdynamicssalesEmail { + activityadditionalparams: string; + activityid: string; // Guid + activitytypecode: string; + actualdurationminutes: number; + actualend: string; + actualstart: string; + attachmentcount: number; + attachmentopencount: number; + baseconversationindexhash: number; + category: string; + community: number; + compressed: boolean; + conversationindex: string; + conversationtrackingid: string; // Guid + correlatedsubjectchanged: boolean; + correlationmethod: number; + createdon: string; + delayedemailsendtime: string; + deliveryattempts: number; + deliverylastattemptedon: string; + deliveryprioritycode: number; + deliveryreceiptrequested: boolean; + description: string; + descriptionblobid: string; // Binary + descriptionblobid_name: string; + directioncode: boolean; + emailreminderexpirytime: string; + emailreminderstatus: number; + emailremindertext: string; + emailremindertype: number; + emailtrackingid: string; // Guid + exchangeitemid: string; + exchangerate: number; + exchangeweblink: string; + followemailuserpreference: boolean; + formattedscheduledend: string; + formattedscheduledstart: string; + importsequencenumber: number; + inreplyto: string; + instancetypecode: number; + internetmessageheaders: string; + isbilled: boolean; + isduplicatesenderunresolved: boolean; + isemailfollowed: boolean; + isemailreminderset: boolean; + ismapiprivate: boolean; + isregularactivity: boolean; + isunsafe: number; + isworkflowcreated: boolean; + lastonholdtime: string; + lastopenedtime: string; + leftvoicemail: boolean; + linksclickedcount: number; + messageid: string; + mimetype: string; + modifiedon: string; + notifications: number; + onholdtime: number; + opencount: number; + overriddencreatedon: string; + postponeactivityprocessinguntil: string; + postponeemailprocessinguntil: string; + prioritycode: number; + processid: string; // Guid + readreceiptrequested: boolean; + reminderactioncardid: string; // Guid + replycount: number; + reservedforinternaluse: string; + safedescription: string; + scheduleddurationminutes: number; + scheduledend: string; + scheduledstart: string; + sender: string; + senton: string; + seriesid: string; // Guid + sortdate: string; + stageid: string; // Guid + statecode: number; + statuscode: number; + subcategory: string; + subject: string; + submittedby: string; + timezoneruleversionnumber: number; + torecipients: string; + trackingtoken: string; + traversedpath: string; + utcconversiontimezonecode: number; + versionnumber: number; + _acceptingentityid_value: string; + _createdby_value: string; + _createdonbehalfby_value: string; + _emailsender_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _receivingmailboxid_value: string; + _regardingobjectid_value: string; + _sendermailboxid_value: string; + _sendersaccount_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _templateid_value: string; + _transactioncurrencyid_value: string; + [key: string]: any; +} + +interface MicrosoftdynamicssalesAppointment { + activityadditionalparams: string; + activityid: string; // Guid + activitytypecode: string; + actualdurationminutes: number; + actualend: string; + actualstart: string; + attachmenterrors: number; + category: string; + community: number; + createdon: string; + deliverylastattemptedon: string; + deliveryprioritycode: number; + description: string; + descriptionblobid: string; // Binary + descriptionblobid_name: string; + exchangeitemid: string; + exchangerate: number; + exchangeweblink: string; + formattedscheduledend: string; + formattedscheduledstart: string; + globalobjectid: string; + importsequencenumber: number; + instancetypecode: number; + isalldayevent: boolean; + isbilled: boolean; + isdraft: boolean; + ismapiprivate: boolean; + isonlinemeeting: boolean; + isregularactivity: boolean; + isunsafe: number; + isworkflowcreated: boolean; + lastonholdtime: string; + leftvoicemail: boolean; + location: string; + modifiedfieldsmask: string; + modifiedon: string; + onholdtime: number; + onlinemeetingchatid: string; + onlinemeetingid: string; + onlinemeetingjoinurl: string; + onlinemeetingtype: number; + originalstartdate: string; + outlookownerapptid: number; + overriddencreatedon: string; + postponeactivityprocessinguntil: string; + prioritycode: number; + processid: string; // Guid + scheduleddurationminutes: number; + scheduledend: string; + scheduledstart: string; + senton: string; + seriesid: string; // Guid + sortdate: string; + stageid: string; // Guid + statecode: number; + statuscode: number; + subcategory: string; + subject: string; + subscriptionid: string; // Guid + timezoneruleversionnumber: number; + traversedpath: string; + utcconversiontimezonecode: number; + versionnumber: number; + _createdby_value: string; + _createdonbehalfby_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _regardingobjectid_value: string; + _sendermailboxid_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _transactioncurrencyid_value: string; + [key: string]: any; +} + +interface MicrosoftdynamicssalesPhoneCall { + activityadditionalparams: string; + activityid: string; // Guid + activitytypecode: string; + actualdurationminutes: number; + actualend: string; + actualstart: string; + category: string; + community: number; + createdon: string; + deliverylastattemptedon: string; + deliveryprioritycode: number; + description: string; + descriptionblobid: string; // Binary + descriptionblobid_name: string; + directioncode: boolean; + exchangeitemid: string; + exchangerate: number; + exchangeweblink: string; + formattedscheduledend: string; + formattedscheduledstart: string; + importsequencenumber: number; + instancetypecode: number; + isbilled: boolean; + ismapiprivate: boolean; + isregularactivity: boolean; + isworkflowcreated: boolean; + lastonholdtime: string; + leftvoicemail: boolean; + modifiedon: string; + onholdtime: number; + overriddencreatedon: string; + phonenumber: string; + postponeactivityprocessinguntil: string; + prioritycode: number; + processid: string; // Guid + scheduleddurationminutes: number; + scheduledend: string; + scheduledstart: string; + senton: string; + seriesid: string; // Guid + sortdate: string; + stageid: string; // Guid + statecode: number; + statuscode: number; + subcategory: string; + subject: string; + subscriptionid: string; // Guid + timezoneruleversionnumber: number; + traversedpath: string; + utcconversiontimezonecode: number; + versionnumber: number; + _createdby_value: string; + _createdonbehalfby_value: string; + _modifiedby_value: string; + _modifiedonbehalfby_value: string; + _ownerid_value: string; + _owningbusinessunit_value: string; + _owningteam_value: string; + _owninguser_value: string; + _regardingobjectid_value: string; + _sendermailboxid_value: string; + _slaid_value: string; + _slainvokedid_value: string; + _transactioncurrencyid_value: string; + [key: string]: any; +} + +export type MicrosoftdynamicssalesEngagementCallInput = Partial; +export type MicrosoftdynamicssalesEngagementCallOutput = MicrosoftdynamicssalesEngagementCallInput; + +export type MicrosoftdynamicssalesEngagementEmailInput = Partial; +export type MicrosoftdynamicssalesEngagementEmailOutput = MicrosoftdynamicssalesEngagementEmailInput; + +export type MicrosoftdynamicssalesEngagementAppointmentInput = Partial; +export type MicrosoftdynamicssalesEngagementAppointmentOutput = MicrosoftdynamicssalesEngagementAppointmentInput; + + +export type MicrosoftdynamicssalesEngagementInput = MicrosoftdynamicssalesEngagementCallInput + | MicrosoftdynamicssalesEngagementEmailInput + | MicrosoftdynamicssalesEngagementAppointmentInput; + +export type MicrosoftdynamicssalesEngagementOutput = MicrosoftdynamicssalesEngagementCallOutput + | MicrosoftdynamicssalesEngagementEmailOutput + | MicrosoftdynamicssalesEngagementAppointmentOutput; +