diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 76ca5006c..dc1a31672 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -2,10 +2,18 @@ import { Controller, Get, Query, Res } from '@nestjs/common'; import { Response } from 'express'; // Importing the Express Response type for type checking import { CrmConnectionsService } from './crm/services/crm-connection.service'; import { ProviderVertical, getProviderVertical } from '../utils/providers'; +import { LoggerService } from '../logger/logger.service'; +import { handleServiceError } from '../utils/errors'; @Controller('connections') export class ConnectionsController { - constructor(private readonly crmConnectionsService: CrmConnectionsService) {} + constructor( + private readonly crmConnectionsService: CrmConnectionsService, + private logger: LoggerService, + ) { + this.logger.setContext(ConnectionsController.name); + } + @Get('oauth/callback') handleCallback( @Res() res: Response, @@ -14,7 +22,9 @@ export class ConnectionsController { @Query('location') zohoLocation?: string, ) { try { - if (!state || !code) throw new Error('no params found'); + if (!state) throw new Error('No Callback Params found for state'); + if (!code) throw new Error('No Callback Params found for code'); + const stateData = JSON.parse(decodeURIComponent(state)); const { projectId, linkedUserId, providerName, returnUrl } = stateData; //TODO; ADD VERIFICATION OF PARAMS @@ -46,7 +56,7 @@ export class ConnectionsController { res.redirect(returnUrl); } catch (error) { - console.error('An error occurred', error.response?.data || error.message); + handleServiceError(error, this.logger); } } } diff --git a/packages/api/src/@core/connections/crm/services/crm-connection.service.ts b/packages/api/src/@core/connections/crm/services/crm-connection.service.ts index d1f85b12b..41d4dabd8 100644 --- a/packages/api/src/@core/connections/crm/services/crm-connection.service.ts +++ b/packages/api/src/@core/connections/crm/services/crm-connection.service.ts @@ -6,6 +6,7 @@ import { HubspotConnectionService } from './hubspot/hubspot.service'; import { PipedriveConnectionService } from './pipedrive/pipedrive.service'; import { ZendeskConnectionService } from './zendesk/zendesk.service'; import { FreshsalesConnectionService } from './freshsales/freshsales.service'; +import { LoggerService } from 'src/@core/logger/logger.service'; @Injectable() export class CrmConnectionsService { @@ -15,7 +16,10 @@ export class CrmConnectionsService { private pipedriveConnectionService: PipedriveConnectionService, private zendeskConnectionService: ZendeskConnectionService, private freshsalesConnectionService: FreshsalesConnectionService, - ) {} + private logger: LoggerService, + ) { + this.logger.setContext(CrmConnectionsService.name); + } //STEP 1:[FRONTEND STEP] //create a frontend SDK snippet in which an authorization embedded link is set up so when users click // on it to grant access => they grant US the access and then when confirmed @@ -37,59 +41,52 @@ export class CrmConnectionsService { code: string, zohoLocation?: string, ) { - try { - switch (providerName) { - case 'hubspot': - if (!code) { - throw new NotFoundError('no hubspot code found'); - } - return this.hubspotConnectionService.handleHubspotCallback( - linkedUserId, - projectId, - code, - ); - case 'zoho': - if (!code) { - throw new NotFoundError('no zoho code'); - } - if (!zohoLocation) { - throw new NotFoundError('no zoho location'); - } - return this.zohoConnectionService.handleZohoCallback( - linkedUserId, - projectId, - code, - zohoLocation, - ); - case 'pipedrive': - if (!code) { - throw new NotFoundError('no pipedrive code found'); - } - return this.pipedriveConnectionService.handlePipedriveCallback( - linkedUserId, - projectId, - code, - ); - case 'freshsales': - //todo: LATER - break; - case 'zendesk': - if (!code) { - throw new NotFoundError('no zendesk code found'); - } - return this.zendeskConnectionService.handleZendeskCallback( - linkedUserId, - projectId, - code, - ); - default: - return; - } - } catch (error) { - if (error instanceof NotFoundError) { - console.log(error); - } - return error; + switch (providerName) { + case 'hubspot': + if (!code) { + throw new NotFoundError('no hubspot code found'); + } + return this.hubspotConnectionService.handleHubspotCallback( + linkedUserId, + projectId, + code, + ); + case 'zoho': + if (!code) { + throw new NotFoundError('no zoho code'); + } + if (!zohoLocation) { + throw new NotFoundError('no zoho location'); + } + return this.zohoConnectionService.handleZohoCallback( + linkedUserId, + projectId, + code, + zohoLocation, + ); + case 'pipedrive': + if (!code) { + throw new NotFoundError('no pipedrive code found'); + } + return this.pipedriveConnectionService.handlePipedriveCallback( + linkedUserId, + projectId, + code, + ); + case 'freshsales': + //todo: LATER + break; + case 'zendesk': + if (!code) { + throw new NotFoundError('no zendesk code found'); + } + return this.zendeskConnectionService.handleZendeskCallback( + linkedUserId, + projectId, + code, + ); + default: + return; } } @@ -99,40 +96,33 @@ export class CrmConnectionsService { refresh_token: string, account_url?: string, ) { - try { - switch (providerId) { - case 'hubspot': - return this.hubspotConnectionService.handleHubspotTokenRefresh( - connectionId, - refresh_token, - ); - case 'zoho': - return this.zohoConnectionService.handleZohoTokenRefresh( - connectionId, - refresh_token, - account_url, - ); - case 'pipedrive': - return this.pipedriveConnectionService.handlePipedriveTokenRefresh( - connectionId, - refresh_token, - ); - case 'freshsales': - //todo: LATER - break; - case 'zendesk': - return this.zendeskConnectionService.handleZendeskTokenRefresh( - connectionId, - refresh_token, - ); - default: - return; - } - } catch (error) { - if (error instanceof NotFoundError) { - console.log(error); - } - return error; + switch (providerId) { + case 'hubspot': + return this.hubspotConnectionService.handleHubspotTokenRefresh( + connectionId, + refresh_token, + ); + case 'zoho': + return this.zohoConnectionService.handleZohoTokenRefresh( + connectionId, + refresh_token, + account_url, + ); + case 'pipedrive': + return this.pipedriveConnectionService.handlePipedriveTokenRefresh( + connectionId, + refresh_token, + ); + case 'freshsales': + //todo: LATER + break; + case 'zendesk': + return this.zendeskConnectionService.handleZendeskTokenRefresh( + connectionId, + refresh_token, + ); + default: + return; } } } diff --git a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts index 21ddbaba2..69d0108e9 100644 --- a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts +++ b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts @@ -5,6 +5,11 @@ import config from 'src/@core/utils/config'; import { Prisma } from '@prisma/client'; import { HubspotOAuthResponse } from '../../types'; import { LoggerService } from 'src/@core/logger/logger.service'; +import { + Action, + NotUniqueRecord, + handleServiceError, +} from 'src/@core/utils/errors'; @Injectable() export class HubspotConnectionService { @@ -51,7 +56,15 @@ export class HubspotConnectionService { code: string, ) { try { - //TODO: make sure no connections already exists for {linkedUserId, projectId} + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + if (isNotUnique) + throw new NotUniqueRecord( + `A connection already exists for userId ${linkedUserId} and the provider hubspot`, + ); //TMP STEP = first create a linked_user and a project id await this.addLinkedUserAndProjectTest(); //reconstruct the redirect URI that was passed in the frontend it must be the same @@ -99,21 +112,10 @@ export class HubspotConnectionService { }); this.logger.log('Successfully added tokens inside DB ' + db_res); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - //console.error('Error with Axios request:', error.response?.data); - this.logger.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - //console.error('Error with Prisma request:', error); - this.logger.error('Error with Prisma request:', error.message); - } - this.logger.error( - 'An error occurred...', - error.response?.data || error.message, - ); + handleServiceError(error, this.logger, 'hubspot', Action.oauthCallback); } } + async handleHubspotTokenRefresh(connectionId: bigint, refresh_token: string) { try { const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/connections/oauth/callback`; //tocheck @@ -149,14 +151,7 @@ export class HubspotConnectionService { }); console.log('OAuth credentials updated : hubspot ', data); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'hubspot', Action.oauthRefresh); } } } diff --git a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts index e89a1ece8..559d9605c 100644 --- a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts +++ b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts @@ -4,10 +4,18 @@ import axios from 'axios'; import config from 'src/@core/utils/config'; import { PipeDriveOAuthResponse } from '../../types'; import { Prisma } from '@prisma/client'; +import { + Action, + NotUniqueRecord, + handleServiceError, +} from 'src/@core/utils/errors'; +import { LoggerService } from 'src/@core/logger/logger.service'; @Injectable() export class PipedriveConnectionService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(PipedriveConnectionService.name); + } async handlePipedriveCallback( linkedUserId: string, @@ -15,6 +23,15 @@ export class PipedriveConnectionService { code: string, ) { try { + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + if (isNotUnique) + throw new NotUniqueRecord( + `A connection already exists for userId ${linkedUserId} and the provider pipedrive`, + ); //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/connections/oauth/callback`; @@ -59,14 +76,7 @@ export class PipedriveConnectionService { }, }); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'pipedrive', Action.oauthCallback); } } @@ -109,14 +119,7 @@ export class PipedriveConnectionService { }); console.log('OAuth credentials updated : pipedrive ', data); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'pipedrive', Action.oauthRefresh); } } } diff --git a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts index f32505bcd..785ed77ff 100644 --- a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts @@ -4,16 +4,32 @@ import config from 'src/@core/utils/config'; import { Prisma } from '@prisma/client'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { ZendeskOAuthResponse } from '../../types'; +import { + Action, + NotUniqueRecord, + handleServiceError, +} from 'src/@core/utils/errors'; +import { LoggerService } from 'src/@core/logger/logger.service'; @Injectable() export class ZendeskConnectionService { - constructor(private prisma: PrismaService) {} - + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(ZendeskConnectionService.name); + } async handleZendeskCallback( linkedUserId: string, projectId: string, code: string, ) { try { + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + if (isNotUnique) + throw new NotUniqueRecord( + `A connection already exists for userId ${linkedUserId} and the provider zendesk`, + ); //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/connections/oauth/callback`; @@ -58,14 +74,7 @@ export class ZendeskConnectionService { }, }); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'zendesk', Action.oauthCallback); } } async handleZendeskTokenRefresh(connectionId: bigint, refresh_token: string) { @@ -101,14 +110,7 @@ export class ZendeskConnectionService { }); console.log('OAuth credentials updated : zendesk ', data); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'zendesk', Action.oauthRefresh); } } } diff --git a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts index 89e9717d8..e0db638f7 100644 --- a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts +++ b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts @@ -4,6 +4,12 @@ import config from 'src/@core/utils/config'; import { Prisma } from '@prisma/client'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { ZohoOAuthResponse } from '../../types'; +import { LoggerService } from 'src/@core/logger/logger.service'; +import { + Action, + NotUniqueRecord, + handleServiceError, +} from 'src/@core/utils/errors'; const ZOHOLocations = { us: 'https://accounts.zoho.com', @@ -14,7 +20,9 @@ const ZOHOLocations = { }; @Injectable() export class ZohoConnectionService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(ZohoConnectionService.name); + } async handleZohoCallback( linkedUserId: string, projectId: string, @@ -22,6 +30,15 @@ export class ZohoConnectionService { zohoLocation: string, ) { try { + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + if (isNotUnique) + throw new NotUniqueRecord( + `A connection already exists for userId ${linkedUserId} and the provider zoho`, + ); //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/connections/oauth/callback`; @@ -65,14 +82,7 @@ export class ZohoConnectionService { }, }); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'zoho', Action.oauthCallback); } } async handleZohoTokenRefresh( @@ -114,14 +124,7 @@ export class ZohoConnectionService { }); console.log('OAuth credentials updated : zoho ', data); } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); + handleServiceError(error, this.logger, 'zoho', Action.oauthRefresh); } } } diff --git a/packages/api/src/@core/utils/errors.ts b/packages/api/src/@core/utils/errors.ts index 752aedfc7..22052e6c5 100644 --- a/packages/api/src/@core/utils/errors.ts +++ b/packages/api/src/@core/utils/errors.ts @@ -2,9 +2,23 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { LoggerService } from '../logger/logger.service'; import axios, { AxiosError } from 'axios'; import { Prisma } from '@prisma/client'; -import { TargetObject } from './unification/types'; +import { TargetObject } from './types'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +type ServiceError = AxiosError | PrismaClientKnownRequestError | Error; + +export enum Action { + oauthCallback = 'oauth-callback', + oauthRefresh = 'oauth-refresh', +} + +export enum ActionType { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', +} + // Custom error for general application errors export class AppError extends Error { constructor(message: string) { @@ -37,13 +51,20 @@ export class UnauthorizedError extends HttpException { } } -type ServiceError = AxiosError | PrismaClientKnownRequestError | Error; +// Custom error for duplicate element inside Prisma DB errors +export class NotUniqueRecord extends Error { + constructor(message: string) { + super(message); + this.name = 'NotUniqueRecord'; + } +} export function handleServiceError( error: ServiceError, logger: LoggerService, - providerName: string, - action: TargetObject, + providerName?: string, + action?: TargetObject | Action, + actionType?: ActionType, ) { let statusCode = 500; // Default to internal server error let errorMessage = error.message; @@ -58,11 +79,19 @@ export function handleServiceError( } else { logger.error('An unknown error occurred...', errorMessage); } - return { data: null, error: errorMessage, - message: `Failed to create ${action} for ${providerName}.`, + message: + action && providerName && actionType + ? actionType == 'POST' + ? `Failed to create ${action} for ${providerName}.` + : actionType == 'GET' + ? `Failed to retrieve ${action} for ${providerName}.` + : actionType == 'PUT' + ? `Failed to update ${action} for ${providerName}.` + : `Failed to delete ${action} for ${providerName}.` + : errorMessage, statusCode: statusCode, }; } diff --git a/packages/api/src/@core/utils/unification/types.ts b/packages/api/src/@core/utils/types.ts similarity index 58% rename from packages/api/src/@core/utils/unification/types.ts rename to packages/api/src/@core/utils/types.ts index b7f72463d..27c1f489b 100644 --- a/packages/api/src/@core/utils/unification/types.ts +++ b/packages/api/src/@core/utils/types.ts @@ -1,11 +1,16 @@ import { CrmObject, FreshsalesContactInput, + FreshsalesContactOutput, HubspotContactInput, + HubspotContactOutput, PipedriveContactInput, + PipedriveContactOutput, UnifiedCrm, ZendeskContactInput, + ZendeskContactOutput, ZohoContactInput, + ZohoContactOutput, } from 'src/crm/@types'; import { HrisObject } from 'src/hris/@types'; @@ -24,14 +29,18 @@ export type ContactInput = | ZohoContactInput | ZendeskContactInput | PipedriveContactInput; - +export type ContactOutput = + | FreshsalesContactOutput + | HubspotContactOutput + | ZohoContactOutput + | ZendeskContactOutput + | PipedriveContactOutput; export type DealInput = ''; +export type DealOutput = ''; export type CompaniesInput = ''; - -/////// +export type CompaniesOutput = ''; // Vertical Input Types - export type CrmObjectInput = ContactInput | DealInput | CompaniesInput; export type TicketingObjectInput = ContactInput | DealInput | CompaniesInput; export type AtsObjectInput = ContactInput | DealInput | CompaniesInput; @@ -43,7 +52,28 @@ export type AccountingObjectInput = ContactInput | DealInput | CompaniesInput; export type FileStorageObjectInput = ContactInput | DealInput | CompaniesInput; export type HrisObjectInput = ContactInput | DealInput | CompaniesInput; -/// +// Vertical Output Types +export type CrmObjectOutput = ContactOutput | DealOutput | CompaniesOutput; +export type TicketingObjectOutput = + | ContactOutput + | DealOutput + | CompaniesOutput; +export type AtsObjectOutput = ContactOutput | DealOutput | CompaniesOutput; +export type MarketingAutomationObjectOutput = + | ContactOutput + | DealOutput + | CompaniesOutput; +export type AccountingObjectOutput = + | ContactOutput + | DealOutput + | CompaniesOutput; +export type FileStorageObjectOutput = + | ContactOutput + | DealOutput + | CompaniesOutput; +export type HrisObjectOutput = ContactOutput | DealOutput | CompaniesOutput; + +// export type TargetObject = | CrmObject | HrisObject @@ -61,3 +91,14 @@ export type DesunifyReturnType = | AccountingObjectInput | FileStorageObjectInput | HrisObjectInput; + +export type UnifyReturnType = Unified | Unified[]; + +export type UnifySourceType = + | CrmObjectOutput + | TicketingObjectOutput + | AtsObjectOutput + | MarketingAutomationObjectOutput + | AccountingObjectOutput + | FileStorageObjectOutput + | HrisObjectOutput; diff --git a/packages/api/src/@core/utils/unification/crm/freshsales/index.ts b/packages/api/src/@core/utils/unification/crm/freshsales/index.ts index bf4bf0d34..00ba292bb 100644 --- a/packages/api/src/@core/utils/unification/crm/freshsales/index.ts +++ b/packages/api/src/@core/utils/unification/crm/freshsales/index.ts @@ -1,5 +1,6 @@ import { CrmObject } from 'src/crm/@types'; -import { CrmObjectInput, Unified } from '../../types'; +import { CrmObjectInput, Unified, UnifySourceType } from '../../../types'; +import { mapToFreshsalesContact, mapToUnifiedContact } from './mappers/contact'; export async function desunifyFreshsales({ sourceObject, @@ -8,5 +9,30 @@ export async function desunifyFreshsales({ sourceObject: T; targetType_: CrmObject; }): Promise { - return; + switch (targetType_) { + case CrmObject.contact: + return mapToFreshsalesContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Hubspot: ${targetType_}`); + } +} +export async function unifyFreshsales< + T extends UnifySourceType | UnifySourceType[], +>({ sourceObject, targetType_ }: { sourceObject: T; targetType_: CrmObject }) { + switch (targetType_) { + case CrmObject.contact: + return mapToUnifiedContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Freshsales: ${targetType_}`); + } } diff --git a/packages/api/src/@core/utils/unification/crm/freshsales/mappers/contact.ts b/packages/api/src/@core/utils/unification/crm/freshsales/mappers/contact.ts new file mode 100644 index 000000000..55d285722 --- /dev/null +++ b/packages/api/src/@core/utils/unification/crm/freshsales/mappers/contact.ts @@ -0,0 +1,28 @@ +import { FreshsalesContactInput, HubspotContactInput } from 'src/crm/@types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from 'src/crm/contact/dto/create-contact.dto'; +import { Unified, UnifySourceType } from '../../../../types'; + +export function mapToFreshsalesContact( + source: T, +): FreshsalesContactInput { + const source_ = source as UnifiedContactInput; + // 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; + + return { + first_name: source_.first_name, + last_name: source_.last_name, + mobile_number: primaryPhone, + }; +} + +//TODO +export function mapToUnifiedContact< + T extends UnifySourceType | UnifySourceType[], +>(source: T): UnifiedContactOutput | UnifiedContactOutput[] { + return; +} diff --git a/packages/api/src/@core/utils/unification/crm/hubspot/index.ts b/packages/api/src/@core/utils/unification/crm/hubspot/index.ts index 77f2d3ff7..35be37e05 100644 --- a/packages/api/src/@core/utils/unification/crm/hubspot/index.ts +++ b/packages/api/src/@core/utils/unification/crm/hubspot/index.ts @@ -1,6 +1,6 @@ import { CrmObject } from 'src/crm/@types'; -import { CrmObjectInput, Unified } from '../../types'; -import { mapToHubspotContact } from './mappers'; +import { CrmObjectInput, Unified, UnifySourceType } from '../../../types'; +import { mapToHubspotContact, mapToUnifiedContact } from './mappers/contact'; export async function desunifyHubspot({ sourceObject, @@ -21,3 +21,18 @@ export async function desunifyHubspot({ throw new Error(`Unsupported target type for Hubspot: ${targetType_}`); } } +export async function unifyHubspot< + T extends UnifySourceType | UnifySourceType[], +>({ sourceObject, targetType_ }: { sourceObject: T; targetType_: CrmObject }) { + switch (targetType_) { + case CrmObject.contact: + return mapToUnifiedContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Hubspot: ${targetType_}`); + } +} diff --git a/packages/api/src/@core/utils/unification/crm/hubspot/mappers/index.ts b/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts similarity index 68% rename from packages/api/src/@core/utils/unification/crm/hubspot/mappers/index.ts rename to packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts index 0757c429a..2a10711e5 100644 --- a/packages/api/src/@core/utils/unification/crm/hubspot/mappers/index.ts +++ b/packages/api/src/@core/utils/unification/crm/hubspot/mappers/contact.ts @@ -1,6 +1,9 @@ import { HubspotContactInput } from 'src/crm/@types'; -import { UnifiedContactInput } from 'src/crm/contact/dto/create-contact.dto'; -import { Unified } from '../../../types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from 'src/crm/contact/dto/create-contact.dto'; +import { Unified, UnifySourceType } from '../../../../types'; export function mapToHubspotContact( source: T, @@ -19,3 +22,10 @@ export function mapToHubspotContact( // If there are fields such as city, country, etc., in your UnifiedContactInput, map them here }; } + +//TODO +export function mapToUnifiedContact< + T extends UnifySourceType | UnifySourceType[], +>(source: T): UnifiedContactOutput | UnifiedContactOutput[] { + return; +} diff --git a/packages/api/src/@core/utils/unification/crm/index.ts b/packages/api/src/@core/utils/unification/crm/index.ts index dc007b47a..0cac8a6a8 100644 --- a/packages/api/src/@core/utils/unification/crm/index.ts +++ b/packages/api/src/@core/utils/unification/crm/index.ts @@ -1,10 +1,16 @@ import { CrmObject } from 'src/crm/@types'; -import { desunifyHubspot } from './hubspot'; -import { CrmObjectInput, Unified } from '../types'; -import { desunifyPipedrive } from './pipedrive'; -import { desunifyZoho } from './zoho'; -import { desunifyZendesk } from './zendesk'; -import { desunifyFreshsales } from './freshsales'; +import { desunifyHubspot, unifyHubspot } from './hubspot'; +import { + CrmObjectInput, + Unified, + UnifyReturnType, + UnifySourceType, +} from '../../types'; +import { desunifyPipedrive, unifyPipedrive } from './pipedrive'; +import { desunifyZoho, unifyZoho } from './zoho'; +import { desunifyZendesk, unifyZendesk } from './zendesk'; +import { desunifyFreshsales, unifyFreshsales } from './freshsales'; +import { UnifiedContactOutput } from 'src/crm/contact/dto/create-contact.dto'; export async function desunifyCrm({ sourceObject, @@ -29,3 +35,27 @@ export async function desunifyCrm({ } return; } + +export async function unifyCrm({ + sourceObject, + targetType_, + providerName, +}: { + sourceObject: T; + targetType_: CrmObject; + providerName: string; +}): Promise { + switch (providerName) { + case 'hubspot': + return unifyHubspot({ sourceObject, targetType_ }); + case 'pipedrive': + return unifyPipedrive({ sourceObject, targetType_ }); + case 'zoho': + return unifyZoho({ sourceObject, targetType_ }); + case 'zendesk': + return unifyZendesk({ sourceObject, targetType_ }); + case 'freshsales': + return unifyFreshsales({ sourceObject, targetType_ }); + } + return; +} diff --git a/packages/api/src/@core/utils/unification/crm/pipedrive/index.ts b/packages/api/src/@core/utils/unification/crm/pipedrive/index.ts index 6e96d16c8..683833c86 100644 --- a/packages/api/src/@core/utils/unification/crm/pipedrive/index.ts +++ b/packages/api/src/@core/utils/unification/crm/pipedrive/index.ts @@ -1,5 +1,6 @@ import { CrmObject } from 'src/crm/@types'; -import { CrmObjectInput, Unified } from '../../types'; +import { CrmObjectInput, Unified, UnifySourceType } from '../../../types'; +import { mapToPipedriveContact, mapToUnifiedContact } from './mappers/contact'; export async function desunifyPipedrive({ sourceObject, @@ -8,5 +9,30 @@ export async function desunifyPipedrive({ sourceObject: T; targetType_: CrmObject; }): Promise { - return; + switch (targetType_) { + case CrmObject.contact: + return mapToPipedriveContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Pipedrive: ${targetType_}`); + } +} +export async function unifyPipedrive< + T extends UnifySourceType | UnifySourceType[], +>({ sourceObject, targetType_ }: { sourceObject: T; targetType_: CrmObject }) { + switch (targetType_) { + case CrmObject.contact: + return mapToUnifiedContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Pipedrive: ${targetType_}`); + } } diff --git a/packages/api/src/@core/utils/unification/crm/pipedrive/mappers/contact.ts b/packages/api/src/@core/utils/unification/crm/pipedrive/mappers/contact.ts new file mode 100644 index 000000000..9090b5f4e --- /dev/null +++ b/packages/api/src/@core/utils/unification/crm/pipedrive/mappers/contact.ts @@ -0,0 +1,39 @@ +import { PipedriveContactInput } from 'src/crm/@types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from 'src/crm/contact/dto/create-contact.dto'; +import { Unified, UnifySourceType } from '../../../../types'; + +export function mapToPipedriveContact( + source: T, +): PipedriveContactInput { + const source_ = source as UnifiedContactInput; + + // Assuming 'email_addresses' and 'phone_numbers' arrays contain at least one entry + const primaryEmail = source_.email_addresses?.[0]?.email_address; + const primaryPhone = source_.phone_numbers?.[0]?.phone_number; + + // Convert to Pipedrive format if needed + const emailObject = primaryEmail + ? [{ value: primaryEmail, primary: true }] + : []; + const phoneObject = primaryPhone + ? [{ value: primaryPhone, primary: true }] + : []; + + return { + name: `${source_.first_name} ${source_.last_name}`, + email: emailObject, + phone: phoneObject, + // Map other optional fields as needed + // label, visible_to, marketing_status, add_time, etc. + }; +} + +//TODO +export function mapToUnifiedContact< + T extends UnifySourceType | UnifySourceType[], +>(source: T): UnifiedContactOutput | UnifiedContactOutput[] { + return; +} diff --git a/packages/api/src/@core/utils/unification/crm/zendesk/index.ts b/packages/api/src/@core/utils/unification/crm/zendesk/index.ts index bc795dfee..724aa5c10 100644 --- a/packages/api/src/@core/utils/unification/crm/zendesk/index.ts +++ b/packages/api/src/@core/utils/unification/crm/zendesk/index.ts @@ -1,5 +1,6 @@ import { CrmObject } from 'src/crm/@types'; -import { CrmObjectInput, Unified } from '../../types'; +import { CrmObjectInput, Unified, UnifySourceType } from '../../../types'; +import { mapToUnifiedContact, mapToZendeskContact } from './mappers/contact'; export async function desunifyZendesk({ sourceObject, @@ -8,5 +9,30 @@ export async function desunifyZendesk({ sourceObject: T; targetType_: CrmObject; }): Promise { - return; + switch (targetType_) { + case CrmObject.contact: + return mapToZendeskContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Zendesk: ${targetType_}`); + } +} +export async function unifyZendesk< + T extends UnifySourceType | UnifySourceType[], +>({ sourceObject, targetType_ }: { sourceObject: T; targetType_: CrmObject }) { + switch (targetType_) { + case CrmObject.contact: + return mapToUnifiedContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Zendesk : ${targetType_}`); + } } diff --git a/packages/api/src/@core/utils/unification/crm/zendesk/mappers/contact.ts b/packages/api/src/@core/utils/unification/crm/zendesk/mappers/contact.ts new file mode 100644 index 000000000..6452126a1 --- /dev/null +++ b/packages/api/src/@core/utils/unification/crm/zendesk/mappers/contact.ts @@ -0,0 +1,29 @@ +import { ZendeskContactInput } from 'src/crm/@types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from 'src/crm/contact/dto/create-contact.dto'; +import { Unified, UnifySourceType } from '../../../../types'; + +export function mapToZendeskContact( + source: T, +): ZendeskContactInput { + const source_ = source as UnifiedContactInput; + // 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; + + return { + name: `${source_.first_name} ${source_.last_name}`, + first_name: source_.first_name, + last_name: source_.last_name, + email: primaryEmail, + phone: primaryPhone, + }; +} +//TODO +export function mapToUnifiedContact< + T extends UnifySourceType | UnifySourceType[], +>(source: T): UnifiedContactOutput | UnifiedContactOutput[] { + return; +} diff --git a/packages/api/src/@core/utils/unification/crm/zoho/index.ts b/packages/api/src/@core/utils/unification/crm/zoho/index.ts index db0e0a008..64f16b821 100644 --- a/packages/api/src/@core/utils/unification/crm/zoho/index.ts +++ b/packages/api/src/@core/utils/unification/crm/zoho/index.ts @@ -1,5 +1,6 @@ import { CrmObject } from 'src/crm/@types'; -import { CrmObjectInput, Unified } from '../../types'; +import { CrmObjectInput, Unified, UnifySourceType } from '../../../types'; +import { mapToUnifiedContact, mapToZohoContact } from './mappers/contact'; export async function desunifyZoho({ sourceObject, @@ -8,5 +9,34 @@ export async function desunifyZoho({ sourceObject: T; targetType_: CrmObject; }): Promise { - return; + switch (targetType_) { + case CrmObject.contact: + return mapToZohoContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Zoho: ${targetType_}`); + } +} +export async function unifyZoho({ + sourceObject, + targetType_, +}: { + sourceObject: T; + targetType_: CrmObject; +}) { + switch (targetType_) { + case CrmObject.contact: + return mapToUnifiedContact(sourceObject); + case CrmObject.deal: + //return mapToHubspotDeal(sourceObject); + case CrmObject.company: + //return mapToHubspotCompany(sourceObject); + // Add other cases for different CrmObject types + default: + throw new Error(`Unsupported target type for Zoho: ${targetType_}`); + } } diff --git a/packages/api/src/@core/utils/unification/crm/zoho/mappers/contact.ts b/packages/api/src/@core/utils/unification/crm/zoho/mappers/contact.ts new file mode 100644 index 000000000..6e4b1ded7 --- /dev/null +++ b/packages/api/src/@core/utils/unification/crm/zoho/mappers/contact.ts @@ -0,0 +1,31 @@ +import { ZohoContactInput } from 'src/crm/@types'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from 'src/crm/contact/dto/create-contact.dto'; +import { Unified, UnifySourceType } from '../../../../types'; + +export function mapToZohoContact( + source: T, +): ZohoContactInput { + const source_ = source as UnifiedContactInput; + // 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; + + //TODO zoho input weird ??? + return; /*{ + firstname: source_.first_name, + lastname: source_.last_name, + email: primaryEmail, + phone: primaryPhone, + // Map other fields as needed + // If there are fields such as city, country, etc., in your UnifiedContactInput, map them here + };*/ +} +//TODO +export function mapToUnifiedContact< + T extends UnifySourceType | UnifySourceType[], +>(source: T): UnifiedContactOutput | UnifiedContactOutput[] { + return; +} diff --git a/packages/api/src/@core/utils/unification/index.ts b/packages/api/src/@core/utils/unification/desunify.ts similarity index 70% rename from packages/api/src/@core/utils/unification/index.ts rename to packages/api/src/@core/utils/unification/desunify.ts index 39e37124c..bf9bebaa3 100644 --- a/packages/api/src/@core/utils/unification/index.ts +++ b/packages/api/src/@core/utils/unification/desunify.ts @@ -1,11 +1,11 @@ import { CrmObject } from 'src/crm/@types'; import { ProviderVertical, getProviderVertical } from '../providers'; import { desunifyCrm } from './crm'; -import { DesunifyReturnType, TargetObject, Unified } from './types'; +import { DesunifyReturnType, TargetObject, Unified } from '../types'; /* to insert data -SaaS > [Panora] > 3rdParties > +SaaS > [Panora] > 3rdParties */ @@ -37,23 +37,4 @@ export async function desunify({ case ProviderVertical.Unknown: break; } - return; -} - -// TODO -/* to fetch data - -3rdParties > [Panora] > SaaS - -*/ -export async function unify>({ - sourceObject, - targetType, - providerName, -}: { - sourceObject: T; - targetType: TargetObject; - providerName: string; -}): Promise { - return; } diff --git a/packages/api/src/@core/utils/unification/unify.ts b/packages/api/src/@core/utils/unification/unify.ts new file mode 100644 index 000000000..191bfcaf5 --- /dev/null +++ b/packages/api/src/@core/utils/unification/unify.ts @@ -0,0 +1,42 @@ +import { CrmObject } from 'src/crm/@types'; +import { ProviderVertical, getProviderVertical } from '../providers'; +import { TargetObject, UnifyReturnType, UnifySourceType } from '../types'; +import { unifyCrm } from './crm'; + +/* to fetch data + +3rdParties > [Panora] > SaaS + +*/ + +export async function unify({ + sourceObject, + targetType, + providerName, +}: { + sourceObject: T; + targetType: TargetObject; + providerName: string; +}): Promise { + if (sourceObject == null) return []; + switch (getProviderVertical(providerName)) { + case ProviderVertical.CRM: + const targetType_ = targetType as CrmObject; + return unifyCrm({ sourceObject, targetType_, providerName }); + case ProviderVertical.ATS: + break; + case ProviderVertical.Accounting: + break; + case ProviderVertical.FileStorage: + break; + case ProviderVertical.HRIS: + break; + case ProviderVertical.MarketingAutomation: + break; + case ProviderVertical.Ticketing: + break; + case ProviderVertical.Unknown: + break; + } + return; +} diff --git a/packages/api/src/crm/@types/index.ts b/packages/api/src/crm/@types/index.ts index e9937e4d7..751db38a3 100644 --- a/packages/api/src/crm/@types/index.ts +++ b/packages/api/src/crm/@types/index.ts @@ -1,4 +1,7 @@ -import { UnifiedContactInput } from '../contact/dto/create-contact.dto'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '../contact/dto/create-contact.dto'; import { UnifiedDealInput } from '../deal/dto/create-deal.dto'; export enum CrmObject { @@ -12,7 +15,10 @@ export enum CrmObject { user = 'user', } -export type UnifiedCrm = UnifiedContactInput | UnifiedDealInput; +export type UnifiedCrm = + | UnifiedContactInput + | UnifiedContactOutput + | UnifiedDealInput; export * from './../contact/services/freshsales/types'; export * from './../contact/services/zendesk/types'; diff --git a/packages/api/src/crm/contact/contact.controller.ts b/packages/api/src/crm/contact/contact.controller.ts index fb79631f1..20345a9ad 100644 --- a/packages/api/src/crm/contact/contact.controller.ts +++ b/packages/api/src/crm/contact/contact.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, Query } from '@nestjs/common'; +import { Controller, Post, Body, Query, Get } from '@nestjs/common'; import { ContactService } from './services/contact.service'; import { UnifiedContactInput } from './dto/create-contact.dto'; import { LoggerService } from 'src/@core/logger/logger.service'; @@ -12,6 +12,14 @@ export class ContactController { this.logger.setContext(ContactController.name); } + @Get() + getContacts( + @Query('integrationId') integrationId: string, + @Query('linkedUserId') linkedUserId: string, + ) { + return this.contactService.getContacts(integrationId, linkedUserId); + } + @Post() addContact( @Body() unfiedContactData: UnifiedContactInput, diff --git a/packages/api/src/crm/contact/dto/create-contact.dto.ts b/packages/api/src/crm/contact/dto/create-contact.dto.ts index 8e6d6ac8b..e0c1b7707 100644 --- a/packages/api/src/crm/contact/dto/create-contact.dto.ts +++ b/packages/api/src/crm/contact/dto/create-contact.dto.ts @@ -6,3 +6,5 @@ export class UnifiedContactInput { email_addresses: Email[]; phone_numbers: Phone[]; } + +export class UnifiedContactOutput extends UnifiedContactInput {} diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index 0240eb12a..46cffc50a 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -1,5 +1,8 @@ import { HttpStatus, Injectable } from '@nestjs/common'; -import { UnifiedContactInput } from '../dto/create-contact.dto'; +import { + UnifiedContactInput, + UnifiedContactOutput, +} from '../dto/create-contact.dto'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { FreshSalesService } from './freshsales'; import { HubspotService } from './hubspot'; @@ -7,7 +10,7 @@ import { ZohoService } from './zoho'; import { ZendeskService } from './zendesk'; import { PipedriveService } from './pipedrive'; import { ApiResponse, Email, NormalizedContactInfo, Phone } from '../types'; -import { desunify } from 'src/@core/utils/unification'; +import { desunify } from 'src/@core/utils/unification/desunify'; import { CrmObject, FreshsalesContactInput, @@ -22,6 +25,7 @@ import { ZohoContactOutput, } from 'src/crm/@types'; import { LoggerService } from 'src/@core/logger/logger.service'; +import { unify } from 'src/@core/utils/unification/unify'; export type ContactOutput = | FreshsalesContactOutput @@ -90,7 +94,7 @@ export class ContactService { unifiedContactData: UnifiedContactInput, integrationId: string, linkedUserId: string, - ) { + ): Promise> { const job_resp_create = await this.prisma.jobs.create({ data: { id_linked_user: BigInt(linkedUserId), @@ -167,4 +171,63 @@ export class ContactService { }); return resp; } + + async getContacts( + integrationId: string, + linkedUserId: string, + ): Promise> { + const job_resp_create = await this.prisma.jobs.create({ + data: { + id_linked_user: BigInt(linkedUserId), + status: 'written', + }, + }); + const job_id = job_resp_create.id_job; + + let resp: ApiResponse; + switch (integrationId) { + case 'freshsales': + resp = await this.freshsales.getContacts(linkedUserId); + break; + + case 'zoho': + resp = await this.zoho.getContacts(linkedUserId); + break; + + case 'zendesk': + resp = await this.zendesk.getContacts(linkedUserId); + break; + + case 'hubspot': + resp = await this.hubspot.getContacts(linkedUserId); + break; + + case 'pipedrive': + resp = await this.pipedrive.getContacts(linkedUserId); + break; + + default: + break; + } + const sourceObject: ContactOutput[] = resp.data; + + //unify the data according to the target obj wanted + const unifiedObject = await unify({ + sourceObject, + targetType: CrmObject.contact, + providerName: integrationId, + }); + + const status_resp = resp.statusCode === HttpStatus.OK ? 'success' : 'fail'; + + const job_resp = await this.prisma.jobs.update({ + where: { + id_job: job_id, + }, + data: { + status: status_resp, + }, + }); + return { ...resp, data: unifiedObject as UnifiedContactOutput[] }; + } } diff --git a/packages/api/src/crm/contact/services/freshsales/index.ts b/packages/api/src/crm/contact/services/freshsales/index.ts index 7f22a9e9a..9c4fb0e45 100644 --- a/packages/api/src/crm/contact/services/freshsales/index.ts +++ b/packages/api/src/crm/contact/services/freshsales/index.ts @@ -9,12 +9,14 @@ import { } from 'src/crm/@types'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { LoggerService } from 'src/@core/logger/logger.service'; -import { handleServiceError } from 'src/@core/utils/errors'; +import { ActionType, handleServiceError } from 'src/@core/utils/errors'; @Injectable() export class FreshSalesService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(FreshSalesService.name); + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + FreshSalesService.name, + ); } async addContact( @@ -46,7 +48,48 @@ export class FreshSalesService { statusCode: 201, }; } catch (error) { - handleServiceError(error, this.logger, 'Freshsales', CrmObject.contact); + handleServiceError( + error, + this.logger, + 'Freshsales', + CrmObject.contact, + ActionType.POST, + ); + } + } + + async getContacts( + linkedUserId: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.READ + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.get( + `https://domain.freshsales.io/api/contacts`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Freshsales contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Freshsales', + CrmObject.contact, + ActionType.GET, + ); } } } diff --git a/packages/api/src/crm/contact/services/hubspot/index.ts b/packages/api/src/crm/contact/services/hubspot/index.ts index ca216786d..e9f24a9aa 100644 --- a/packages/api/src/crm/contact/services/hubspot/index.ts +++ b/packages/api/src/crm/contact/services/hubspot/index.ts @@ -8,12 +8,14 @@ import { import axios from 'axios'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { LoggerService } from 'src/@core/logger/logger.service'; -import { handleServiceError } from 'src/@core/utils/errors'; +import { ActionType, handleServiceError } from 'src/@core/utils/errors'; @Injectable() export class HubspotService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(HubspotService.name); + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + HubspotService.name, + ); } async addContact( contactData: HubspotContactInput, @@ -45,8 +47,48 @@ export class HubspotService { statusCode: 201, }; } catch (error) { - handleServiceError(error, this.logger, 'Hubspot', CrmObject.contact); + handleServiceError( + error, + this.logger, + 'Hubspot', + CrmObject.contact, + ActionType.POST, + ); } return; } + async getContacts( + linkedUserId: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.READ + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.get( + `https://api.hubapi.com/crm/v3/objects/contacts/`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Hubspot contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Hubspot', + CrmObject.contact, + ActionType.GET, + ); + } + } } diff --git a/packages/api/src/crm/contact/services/pipedrive/index.ts b/packages/api/src/crm/contact/services/pipedrive/index.ts index 4f8dad276..f219609bd 100644 --- a/packages/api/src/crm/contact/services/pipedrive/index.ts +++ b/packages/api/src/crm/contact/services/pipedrive/index.ts @@ -8,12 +8,14 @@ import { import axios from 'axios'; import { PrismaService } from 'src/@core/prisma/prisma.service'; import { LoggerService } from 'src/@core/logger/logger.service'; -import { handleServiceError } from 'src/@core/utils/errors'; +import { ActionType, handleServiceError } from 'src/@core/utils/errors'; @Injectable() export class PipedriveService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(PipedriveService.name); + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + PipedriveService.name, + ); } async addContact( @@ -43,8 +45,46 @@ export class PipedriveService { statusCode: 201, }; } catch (error) { - handleServiceError(error, this.logger, 'Pipedrive', CrmObject.contact); + handleServiceError( + error, + this.logger, + 'Pipedrive', + CrmObject.contact, + ActionType.POST, + ); } return; } + + async getContacts( + linkedUserId: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.READ + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.get(`https://api.pipedrive.com/v1/persons`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }); + return { + data: resp.data, + message: 'Pipedrive contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Pipedrive', + CrmObject.contact, + ActionType.GET, + ); + } + } } diff --git a/packages/api/src/crm/contact/services/zendesk/index.ts b/packages/api/src/crm/contact/services/zendesk/index.ts index 7d6330fd8..4f521195b 100644 --- a/packages/api/src/crm/contact/services/zendesk/index.ts +++ b/packages/api/src/crm/contact/services/zendesk/index.ts @@ -8,11 +8,13 @@ import { import axios from 'axios'; import { LoggerService } from 'src/@core/logger/logger.service'; import { PrismaService } from 'src/@core/prisma/prisma.service'; -import { handleServiceError } from 'src/@core/utils/errors'; +import { ActionType, handleServiceError } from 'src/@core/utils/errors'; @Injectable() export class ZendeskService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(ZendeskService.name); + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + ZendeskService.name, + ); } async addContact( @@ -27,7 +29,6 @@ export class ZendeskService { }, }); const resp = await axios.post( - //TODO `https://api.getbase.com/v2/contacts`, JSON.stringify(contactData), { @@ -43,8 +44,46 @@ export class ZendeskService { statusCode: 201, }; } catch (error) { - handleServiceError(error, this.logger, 'Zendesk', CrmObject.contact); + handleServiceError( + error, + this.logger, + 'Zendesk', + CrmObject.contact, + ActionType.POST, + ); } return; } + + async getContacts( + linkedUserId: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.READ + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.get(`https://api.getbase.com/v2/contacts`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }); + return { + data: resp.data, + message: 'Zendesk contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zendesk', + CrmObject.contact, + ActionType.GET, + ); + } + } } diff --git a/packages/api/src/crm/contact/services/zendesk/types.ts b/packages/api/src/crm/contact/services/zendesk/types.ts index f519e605f..97792437a 100644 --- a/packages/api/src/crm/contact/services/zendesk/types.ts +++ b/packages/api/src/crm/contact/services/zendesk/types.ts @@ -1,23 +1,23 @@ export interface ZendeskContactInput { - contact_id: number; + contact_id?: number; name: string; first_name: string; last_name: string; - title: string; - description: string; - industry: string; - website: string; - email: string; - phone: string; - mobile: string; - fax: string; - twitter: string; - facebook: string; - linkedin: string; - skype: string; - address: Address; - tags: string[]; - custom_fields: CustomFields; + title?: string; + description?: string; + industry?: string; + website?: string; + email?: string; + phone?: string; + mobile?: string; + fax?: string; + twitter?: string; + facebook?: string; + linkedin?: string; + skype?: string; + address?: Address; + tags?: string[]; + custom_fields?: CustomFields; // Include any additional fields specific to Zendesk if needed } export interface ZendeskContactOutput { diff --git a/packages/api/src/crm/contact/services/zoho/index.ts b/packages/api/src/crm/contact/services/zoho/index.ts index cb493067f..a6cac53fc 100644 --- a/packages/api/src/crm/contact/services/zoho/index.ts +++ b/packages/api/src/crm/contact/services/zoho/index.ts @@ -5,12 +5,14 @@ import axios from 'axios'; import { Prisma } from '@prisma/client'; import { LoggerService } from 'src/@core/logger/logger.service'; import { PrismaService } from 'src/@core/prisma/prisma.service'; -import { handleServiceError } from 'src/@core/utils/errors'; +import { ActionType, handleServiceError } from 'src/@core/utils/errors'; @Injectable() export class ZohoService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(ZohoService.name); + this.logger.setContext( + CrmObject.contact.toUpperCase() + ':' + ZohoService.name, + ); } async addContact( @@ -41,8 +43,46 @@ export class ZohoService { statusCode: 201, }; } catch (error) { - handleServiceError(error, this.logger, 'Zoho', CrmObject.contact); + handleServiceError( + error, + this.logger, + 'Zoho', + CrmObject.contact, + ActionType.POST, + ); } return; } + + async getContacts( + linkedUserId: string, + ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.READ + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.get(`https://www.zohoapis.com/crm/v3/Contacts`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }); + return { + data: resp.data, + message: 'Zoho contacts retrieved', + statusCode: 200, + }; + } catch (error) { + handleServiceError( + error, + this.logger, + 'Zoho', + CrmObject.contact, + ActionType.GET, + ); + } + } } diff --git a/packages/api/src/crm/deal/deal.controller.ts b/packages/api/src/crm/deal/deal.controller.ts new file mode 100644 index 000000000..737e66a2c --- /dev/null +++ b/packages/api/src/crm/deal/deal.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Post, Body, Query } from '@nestjs/common'; +import { LoggerService } from 'src/@core/logger/logger.service'; +import { DealService } from './services/deal.service'; + +@Controller('crm/deal') +export class DealController { + constructor( + private readonly dealService: DealService, + private logger: LoggerService, + ) { + this.logger.setContext(DealController.name); + } +} diff --git a/packages/api/src/crm/deal/deal.module.ts b/packages/api/src/crm/deal/deal.module.ts index 25be98ee1..5a5e23b67 100644 --- a/packages/api/src/crm/deal/deal.module.ts +++ b/packages/api/src/crm/deal/deal.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; -import { DealService } from './deal.service'; +import { DealController } from './deal.controller'; +import { DealService } from './services/deal.service'; @Module({ + controllers: [DealController], providers: [DealService], }) export class DealModule {} diff --git a/packages/api/src/crm/deal/deal.service.spec.ts b/packages/api/src/crm/deal/deal.service.spec.ts deleted file mode 100644 index 5318cc44e..000000000 --- a/packages/api/src/crm/deal/deal.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { DealService } from './deal.service'; - -describe('DealService', () => { - let service: DealService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [DealService], - }).compile(); - - service = module.get(DealService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/packages/api/src/crm/deal/deal.service.ts b/packages/api/src/crm/deal/deal.service.ts deleted file mode 100644 index 1df732f72..000000000 --- a/packages/api/src/crm/deal/deal.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class DealService {} diff --git a/packages/api/src/crm/deal/services/deal.service.ts b/packages/api/src/crm/deal/services/deal.service.ts new file mode 100644 index 000000000..ecc532bdd --- /dev/null +++ b/packages/api/src/crm/deal/services/deal.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { LoggerService } from 'src/@core/logger/logger.service'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; + +@Injectable() +export class DealService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(DealService.name); + } +}