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;