zoho crm flow added
This commit is contained in:
parent
1dae751ce4
commit
53e0f7ba50
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ZOHO_CLIENT_ID=1000.PY0FB5ABLLFK2WIBDCNCGKQ0EUIJMY
|
||||||
|
ZOHO_CLIENT_SECRET=772c42df00054668efb6a5839f1874b1dc89e1a127
|
||||||
|
ZOHO_REDIRECT_URI=centralizedreportingsystem://oauth/callback
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
|
||||||
uploads/
|
uploads/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
@ -7,11 +7,11 @@ const session = require('../../auth/session.service');
|
|||||||
async function login(req, res) {
|
async function login(req, res) {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
const user = await repo.findByEmail(email);
|
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);
|
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() });
|
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 accessToken = jwtService.sign({ id: user.id, uuid: user.uuid, role: user.role });
|
||||||
const refreshToken = jwtService.sign({ uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' });
|
const refreshToken = jwtService.sign({ id: user.id, uuid: user.uuid, type: 'refresh' }, { expiresIn: '7d' });
|
||||||
await session.storeRefreshToken(user.uuid, refreshToken);
|
await session.storeRefreshToken(user.uuid, refreshToken);
|
||||||
const displayName = [user.firstName, user.lastName].filter(Boolean).join(' ');
|
const displayName = [user.firstName, user.lastName].filter(Boolean).join(' ');
|
||||||
res.json(
|
res.json(
|
||||||
@ -36,7 +36,7 @@ async function refresh(req, res) {
|
|||||||
if (payload.type !== 'refresh') throw new Error('Invalid token');
|
if (payload.type !== 'refresh') throw new Error('Invalid token');
|
||||||
const stored = await session.getRefreshToken(payload.uuid);
|
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() });
|
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 }));
|
res.json(success('Token refreshed', { accessToken }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() });
|
return res.status(401).json({ status: 'error', message: 'Invalid refresh token', errorCode: 'INVALID_REFRESH', timestamp: new Date().toISOString() });
|
||||||
|
|||||||
48
src/api/controllers/integrationController.js
Normal file
48
src/api/controllers/integrationController.js
Normal file
@ -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 };
|
||||||
@ -59,8 +59,8 @@ async function exchangeZohoToken(req, res) {
|
|||||||
await userAuthTokenRepo.upsertToken({
|
await userAuthTokenRepo.upsertToken({
|
||||||
userId: id,
|
userId: id,
|
||||||
serviceName: service_name,
|
serviceName: service_name,
|
||||||
accessToken: encrypt(access_token),
|
accessToken: access_token,
|
||||||
refreshToken: refresh_token ? encrypt(refresh_token) : null,
|
refreshToken: refresh_token? refresh_token : null,
|
||||||
expiresAt
|
expiresAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
59
src/api/routes/integrationRoutes.js
Normal file
59
src/api/routes/integrationRoutes.js
Normal file
@ -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;
|
||||||
@ -10,6 +10,7 @@ const { success } = require('./utils/response');
|
|||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const userRoutes = require('./api/routes/userRoutes');
|
const userRoutes = require('./api/routes/userRoutes');
|
||||||
const authRoutes = require('./api/routes/authRoutes');
|
const authRoutes = require('./api/routes/authRoutes');
|
||||||
|
const integrationRoutes = require('./api/routes/integrationRoutes');
|
||||||
const sequelize = require('./db/pool');
|
const sequelize = require('./db/pool');
|
||||||
|
|
||||||
const app = express();
|
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}/auth`, authRoutes);
|
||||||
app.use(`${config.app.apiPrefix}/users`, userRoutes);
|
app.use(`${config.app.apiPrefix}/users`, userRoutes);
|
||||||
|
app.use(`${config.app.apiPrefix}/integrations`, integrationRoutes);
|
||||||
|
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
139
src/integrations/zoho/client.js
Normal file
139
src/integrations/zoho/client.js
Normal file
@ -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;
|
||||||
254
src/integrations/zoho/handler.js
Normal file
254
src/integrations/zoho/handler.js
Normal file
@ -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;
|
||||||
152
src/integrations/zoho/mapper.js
Normal file
152
src/integrations/zoho/mapper.js
Normal file
@ -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;
|
||||||
@ -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));
|
|
||||||
};
|
|
||||||
|
|
||||||
73
src/services/integration/integrationService.js
Normal file
73
src/services/integration/integrationService.js
Normal file
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user