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..d7f7aea4d --- /dev/null +++ b/packages/api/src/crm/contact/services/leadsquared/index.ts @@ -0,0 +1,156 @@ +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, TargetObject } 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('leadsquared', this); + } + + formatDateForLeadSquared(date: Date): string { + const year = date.getUTCFullYear(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const currentDate = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.contact, + ActionType.POST, + ); + } + } + + 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.formatDateForLeadSquared(new Date(0)); + const toDate = this.formatDateForLeadSquared(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) => + lead.LeadPropertyList.reduce( + ( + acc, + { Attribute, Value }: { Attribute: string; Value: string }, + ) => { + acc[Attribute] = Value; + return acc; + }, + {} as LeadSquaredContactOutput, + ), + ); + + //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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.contact, + ActionType.GET, + ); + } + } +} 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..c139b17e7 --- /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', 'leadsquared', 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?.[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?.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?.Account_Fax) { + phone_numbers.push({ + phone_number: contact.Account_Fax, + phone_type: 'fax', + }); + } + if (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..6e06a7692 --- /dev/null +++ b/packages/api/src/crm/contact/services/leadsquared/types.ts @@ -0,0 +1,100 @@ +type LeadSquaredContact = { + 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; +}; + +type LeadProperty = { + Attribute: string; + Value: string; +}; + +export type LeadSquaredContactResponse = { + LeadPropertyList: LeadProperty[]; +}; + +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..be0987599 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/index.ts @@ -0,0 +1,129 @@ +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 { 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'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; + +@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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.deal, + ActionType.POST, + ); + } + } + + async sync(data: SyncParam): Promise> { + try { + // Have to pass the leadId and opportunityType + const { linkedUserId, leadId, opportunityType } = data; + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.deal, + ActionType.POST, + ); + } + } +} 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..3947eae7b --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/mappers.ts @@ -0,0 +1,173 @@ +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'; + +@Injectable() +export class LeadSquaredDealMapper implements IDealMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + ) { + this.mappersRegistry.registerService('crm', 'deal', 'leadsquared', this); + } + formatDateForLeadSquared(date: Date): string { + const year = date.getUTCFullYear(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const currentDate = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${currentDate} ${hours}:${minutes}:${seconds}`; + } + + async desunify( + source: UnifiedCrmDealInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: LeadSquaredDealInput = { + LeadDetails: [], + Opportunity: { + OpportunityNote: source.name, + OpportunityDateTime: this.formatDateForLeadSquared(new Date()), + OpportunityEventCode: 11, + UpdateEmptyFields: true, + DoNotPostDuplicateActivity: true, + DoNotChangeOwner: true, + Fields: [ + { + SchemaName: 'Ammount', + Value: source.amount.toString(), + }, + { + SchemaName: 'Description', + Value: source.description, + }, + ], + }, + }; + if (source.company_id) { + const leadDetails = [ + { + Attribute: 'AccountName', + Value: + (await this.utils.getCompanyNameFromUuid(source.company_id)) || '', + }, + { + Attribute: 'ProspectID', + Value: await this.utils.getRemoteIdFromCompanyUuid(source.company_id), + }, + { + Attribute: 'SearchBy', + Value: 'ProspectId', + }, + { + Attribute: '__UseUserDefinedGuid__', + Value: 'true', + }, + ]; + result.LeadDetails = [...result.LeadDetails, ...leadDetails]; + } + if (source.stage_id) { + result.Opportunity.Fields.push({ + SchemaName: 'StageId', + Value: await this.utils.getStageNameFromStageUuid(source.stage_id), + }); + } + if (source.user_id) { + result.Opportunity.Fields.push({ + SchemaName: 'OwnerId', + Value: await this.utils.getRemoteIdFromUserUuid(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; + result.Opportunity.Fields.push({ + SchemaName: k, + Value: 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['Description']?.toString() || deal.OpportunityNote, + amount: Number(deal.Amount), + field_mappings, + }; + + if (deal.OwnerId) { + res.user_id = await this.utils.getUserUuidFromRemoteId( + deal.OwnerId as string, + 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..b024012e7 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/types.ts @@ -0,0 +1,52 @@ +type LeadDetail = { + Attribute: string; + Value: string; +}; + +type Field = { + SchemaName: string; + Value: string; +}; + +type Opportunity = { + Fields?: 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..bda8cf7b0 --- /dev/null +++ b/packages/api/src/crm/engagement/services/leadsquared/index.ts @@ -0,0 +1,340 @@ +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'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; + +@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); + } + + formatDateForLeadSquared(date: Date): string { + const year = date.getUTCFullYear(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const currentDate = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.POST, + ); + } + } + + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.POST, + ); + } + } + + 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 (error) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.POST, + ); + } + } + + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.POST, + ); + } + } + + 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.formatDateForLeadSquared(new Date(0)); + const toDate = this.formatDateForLeadSquared(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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.GET, + ); + } + } + + 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.formatDateForLeadSquared(new Date(0)); + const toDate = this.formatDateForLeadSquared(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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.GET, + ); + } + } + + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.engagement, + ActionType.GET, + ); + } + } +} 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..5193787bd --- /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, + ); + } + + formatDateForLeadSquared(date: Date): string { + const year = date.getUTCFullYear(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const currentDate = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + 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.formatDateForLeadSquared(new Date(source.start_at)); + } + + if (source.end_time) { + result.EndDate = this.formatDateForLeadSquared(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: source.source_number || '', + DisplayNumber: source.display_number || '', + DestinationNumber: source.destination_number || '', + }; + + 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.formatDateForLeadSquared(startDate); + result.EndTime = this.formatDateForLeadSquared(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..bf134c9de --- /dev/null +++ b/packages/api/src/crm/engagement/services/leadsquared/types.ts @@ -0,0 +1,114 @@ +type KeyValuePair = Record; + +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..31b3bbe40 --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/index.ts @@ -0,0 +1,138 @@ +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'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; + +@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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.task, + ActionType.POST, + ); + return { + data: null, + message: 'Failed to create Leadsquared task', + statusCode: 500, + }; + } + } + + 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) { + handle3rdPartyServiceError( + error, + this.logger, + 'leadsquared', + CrmObject.task, + ActionType.POST, + ); + return { + data: [], + message: 'Failed to retrieve Leadsquared tasks', + statusCode: 500, + }; + } + } +} 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..00becf6d3 --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/mappers.ts @@ -0,0 +1,184 @@ +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); + } + + formatDateForLeadSquared(date: Date): string { + const year = date.getUTCFullYear(); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); + const currentDate = date.getUTCDate().toString().padStart(2, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${currentDate} ${hours}:${minutes}:${seconds}`; + } + + 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.formatDateForLeadSquared(source.due_date); + } + + if (source.finished_date) { + result.EndDate = this.formatDateForLeadSquared(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..9712c46d3 --- /dev/null +++ b/packages/api/src/crm/task/services/leadsquared/types.ts @@ -0,0 +1,39 @@ +type KeyValuePair = Record; + +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; diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 74656d7fb..f9ce481cd 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -50,7 +50,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, 'pipedrive': { - urls: { + urls: { docsUrl: 'https://developers.pipedrive.com/docs/api/v1', authBaseUrl: 'https://oauth.pipedrive.com/oauth/authorize', apiUrl: 'https://api.pipedrive.com', @@ -93,7 +93,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { strategy: AuthStrategy.oauth2 } }, - 'accelo': { + 'accelo': { urls: { docsUrl: 'https://api.accelo.com/docs/#introduction', authBaseUrl: (domain) => `https://${domain}.api.accelo.com/oauth2/v0/authorize`, @@ -136,7 +136,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { properties: ['password'] } }, - 'capsule': { + 'capsule': { urls: { docsUrl: 'https://developer.capsulecrm.com/', authBaseUrl: 'https://api.capsulecrm.com/oauth/authorise', @@ -368,6 +368,30 @@ export const CONNECTORS_METADATA: ProvidersConfig = { strategy: AuthStrategy.oauth2 }, active: false + }, + 'leadsquared': { + scopes: '', + urls: { + docsUrl: 'https://apidocs.leadsquared.com/overview/#api', + apiUrl: 'https://api-us11.leadsquared.com', // it has different different endpoint for various regions + }, + logoPath: 'https://www.leadsquared.com/wp-content/uploads/2023/12/340-x-156-300x138-1.png', + description: 'Sync & Create contacts, deals, engagements and tasks', + authStrategy: { + strategy: AuthStrategy.api_key, + authStructure: [ + { + headerParamName: 'x-LSQ-AccessKey', + valueName: 'access_token', + }, + { + headerParamName: 'x-LSQ-SecretKey', + valueName: 'secret_token', + }, + ], + properties: ['access_token', 'secret_token'] + }, + active: false } }, 'ticketing': { @@ -392,7 +416,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { docsUrl: 'https://developer.zendesk.com/api-reference/sales-crm/introduction/', apiUrl: (myDomain) => `https://${myDomain}.zendesk.com/api`, authBaseUrl: (myDomain) => `https://${myDomain}.zendesk.com/oauth/authorizations/new` - }, + }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKVceZGVM7PbARp_2bjdOICUxlpS5B29UYlurvh6Z2Q&s', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', authStrategy: { @@ -407,7 +431,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { 'ticketing.tickets.events', 'ticketing.comments.events', 'ticketing.tags.events', - 'ticketing.attachments.events', + 'ticketing.attachments.events', 'ticketing.accounts.events', 'ticketing.users.events', 'ticketing.contacts.events', @@ -435,8 +459,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { scopes: 'read:jira-work manage:jira-project manage:jira-configuration read:jira-user write:jira-work manage:jira-webhook manage:jira-data-provider offline_access', urls: { docsUrl: 'https://developer.atlassian.com/cloud/jira/platform/rest/v3', - apiUrl: (cloudId) => `https://api.atlassian.com/ex/jira/${cloudId}/rest/api`, - authBaseUrl: 'https://auth.atlassian.com/authorize', + apiUrl: (cloudId) => `https://api.atlassian.com/ex/jira/${cloudId}/rest/api`, + authBaseUrl: 'https://auth.atlassian.com/authorize', }, options: { local_redirect_uri_in_https: true @@ -444,7 +468,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://logowik.com/content/uploads/images/jira3124.jpg', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', authStrategy: { - strategy: AuthStrategy.oauth2 + strategy: AuthStrategy.oauth2 } }, 'linear': { @@ -551,7 +575,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { docsUrl: 'https://github.com/basecamp/api/blob/master/sections/authentication.md', apiUrl: '', authBaseUrl: 'https://launchpad.37signals.com/authorization/new', - }, + }, logoPath: 'https://asset.brandfetch.io/id7Kew_cLD/idx-Jcj2Qo.jpeg', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, @@ -894,7 +918,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, 'freshbooks': { - scopes: '', + scopes: '', urls: { docsUrl: 'https://www.freshbooks.com/api/start', apiUrl: 'https://api.freshbooks.com', @@ -1054,7 +1078,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, 'xero': { - scopes: 'offline_access openid profile email accounting.transactions', + scopes: 'offline_access openid profile email accounting.transactions', urls: { docsUrl: 'https://developer.xero.com/documentation/getting-started-guide/', apiUrl: 'https://api.xero.com/api.xro/2.0', @@ -1062,7 +1086,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://i.ibb.co/qpc2RQZ/xeroappicon.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: false, authStrategy: { strategy: AuthStrategy.oauth2 } @@ -1203,7 +1227,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, 'brevo': { - urls: { + urls: { docsUrl: 'https://developers.brevo.com/docs/getting-started', apiUrl: 'https://api.brevo.com/v3' }, @@ -1216,7 +1240,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, }, - 'ats': { + 'ats': { 'applicantstack': { scopes: '', urls: { @@ -2460,7 +2484,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: false, authStrategy: { strategy: AuthStrategy.oauth2 } @@ -2771,7 +2795,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://upload.wikimedia.org/wikipedia/commons/4/45/Notion_app_logo.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, - authStrategy: { + authStrategy: { strategy: AuthStrategy.oauth2 } }, @@ -2913,7 +2937,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: { strategy: AuthStrategy.oauth2 }, - }, + }, 'mercadolibre': { scopes: '', urls: {