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;