diff --git a/packages/api/src/crm/deal/services/leadsquared/index.ts b/packages/api/src/crm/deal/services/leadsquared/index.ts new file mode 100644 index 000000000..b81aac615 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/index.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@nestjs/common'; +import { IDealService } from '@crm/deal/types'; +import { CrmObject } from '@crm/@lib/@types'; +import { LeadSquaredDealInput, LeadSquaredDealOutput } from './types'; +import axios from 'axios'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { ApiResponse } from '@@core/utils/types'; +import { ServiceRegistry } from '../registry.service'; +import { SyncParam } from '@@core/utils/types/interface'; + +@Injectable() +export class LeadSquaredService implements IDealService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + CrmObject.deal.toUpperCase() + ':' + LeadSquaredService.name, + ); + this.registry.registerService('leadsquared', this); + } + + async addDeal( + dealData: LeadSquaredDealInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'leadsquared', + vertical: 'crm', + }, + }); + const resp = await axios.post( + `${connection.account_url}/v2/OpportunityManagement.svc/Capture`, + dealData, + { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + const opportunityId = resp.data.CreatedOpportunityId; + const opportunityResp = await axios.get( + `${connection.account_url}/v2/OpportunityManagement.svc/GetOpportunityDetails?OpportunityId=${opportunityId}`, + { + headers: { + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }, + ); + return { + data: opportunityResp.data, + message: 'Leadsquared deal created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + // TODO: I'm not sure about this + const { linkedUserId, leadId, opportunityType } = data; + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'pipedrive', + vertical: 'crm', + }, + }); + let url = `${connection.account_url}/v2/OpportunityManagement.svc/GetOpportunitiesOfLead?leadId=${leadId}`; + + if (opportunityType) { + url += `&opportunityType=${opportunityType}`; + } + + const resp = await axios.post(url, { + headers: { + 'Content-Type': 'application/json', + 'x-LSQ-AccessKey': this.cryptoService.decrypt( + connection.access_token, + ), + 'x-LSQ-SecretKey': this.cryptoService.decrypt( + connection.secret_token, + ), + }, + }); + + return { + data: resp.data['List'], + message: 'Leadsquared deals retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/crm/deal/services/leadsquared/mappers.ts b/packages/api/src/crm/deal/services/leadsquared/mappers.ts new file mode 100644 index 000000000..8e6d3c0ad --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/mappers.ts @@ -0,0 +1,147 @@ +import { LeadSquaredDealInput, LeadSquaredDealOutput } from './types'; +import { + UnifiedCrmDealInput, + UnifiedCrmDealOutput, +} from '@crm/deal/types/model.unified'; +import { IDealMapper } from '@crm/deal/types'; +import { Utils } from '@crm/@lib/@utils'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { CrmObject } from '@crm/@lib/@types'; +import { UnifiedCrmStageOutput } from '@crm/stage/types/model.unified'; +import { ZohoStageOutput } from '@crm/stage/services/zoho/types'; + +@Injectable() +export class LeadSquaredDealMapper implements IDealMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + ) { + this.mappersRegistry.registerService('crm', 'deal', 'leadsquared', this); + } + async desunify( + source: UnifiedCrmDealInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: LeadSquaredDealInput = { + OpportunityNote: source.description, + OpportunityName: source.name, + Amount: source.amount ?? 0, + }; + if (source.company_id) { + result.Account_Name = { + id: await this.utils.getRemoteIdFromCompanyUuid(source.company_id), + name: await this.utils.getCompanyNameFromUuid(source.company_id), + }; + } + if (source.stage_id) { + result.Stage = await this.utils.getStageNameFromStageUuid( + source.stage_id, + ); + } + if (source.user_id) { + result.Owner = { + id: await this.utils.getRemoteIdFromUserUuid(source.user_id), + } as any; + } + + if (customFieldMappings && source.field_mappings) { + for (const [k, v] of Object.entries(source.field_mappings)) { + const mapping = customFieldMappings.find( + (mapping) => mapping.slug === k, + ); + if (mapping) { + result[mapping.remote_id] = v; + } + } + } + + return result; + } + + async unify( + source: LeadSquaredDealOutput | LeadSquaredDealOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleDealToUnified( + source, + connectionId, + customFieldMappings, + ); + } + + return Promise.all( + source.map((deal) => + this.mapSingleDealToUnified(deal, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleDealToUnified( + deal: LeadSquaredDealOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const field_mappings: { [key: string]: any } = {}; + if (customFieldMappings) { + for (const mapping of customFieldMappings) { + field_mappings[mapping.slug] = deal[mapping.remote_id]; + } + } + const res: UnifiedCrmDealOutput = { + remote_id: deal.OpportunityId, + remote_data: deal, + name: deal.OpportunityNote, + description: deal.OpportunityNote ?? '', // todo null + amount: deal.Amount, + field_mappings, + }; + + if (deal.Stage) { + // we insert right way inside our db as there are no endpoint to do so in the Zoho api + const stage = await this.ingestService.ingestData< + UnifiedCrmStageOutput, + ZohoStageOutput + >( + [ + { + Stage_Name: deal.Stage, + }, + ], + 'leadsquared', + connectionId, + 'crm', + CrmObject.stage, + [], + ); + res.stage_id = stage[0].id_crm_deals_stage; + } + + if (deal.ProspectId) { + res.user_id = await this.utils.getUserUuidFromRemoteId( + deal.ProspectId, + connectionId, + ); + } + if (deal.LeadOwner) { + res.company_id = await this.utils.getCompanyUuidFromRemoteId( + deal.LeadOwner, + connectionId, + ); + } + return res; + } +} diff --git a/packages/api/src/crm/deal/services/leadsquared/types.ts b/packages/api/src/crm/deal/services/leadsquared/types.ts new file mode 100644 index 000000000..287111e34 --- /dev/null +++ b/packages/api/src/crm/deal/services/leadsquared/types.ts @@ -0,0 +1,52 @@ +type LeadDetail = { + Attribute: string; + Value: string; +}; +// If comments are enabled on opportunity status change, you must pass the mx_Custom_19 (of datatype ‘String’) +type Field = { + SchemaName: string; + Value: string; +}; + +type Opportunity = { + Filelds: Field[]; + OpportunityEventCode: number; + OpportunityNote?: string; + OpportunityDateTime?: string; //date and time is in the yyyy-mm-dd hh:mm:ss format. + OverwriteFields?: boolean; + UpdateEmptyFields?: boolean; + DoNotPostDuplicateActivity?: boolean; + DoNotChangeOwner?: boolean; +}; + +export type LeadSquaredDealInput = { + LeadDetails: LeadDetail[]; // atleast 1 unique field is required. Attribute 'SearchBy' is required + Opportunity: Opportunity; +}; + +export type LeadSquaredDealOutput = { + ProspectId: string; + FirstName: string; + LastName: string; + EmailAddress: string; + Phone: string; + DoNotCall: '0' | '1'; + DoNotEmail: '0' | '1'; + LeadName: string; + LeadOwner: string; + OpportunityEventType: string; + OpportunityEvent: string; + OpportunityNote: string; + Score: string; + PACreatedOn: string; //'2020-09-16 05:43:00'; + PAModifiedOn: string; //'2020-09-16 07:26:09'; + IP_Latitude: string | null; + IP_Longitude: string | null; + PACreatedByName: string; + Status: string; + Owner: string; + OwnerName: string; + OpportunityId: string; + Total: string; + [key: string]: string | number | null; +};