From 53e0f7ba5009346a4c4829f2236134520c7b54fd Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Thu, 11 Sep 2025 17:12:47 +0530 Subject: [PATCH] zoho crm flow added --- .env | 3 + .gitignore | 1 - src/api/controllers/authController.js | 8 +- src/api/controllers/integrationController.js | 48 ++++ src/api/controllers/userController.js | 4 +- src/api/routes/integrationRoutes.js | 59 ++++ src/app.js | 2 + src/integrations/zoho/client.js | 139 ++++++++++ src/integrations/zoho/handler.js | 254 ++++++++++++++++++ src/integrations/zoho/mapper.js | 152 +++++++++++ src/middlewares/errorHandler.js | 21 -- .../integration/integrationService.js | 73 +++++ 12 files changed, 736 insertions(+), 28 deletions(-) create mode 100644 .env create mode 100644 src/api/controllers/integrationController.js create mode 100644 src/api/routes/integrationRoutes.js create mode 100644 src/integrations/zoho/client.js create mode 100644 src/integrations/zoho/handler.js create mode 100644 src/integrations/zoho/mapper.js delete mode 100644 src/middlewares/errorHandler.js create mode 100644 src/services/integration/integrationService.js diff --git a/.env b/.env new file mode 100644 index 0000000..0eda4d2 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ZOHO_CLIENT_ID=1000.PY0FB5ABLLFK2WIBDCNCGKQ0EUIJMY +ZOHO_CLIENT_SECRET=772c42df00054668efb6a5839f1874b1dc89e1a127 +ZOHO_REDIRECT_URI=centralizedreportingsystem://oauth/callback \ No newline at end of file diff --git a/.gitignore b/.gitignore index 346ae83..7ed3362 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -.env uploads/ npm-debug.log* coverage/ diff --git a/src/api/controllers/authController.js b/src/api/controllers/authController.js index 6184e98..5d3f2ff 100644 --- a/src/api/controllers/authController.js +++ b/src/api/controllers/authController.js @@ -7,11 +7,11 @@ const session = require('../../auth/session.service'); async function login(req, res) { const { email, password } = req.body; const user = await repo.findByEmail(email); - if (!user) return res.status(401).json({ status: 'error', message: 'Invalid credentials', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() }); + if (!user) return res.status(401).json({ status: 'error', message: 'User Not Found', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() }); const ok = await bcrypt.compare(password, user.passwordHash); if (!ok) return res.status(401).json({ status: 'error', message: 'Invalid credentials', errorCode: 'BAD_CREDENTIALS', timestamp: new Date().toISOString() }); - const accessToken = jwtService.sign({ uuid: user.uuid, role: user.role }); - const refreshToken = jwtService.sign({ uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' }); + const accessToken = jwtService.sign({ id: user.id, uuid: user.uuid, role: user.role }); + const refreshToken = jwtService.sign({ id: user.id, uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' }); await session.storeRefreshToken(user.uuid, refreshToken); const displayName = [user.firstName, user.lastName].filter(Boolean).join(' '); res.json( @@ -36,7 +36,7 @@ async function refresh(req, res) { if (payload.type !== 'refresh') throw new Error('Invalid token'); const stored = await session.getRefreshToken(payload.uuid); if (stored !== refreshToken) return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() }); - const accessToken = jwtService.sign({ uuid: payload.uuid, role: payload.role }); + const accessToken = jwtService.sign({ id: payload.id, uuid: payload.uuid, role: payload.role }); res.json(success('Token refreshed', { accessToken })); } catch (e) { return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() }); diff --git a/src/api/controllers/integrationController.js b/src/api/controllers/integrationController.js new file mode 100644 index 0000000..ea2af8c --- /dev/null +++ b/src/api/controllers/integrationController.js @@ -0,0 +1,48 @@ +const { success, failure } = require('../../utils/response'); +const IntegrationService = require('../../services/integration/integrationService'); + +async function getData(req, res) { + try { + const { provider, service, resource, page, limit, filters } = req.query; + console.log('query is', req.query); + const integrationService = new IntegrationService(req.user.id); + + const params = { page, limit }; + if (filters) { + try { + params.filters = JSON.parse(filters); + } catch (e) { + return res.status(400).json(failure('Invalid filters format', 'INVALID_FILTERS')); + } + } + + const data = await integrationService.getData(provider, service, resource, params); + res.json(success(`${provider} ${service} ${resource} data`, data)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getServices(req, res) { + try { + const { provider } = req.query; + const integrationService = new IntegrationService(req.user.id); + const services = await integrationService.getAvailableServices(provider); + res.json(success(`${provider} available services`, services)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getResources(req, res) { + try { + const { provider, service } = req.query; + const integrationService = new IntegrationService(req.user.id); + const resources = await integrationService.getAvailableResources(provider, service); + res.json(success(`${provider} ${service} available resources`, resources)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +module.exports = { getData, getServices, getResources }; diff --git a/src/api/controllers/userController.js b/src/api/controllers/userController.js index 9ecef4f..0facbfa 100644 --- a/src/api/controllers/userController.js +++ b/src/api/controllers/userController.js @@ -59,8 +59,8 @@ async function exchangeZohoToken(req, res) { await userAuthTokenRepo.upsertToken({ userId: id, serviceName: service_name, - accessToken: encrypt(access_token), - refreshToken: refresh_token ? encrypt(refresh_token) : null, + accessToken: access_token, + refreshToken: refresh_token? refresh_token : null, expiresAt }); diff --git a/src/api/routes/integrationRoutes.js b/src/api/routes/integrationRoutes.js new file mode 100644 index 0000000..fd1d409 --- /dev/null +++ b/src/api/routes/integrationRoutes.js @@ -0,0 +1,59 @@ +const express = require('express'); +const Joi = require('joi'); +const { getData, getServices, getResources } = require('../controllers/integrationController'); +const auth = require('../middlewares/auth'); +const ZohoHandler = require('../../integrations/zoho/handler'); + +const router = express.Router(); + +function validate(schema) { + return (req, res, next) => { + const { error, value } = schema.validate(req.query, { abortEarly: false, stripUnknown: true }); + if (error) { + return res.status(400).json({ + status: 'error', + message: 'Validation failed', + errorCode: 'VALIDATION_ERROR', + details: error.details, + timestamp: new Date().toISOString() + }); + } + req.query = value; + next(); + }; +} + +// Get data from any provider/service/resource +const dataSchema = Joi.object({ + provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required(), + service: Joi.string().required(), + resource: Joi.string().required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +router.get('/data', auth, validate(dataSchema), getData); + +// Get available services +const servicesSchema = Joi.object({ + provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required() +}); + +router.get('/services', auth, validate(servicesSchema), getServices); + +// Get available resources +const resourcesSchema = Joi.object({ + provider: Joi.string().valid('zoho', 'hubspot', 'quickbooks', 'bamboohr').required(), + service: Joi.string().required() +}); + +router.get('/resources', auth, validate(resourcesSchema), getResources); + +// Webhook endpoints (no auth required - uses signature verification) +const zohoHandler = new ZohoHandler(); +router.post('/webhooks/zoho/crm', zohoHandler.handleCrmWebhook.bind(zohoHandler)); +router.post('/webhooks/zoho/people', zohoHandler.handlePeopleWebhook.bind(zohoHandler)); +router.post('/webhooks/zoho/projects', zohoHandler.handleProjectsWebhook.bind(zohoHandler)); + +module.exports = router; diff --git a/src/app.js b/src/app.js index 312398e..9580aa0 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ const { success } = require('./utils/response'); const config = require('./config'); const userRoutes = require('./api/routes/userRoutes'); const authRoutes = require('./api/routes/authRoutes'); +const integrationRoutes = require('./api/routes/integrationRoutes'); const sequelize = require('./db/pool'); const app = express(); @@ -39,6 +40,7 @@ app.get('/health', async (req, res) => { app.use(`${config.app.apiPrefix}/auth`, authRoutes); app.use(`${config.app.apiPrefix}/users`, userRoutes); +app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes); module.exports = app; diff --git a/src/integrations/zoho/client.js b/src/integrations/zoho/client.js new file mode 100644 index 0000000..4e7baba --- /dev/null +++ b/src/integrations/zoho/client.js @@ -0,0 +1,139 @@ +const axios = require('axios'); +const { decrypt, encrypt } = require('../../utils/crypto'); +const userAuthTokenRepo = require('../../data/repositories/userAuthTokenRepository'); +const ZohoMapper = require('./mapper'); + +class ZohoClient { + constructor(userId) { + this.userId = userId; + this.baseUrl = 'https://www.zohoapis.com'; + } + + async getTokens() { + const token = await userAuthTokenRepo.findByUserAndService(this.userId, 'zoho'); + if (!token) { + throw new Error('Zoho tokens not found. Please authenticate first.'); + } + return { + accessToken: decrypt(token.accessToken), + refreshToken: token.refreshToken ? decrypt(token.refreshToken) : null, + expiresAt: token.expiresAt + }; + } + + async makeRequest(endpoint, options = {}) { + const { accessToken } = await this.getTokens(); + console.log('i am in make request with token',accessToken) + const url = `${this.baseUrl}${endpoint}`; + + const config = { + ...options, + headers: { + 'Authorization': `Zoho-oauthtoken ${accessToken}`, + 'Content-Type': 'application/json', + ...options.headers + } + }; + + try { + const response = await axios(url, config); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + await this.refreshToken(); + const newTokens = await this.getTokens(); + config.headers.Authorization = `Zoho-oauthtoken ${newTokens.accessToken}`; + const retryResponse = await axios(url, config); + return retryResponse.data; + } + throw error; + } + } + + async refreshToken() { + const { refreshToken } = await this.getTokens(); + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + const params = new URLSearchParams(); + params.append('refresh_token', refreshToken); + params.append('client_id', process.env.ZOHO_CLIENT_ID); + params.append('client_secret', process.env.ZOHO_CLIENT_SECRET); + params.append('grant_type', 'refresh_token'); + + const response = await axios.post('https://accounts.zoho.com/oauth/v2/token', params.toString(), { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + + const { access_token, expires_in } = response.data; + const expiresAt = expires_in ? new Date(Date.now() + expires_in * 1000) : null; + + await userAuthTokenRepo.upsertToken({ + userId: this.userId, + serviceName: 'zoho', + accessToken: encrypt(access_token), + refreshToken: refreshToken ? encrypt(refreshToken) : null, + expiresAt + }); + } + + // Zoho CRM methods + async getLeads(params = {}) { + const response = await this.makeRequest('/crm/v2/Leads', { params }); + return ZohoMapper.mapApiResponse(response, 'leads'); + } + + async getContacts(params = {}) { + const response = await this.makeRequest('/crm/v2/Contacts', { params }); + return ZohoMapper.mapApiResponse(response, 'contacts'); + } + + async getDeals(params = {}) { + const response = await this.makeRequest('/crm/v2/Deals', { params }); + return ZohoMapper.mapApiResponse(response, 'deals'); + } + + async getTasks(params = {}) { + const response = await this.makeRequest('/crm/v2/Tasks', { params }); + return ZohoMapper.mapApiResponse(response, 'tasks'); + } + + // Zoho People methods + async getEmployees(params = {}) { + const response = await this.makeRequest('/people/api/v1/employees', { params }); + return ZohoMapper.mapApiResponse(response, 'employees'); + } + + // Zoho Projects methods + async getProjects(params = {}) { + const response = await this.makeRequest('/projects/v1/projects', { params }); + return ZohoMapper.mapApiResponse(response, 'projects'); + } + + async getProjectTasks(projectId, params = {}) { + const response = await this.makeRequest(`/projects/v1/projects/${projectId}/tasks`, { params }); + return ZohoMapper.mapApiResponse(response, 'tasks'); + } + + async getAllProjectTasks(params = {}) { + const response = await this.makeRequest('/projects/v1/tasks', { params }); + return ZohoMapper.mapApiResponse(response, 'tasks'); + } + + // Service discovery + getAvailableServices() { + return ['crm', 'people', 'projects']; + } + + getAvailableResources(service) { + const resources = { + 'crm': ['leads', 'contacts', 'deals', 'tasks'], + 'people': ['employees', 'departments'], + 'projects': ['projects', 'tasks', 'timesheets'] + }; + return resources[service] || []; + } +} + +module.exports = ZohoClient; diff --git a/src/integrations/zoho/handler.js b/src/integrations/zoho/handler.js new file mode 100644 index 0000000..8624207 --- /dev/null +++ b/src/integrations/zoho/handler.js @@ -0,0 +1,254 @@ +// Webhook/event handler for Zoho integration +const crypto = require('crypto'); +const logger = require('../../utils/logger'); + +class ZohoHandler { + constructor() { + this.webhookSecret = process.env.ZOHO_WEBHOOK_SECRET || 'changeme'; + } + + // Verify webhook signature from Zoho + verifyWebhookSignature(payload, signature, secret = null) { + const webhookSecret = secret || this.webhookSecret; + const expectedSignature = crypto + .createHmac('sha256', webhookSecret) + .update(payload, 'utf8') + .digest('hex'); + + return crypto.timingSafeEqual( + Buffer.from(signature, 'hex'), + Buffer.from(expectedSignature, 'hex') + ); + } + + // Handle Zoho CRM webhook events + async handleCrmWebhook(req, res) { + try { + const signature = req.headers['x-zoho-signature']; + const payload = JSON.stringify(req.body); + + if (!this.verifyWebhookSignature(payload, signature)) { + logger.warn('Invalid Zoho CRM webhook signature', { + correlationId: logger.getCorrelationId(req), + ip: req.ip + }); + return res.status(401).json({ status: 'error', message: 'Invalid signature' }); + } + + const { event, data } = req.body; + logger.info('Zoho CRM webhook received', { + correlationId: logger.getCorrelationId(req), + event, + recordId: data?.id + }); + + // Process different CRM events + switch (event) { + case 'leads.create': + await this.handleLeadCreated(data); + break; + case 'leads.update': + await this.handleLeadUpdated(data); + break; + case 'leads.delete': + await this.handleLeadDeleted(data); + break; + case 'contacts.create': + await this.handleContactCreated(data); + break; + case 'contacts.update': + await this.handleContactUpdated(data); + break; + case 'deals.create': + await this.handleDealCreated(data); + break; + case 'deals.update': + await this.handleDealUpdated(data); + break; + default: + logger.warn('Unknown Zoho CRM event', { event }); + } + + res.json({ status: 'success', message: 'Webhook processed' }); + } catch (error) { + logger.error('Zoho CRM webhook processing failed', { + correlationId: logger.getCorrelationId(req), + error: error.message, + stack: error.stack + }); + res.status(500).json({ status: 'error', message: 'Webhook processing failed' }); + } + } + + // Handle Zoho People webhook events + async handlePeopleWebhook(req, res) { + try { + const signature = req.headers['x-zoho-signature']; + const payload = JSON.stringify(req.body); + + if (!this.verifyWebhookSignature(payload, signature)) { + logger.warn('Invalid Zoho People webhook signature', { + correlationId: logger.getCorrelationId(req), + ip: req.ip + }); + return res.status(401).json({ status: 'error', message: 'Invalid signature' }); + } + + const { event, data } = req.body; + logger.info('Zoho People webhook received', { + correlationId: logger.getCorrelationId(req), + event, + employeeId: data?.id + }); + + // Process different People events + switch (event) { + case 'employees.create': + await this.handleEmployeeCreated(data); + break; + case 'employees.update': + await this.handleEmployeeUpdated(data); + break; + case 'employees.delete': + await this.handleEmployeeDeleted(data); + break; + default: + logger.warn('Unknown Zoho People event', { event }); + } + + res.json({ status: 'success', message: 'Webhook processed' }); + } catch (error) { + logger.error('Zoho People webhook processing failed', { + correlationId: logger.getCorrelationId(req), + error: error.message, + stack: error.stack + }); + res.status(500).json({ status: 'error', message: 'Webhook processing failed' }); + } + } + + // Handle Zoho Projects webhook events + async handleProjectsWebhook(req, res) { + try { + const signature = req.headers['x-zoho-signature']; + const payload = JSON.stringify(req.body); + + if (!this.verifyWebhookSignature(payload, signature)) { + logger.warn('Invalid Zoho Projects webhook signature', { + correlationId: logger.getCorrelationId(req), + ip: req.ip + }); + return res.status(401).json({ status: 'error', message: 'Invalid signature' }); + } + + const { event, data } = req.body; + logger.info('Zoho Projects webhook received', { + correlationId: logger.getCorrelationId(req), + event, + projectId: data?.id + }); + + // Process different Projects events + switch (event) { + case 'projects.create': + await this.handleProjectCreated(data); + break; + case 'projects.update': + await this.handleProjectUpdated(data); + break; + case 'tasks.create': + await this.handleTaskCreated(data); + break; + case 'tasks.update': + await this.handleTaskUpdated(data); + break; + default: + logger.warn('Unknown Zoho Projects event', { event }); + } + + res.json({ status: 'success', message: 'Webhook processed' }); + } catch (error) { + logger.error('Zoho Projects webhook processing failed', { + correlationId: logger.getCorrelationId(req), + error: error.message, + stack: error.stack + }); + res.status(500).json({ status: 'error', message: 'Webhook processing failed' }); + } + } + + // CRM Event Handlers + async handleLeadCreated(data) { + logger.info('Lead created', { leadId: data.id, name: data.Full_Name }); + // Add your business logic here - e.g., sync to other systems + } + + async handleLeadUpdated(data) { + logger.info('Lead updated', { leadId: data.id, name: data.Full_Name }); + // Add your business logic here + } + + async handleLeadDeleted(data) { + logger.info('Lead deleted', { leadId: data.id }); + // Add your business logic here + } + + async handleContactCreated(data) { + logger.info('Contact created', { contactId: data.id, name: `${data.First_Name} ${data.Last_Name}` }); + // Add your business logic here + } + + async handleContactUpdated(data) { + logger.info('Contact updated', { contactId: data.id, name: `${data.First_Name} ${data.Last_Name}` }); + // Add your business logic here + } + + async handleDealCreated(data) { + logger.info('Deal created', { dealId: data.id, name: data.Deal_Name, amount: data.Amount }); + // Add your business logic here + } + + async handleDealUpdated(data) { + logger.info('Deal updated', { dealId: data.id, name: data.Deal_Name, amount: data.Amount }); + // Add your business logic here + } + + // People Event Handlers + async handleEmployeeCreated(data) { + logger.info('Employee created', { employeeId: data.id, name: `${data.firstName} ${data.lastName}` }); + // Add your business logic here + } + + async handleEmployeeUpdated(data) { + logger.info('Employee updated', { employeeId: data.id, name: `${data.firstName} ${data.lastName}` }); + // Add your business logic here + } + + async handleEmployeeDeleted(data) { + logger.info('Employee deleted', { employeeId: data.id }); + // Add your business logic here + } + + // Projects Event Handlers + async handleProjectCreated(data) { + logger.info('Project created', { projectId: data.id, name: data.name }); + // Add your business logic here + } + + async handleProjectUpdated(data) { + logger.info('Project updated', { projectId: data.id, name: data.name }); + // Add your business logic here + } + + async handleTaskCreated(data) { + logger.info('Task created', { taskId: data.id, name: data.name, projectId: data.project?.id }); + // Add your business logic here + } + + async handleTaskUpdated(data) { + logger.info('Task updated', { taskId: data.id, name: data.name, projectId: data.project?.id }); + // Add your business logic here + } +} + +module.exports = ZohoHandler; diff --git a/src/integrations/zoho/mapper.js b/src/integrations/zoho/mapper.js new file mode 100644 index 0000000..72599a2 --- /dev/null +++ b/src/integrations/zoho/mapper.js @@ -0,0 +1,152 @@ +// Data mapping/transformation utilities for Zoho integration +class ZohoMapper { + // Map Zoho CRM Lead to standardized format + static mapLead(zohoLead) { + return { + id: zohoLead.id, + name: zohoLead.Full_Name || zohoLead.Lead_Name, + email: zohoLead.Email, + phone: zohoLead.Phone, + company: zohoLead.Company, + status: zohoLead.Lead_Status, + source: zohoLead.Lead_Source, + createdTime: zohoLead.Created_Time, + modifiedTime: zohoLead.Modified_Time, + owner: zohoLead.Owner?.name, + // Map custom fields if they exist + customFields: this.mapCustomFields(zohoLead) + }; + } + + // Map Zoho CRM Contact to standardized format + static mapContact(zohoContact) { + return { + id: zohoContact.id, + firstName: zohoContact.First_Name, + lastName: zohoContact.Last_Name, + email: zohoContact.Email, + phone: zohoContact.Phone, + account: zohoContact.Account_Name, + title: zohoContact.Title, + department: zohoContact.Department, + createdTime: zohoContact.Created_Time, + modifiedTime: zohoContact.Modified_Time, + owner: zohoContact.Owner?.name, + customFields: this.mapCustomFields(zohoContact) + }; + } + + // Map Zoho CRM Deal to standardized format + static mapDeal(zohoDeal) { + return { + id: zohoDeal.id, + name: zohoDeal.Deal_Name, + amount: zohoDeal.Amount, + stage: zohoDeal.Stage, + probability: zohoDeal.Probability, + closeDate: zohoDeal.Closing_Date, + account: zohoDeal.Account_Name, + contact: zohoDeal.Contact_Name, + createdTime: zohoDeal.Created_Time, + modifiedTime: zohoDeal.Modified_Time, + owner: zohoDeal.Owner?.name, + customFields: this.mapCustomFields(zohoDeal) + }; + } + + // Map Zoho People Employee to standardized format + static mapEmployee(zohoEmployee) { + return { + id: zohoEmployee.id, + employeeId: zohoEmployee.employeeId, + firstName: zohoEmployee.firstName, + lastName: zohoEmployee.lastName, + email: zohoEmployee.email, + phone: zohoEmployee.phone, + department: zohoEmployee.department, + designation: zohoEmployee.designation, + reportingTo: zohoEmployee.reportingTo, + joiningDate: zohoEmployee.joiningDate, + status: zohoEmployee.status, + customFields: this.mapCustomFields(zohoEmployee) + }; + } + + // Map Zoho Projects Project to standardized format + static mapProject(zohoProject) { + return { + id: zohoProject.id, + name: zohoProject.name, + description: zohoProject.description, + status: zohoProject.status, + startDate: zohoProject.start_date, + endDate: zohoProject.end_date, + owner: zohoProject.owner?.name, + createdTime: zohoProject.created_time, + customFields: this.mapCustomFields(zohoProject) + }; + } + + // Map Zoho Projects Task to standardized format + static mapTask(zohoTask) { + return { + id: zohoTask.id, + name: zohoTask.name, + description: zohoTask.description, + status: zohoTask.status, + priority: zohoTask.priority, + startDate: zohoTask.start_date, + endDate: zohoTask.end_date, + assignee: zohoTask.assignee?.name, + project: zohoTask.project?.name, + customFields: this.mapCustomFields(zohoTask) + }; + } + + // Map custom fields from Zoho response + static mapCustomFields(zohoRecord) { + const customFields = {}; + if (zohoRecord.Custom_Fields) { + zohoRecord.Custom_Fields.forEach(field => { + customFields[field.api_name] = field.value; + }); + } + return customFields; + } + + // Map multiple records using appropriate mapper + static mapRecords(records, recordType) { + const mapperMap = { + 'leads': this.mapLead, + 'contacts': this.mapContact, + 'deals': this.mapDeal, + 'employees': this.mapEmployee, + 'projects': this.mapProject, + 'tasks': this.mapTask + }; + + const mapper = mapperMap[recordType]; + if (!mapper) { + throw new Error(`No mapper found for record type: ${recordType}`); + } + + return records.map(record => mapper(record)); + } + + // Map Zoho API response to standardized format + static mapApiResponse(zohoResponse, recordType) { + const records = zohoResponse.data || []; + // const mappedRecords = this.mapRecords(records, recordType); + + return { + data: records, + info: { + count: zohoResponse.info?.count || mappedRecords.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + } + }; + } +} + +module.exports = ZohoMapper; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js deleted file mode 100644 index 2545db4..0000000 --- a/src/middlewares/errorHandler.js +++ /dev/null @@ -1,21 +0,0 @@ -const { failure } = require('../utils/response'); -const logger = require('../utils/logger'); - -module.exports = function errorHandler(err, req, res, next) { - const correlationId = logger.getCorrelationId(req); - const status = err.status || 500; - const errorCode = err.code || 'INTERNAL_SERVER_ERROR'; - const message = status === 500 ? 'Something went wrong' : err.message || 'Error'; - - logger.error('Request failed', { - correlationId, - path: req.originalUrl, - method: req.method, - status, - errorCode, - stack: status === 500 ? err.stack : undefined - }); - - res.status(status).json(failure(message, errorCode)); -}; - diff --git a/src/services/integration/integrationService.js b/src/services/integration/integrationService.js new file mode 100644 index 0000000..a8480fa --- /dev/null +++ b/src/services/integration/integrationService.js @@ -0,0 +1,73 @@ +const ZohoClient = require('../../integrations/zoho/client'); + +class IntegrationService { + constructor(userId) { + this.userId = userId; + this.clients = { + zoho: new ZohoClient(userId) + }; + } + + async getData(provider, service, resource, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + + const method = this.getMethodName(service, resource); + + if (!client[method]) { + throw new Error(`Resource ${resource} not supported for ${provider} ${service}`); + } + + // Handle special cases that require additional parameters + if (method === 'getTasks' && service === 'projects' && params.projectId) { + return await client.getProjectTasks(params.projectId, params); + } + + // Handle getAllProjectTasks for projects tasks without projectId + if (method === 'getTasks' && service === 'projects' && !params.projectId) { + return await client.getAllProjectTasks(params); + } + + // Handle CRM tasks (no special parameters needed) + if (method === 'getTasks' && service === 'crm') { + return await client[method](params); + } + + return await client[method](params); + } + + getMethodName(service, resource) { + const resourceMap = { + 'leads': 'Leads', + 'contacts': 'Contacts', + 'deals': 'Deals', + 'employees': 'Employees', + 'projects': 'Projects', + 'tasks': 'Tasks', + 'timesheets': 'Timesheets' + }; + + const resourceName = resourceMap[resource] || resource; + return `get${resourceName}`; + } + + async getAvailableServices(provider) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + return client.getAvailableServices(); + } + + async getAvailableResources(provider, service) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + return client.getAvailableResources(service); + } +} + +module.exports = IntegrationService;