diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 8c434c3b5..b26265aec 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -56,8 +56,12 @@ model jobs { id_job Int @id(map: "pk_jobs") @default(autoincrement()) status String timestamp DateTime @default(now()) @db.Timestamp(6) + id_linked_user BigInt crm_contacts crm_contacts[] + linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") jobs_status_history jobs_status_history[] + + @@index([id_linked_user], map: "fk_linkeduserid_projectid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -136,6 +140,7 @@ model linked_users { status String id_project BigInt connections connections[] + jobs jobs[] projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_10") @@index([id_project], map: "fk_proectid_linked_users") diff --git a/packages/api/src/@core/utils/errors.ts b/packages/api/src/@core/utils/errors.ts index b7d4af4fd..752aedfc7 100644 --- a/packages/api/src/@core/utils/errors.ts +++ b/packages/api/src/@core/utils/errors.ts @@ -1,4 +1,9 @@ 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 { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; // Custom error for general application errors export class AppError extends Error { @@ -31,3 +36,33 @@ export class UnauthorizedError extends HttpException { this.name = 'UnauthorizedError'; } } + +type ServiceError = AxiosError | PrismaClientKnownRequestError | Error; + +export function handleServiceError( + error: ServiceError, + logger: LoggerService, + providerName: string, + action: TargetObject, +) { + let statusCode = 500; // Default to internal server error + let errorMessage = error.message; + + if (axios.isAxiosError(error)) { + statusCode = error.response?.status || 500; // Use HTTP status code from Axios error or default to 500 + errorMessage = error.response?.data || error.message; + logger.error('Error with Axios request:', errorMessage); + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + // Handle Prisma errors + logger.error('Error with Prisma request:', errorMessage); + } else { + logger.error('An unknown error occurred...', errorMessage); + } + + return { + data: null, + error: errorMessage, + message: `Failed to create ${action} for ${providerName}.`, + statusCode: statusCode, + }; +} diff --git a/packages/api/src/crm/contact/services/contact.service.ts b/packages/api/src/crm/contact/services/contact.service.ts index f997a4ffb..6adeb611a 100644 --- a/packages/api/src/crm/contact/services/contact.service.ts +++ b/packages/api/src/crm/contact/services/contact.service.ts @@ -15,11 +15,19 @@ import { HubspotContactInput, HubspotContactOutput, PipedriveContactInput, + PipedriveContactOutput, ZendeskContactInput, + ZendeskContactOutput, ZohoContactInput, + ZohoContactOutput, } from 'src/crm/@types'; -export type ContactOutput = FreshsalesContactOutput | HubspotContactOutput; +export type ContactOutput = + | FreshsalesContactOutput + | HubspotContactOutput + | ZohoContactOutput + | ZendeskContactOutput + | PipedriveContactOutput; @Injectable() export class ContactService { @@ -79,9 +87,9 @@ export class ContactService { integrationId: string, linkedUserId: string, ) { - //TODO; customerId must be passed here const job_resp_create = await this.prisma.jobs.create({ data: { + id_linked_user: BigInt(linkedUserId), status: 'initialized', }, }); @@ -107,28 +115,35 @@ export class ContactService { case 'freshsales': resp = await this.freshsales.addContact( desunifiedObject as FreshsalesContactInput, + linkedUserId, ); break; case 'zoho': - resp = await this.zoho.addContact(desunifiedObject as ZohoContactInput); + resp = await this.zoho.addContact( + desunifiedObject as ZohoContactInput, + linkedUserId, + ); break; case 'zendesk': resp = await this.zendesk.addContact( desunifiedObject as ZendeskContactInput, + linkedUserId, ); break; case 'hubspot': resp = await this.hubspot.addContact( desunifiedObject as HubspotContactInput, + linkedUserId, ); break; case 'pipedrive': resp = await this.pipedrive.addContact( desunifiedObject as PipedriveContactInput, + linkedUserId, ); break; diff --git a/packages/api/src/crm/contact/services/freshsales/index.ts b/packages/api/src/crm/contact/services/freshsales/index.ts index 654669549..7f22a9e9a 100644 --- a/packages/api/src/crm/contact/services/freshsales/index.ts +++ b/packages/api/src/crm/contact/services/freshsales/index.ts @@ -1,55 +1,52 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ApiResponse } from '../../types'; -import axios, { AxiosResponse } from 'axios'; +import axios from 'axios'; import { + CrmObject, FreshsalesContactInput, FreshsalesContactOutput, } 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'; @Injectable() export class FreshSalesService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(FreshSalesService.name); + } + async addContact( contactData: FreshsalesContactInput, + linkedUserId: string, ): Promise> { - const mobile = contactData.phone_numbers[0]; - const url = 'https://domain.freshsales.io/api/contacts'; - const data = { - contact: { - first_name: contactData.first_name, - last_name: contactData.last_name, - mobile_number: mobile, - }, - }; - const token = process.env.FRESHSALES_API_KEY; - const headers = { - Authorization: `Token token=${token}`, - 'Content-Type': 'application/json', - }; - try { - const response: AxiosResponse = await axios.post( - url, - data, - { headers: headers }, + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const dataBody = { + contact: contactData, + }; + const resp = await axios.post( + 'https://domain.freshsales.io/api/contacts', + JSON.stringify(dataBody), + { + headers: { + Authorization: `Token token=${connection.access_token}`, + 'Content-Type': 'application/json', + }, + }, ); - console.log(response.data); return { - data: response.data, - message: 'Contact created successfully.', - statusCode: HttpStatus.OK, + data: resp.data, + message: 'Freshsales contact created', + statusCode: 201, }; } catch (error) { - console.error(error.response ? error.response.data : error.message); - const status: number = error.response - ? error.response.status - : HttpStatus.INTERNAL_SERVER_ERROR; - return { - data: null, - error: error.message, - message: 'Failed to create contact.', - statusCode: status, - }; + handleServiceError(error, this.logger, 'Freshsales', CrmObject.contact); } } } diff --git a/packages/api/src/crm/contact/services/freshsales/types.ts b/packages/api/src/crm/contact/services/freshsales/types.ts index 6112d784c..484e386ef 100644 --- a/packages/api/src/crm/contact/services/freshsales/types.ts +++ b/packages/api/src/crm/contact/services/freshsales/types.ts @@ -1,7 +1,7 @@ export interface FreshsalesContactInput { first_name: string; last_name: string; - phone_numbers: string[]; + mobile_number: string | string[]; } export interface FreshsalesContactOutput { id: number; diff --git a/packages/api/src/crm/contact/services/hubspot/index.ts b/packages/api/src/crm/contact/services/hubspot/index.ts index e11c36961..ca216786d 100644 --- a/packages/api/src/crm/contact/services/hubspot/index.ts +++ b/packages/api/src/crm/contact/services/hubspot/index.ts @@ -1,12 +1,52 @@ import { Injectable } from '@nestjs/common'; import { ApiResponse } from '../../types'; -import { HubspotContactInput, HubspotContactOutput } from 'src/crm/@types'; +import { + CrmObject, + HubspotContactInput, + HubspotContactOutput, +} from 'src/crm/@types'; +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'; @Injectable() export class HubspotService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(HubspotService.name); + } async addContact( contactData: HubspotContactInput, + linkedUserId: string, ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const dataBody = { + properties: contactData, + }; + const resp = await axios.post( + `https://api.hubapi.com/crm/v3/objects/contacts/`, + JSON.stringify(dataBody), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Hubspot contact created', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger, 'Hubspot', CrmObject.contact); + } return; } } diff --git a/packages/api/src/crm/contact/services/hubspot/types.ts b/packages/api/src/crm/contact/services/hubspot/types.ts index 65cb39eae..4e02e26ac 100644 --- a/packages/api/src/crm/contact/services/hubspot/types.ts +++ b/packages/api/src/crm/contact/services/hubspot/types.ts @@ -1,6 +1,27 @@ export interface HubspotContactInput { - company_size: string; + email?: string; + firstname?: string; + phone?: string; + lastname?: string; + city?: string; + country?: string; + zip?: string; + state?: string; + address?: string; + mobilephone?: string; + hubspot_owner_id?: string; + associatedcompanyid?: string; + fax?: string; + jobtitle?: string; } + export interface HubspotContactOutput { - id: number; + company: string; + createdate: string; // Use 'Date' if you prefer a Date object + email: string; + firstname: string; + lastmodifieddate: string; // Use 'Date' if you prefer a Date object + lastname: string; + phone: string; + website: string; } diff --git a/packages/api/src/crm/contact/services/pipedrive/index.ts b/packages/api/src/crm/contact/services/pipedrive/index.ts index f2fc55815..4f8dad276 100644 --- a/packages/api/src/crm/contact/services/pipedrive/index.ts +++ b/packages/api/src/crm/contact/services/pipedrive/index.ts @@ -1,12 +1,50 @@ import { Injectable } from '@nestjs/common'; import { ApiResponse } from '../../types'; -import { PipedriveContactInput, PipedriveContactOutput } from 'src/crm/@types'; +import { + CrmObject, + PipedriveContactInput, + PipedriveContactOutput, +} from 'src/crm/@types'; +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'; @Injectable() export class PipedriveService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(PipedriveService.name); + } + async addContact( contactData: PipedriveContactInput, + linkedUserId: string, ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.post( + `https://api.pipedrive.com/v1/persons`, + JSON.stringify(contactData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Pipedrive contact created', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger, 'Pipedrive', CrmObject.contact); + } return; } } diff --git a/packages/api/src/crm/contact/services/pipedrive/types.ts b/packages/api/src/crm/contact/services/pipedrive/types.ts index 946586305..f5b8c84f2 100644 --- a/packages/api/src/crm/contact/services/pipedrive/types.ts +++ b/packages/api/src/crm/contact/services/pipedrive/types.ts @@ -1,6 +1,97 @@ export interface PipedriveContactInput { - company_size: string; + name: string; + owner_id?: number; + org_id?: number; + email: string | EmailObject[]; + phone: string | PhoneObject[]; + label?: number; + visible_to?: VisibleTo; + marketing_status?: MarketingStatus; + add_time?: string; } + export interface PipedriveContactOutput { id: number; + company_id: number; + owner_id: User; + org_id: OrgId; + name: string; + first_name: string; + last_name: string; + // ... other properties + phone: Phone[]; + email: Email[]; + primary_email: string; + // ... other properties + picture_id: PictureId; + // ... other properties + label: number; + org_name: string; + owner_name: string; + cc_email: string; } + +interface User { + id: number; + name: string; + email: string; + has_pic: number; + pic_hash: string; + active_flag: boolean; +} + +interface OrgId { + name: string; + people_count: number; + owner_id: number; + address: string; + active_flag: boolean; + cc_email: string; + value: number; +} + +//OUTPUT +interface Phone { + value: string; + primary: boolean; + label: string; +} + +//OUTPUT +interface Email { + value: string; + primary: boolean; + label: string; +} + +//OUTPUT +interface PictureId { + item_type: string; + item_id: number; + active_flag: boolean; + add_time: string; + update_time: string; + added_by_user_id: number; + pictures: Record; + value: number; +} + +//INPUT +interface EmailObject { + value: string; + primary?: boolean; + label?: string; +} + +//INPUT +type PhoneObject = EmailObject; + +//INPUT +type MarketingStatus = + | 'no_consent' + | 'unsubscribed' + | 'subscribed' + | 'archived'; + +//INPUT +type VisibleTo = 1 | 3 | 5 | 7; diff --git a/packages/api/src/crm/contact/services/zendesk/index.ts b/packages/api/src/crm/contact/services/zendesk/index.ts index 8fd244aef..7d6330fd8 100644 --- a/packages/api/src/crm/contact/services/zendesk/index.ts +++ b/packages/api/src/crm/contact/services/zendesk/index.ts @@ -1,11 +1,50 @@ import { Injectable } from '@nestjs/common'; import { ApiResponse } from '../../types'; -import { ZendeskContactInput, ZendeskContactOutput } from 'src/crm/@types'; +import { + CrmObject, + ZendeskContactInput, + ZendeskContactOutput, +} from 'src/crm/@types'; +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'; @Injectable() export class ZendeskService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(ZendeskService.name); + } + async addContact( contactData: ZendeskContactInput, + linkedUserId: string, ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.post( + //TODO + `https://api.getbase.com/v2/contacts`, + JSON.stringify(contactData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Zendesk contact created', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger, 'Zendesk', CrmObject.contact); + } return; } } diff --git a/packages/api/src/crm/contact/services/zendesk/types.ts b/packages/api/src/crm/contact/services/zendesk/types.ts index 8b65c72de..f519e605f 100644 --- a/packages/api/src/crm/contact/services/zendesk/types.ts +++ b/packages/api/src/crm/contact/services/zendesk/types.ts @@ -1,6 +1,70 @@ export interface ZendeskContactInput { - company_size: string; + 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; + // Include any additional fields specific to Zendesk if needed } export interface ZendeskContactOutput { id: number; + creator_id: number; + owner_id: number; + is_organization: boolean; + contact_id: number; + parent_organization_id: number | null; + name: string; + first_name: string; + last_name: string; + customer_status: string; + prospect_status: 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 | null; + billing_address: Address | null; + shipping_address: Address | null; + created_at: string; // or Date if you convert the string to a Date object + updated_at: string; // or Date + meta: Meta; +} + +interface Meta { + type: string; +} + +interface Address { + line1: string; + city: string; + postal_code: string; + state: string; + country: string; +} + +interface CustomFields { + referral_website: string; + // Include any other custom fields as needed } diff --git a/packages/api/src/crm/contact/services/zoho/index.ts b/packages/api/src/crm/contact/services/zoho/index.ts index 7e3bc5415..cb493067f 100644 --- a/packages/api/src/crm/contact/services/zoho/index.ts +++ b/packages/api/src/crm/contact/services/zoho/index.ts @@ -1,12 +1,48 @@ import { Injectable } from '@nestjs/common'; import { ApiResponse } from '../../types'; -import { ZohoContactInput, ZohoContactOutput } from 'src/crm/@types'; +import { CrmObject, ZohoContactInput, ZohoContactOutput } from 'src/crm/@types'; +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'; @Injectable() export class ZohoService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(ZohoService.name); + } + async addContact( contactData: ZohoContactInput, + linkedUserId: string, ): Promise> { + try { + //TODO: check required scope => crm.objects.contacts.write + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: BigInt(linkedUserId), + }, + }); + const resp = await axios.post( + //TODO + `https://www.zohoapis.com/crm/v3/Contacts`, + JSON.stringify(contactData), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Zoho-oauthtoken ${connection.access_token}`, + }, + }, + ); + return { + data: resp.data, + message: 'Zoho contact created', + statusCode: 201, + }; + } catch (error) { + handleServiceError(error, this.logger, 'Zoho', CrmObject.contact); + } return; } } diff --git a/packages/api/src/crm/contact/services/zoho/types.ts b/packages/api/src/crm/contact/services/zoho/types.ts index 454151607..8dfeed86e 100644 --- a/packages/api/src/crm/contact/services/zoho/types.ts +++ b/packages/api/src/crm/contact/services/zoho/types.ts @@ -1,6 +1,164 @@ export interface ZohoContactInput { - company_size: string; + contact_name: string; + company_name: string; + website: string; + language_code: string; + contact_type: string; + customer_sub_type: string; + credit_limit: number; + tags: { tag_id: number; tag_option_id: number }[]; + is_portal_enabled: boolean; + currency_id: number; + payment_terms: number; + payment_terms_label: string; + notes: string; + billing_address: Address; + shipping_address: Address; + contact_persons: string; + default_templates: DefaultTemplates; + custom_fields: CustomFieldInput[]; + opening_balance_amount: number; + exchange_rate: number; + vat_reg_no: string; + owner_id: number; + tax_reg_no: number; + country_code: string; + vat_treatment: string; + tax_treatment: string; + tax_regime: string; + is_tds_registered: boolean; + place_of_contact: string; + gst_no: string; + gst_treatment: string; + tax_authority_name: string; + avatax_exempt_no: string; + avatax_use_code: string; + tax_exemption_id: number; + tax_exemption_code: string; + tax_authority_id: number; + tax_id: number; + tds_tax_id: string; + is_taxable: boolean; + facebook: string; + twitter: string; + track_1099: boolean; + tax_id_type: string; + tax_id_value: string; } + export interface ZohoContactOutput { - id: number; + contact_id: number; + contact_name: string; + company_name: string; + has_transaction: boolean; + contact_type: string; + customer_sub_type: string; + credit_limit: number; + is_portal_enabled: boolean; + language_code: string; + is_taxable: boolean; + tax_id: number; + tds_tax_id: string; + tax_name: string; + tax_percentage: number; + tax_authority_id: number; + tax_exemption_id: number; + tax_authority_name: string; + tax_exemption_code: string; + place_of_contact: string; + gst_no: string; + vat_treatment: string; + tax_treatment: string; + tax_regime: string; + is_tds_registered: boolean; + gst_treatment: string; + is_linked_with_zohocrm: boolean; + website: string; + owner_id: number; + primary_contact_id: number; + payment_terms: number; + payment_terms_label: string; + currency_id: number; + currency_code: string; + currency_symbol: string; + opening_balance_amount: number; + exchange_rate: number; + outstanding_receivable_amount: number; + outstanding_receivable_amount_bcy: number; + unused_credits_receivable_amount: number; + unused_credits_receivable_amount_bcy: number; + status: string; + payment_reminder_enabled: boolean; + custom_fields: CustomFieldOutput[]; + billing_address: Address; + shipping_address: Address; + facebook: string; + twitter: string; + contact_persons: ContactPerson[]; + default_templates: DefaultTemplates; + notes: string; + created_time: string; + last_modified_time: string; +} + +//OUTPUT + +interface CustomFieldOutput { + index: number; + value: string; + label: string; +} + +interface Address { + attention: string; + address: string; + street2: string; + state_code: string; + city: string; + state: string; + zip: number; + country: string; + fax: string; + phone: string; +} + +interface ContactPerson { + contact_person_id: number; + salutation: string; + first_name: string; + last_name: string; + email: string; + phone: string; + mobile: string; + designation: string; + department: string; + skype: string; + is_primary_contact: boolean; + enable_portal: boolean; +} + +//INPUT + +interface DefaultTemplates { + invoice_template_id: number; + estimate_template_id: number; + creditnote_template_id: number; + purchaseorder_template_id: number; + salesorder_template_id: number; + retainerinvoice_template_id: number; + paymentthankyou_template_id: number; + retainerinvoice_paymentthankyou_template_id: number; + invoice_email_template_id: number; + estimate_email_template_id: number; + creditnote_email_template_id: number; + purchaseorder_email_template_id: number; + salesorder_email_template_id: number; + retainerinvoice_email_template_id: number; + paymentthankyou_email_template_id: number; + retainerinvoice_paymentthankyou_email_template_id: number; +} + +interface CustomFieldInput { + index: number; + value: string; }