diff --git a/packages/api/src/crm/contact/services/leadsquared/index.ts b/packages/api/src/crm/contact/services/leadsquared/index.ts new file mode 100644 index 000000000..8296167dc --- /dev/null +++ b/packages/api/src/crm/contact/services/leadsquared/index.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@nestjs/common'; +import { IContactService } from '@crm/contact/types'; +import { CrmObject } from '@crm/@lib/@types'; +import axios from 'axios'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.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 { + LeadSquaredContactInput, + LeadSquaredContactOutput, + LeadSquaredContactResponse, +} from './types'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LeadSquaredService implements IContactService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + LeadSquaredService.name, + ); + this.registry.registerService('zoho', this); + } + + formatDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const currentDate = date.getUTCDate(); + const hours = date.getUTCHours(); + const minutes = date.getUTCMinutes(); + const seconds = date.getUTCSeconds(); + return `${year}-${month}-${currentDate} ${hours}:${minutes}:${seconds}`; + } + + async addContact( + contactData: LeadSquaredContactInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const resp = await axios.post( + `${connection.account_url}/v2/LeadManagement.svc/Lead.Create`, + contactData, + { + headers, + }, + ); + const userId = resp.data['Message']['Id']; + const final_res = await axios.get( + `${connection.account_url}/v2/LeadManagement.svc/Leads.GetById?id=${userId}`, + { + headers, + }, + ); + + return { + data: final_res.data.data[0], + message: 'Leadsquared 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: 'leadsquared', + vertical: 'crm', + }, + }); + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const fromDate = this.formatDate(new Date(0)); + const toDate = this.formatDate(new Date()); + + const resp = await axios.get( + `${connection.account_url}/v2/LeadManagement.svc/Leads.RecentlyModified`, + { + Parameter: { + FromDate: fromDate, + ToDate: toDate, + }, + }, + { + headers, + }, + ); + const leads = resp?.data['Leads'].map( + (lead: LeadSquaredContactResponse) => { + const leadSquaredContact: LeadSquaredContactOutput = {}; + lead.LeadPropertyList.map( + ({ Attribute, Value }: { Attribute: string; Value: string }) => { + leadSquaredContact[Attribute] = Value; + }, + ); + return leadSquaredContact; + }, + ); + //this.logger.log('CONTACTS LEADSQUARED ' + JSON.stringify(resp.data.data)); + this.logger.log(`Synced leadsquared contacts !`); + return { + data: leads || [], + message: 'Leadsquared contacts retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/contact/services/leadsquared/mappers.ts b/packages/api/src/crm/contact/services/leadsquared/mappers.ts new file mode 100644 index 000000000..96334dd6e --- /dev/null +++ b/packages/api/src/crm/contact/services/leadsquared/mappers.ts @@ -0,0 +1,176 @@ +import { Address } from '@crm/@lib/@types'; +import { + UnifiedCrmContactInput, + UnifiedCrmContactOutput, +} from '@crm/contact/types/model.unified'; +import { IContactMapper } from '@crm/contact/types'; +import { LeadSquaredContactInput, LeadSquaredContactOutput } from './types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LeadSquaredContactMapper implements IContactMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + ) { + this.mappersRegistry.registerService('crm', 'contact', 'zoho', this); + } + desunify( + source: UnifiedCrmContactInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): LeadSquaredContactInput { + // Assuming 'email_addresses' array contains at least one email and 'phone_numbers' array contains at least one phone number + const primaryEmail = source.email_addresses?.[0]?.email_address; + const primaryPhone = source.phone_numbers?.[0]?.phone_number; + + const result: LeadSquaredContactInput = { + First_Name: source.first_name, + Last_Name: source.last_name, + }; + if (primaryEmail) { + result.EmailAddress = primaryEmail; + } + if (primaryPhone && source.phone_numbers?.[0]?.phone_type == 'WORK') { + result.Account_Phone = primaryPhone; + } + if (primaryPhone && source.phone_numbers?.[0]?.phone_type == 'MOBILE') { + result.Mobile = primaryPhone; + } + if (source.addresses && source.addresses[0]) { + result.Account_Street1 = source.addresses[0].street_1; + result.Account_City = source.addresses[0].city; + result.Account_State = source.addresses[0].state; + result.Account_Zip = source.addresses[0].postal_code; + result.Account_Country = source.addresses[0].country; + } + if (source.user_id) { + result.OwnerId = source.user_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: LeadSquaredContactOutput | LeadSquaredContactOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleContactToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + // Handling array of HubspotContactOutput + return Promise.all( + source.map((contact) => + this.mapSingleContactToUnified( + contact, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleContactToUnified( + contact: LeadSquaredContactOutput, + 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]; + } + } + // Constructing email and phone details + const email_addresses = + contact && contact.EmailAddress + ? [ + { + email_address: contact.EmailAddress, + email_address_type: 'PERSONAL', + }, + ] + : []; + + const phone_numbers = []; + + if (contact && contact.Account_Phone) { + phone_numbers.push({ + phone_number: contact.Account_Phone, + phone_type: 'WORK', + }); + } + if (contact && contact.Mobile) { + phone_numbers.push({ + phone_number: contact.Mobile, + phone_type: 'MOBILE', + }); + } + if (contact && contact.Account_Fax) { + phone_numbers.push({ + phone_number: contact.Account_Fax, + phone_type: 'fax', + }); + } + if (contact && contact.Phone) { + phone_numbers.push({ + phone_number: contact.Phone, + phone_type: 'home', + }); + } + + const address: Address = { + street_1: contact.Account_Street1, + city: contact.Account_City, + state: contact.Account_State, + postal_code: contact.Account_Zip, + country: contact.Account_Country, + }; + + const opts: any = {}; + if (contact.OwnerId) { + opts.user_id = await this.utils.getUserUuidFromRemoteId( + contact.OwnerId, + connectionId, + ); + } + + return { + remote_id: String(contact.pros), + remote_data: contact, + first_name: contact.First_Name ?? null, + last_name: contact.Last_Name ?? null, + email_addresses, + phone_numbers, + field_mappings, + ...opts, + addresses: [address], + }; + } +} diff --git a/packages/api/src/crm/contact/services/leadsquared/types.ts b/packages/api/src/crm/contact/services/leadsquared/types.ts new file mode 100644 index 000000000..8f17b62b3 --- /dev/null +++ b/packages/api/src/crm/contact/services/leadsquared/types.ts @@ -0,0 +1,108 @@ +type LeadSquaredContact = { + /* + * user_id + * first_name + * last_name + * email + * phone + * address + */ + ProspectID: string; + FirstName: string; + LastName: string; + EmailAddress: string; + Company: string; + OwnerId: string; + Origin: string; + Phone: string | null; + Mobile: string | null; + Website: string | null; + TimeZone: string | null; + Source: string; + SourceMedium: string | null; + Notes: string | null; + SourceCampaign: string | null; + SourceContent: string | null; + DoNotEmail: '0' | '1'; + DoNotCall: '0' | '1'; + ProspectStage: string; + Score: string; + Revenue: string; + EngagementScore: string; + TotalVisits: string | null; + PageViewsPerVisit: string | null; + AvgTimePerVisit: string | null; + RelatedProspectId: string | null; + ProspectActivityId_Min: string; + ProspectActivityDate_Min: string; + Web_Referrer: string | null; + Web_RefKeyword: string | null; + ProspectActivityId_Max: string; + ProspectActivityName_Max: string; + ProspectActivityDate_Max: string; + RelatedLandingPageId: string | null; + FirstLandingPageSubmissionId: string | null; + FirstLandingPageSubmissionDate: string | null; + CreatedBy: string; + CreatedOn: string; + ModifiedBy: string; + ModifiedOn: string; + LeadConversionDate: string | null; + StatusCode: '0'; + StatusReason: '0'; + IsLead: '1'; + NotableEvent: 'Modified'; + NotableEventdate: string; + SourceReferrer: string | null; + LastVisitDate: string; + CompanyType: string | null; + RelatedCompanyId: string | null; + IsPrimaryContact: string; + MailingPreferences: string | null; + LastOptInEmailSentDate: null; + DoNotTrack: null; + RelatedCompanyIdName: null; + RelatedCompanyOwnerId: null; + CompanyTypeName: null; + CompanyTypePluralName: null; + LeadLastModifiedOn: string; + OwnerIdName: string; + OwnerIdEmailAddress: string; + Groups: string; + CreatedByName: string; + ModifiedByName: string; + Account_CompanyName: string; + Account_ShortName: string; + Account_TimeZone: string; + Account_Website: string; + Account_Street1: string; + Account_Street2: string; + Account_City: string; + Account_State: string; + Account_Country: string; + Account_Zip: string; + Account_Fax: string; + Account_Phone: string; + Owner_FirstName: string; + Owner_MiddleName: string; + Owner_LastName: string; + Owner_EmailAddress: string; + Owner_FullName: string; + Owner_TimeZone: string; + Owner_AssociatedPhoneNumbers: string; + Org_ShortCode: string; + Account_Address: string; + [key: string]: string | number | boolean | null; +}; + +export type LeadSquaredContactResponse = { + LeadPropertyList: [ + { + Attribute: string; + Value: string; + }, + ]; +}; + +export type LeadSquaredContactInput = Partial; +export type LeadSquaredContactOutput = LeadSquaredContactInput; diff --git a/packages/api/src/crm/deal/services/leadsquared/index.ts b/packages/api/src/crm/deal/services/leadsquared/index.ts new file mode 100644 index 000000000..b81aac615 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/index.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@nestjs/common'; +import { IDealService } from '@crm/deal/types'; +import { CrmObject } from '@crm/@lib/@types'; +import { LeadSquaredDealInput, LeadSquaredDealOutput } from './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 { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LeadSquaredService implements IDealService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.deal.toUpperCase() + ':' + LeadSquaredService.name, + ); + this.registry.registerService('leadsquared', this); + } + + async addDeal( + dealData: LeadSquaredDealInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + const resp = await axios.post( + `${connection.account_url}/v2/OpportunityManagement.svc/Capture`, + dealData, + { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + const opportunityId = resp.data.CreatedOpportunityId; + const opportunityResp = await axios.get( + `${connection.account_url}/v2/OpportunityManagement.svc/GetOpportunityDetails?OpportunityId=${opportunityId}`, + { + headers: { + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + return { + data: opportunityResp.data, + message: 'Leadsquared deal created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + // TODO: I'm not sure about this + const { linkedUserId, leadId, opportunityType } = data; + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'pipedrive', + vertical: 'crm', + }, + }); + let url = `${connection.account_url}/v2/OpportunityManagement.svc/GetOpportunitiesOfLead?leadId=${leadId}`; + + if (opportunityType) { + url += `&opportunityType=${opportunityType}`; + } + + const resp = await axios.post(url, { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }); + + return { + data: resp.data['List'], + message: 'Leadsquared deals retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/deal/services/leadsquared/mappers.ts b/packages/api/src/crm/deal/services/leadsquared/mappers.ts new file mode 100644 index 000000000..8e6d3c0ad --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/mappers.ts @@ -0,0 +1,147 @@ +import { LeadSquaredDealInput, LeadSquaredDealOutput } from './types'; +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 { Injectable } from '@nestjs/common'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { CrmObject } from '@crm/@lib/@types'; +import { UnifiedCrmStageOutput } from '@crm/stage/types/model.unified'; +import { ZohoStageOutput } from '@crm/stage/services/zoho/types'; + +@Injectable() +export class LeadSquaredDealMapper implements IDealMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + ) { + this.mappersRegistry.registerService('crm', 'deal', 'leadsquared', this); + } + async desunify( + source: UnifiedCrmDealInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: LeadSquaredDealInput = { + OpportunityNote: source.description, + OpportunityName: source.name, + Amount: source.amount ?? 0, + }; + if (source.company_id) { + result.Account_Name = { + id: await this.utils.getRemoteIdFromCompanyUuid(source.company_id), + name: await this.utils.getCompanyNameFromUuid(source.company_id), + }; + } + if (source.stage_id) { + result.Stage = await this.utils.getStageNameFromStageUuid( + source.stage_id, + ); + } + if (source.user_id) { + result.Owner = { + id: await this.utils.getRemoteIdFromUserUuid(source.user_id), + } as any; + } + + 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: LeadSquaredDealOutput | LeadSquaredDealOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleDealToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + return Promise.all( + source.map((deal) => + this.mapSingleDealToUnified(deal, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleDealToUnified( + deal: LeadSquaredDealOutput, + 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]; + } + } + const res: UnifiedCrmDealOutput = { + remote_id: deal.OpportunityId, + remote_data: deal, + name: deal.OpportunityNote, + description: deal.OpportunityNote ?? '', // todo null + amount: deal.Amount, + field_mappings, + }; + + if (deal.Stage) { + // we insert right way inside our db as there are no endpoint to do so in the Zoho api + const stage = await this.ingestService.ingestData< + UnifiedCrmStageOutput, + ZohoStageOutput + >( + [ + { + Stage_Name: deal.Stage, + }, + ], + 'leadsquared', + connectionId, + 'crm', + CrmObject.stage, + [], + ); + res.stage_id = stage[0].id_crm_deals_stage; + } + + if (deal.ProspectId) { + res.user_id = await this.utils.getUserUuidFromRemoteId( + deal.ProspectId, + connectionId, + ); + } + if (deal.LeadOwner) { + res.company_id = await this.utils.getCompanyUuidFromRemoteId( + deal.LeadOwner, + connectionId, + ); + } + return res; + } +} diff --git a/packages/api/src/crm/deal/services/leadsquared/types.ts b/packages/api/src/crm/deal/services/leadsquared/types.ts new file mode 100644 index 000000000..287111e34 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/types.ts @@ -0,0 +1,52 @@ +type LeadDetail = { + Attribute: string; + Value: string; +}; +// If comments are enabled on opportunity status change, you must pass the mx_Custom_19 (of datatype ‘String’) +type Field = { + SchemaName: string; + Value: string; +}; + +type Opportunity = { + Filelds: Field[]; + OpportunityEventCode: number; + OpportunityNote?: string; + OpportunityDateTime?: string; //date and time is in the yyyy-mm-dd hh:mm:ss format. + OverwriteFields?: boolean; + UpdateEmptyFields?: boolean; + DoNotPostDuplicateActivity?: boolean; + DoNotChangeOwner?: boolean; +}; + +export type LeadSquaredDealInput = { + LeadDetails: LeadDetail[]; // atleast 1 unique field is required. Attribute 'SearchBy' is required + Opportunity: Opportunity; +}; + +export type LeadSquaredDealOutput = { + ProspectId: string; + FirstName: string; + LastName: string; + EmailAddress: string; + Phone: string; + DoNotCall: '0' | '1'; + DoNotEmail: '0' | '1'; + LeadName: string; + LeadOwner: string; + OpportunityEventType: string; + OpportunityEvent: string; + OpportunityNote: string; + Score: string; + PACreatedOn: string; //'2020-09-16 05:43:00'; + PAModifiedOn: string; //'2020-09-16 07:26:09'; + IP_Latitude: string | null; + IP_Longitude: string | null; + PACreatedByName: string; + Status: string; + Owner: string; + OwnerName: string; + OpportunityId: string; + Total: string; + [key: string]: string | number | null; +}; diff --git a/packages/api/src/crm/engagement/services/leadsquared/index.ts b/packages/api/src/crm/engagement/services/leadsquared/index.ts new file mode 100644 index 000000000..c001c63f9 --- /dev/null +++ b/packages/api/src/crm/engagement/services/leadsquared/index.ts @@ -0,0 +1,297 @@ +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 { + LeadSquaredEngagementCallInput, + LeadSquaredEngagementEmailInput, + LeadSquaredEngagementEmailOutput, + LeadSquaredEngagementInput, + LeadSquaredEngagementMeetingInput, + LeadSquaredEngagementMeetingOutput, + LeadSquaredEngagementOutput, +} from './types'; + +@Injectable() +export class LeadSquaredService implements IEngagementService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.engagement.toUpperCase() + ':' + LeadSquaredService.name, + ); + this.registry.registerService('leadsquared', this); + } + + formatDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const currentDate = date.getUTCDate(); + const hours = date.getUTCHours(); + const minutes = date.getUTCMinutes(); + const seconds = date.getUTCSeconds(); + return `${year}-${month}-${currentDate} ${hours}:${minutes}:${seconds}`; + } + + async addEngagement( + engagementData: LeadSquaredEngagementInput, + linkedUserId: string, + engagement_type: string, + ): Promise> { + try { + switch (engagement_type) { + case 'CALL': + return this.addCall( + engagementData as LeadSquaredEngagementCallInput, + linkedUserId, + ); + case 'MEETING': + return this.addMeeting( + engagementData as LeadSquaredEngagementMeetingInput, + linkedUserId, + ); + case 'EMAIL': + return this.addEmail( + engagementData as LeadSquaredEngagementEmailInput, + linkedUserId, + ); + default: + break; + } + } catch (error) { + throw error; + } + } + + private async addCall( + engagementData: LeadSquaredEngagementCallInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const resp = await axios.post( + `${connection.account_url}/v2/Telephony.svc/LogCall`, + engagementData, + { + headers, + }, + ); + return { + data: resp.data, + message: 'LeadSquared call created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + private async addMeeting( + engagementData: LeadSquaredEngagementMeetingInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const resp = await axios.post( + `${connection.account_url}/v2/Task.svc/Create`, + engagementData, + { + headers, + }, + ); + const taskId = resp.data['Message']['Id']; + const taskResponse = await axios.get( + `${connection.account_url}/v2/Task.svc/Retrieve.GetById?id=${taskId}`, + { + headers, + }, + ); + return { + data: taskResponse.data[0], + message: 'Leadsquared meeting created', + statusCode: 201, + }; + } catch (e) { + throw e; + } + } + + private async addEmail( + engagementData: LeadSquaredEngagementEmailInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const resp = await axios.post( + `${connection.account_url}/v2/EmailMarketing.svc/SendEmailToLead`, + engagementData, + { + headers, + }, + ); + return { + data: resp.data, + message: 'LeadSquared email created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + private async syncEmails(linkedUserId: string) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + const fromDate = this.formatDate(new Date(0)); + const toDate = this.formatDate(new Date()); + const requestBody = { + Parameter: { + FromDate: fromDate, + ToDate: toDate, + EmailEvent: 'Sent', + }, + }; + const resp = await axios.get( + `${connection.account_url}/v2/EmailMarketing.svc/RetrieveSentEmails`, + requestBody, + { + headers, + }, + ); + this.logger.log(`Synced leadsquared emails engagements !`); + return { + data: resp.data['Records'], + message: 'LeadSquared engagements retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } + + private async syncMeetings(linkedUserId: string) { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + + const fromDate = this.formatDate(new Date(0)); + const toDate = this.formatDate(new Date()); + + const payload = { + FromDate: fromDate, + ToDate: toDate, + Users: [linkedUserId], + }; + + const resp = await axios.post( + `${connection.account_url}/v2/Task.svc/RetrieveAppointments/ByUserId`, + payload, + { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + this.logger.log(`Synced leadsquared meetings !`); + return { + data: resp.data['List'], + message: 'Leadsquared meetings retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } + + async sync( + data: SyncParam, + ): Promise> { + try { + const { linkedUserId, engagement_type } = data; + + switch (engagement_type as string) { + // there was no any endpoint to sync calls + // case 'CALL': + // return this.syncCalls(linkedUserId); + case 'MEETING': + return this.syncMeetings(linkedUserId); + case 'EMAIL': + return this.syncEmails(linkedUserId); + + default: + break; + } + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/engagement/services/leadsquared/mappers.ts b/packages/api/src/crm/engagement/services/leadsquared/mappers.ts new file mode 100644 index 000000000..5c97b4ace --- /dev/null +++ b/packages/api/src/crm/engagement/services/leadsquared/mappers.ts @@ -0,0 +1,364 @@ +import { + LeadSquaredEngagementCallInput, + LeadSquaredEngagementEmailInput, + LeadSquaredEngagementEmailOutput, + LeadSquaredEngagementInput, + LeadSquaredEngagementMeetingInput, + LeadSquaredEngagementMeetingOutput, + LeadSquaredEngagementOutput, +} 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 LeadSquaredEngagementMapper implements IEngagementMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + ) { + this.mappersRegistry.registerService( + 'crm', + 'engagement', + 'leadsquared', + this, + ); + } + + formatDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const currentDate = date.getUTCDate(); + const hours = date.getUTCHours(); + const minutes = date.getUTCMinutes(); + const seconds = date.getUTCSeconds(); + return `${year}-${month}-${currentDate} ${hours}:${minutes}:${seconds}`; + } + + 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 desunifyEmail( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: LeadSquaredEngagementEmailInput = { + Subject: source.subject, + EmailType: 'Html', + ContentHTML: source.content, + ContentText: source.content, + IncludeEmailFooter: true, + }; + + if (source.user_id) { + const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (owner_id) { + result.SenderType = 'UserId'; + result.Sender = owner_id; + } + } + + if (source.company_id) { + const company_id = await this.utils.getRemoteIdFromCompanyUuid( + source.company_id, + ); + if (company_id) { + result.RecipientType = 'LeadId'; + result.Recipient = company_id; + } + } + + // contact is lead in this case + if (source.contacts && source.contacts.length > 0) { + const contact_id = await this.utils.getRemoteIdFromContactUuid( + source.contacts[0], + ); + result.RecipientType = 'LeadId'; + result.Recipient = contact_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: LeadSquaredEngagementMeetingInput = { + Name: source.subject, + Description: source.content, + StatusCode: '0', + NotifyBy: '1100', + Reminder: 30, + TaskType: { + Name: 'Meeting', + }, + }; + + if (source.start_at) { + result.DueDate = this.formatDate(new Date(source.start_at)); + } + + if (source.end_time) { + result.EndDate = this.formatDate(new Date(source.end_time)); + } + + if (source.user_id) { + const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (owner_id) { + result.OwnerId = owner_id; + } + } + + if (source.company_id) { + const company_id = await this.utils.getRemoteIdFromCompanyUuid( + source.company_id, + ); + if (company_id) { + result.RelatedEntity = '1'; + result.RelatedEntityId = company_id; + } + } + + // contact is lead in this case + if (source.contacts && source.contacts.length > 0) { + const lead_id = await this.utils.getRemoteIdFromCompanyUuid( + source.company_id, + ); + if (lead_id) { + result.RelatedEntity = '1'; + result.RelatedEntityId = lead_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 desunifyCall( + source: UnifiedCrmEngagementInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: LeadSquaredEngagementCallInput = { + Direction: source.direction === 'INBOUND' ? 'Inbound' : 'Outbound', + CallerSource: source.content || '', + LeadId: source.company_id || '', + SourceNumber: '', // todo, + DisplayNumber: '', // todo + DestinationNumber: '', // todo, + }; + + if (source.start_at && source.end_time) { + const startDate = new Date(source.start_at); + const endDate = new Date(source.end_time); + + // Calculate the difference in milliseconds + const diffMilliseconds = endDate.getTime() - startDate.getTime(); + + // Convert milliseconds to seconds + const durationInSeconds = Math.round(diffMilliseconds / 1000); + result.StartTime = this.formatDate(startDate); + result.EndTime = this.formatDate(endDate); + result.CallDuration = durationInSeconds; + result.Status = 'Answered'; + } + + if (source.user_id) { + const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (owner_id) { + result.UserId = owner_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: LeadSquaredEngagementOutput | LeadSquaredEngagementOutput[], + engagement_type: string, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + switch (engagement_type) { + case 'CALL': + return; + case 'MEETING': + return await this.unifyMeeting( + source as LeadSquaredEngagementMeetingOutput, + connectionId, + customFieldMappings, + ); + case 'EMAIL': + return await this.unifyEmail( + source as LeadSquaredEngagementEmailOutput, + connectionId, + customFieldMappings, + ); + + default: + break; + } + } + + private async unifyMeeting( + engagement: LeadSquaredEngagementMeetingOutput, + 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) { + const owner_id = await this.utils.getUserUuidFromRemoteId( + String(engagement.OwnerId), + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + + return { + remote_id: String(engagement.UserTaskId), + remote_data: engagement, + content: engagement.Description || engagement.Name, + subject: null, + start_at: new Date(engagement.DueDate), + end_time: new Date(engagement.EndDate), + type: 'MEETING', + field_mappings, + direction: '', + ...opts, + }; + } + private async unifyEmail( + engagement: LeadSquaredEngagementEmailOutput, + 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.SenderType === 'UserId') { + const owner_id = await this.utils.getUserUuidFromRemoteId( + String(engagement.Sender), + connectionId, + ); + if (owner_id) { + opts = { + ...opts, + user_id: owner_id, + }; + } + } + if (engagement.RecipientType === 'LeadId') { + const lead_id = await this.utils.getCompanyUuidFromRemoteId( + String(engagement.Recipient), + connectionId, + ); + if (lead_id) { + opts = { + ...opts, + company_id: lead_id, + }; + } + } + + return { + remote_id: String(engagement.EmailId), + remote_data: engagement, + content: engagement.ContentText, + subject: engagement.Subject, + type: 'EMAIL', + field_mappings, + direction: '', + ...opts, + }; + } +} diff --git a/packages/api/src/crm/engagement/services/leadsquared/types.ts b/packages/api/src/crm/engagement/services/leadsquared/types.ts new file mode 100644 index 000000000..4ca6e46fa --- /dev/null +++ b/packages/api/src/crm/engagement/services/leadsquared/types.ts @@ -0,0 +1,116 @@ +interface KeyValuePair { + [key: string]: unknown; +} + +export type LeadSquaredEngagementCall = { + SourceNumber: string; //'+91-8611795988'; + CallerSource: string; //'Example Source'; + DestinationNumber: string; //'+91-9611795983'; + DisplayNumber: string; //'+91-8611795989'; + StartTime: string; //'2017-07-07 18:26:38'; + EndTime: string; //'2017-07-07 18:26:38'; + CallDuration: number; // in seconds + Status: + | 'Answered' + | 'Missed' + | 'Declined' + | 'Busy' + | 'Cancelled' + | 'No Answer'; + ResourceURL: string; + Direction: 'Inbound' | 'Outbound'; + CallSessionId: string; + LeadId: string; + Tag: string; + UserId: string; +}; + +type SenderType = + | 'UserId' // Sender is a user ID + | 'UserEmailAddress' // Sender is a user email address + | 'APICaller' // Sender = "" + | 'LeadOwner'; // Sender = "" + +type RecipientType = + | 'LeadEmailAddress' // Recipient is a email address + | 'Lead Number' // Recipient is lead number + | 'LeadId'; // Recipient is lead id + +type LeadSquaredEngagementEmail = { + SenderType: SenderType; + Sender: string; + CCEmailAddress: string; + RecipientType: RecipientType; + RecipientEmailFields?: ''; + Recipient: string; + EmailType: 'Html' | 'Template'; + EmailLibraryName: ''; // if EmailType = 'Template'; + ContentHTML: string; + ContentText: string; + Subject: string; + IncludeEmailFooter: boolean; + Schedule: string; //'2017-10-13 10:00:00'; + EmailCategory?: string; + EmailId: string; + RelatedProspectId: string; + FromEmail: string; + RecipientEmailIds: string; + Content_Html: string; + Content_Text: string; + SentOn: string; //'2017 +}; + +export type LeadSquaredEngagementEmailInput = + Partial; +export type LeadSquaredEngagementEmailOutput = LeadSquaredEngagementEmailInput; + +interface LeadSquaredEngagementMeeting { + UserTaskId: string; + Name: string; + Category: number; + Description: string; + // "0" if you're not passing any value + // "1" if you're passing the LeadId + // "5" if you're passing the OpportunityId + RelatedEntity: '0' | '1' | '5'; + RelatedEntityId: string; + DueDate: string; //"2022-02-23 13:10:00.000" + Reminder: number; + ReminderBeforeDays: number; + NotifyBy: '1100' | '1000'; // "1100" if you want the task owner to notify by email + // "1000" Do'nt want any notification + StatusCode: '0' | '1'; // 0 = incomplete, 1 = completed + OwnerId: string; + OwnerName: string; + CreatedBy: string; + CreatedByName: string; + CreatedOn: string; //"2021-11-24 09:43:28.000", + ModifiedBy: string; + ModifiedByName: string; + ModifiedOn: string; //"2022-02-23 07:00:41.000", + RelatedEntityIdName: string; + CompletedOn: string; //"0001-01-01 00:00:00.000", + TaskType: KeyValuePair; + OwnerEmailAddress: string; + EndDate: string; //"2022-02-23 13:15:00", + PercentCompleted: number; + EffortEstimateUnit: string; + Priority: string; + Location: string; + CustomFields: KeyValuePair; +} +export type LeadSquaredEngagementCallInput = Partial; + +export type LeadSquaredEngagementMeetingInput = + Partial; + +export type LeadSquaredEngagementMeetingOutput = + LeadSquaredEngagementMeetingInput; + +export type LeadSquaredEngagementOutput = + | LeadSquaredEngagementEmailOutput + | LeadSquaredEngagementMeetingOutput; +export type LeadSquaredEngagementInput = + | LeadSquaredEngagementEmailInput + | LeadSquaredEngagementMeetingInput + | LeadSquaredEngagementCallInput; diff --git a/packages/api/src/crm/task/services/leadsquared/index.ts b/packages/api/src/crm/task/services/leadsquared/index.ts new file mode 100644 index 000000000..16c2287eb --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/index.ts @@ -0,0 +1,115 @@ +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 { LeadSquaredTaskInput, LeadSquaredTaskOutput } from './types'; + +@Injectable() +export class LeadSquaredService implements ITaskService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.task.toUpperCase() + ':' + LeadSquaredService.name, + ); + this.registry.registerService('leadsquared', this); + } + + async addTask( + taskData: LeadSquaredTaskInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + + const headers = { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt(connection.access_token), + 'x-LSQ-SecretKey': this.cryptoService.decrypt(connection.secret_token), + }; + + const resp = await axios.post( + `${connection.account_url}/v2/Task.svc/Create`, + taskData, + { + headers, + }, + ); + const taskId = resp.data['Message']['Id']; + const taskResponse = await axios.get( + `${connection.account_url}/v2/Task.svc/Retrieve.GetById?id=${taskId}`, + { + headers, + }, + ); + return { + data: taskResponse.data[0], + message: 'Leadsquared task created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, ownerEmailAddress, statusCode } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + + const payload = { + Parameter: { + LookupName: 'ownerEmailAddress', + LookupValue: ownerEmailAddress, + StatusCode: statusCode, // 0 = incomplete, 1 = completed + }, + }; + + const resp = await axios.post( + `${connection.account_url}/v2/Task.svc/Retrieve`, + payload, + { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + this.logger.log(`Synced leadsquared tasks !`); + return { + data: resp.data['List'], + message: 'Leadsquared tasks retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/task/services/leadsquared/mappers.ts b/packages/api/src/crm/task/services/leadsquared/mappers.ts new file mode 100644 index 000000000..b78d38946 --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/mappers.ts @@ -0,0 +1,178 @@ +import { LeadSquaredTaskInput, LeadSquaredTaskOutput } 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 LeadSquaredTaskMapper implements ITaskMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + ) { + this.mappersRegistry.registerService('crm', 'task', 'leadsquared', this); + } + + formatToRequiredDateString(date: Date): string { + return `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()} ${date.getUTCHours()}:${date.getUTCMinutes()}:${date.getUTCMilliseconds()}`; + } + + async desunify( + source: UnifiedCrmTaskInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + // Asuming deal_id = opportunity & company_id = lead + const result: LeadSquaredTaskInput = { + StatusCode: source.status === 'COMPLETED' ? '1' : '0', + Name: source.subject, + Description: source.content, + RelatedEntity: '0', + }; + + if (source.due_date) { + result.DueDate = this.formatToRequiredDateString(source.due_date); + } + + if (source.finished_date) { + result.EndDate = this.formatToRequiredDateString(source.due_date); + } + + // deal -> opportunity + if (source.deal_id) { + const opportunity_id = await this.utils.getRemoteIdFromDealUuid( + source.deal_id, + ); + if (opportunity_id) { + (result.RelatedEntity = '5'), (result.RelatedEntityId = opportunity_id); + } + } + if (source.user_id) { + const owner_id = await this.utils.getRemoteIdFromUserUuid(source.user_id); + if (owner_id) { + result.OwnerId = owner_id; + } + } + + // company -> lead + if (source.company_id) { + const lead_id = await this.utils.getRemoteIdFromCompanyUuid( + source.company_id, + ); + if (lead_id) { + (result.RelatedEntity = '1'), (result.RelatedEntityId = lead_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: LeadSquaredTaskOutput | LeadSquaredTaskOutput[], + 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: LeadSquaredTaskOutput, + 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 = {}; + + // Means RelatedEntityId = LeadId + if (task.RelatedEntity === '1') { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + task.RelatedEntityId, + connectionId, + ); + if (company_id) { + opts = { + ...opts, + company_id: company_id, + }; + } + } + // Means RelatedEntityId = OpportunityId + if (task.RelatedEntity === '5') { + const deal_id = await this.utils.getDealUuidFromRemoteId( + task.RelatedEntityId, + connectionId, + ); + if (deal_id) { + opts = { + ...opts, + deal_id, + }; + } + } + + if (task.OwnerId) { + const user_id = await this.utils.getUserUuidFromRemoteId( + task.OwnerId, + connectionId, + ); + if (user_id) { + opts = { + ...opts, + user_id: user_id, + }; + } + } + + return { + remote_id: task.UserTaskId, + remote_data: task, + content: task.Description, + status: task.StatusCode === '1' ? 'COMPLETED' : 'PENDING', + finished_date: task.EndDate ? new Date(task.EndDate.split(' ')[0]) : null, + due_date: task.DueDate ? new Date(task.DueDate.split(' ')[0]) : null, + field_mappings, + ...opts, + }; + } +} diff --git a/packages/api/src/crm/task/services/leadsquared/types.ts b/packages/api/src/crm/task/services/leadsquared/types.ts new file mode 100644 index 000000000..3d2473e1a --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/types.ts @@ -0,0 +1,41 @@ +interface KeyValuePair { + [key: string]: unknown; +} + +interface LeadSquaredTask { + UserTaskId: string; + Name: string; + Category: number; + Description: string; + // "1" if you're passing the LeadId + // "5" if you're passing the OpportunityId + // "0" if you're not passing any value + RelatedEntity: '0' | '1' | '5'; + RelatedEntityId: string; + DueDate: string; //"2022-02-23 13:10:00.000" + Reminder: number; + ReminderBeforeDays: number; + NotifyBy: '1100' | '1000'; // "1100" if you want the task owner to notify by email + // "1000" Do'nt want any notification + StatusCode: '0' | '1'; // 0 = incomplete, 1 = completed + OwnerId: string; + OwnerName: string; + CreatedBy: string; + CreatedByName: string; + CreatedOn: string; //"2021-11-24 09:43:28.000", + ModifiedBy: string; + ModifiedByName: string; + ModifiedOn: string; //"2022-02-23 07:00:41.000", + RelatedEntityIdName: string; + CompletedOn: string; //"0001-01-01 00:00:00.000", + TaskType: KeyValuePair; + OwnerEmailAddress: string; + EndDate: string; //"2022-02-23 13:15:00", + PercentCompleted: number; + Priority: string; + Location: string; + CustomFields: KeyValuePair; +} + +export type LeadSquaredTaskInput = Partial; +export type LeadSquaredTaskOutput = LeadSquaredTaskInput;