Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add integration with leadsquared #672

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
156 changes: 156 additions & 0 deletions packages/api/src/crm/contact/services/leadsquared/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
);
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<ApiResponse<LeadSquaredContactOutput>> {
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<ApiResponse<LeadSquaredContactOutput[]>> {
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,
);
}
}
}
176 changes: 176 additions & 0 deletions packages/api/src/crm/contact/services/leadsquared/mappers.ts
Original file line number Diff line number Diff line change
@@ -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 && source.addresses[0]) {
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
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<UnifiedCrmContactOutput | UnifiedCrmContactOutput[]> {
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<UnifiedCrmContactOutput> {
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
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
? [
{
email_address: contact.EmailAddress,
email_address_type: 'PERSONAL',
},
]
: [];

const phone_numbers = [];

if (contact && contact.Account_Phone) {
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
phone_numbers.push({
phone_number: contact.Account_Phone,
phone_type: 'WORK',
});
}
if (contact && contact.Mobile) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use optional chaining for accessing object properties.

Consider using optional chaining to safely access object properties.

- contact && contact.Mobile
+ contact?.Mobile
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (contact && contact.Mobile) {
if (contact?.Mobile) {
Tools
Biome

[error] 129-129: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

phone_numbers.push({
phone_number: contact.Mobile,
phone_type: 'MOBILE',
});
}
if (contact && contact.Account_Fax) {
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
phone_numbers.push({
phone_number: contact.Account_Fax,
phone_type: 'fax',
});
}
if (contact && contact.Phone) {
Iamsidar07 marked this conversation as resolved.
Show resolved Hide resolved
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],
};
}
}
Loading
Loading