From efce4cc90d5ae982533447f4c3c97d4bd1eba661 Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Fri, 19 Sep 2025 17:42:25 +0530 Subject: [PATCH] zoho books api added along with zoho people (employee api tested) --- src/api/controllers/integrationController.js | 585 +++++++++++++++++- src/api/routes/integrationRoutes.js | 220 ++++++- src/integrations/zoho/client.js | 141 ++++- src/integrations/zoho/handler.js | 103 +++ src/integrations/zoho/mapper.js | 557 +++++++++++++++-- .../integration/integrationService.js | 280 ++++++++- 6 files changed, 1825 insertions(+), 61 deletions(-) diff --git a/src/api/controllers/integrationController.js b/src/api/controllers/integrationController.js index 808ac89..8af586c 100644 --- a/src/api/controllers/integrationController.js +++ b/src/api/controllers/integrationController.js @@ -278,4 +278,587 @@ async function getInvoices(req, res) { } } -module.exports = { getData, getServices, getResources, getPortals, getAllProjects, getAllProjectTasks, getAllProjectTaskLists, getAllProjectIssues, getAllProjectPhases, getSalesOrders, getPurchaseOrders, getInvoices }; +// Zoho People specific controllers +async function getDepartments(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Departments are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 departments = await integrationService.getDepartments(provider, params); + res.json(success('Zoho Departments retrieved successfully', departments)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getLeaveRequests(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Leave Requests are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 leaveRequests = await integrationService.getLeaveRequests(provider, params); + res.json(success('Zoho Leave Requests retrieved successfully', leaveRequests)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getAttendance(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Attendance is only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 attendance = await integrationService.getAttendance(provider, params); + res.json(success('Zoho Attendance retrieved successfully', attendance)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +// Zoho Books specific controllers +async function getOrganizations(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Organizations are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 organizations = await integrationService.getOrganizations(provider, params); + res.json(success('Zoho Organizations retrieved successfully', organizations)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getCustomers(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Customers are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 customers = await integrationService.getCustomers(provider, params); + + res.json(success('Zoho Customers retrieved successfully', customers)); + } catch (error) { + console.log('customer response error i got', JSON.stringify(error)) + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getVendors(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Vendors are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 vendors = await integrationService.getVendors(provider, params); + res.json(success('Zoho Vendors retrieved successfully', vendors)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getItems(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Items are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 items = await integrationService.getItems(provider, params); + res.json(success('Zoho Items retrieved successfully', items)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getEstimates(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Estimates are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 estimates = await integrationService.getEstimates(provider, params); + res.json(success('Zoho Estimates retrieved successfully', estimates)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getBills(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Bills are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 bills = await integrationService.getBills(provider, params); + res.json(success('Zoho Bills retrieved successfully', bills)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getExpenses(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Expenses are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 expenses = await integrationService.getExpenses(provider, params); + res.json(success('Zoho Expenses retrieved successfully', expenses)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getBankAccounts(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Bank Accounts are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 bankAccounts = await integrationService.getBankAccounts(provider, params); + res.json(success('Zoho Bank Accounts retrieved successfully', bankAccounts)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getBankTransactions(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Bank Transactions are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 bankTransactions = await integrationService.getBankTransactions(provider, params); + res.json(success('Zoho Bank Transactions retrieved successfully', bankTransactions)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getReports(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Reports are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 reports = await integrationService.getReports(provider, params); + res.json(success('Zoho Reports retrieved successfully', reports)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getBooksSalesOrders(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Books Sales Orders are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 salesOrders = await integrationService.getBooksSalesOrders(provider, params); + res.json(success('Zoho Books Sales Orders retrieved successfully', salesOrders)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getBooksPurchaseOrders(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Books Purchase Orders are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 purchaseOrders = await integrationService.getBooksPurchaseOrders(provider, params); + res.json(success('Zoho Books Purchase Orders retrieved successfully', purchaseOrders)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getContacts(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Contacts are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 contacts = await integrationService.getContacts(provider, params); + res.json(success('Zoho Books Contacts retrieved successfully', contacts)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +// Zoho People Forms API controllers +async function getEmployeeForms(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Employee Forms are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 employeeForms = await integrationService.getEmployeeForms(provider, params); + res.json(success('Zoho People Employee Forms retrieved successfully', employeeForms)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getEmployeeById(req, res) { + try { + const { provider, recordId, formLinkName } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Employee Details are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + if (!recordId) { + return res.status(400).json(failure('recordId is required', 'MISSING_RECORD_ID')); + } + + const integrationService = new IntegrationService(req.user.uuid); + const employeeDetail = await integrationService.getEmployeeById(provider, recordId, formLinkName); + res.json(success('Zoho People Employee Details retrieved successfully', employeeDetail)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getAttendanceEntries(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Attendance Entries are only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 attendanceEntries = await integrationService.getAttendanceEntries(provider, params); + res.json(success('Zoho People Attendance Entries retrieved successfully', attendanceEntries)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getShiftConfiguration(req, res) { + try { + const { provider, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Shift Configuration is only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 shiftConfig = await integrationService.getShiftConfiguration(provider, params); + res.json(success('Zoho People Shift Configuration retrieved successfully', shiftConfig)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getLeaveData(req, res) { + try { + const { provider, formLinkName, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Leave Data is only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 leaveData = await integrationService.getLeaveData(provider, formLinkName, params); + res.json(success('Zoho People Leave Data retrieved successfully', leaveData)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getGoalsData(req, res) { + try { + const { provider, formLinkName, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Goals Data is only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 goalsData = await integrationService.getGoalsData(provider, formLinkName, params); + res.json(success('Zoho People Goals Data retrieved successfully', goalsData)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +async function getPerformanceData(req, res) { + try { + const { provider, formLinkName, page, limit, filters } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Performance Data is only available for Zoho provider', 'INVALID_PROVIDER')); + } + + const integrationService = new IntegrationService(req.user.uuid); + 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 performanceData = await integrationService.getPerformanceData(provider, formLinkName, params); + res.json(success('Zoho People Performance Data retrieved successfully', performanceData)); + } catch (error) { + res.status(400).json(failure(error.message, 'INTEGRATION_ERROR')); + } +} + +module.exports = { + getData, getServices, getResources, getPortals, getAllProjects, getAllProjectTasks, + getAllProjectTaskLists, getAllProjectIssues, getAllProjectPhases, getSalesOrders, + getPurchaseOrders, getInvoices, getDepartments, getLeaveRequests, getAttendance, + getOrganizations, getCustomers, getVendors, getItems, getEstimates, getBills, + getExpenses, getBankAccounts, getBankTransactions, getReports, getBooksSalesOrders, + getBooksPurchaseOrders, getContacts, getEmployeeForms, getEmployeeById, + getAttendanceEntries, getShiftConfiguration, getLeaveData, getGoalsData, getPerformanceData +}; diff --git a/src/api/routes/integrationRoutes.js b/src/api/routes/integrationRoutes.js index 229b11c..9f3e0a5 100644 --- a/src/api/routes/integrationRoutes.js +++ b/src/api/routes/integrationRoutes.js @@ -1,6 +1,14 @@ const express = require('express'); const Joi = require('joi'); -const { getData, getServices, getResources, getPortals, getAllProjects, getAllProjectTasks, getAllProjectTaskLists, getAllProjectIssues, getAllProjectPhases, getSalesOrders, getPurchaseOrders, getInvoices } = require('../controllers/integrationController'); +const { + getData, getServices, getResources, getPortals, getAllProjects, getAllProjectTasks, + getAllProjectTaskLists, getAllProjectIssues, getAllProjectPhases, getSalesOrders, + getPurchaseOrders, getInvoices, getDepartments, getLeaveRequests, getAttendance, + getOrganizations, getCustomers, getVendors, getItems, getEstimates, getBills, + getExpenses, getBankAccounts, getBankTransactions, getReports, getBooksSalesOrders, + getBooksPurchaseOrders, getContacts, getEmployeeForms, getEmployeeById, + getAttendanceEntries, getShiftConfiguration, getLeaveData, getGoalsData, getPerformanceData +} = require('../controllers/integrationController'); const auth = require('../middlewares/auth'); const ZohoHandler = require('../../integrations/zoho/handler'); @@ -60,7 +68,8 @@ const portalsSchema = Joi.object({ provider: Joi.string().valid('zoho').required() }); -router.get('/portals', auth, validate(portalsSchema), getPortals); +// Zoho Projects specific routes with clear service identification +router.get('/zoho/projects/portals', auth, validate(portalsSchema), getPortals); // Get all Zoho projects (across all portals) const allProjectsSchema = Joi.object({ @@ -70,7 +79,7 @@ const allProjectsSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/all-projects', auth, validate(allProjectsSchema), getAllProjects); +router.get('/zoho/projects/all-projects', auth, validate(allProjectsSchema), getAllProjects); // Get all project tasks for a specific portal const allProjectTasksSchema = Joi.object({ @@ -81,7 +90,7 @@ const allProjectTasksSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/all-project-tasks', auth, validate(allProjectTasksSchema), getAllProjectTasks); +router.get('/zoho/projects/all-project-tasks', auth, validate(allProjectTasksSchema), getAllProjectTasks); // Get all project task lists for a specific portal const allProjectTaskListsSchema = Joi.object({ @@ -92,7 +101,7 @@ const allProjectTaskListsSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/all-project-tasklists', auth, validate(allProjectTaskListsSchema), getAllProjectTaskLists); +router.get('/zoho/projects/all-project-tasklists', auth, validate(allProjectTaskListsSchema), getAllProjectTaskLists); // Get all project issues for a specific portal const allProjectIssuesSchema = Joi.object({ @@ -103,7 +112,7 @@ const allProjectIssuesSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/all-project-issues', auth, validate(allProjectIssuesSchema), getAllProjectIssues); +router.get('/zoho/projects/all-project-issues', auth, validate(allProjectIssuesSchema), getAllProjectIssues); // Get all project phases for a specific portal const allProjectPhasesSchema = Joi.object({ @@ -114,7 +123,7 @@ const allProjectPhasesSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/all-project-phases', auth, validate(allProjectPhasesSchema), getAllProjectPhases); +router.get('/zoho/projects/all-project-phases', auth, validate(allProjectPhasesSchema), getAllProjectPhases); // Get Zoho Sales Orders const salesOrdersSchema = Joi.object({ @@ -124,7 +133,8 @@ const salesOrdersSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/sales-orders', auth, validate(salesOrdersSchema), getSalesOrders); +// Zoho CRM specific routes with clear service identification +router.get('/zoho/crm/sales-orders', auth, validate(salesOrdersSchema), getSalesOrders); // Get Zoho Purchase Orders const purchaseOrdersSchema = Joi.object({ @@ -134,7 +144,7 @@ const purchaseOrdersSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/purchase-orders', auth, validate(purchaseOrdersSchema), getPurchaseOrders); +router.get('/zoho/crm/purchase-orders', auth, validate(purchaseOrdersSchema), getPurchaseOrders); // Get Zoho Invoices const invoicesSchema = Joi.object({ @@ -144,12 +154,202 @@ const invoicesSchema = Joi.object({ filters: Joi.string().optional() }); -router.get('/invoices', auth, validate(invoicesSchema), getInvoices); +router.get('/zoho/crm/invoices', auth, validate(invoicesSchema), getInvoices); + +// Zoho People specific routes +const departmentsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const leaveRequestsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const attendanceSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +// Zoho People Forms API schemas +const employeeFormsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const employeeByIdSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + recordId: Joi.string().required(), + formLinkName: Joi.string().default('employee') +}); + +const attendanceEntriesSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const shiftConfigurationSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const leaveDataSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + formLinkName: Joi.string().default('leave'), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const goalsDataSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + formLinkName: Joi.string().default('goals'), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const performanceDataSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + formLinkName: Joi.string().default('performance'), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +// Zoho People specific routes with clear service identification +router.get('/zoho/people/departments', auth, validate(departmentsSchema), getDepartments); +router.get('/zoho/people/leave-requests', auth, validate(leaveRequestsSchema), getLeaveRequests); +router.get('/zoho/people/attendance', auth, validate(attendanceSchema), getAttendance); + +// Zoho People Forms API routes +router.get('/zoho/people/employee-forms', auth, validate(employeeFormsSchema), getEmployeeForms); +router.get('/zoho/people/employee/:recordId', auth, validate(employeeByIdSchema), getEmployeeById); +router.get('/zoho/people/attendance-entries', auth, validate(attendanceEntriesSchema), getAttendanceEntries); +router.get('/zoho/people/shift-configuration', auth, validate(shiftConfigurationSchema), getShiftConfiguration); +router.get('/zoho/people/leave-data', auth, validate(leaveDataSchema), getLeaveData); +router.get('/zoho/people/goals-data', auth, validate(goalsDataSchema), getGoalsData); +router.get('/zoho/people/performance-data', auth, validate(performanceDataSchema), getPerformanceData); + +// Zoho Books specific routes +const organizationsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const customersSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const vendorsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const itemsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const estimatesSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const billsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const expensesSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const bankAccountsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const bankTransactionsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const reportsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const booksSalesOrdersSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +const booksPurchaseOrdersSchema = Joi.object({ + provider: Joi.string().valid('zoho').required(), + page: Joi.number().min(1).default(1), + limit: Joi.number().min(1).max(100).default(20), + filters: Joi.string().optional() +}); + +// Zoho Books specific routes with clear service identification +router.get('/zoho/books/organizations', auth, validate(organizationsSchema), getOrganizations); +router.get('/zoho/books/contacts', auth, validate(customersSchema), getContacts); +router.get('/zoho/books/vendors', auth, validate(vendorsSchema), getVendors); +router.get('/zoho/books/items', auth, validate(itemsSchema), getItems); +router.get('/zoho/books/customers', auth, validate(customersSchema), getCustomers); +router.get('/zoho/books/invoices', auth, validate(invoicesSchema), getInvoices); +router.get('/zoho/books/estimates', auth, validate(estimatesSchema), getEstimates); +router.get('/zoho/books/bills', auth, validate(billsSchema), getBills); +router.get('/zoho/books/expenses', auth, validate(expensesSchema), getExpenses); +router.get('/zoho/books/bank-accounts', auth, validate(bankAccountsSchema), getBankAccounts); +router.get('/zoho/books/bank-transactions', auth, validate(bankTransactionsSchema), getBankTransactions); +router.get('/zoho/books/reports', auth, validate(reportsSchema), getReports); +router.get('/zoho/books/sales-orders', auth, validate(booksSalesOrdersSchema), getBooksSalesOrders); +router.get('/zoho/books/purchase-orders', auth, validate(booksPurchaseOrdersSchema), getBooksPurchaseOrders); // 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/books', zohoHandler.handleBooksWebhook.bind(zohoHandler)); router.post('/webhooks/zoho/projects', zohoHandler.handleProjectsWebhook.bind(zohoHandler)); router.post('/webhooks/zoho/bulkread', zohoHandler.handleBulkReadWebhook.bind(zohoHandler)); diff --git a/src/integrations/zoho/client.js b/src/integrations/zoho/client.js index 10ef870..e44920b 100644 --- a/src/integrations/zoho/client.js +++ b/src/integrations/zoho/client.js @@ -8,7 +8,8 @@ class ZohoClient { this.userId = userId; this.baseUrls = { crm: 'https://www.zohoapis.com', - people: 'https://www.zohoapis.com', + people: 'https://people.zoho.com', + books: 'https://www.zohoapis.com', projects: 'https://projectsapi.zoho.com' }; } @@ -30,12 +31,13 @@ class ZohoClient { const baseUrl = this.baseUrls[service] || this.baseUrls.crm; const url = `${baseUrl}${endpoint}`; - // Transform limit to per_page for Zoho APIs + // Transform limit to per_page for Zoho APIs (except People APIs) let params = options.params || {}; - if (params.limit) { + if (params.limit && service !== 'people') { params.per_page = params.limit; delete params.limit; } + // For People APIs, keep limit as is const config = { ...options, @@ -133,6 +135,134 @@ class ZohoClient { return ZohoMapper.mapApiResponse(response, 'employees'); } + async getDepartments(params = {}) { + const response = await this.makeRequest('/people/api/v1/departments', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'departments'); + } + + async getTimesheets(params = {}) { + const response = await this.makeRequest('/people/api/v1/timesheets', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'timesheets'); + } + + async getLeaveRequests(params = {}) { + const response = await this.makeRequest('/people/api/v1/leave', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'leave_requests'); + } + + async getAttendance(params = {}) { + const response = await this.makeRequest('/people/api/v1/attendance', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'attendance'); + } + + // Zoho People Forms API methods + async getEmployeeForms(params = {}) { + const response = await this.makeRequest('/people/api/forms/employee/getRecords', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'employee_forms'); + } + + async getEmployeeById(recordId, formLinkName = 'employee') { + const response = await this.makeRequest(`/people/api/forms/${formLinkName}/getDataByID`, { + params: { recordId } + }, 'people'); + return ZohoMapper.mapApiResponse(response, 'employee_detail'); + } + + async getAttendanceEntries(params = {}) { + const response = await this.makeRequest('/people/api/attendance/getAttendanceEntries', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'attendance_entries'); + } + + async getShiftConfiguration(params = {}) { + const response = await this.makeRequest('/people/api/attendance/getShiftConfiguration', { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'shift_configuration'); + } + + async getLeaveData(formLinkName = 'leave', params = {}) { + const response = await this.makeRequest(`/people/api/forms/${formLinkName}/getRecords`, { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'leave_data'); + } + + async getGoalsData(formLinkName = 'goals', params = {}) { + const response = await this.makeRequest(`/people/api/forms/${formLinkName}/getRecords`, { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'goals_data'); + } + + async getPerformanceData(formLinkName = 'performance', params = {}) { + const response = await this.makeRequest(`/people/api/forms/${formLinkName}/getRecords`, { params }, 'people'); + return ZohoMapper.mapApiResponse(response, 'performance_data'); + } + + // Zoho Books methods + async getOrganizations(params = {}) { + const response = await this.makeRequest('/books/v3/organizations', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'organizations'); + } + + async getCustomers(params = {}) { + const response = await this.makeRequest('/books/v3/contacts', { params :{ ...params, contact_type: 'customer' }}, 'books'); + return ZohoMapper.mapApiResponse(response, 'contacts'); + } + async getContacts(params = {}) { + const response = await this.makeRequest('/books/v3/contacts', { params}, 'books'); + return ZohoMapper.mapApiResponse(response, 'contacts'); + } + + async getVendors(params = {}) { + const response = await this.makeRequest('/books/v3/contacts', { params: { ...params, contact_type: 'vendor' } }, 'books'); + return ZohoMapper.mapApiResponse(response, 'vendors'); + } + + async getItems(params = {}) { + const response = await this.makeRequest('/books/v3/items', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'items'); + } + + async getInvoices(params = {}) { + const response = await this.makeRequest('/books/v3/invoices', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'invoices'); + } + + async getEstimates(params = {}) { + const response = await this.makeRequest('/books/v3/estimates', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'estimates'); + } + + async getBills(params = {}) { + const response = await this.makeRequest('/books/v3/bills', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'bills'); + } + + async getExpenses(params = {}) { + const response = await this.makeRequest('/books/v3/expenses', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'expenses'); + } + + async getBankAccounts(params = {}) { + const response = await this.makeRequest('/books/v3/bankaccounts', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'bank_accounts'); + } + + async getBankTransactions(params = {}) { + const response = await this.makeRequest('/books/v3/banktransactions', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'bank_transactions'); + } + + async getReports(params = {}) { + const response = await this.makeRequest('/books/v3/reports', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'reports'); + } + + async getBooksSalesOrders(params = {}) { + const response = await this.makeRequest('/books/v3/salesorders', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'sales_orders'); + } + + async getBooksPurchaseOrders(params = {}) { + const response = await this.makeRequest('/books/v3/purchaseorders', { params }, 'books'); + return ZohoMapper.mapApiResponse(response, 'purchase_orders'); + } + // Zoho Projects methods async getPortals() { const response = await this.makeRequest('/api/v3/portals', {}, 'projects'); @@ -211,13 +341,14 @@ class ZohoClient { // Service discovery getAvailableServices() { - return ['crm', 'people', 'projects']; + return ['crm', 'people', 'books', 'projects']; } getAvailableResources(service) { const resources = { 'crm': ['leads', 'contacts', 'deals', 'tasks', 'sales_orders', 'purchase_orders', 'invoices'], - 'people': ['employees', 'departments'], + 'people': ['employees', 'departments', 'timesheets', 'leave_requests', 'attendance', 'employee_forms', 'attendance_entries', 'shift_configuration', 'leave_data', 'goals_data', 'performance_data'], + 'books': ['organizations', 'customers', 'vendors', 'items', 'invoices', 'estimates', 'bills', 'expenses', 'bank_accounts', 'bank_transactions', 'reports', 'sales_orders', 'purchase_orders'], 'projects': ['projects', 'tasks', 'timesheets'] }; return resources[service] || []; diff --git a/src/integrations/zoho/handler.js b/src/integrations/zoho/handler.js index ebeae08..f01058c 100644 --- a/src/integrations/zoho/handler.js +++ b/src/integrations/zoho/handler.js @@ -178,6 +178,68 @@ class ZohoHandler { } } + // Handle Zoho Books webhook events + async handleBooksWebhook(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 Books 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 Books webhook received', { + correlationId: logger.getCorrelationId(req), + event, + recordId: data?.id + }); + + // Process different Books events + switch (event) { + case 'customers.create': + await this.handleCustomerCreated(data); + break; + case 'customers.update': + await this.handleCustomerUpdated(data); + break; + case 'invoices.create': + await this.handleBooksInvoiceCreated(data); + break; + case 'invoices.update': + await this.handleBooksInvoiceUpdated(data); + break; + case 'bills.create': + await this.handleBillCreated(data); + break; + case 'bills.update': + await this.handleBillUpdated(data); + break; + case 'expenses.create': + await this.handleExpenseCreated(data); + break; + case 'expenses.update': + await this.handleExpenseUpdated(data); + break; + default: + logger.warn('Unknown Zoho Books event', { event }); + } + + res.json({ status: 'success', message: 'Webhook processed' }); + } catch (error) { + logger.error('Zoho Books 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 }); @@ -616,6 +678,47 @@ class ZohoHandler { logger.info('Task updated', { taskId: data.id, name: data.name, projectId: data.project?.id }); // Add your business logic here } + + // Books Event Handlers + async handleCustomerCreated(data) { + logger.info('Customer created', { customerId: data.contact_id, name: data.contact_name }); + // Add your business logic here + } + + async handleCustomerUpdated(data) { + logger.info('Customer updated', { customerId: data.contact_id, name: data.contact_name }); + // Add your business logic here + } + + async handleBooksInvoiceCreated(data) { + logger.info('Books Invoice created', { invoiceId: data.invoice_id, invoiceNumber: data.invoice_number, total: data.total }); + // Add your business logic here + } + + async handleBooksInvoiceUpdated(data) { + logger.info('Books Invoice updated', { invoiceId: data.invoice_id, invoiceNumber: data.invoice_number, total: data.total }); + // Add your business logic here + } + + async handleBillCreated(data) { + logger.info('Bill created', { billId: data.bill_id, billNumber: data.bill_number, total: data.total }); + // Add your business logic here + } + + async handleBillUpdated(data) { + logger.info('Bill updated', { billId: data.bill_id, billNumber: data.bill_number, total: data.total }); + // Add your business logic here + } + + async handleExpenseCreated(data) { + logger.info('Expense created', { expenseId: data.expense_id, amount: data.amount, description: data.description }); + // Add your business logic here + } + + async handleExpenseUpdated(data) { + logger.info('Expense updated', { expenseId: data.expense_id, amount: data.amount, description: data.description }); + // Add your business logic here + } } module.exports = ZohoHandler; diff --git a/src/integrations/zoho/mapper.js b/src/integrations/zoho/mapper.js index f842852..7c67829 100644 --- a/src/integrations/zoho/mapper.js +++ b/src/integrations/zoho/mapper.js @@ -54,61 +54,61 @@ class ZohoMapper { }; } - // Map Zoho CRM Sales Order to standardized format + // Map Zoho Sales Order to standardized format (handles both CRM and Books) static mapSalesOrder(zohoSalesOrder) { return { - id: zohoSalesOrder.id, - subject: zohoSalesOrder.Subject, - orderNumber: zohoSalesOrder.SO_Number, - account: zohoSalesOrder.Account_Name, - contact: zohoSalesOrder.Contact_Name, + id: zohoSalesOrder.id || zohoSalesOrder.salesorder_id, + subject: zohoSalesOrder.Subject || zohoSalesOrder.subject, + orderNumber: zohoSalesOrder.SO_Number || zohoSalesOrder.salesorder_number, + account: zohoSalesOrder.Account_Name || zohoSalesOrder.customer_name, + contact: zohoSalesOrder.Contact_Name || zohoSalesOrder.contact_name, deal: zohoSalesOrder.Deal_Name, - grandTotal: zohoSalesOrder.Grand_Total, - status: zohoSalesOrder.Status, - orderDate: zohoSalesOrder.Order_Date, - dueDate: zohoSalesOrder.Due_Date, - createdTime: zohoSalesOrder.Created_Time, - modifiedTime: zohoSalesOrder.Modified_Time, - owner: zohoSalesOrder.Owner?.name, + grandTotal: zohoSalesOrder.Grand_Total || zohoSalesOrder.total, + status: zohoSalesOrder.Status || zohoSalesOrder.status, + orderDate: zohoSalesOrder.Order_Date || zohoSalesOrder.date, + dueDate: zohoSalesOrder.Due_Date || zohoSalesOrder.due_date, + createdTime: zohoSalesOrder.Created_Time || zohoSalesOrder.created_time, + modifiedTime: zohoSalesOrder.Modified_Time || zohoSalesOrder.last_modified_time, + owner: zohoSalesOrder.Owner?.name || zohoSalesOrder.owner?.name, customFields: this.mapCustomFields(zohoSalesOrder) }; } - // Map Zoho CRM Purchase Order to standardized format + // Map Zoho Purchase Order to standardized format (handles both CRM and Books) static mapPurchaseOrder(zohoPurchaseOrder) { return { - id: zohoPurchaseOrder.id, - subject: zohoPurchaseOrder.Subject, - orderNumber: zohoPurchaseOrder.PO_Number, - vendor: zohoPurchaseOrder.Vendor_Name, - account: zohoPurchaseOrder.Account_Name, - grandTotal: zohoPurchaseOrder.Grand_Total, - status: zohoPurchaseOrder.Status, - orderDate: zohoPurchaseOrder.PO_Date, - dueDate: zohoPurchaseOrder.Expected_Delivery_Date, - createdTime: zohoPurchaseOrder.Created_Time, - modifiedTime: zohoPurchaseOrder.Modified_Time, - owner: zohoPurchaseOrder.Owner?.name, + id: zohoPurchaseOrder.id || zohoPurchaseOrder.purchaseorder_id, + subject: zohoPurchaseOrder.Subject || zohoPurchaseOrder.subject, + orderNumber: zohoPurchaseOrder.PO_Number || zohoPurchaseOrder.purchaseorder_number, + vendor: zohoPurchaseOrder.Vendor_Name || zohoPurchaseOrder.vendor_name, + account: zohoPurchaseOrder.Account_Name || zohoPurchaseOrder.customer_name, + grandTotal: zohoPurchaseOrder.Grand_Total || zohoPurchaseOrder.total, + status: zohoPurchaseOrder.Status || zohoPurchaseOrder.status, + orderDate: zohoPurchaseOrder.PO_Date || zohoPurchaseOrder.date, + dueDate: zohoPurchaseOrder.Expected_Delivery_Date || zohoPurchaseOrder.due_date, + createdTime: zohoPurchaseOrder.Created_Time || zohoPurchaseOrder.created_time, + modifiedTime: zohoPurchaseOrder.Modified_Time || zohoPurchaseOrder.last_modified_time, + owner: zohoPurchaseOrder.Owner?.name || zohoPurchaseOrder.owner?.name, customFields: this.mapCustomFields(zohoPurchaseOrder) }; } - // Map Zoho CRM Invoice to standardized format + // Map Zoho Invoice to standardized format (handles both CRM and Books) static mapInvoice(zohoInvoice) { return { - id: zohoInvoice.id, - subject: zohoInvoice.Subject, - invoiceNumber: zohoInvoice.Invoice_Number, - account: zohoInvoice.Account_Name, - contact: zohoInvoice.Contact_Name, + id: zohoInvoice.id || zohoInvoice.invoice_id, + subject: zohoInvoice.Subject || zohoInvoice.subject, + invoiceNumber: zohoInvoice.Invoice_Number || zohoInvoice.invoice_number, + account: zohoInvoice.Account_Name || zohoInvoice.customer_name, + contact: zohoInvoice.Contact_Name || zohoInvoice.contact_name, deal: zohoInvoice.Deal_Name, - grandTotal: zohoInvoice.Grand_Total, - status: zohoInvoice.Status, - invoiceDate: zohoInvoice.Invoice_Date, - dueDate: zohoInvoice.Due_Date, - createdTime: zohoInvoice.Created_Time, - modifiedTime: zohoInvoice.Modified_Time, - owner: zohoInvoice.Owner?.name, + grandTotal: zohoInvoice.Grand_Total || zohoInvoice.total, + status: zohoInvoice.Status || zohoInvoice.status, + invoiceDate: zohoInvoice.Invoice_Date || zohoInvoice.date, + dueDate: zohoInvoice.Due_Date || zohoInvoice.due_date, + createdTime: zohoInvoice.Created_Time || zohoInvoice.created_time, + modifiedTime: zohoInvoice.Modified_Time || zohoInvoice.last_modified_time, + owner: zohoInvoice.Owner?.name || zohoInvoice.owner?.name, customFields: this.mapCustomFields(zohoInvoice) }; } @@ -131,6 +131,310 @@ class ZohoMapper { }; } + // Map Zoho People Department to standardized format + static mapDepartment(zohoDepartment) { + return { + id: zohoDepartment.id, + name: zohoDepartment.name, + description: zohoDepartment.description, + head: zohoDepartment.head, + employeeCount: zohoDepartment.employeeCount, + customFields: this.mapCustomFields(zohoDepartment) + }; + } + + // Map Zoho People Timesheet to standardized format + static mapTimesheet(zohoTimesheet) { + return { + id: zohoTimesheet.id, + employeeId: zohoTimesheet.employeeId, + projectId: zohoTimesheet.projectId, + taskId: zohoTimesheet.taskId, + date: zohoTimesheet.date, + hours: zohoTimesheet.hours, + description: zohoTimesheet.description, + status: zohoTimesheet.status, + customFields: this.mapCustomFields(zohoTimesheet) + }; + } + + // Map Zoho People Leave Request to standardized format + static mapLeaveRequest(zohoLeaveRequest) { + return { + id: zohoLeaveRequest.id, + employeeId: zohoLeaveRequest.employeeId, + leaveType: zohoLeaveRequest.leaveType, + startDate: zohoLeaveRequest.startDate, + endDate: zohoLeaveRequest.endDate, + days: zohoLeaveRequest.days, + reason: zohoLeaveRequest.reason, + status: zohoLeaveRequest.status, + customFields: this.mapCustomFields(zohoLeaveRequest) + }; + } + + // Map Zoho People Attendance to standardized format + static mapAttendance(zohoAttendance) { + return { + id: zohoAttendance.id, + employeeId: zohoAttendance.employeeId, + date: zohoAttendance.date, + checkIn: zohoAttendance.checkIn, + checkOut: zohoAttendance.checkOut, + hoursWorked: zohoAttendance.hoursWorked, + status: zohoAttendance.status, + customFields: this.mapCustomFields(zohoAttendance) + }; + } + + // Map Zoho People Employee Forms to standardized format + static mapEmployeeForm(zohoEmployeeForm) { + return { + id: zohoEmployeeForm.id, + employeeId: zohoEmployeeForm.employeeId, + firstName: zohoEmployeeForm.firstName, + lastName: zohoEmployeeForm.lastName, + email: zohoEmployeeForm.email, + phone: zohoEmployeeForm.phone, + department: zohoEmployeeForm.department, + designation: zohoEmployeeForm.designation, + joiningDate: zohoEmployeeForm.joiningDate, + status: zohoEmployeeForm.status, + customFields: this.mapCustomFields(zohoEmployeeForm) + }; + } + + // Map Zoho People Attendance Entries to standardized format + static mapAttendanceEntry(zohoAttendanceEntry) { + return { + id: zohoAttendanceEntry.id, + employeeId: zohoAttendanceEntry.empId, + emailId: zohoAttendanceEntry.emailId, + date: zohoAttendanceEntry.date, + checkIn: zohoAttendanceEntry.checkIn, + checkOut: zohoAttendanceEntry.checkOut, + hoursWorked: zohoAttendanceEntry.hoursWorked, + status: zohoAttendanceEntry.status, + customFields: this.mapCustomFields(zohoAttendanceEntry) + }; + } + + // Map Zoho People Shift Configuration to standardized format + static mapShiftConfiguration(zohoShiftConfig) { + return { + id: zohoShiftConfig.id, + employeeId: zohoShiftConfig.empId, + shiftName: zohoShiftConfig.shiftName, + startTime: zohoShiftConfig.startTime, + endTime: zohoShiftConfig.endTime, + breakDuration: zohoShiftConfig.breakDuration, + workingDays: zohoShiftConfig.workingDays, + customFields: this.mapCustomFields(zohoShiftConfig) + }; + } + + // Map Zoho People Leave Data to standardized format + static mapLeaveData(zohoLeaveData) { + return { + id: zohoLeaveData.id, + employeeId: zohoLeaveData.employeeId, + leaveType: zohoLeaveData.leaveType, + startDate: zohoLeaveData.startDate, + endDate: zohoLeaveData.endDate, + days: zohoLeaveData.days, + reason: zohoLeaveData.reason, + status: zohoLeaveData.status, + customFields: this.mapCustomFields(zohoLeaveData) + }; + } + + // Map Zoho People Goals Data to standardized format + static mapGoalsData(zohoGoalsData) { + return { + id: zohoGoalsData.id, + employeeId: zohoGoalsData.employeeId, + goalTitle: zohoGoalsData.goalTitle, + description: zohoGoalsData.description, + targetValue: zohoGoalsData.targetValue, + currentValue: zohoGoalsData.currentValue, + startDate: zohoGoalsData.startDate, + endDate: zohoGoalsData.endDate, + status: zohoGoalsData.status, + customFields: this.mapCustomFields(zohoGoalsData) + }; + } + + // Map Zoho People Performance Data to standardized format + static mapPerformanceData(zohoPerformanceData) { + return { + id: zohoPerformanceData.id, + employeeId: zohoPerformanceData.employeeId, + reviewPeriod: zohoPerformanceData.reviewPeriod, + rating: zohoPerformanceData.rating, + comments: zohoPerformanceData.comments, + reviewer: zohoPerformanceData.reviewer, + reviewDate: zohoPerformanceData.reviewDate, + status: zohoPerformanceData.status, + customFields: this.mapCustomFields(zohoPerformanceData) + }; + } + + // Map Zoho Books Organization to standardized format + static mapOrganization(zohoOrganization) { + return { + id: zohoOrganization.organization_id, + name: zohoOrganization.organization_name, + currency: zohoOrganization.currency_code, + timezone: zohoOrganization.time_zone, + fiscalYearStart: zohoOrganization.fiscal_year_start_month, + accountCreatedDate: zohoOrganization.account_created_date, + customFields: this.mapCustomFields(zohoOrganization) + }; + } + + // Map Zoho Books Customer to standardized format + static mapCustomer(zohoCustomer) { + return { + id: zohoCustomer.contact_id, + name: zohoCustomer.contact_name, + email: zohoCustomer.contact_persons?.[0]?.email, + phone: zohoCustomer.contact_persons?.[0]?.phone, + company: zohoCustomer.company_name, + billingAddress: zohoCustomer.billing_address, + shippingAddress: zohoCustomer.shipping_address, + currency: zohoCustomer.currency_code, + paymentTerms: zohoCustomer.payment_terms, + customFields: this.mapCustomFields(zohoCustomer) + }; + } + + // Map Zoho Books Vendor to standardized format + static mapVendor(zohoVendor) { + return { + id: zohoVendor.contact_id, + name: zohoVendor.contact_name, + email: zohoVendor.contact_persons?.[0]?.email, + phone: zohoVendor.contact_persons?.[0]?.phone, + company: zohoVendor.company_name, + billingAddress: zohoVendor.billing_address, + shippingAddress: zohoVendor.shipping_address, + currency: zohoVendor.currency_code, + paymentTerms: zohoVendor.payment_terms, + customFields: this.mapCustomFields(zohoVendor) + }; + } + + // Map Zoho Books Item to standardized format + static mapItem(zohoItem) { + return { + id: zohoItem.item_id, + name: zohoItem.name, + description: zohoItem.description, + sku: zohoItem.sku, + unit: zohoItem.unit, + rate: zohoItem.rate, + purchaseRate: zohoItem.purchase_rate, + salesRate: zohoItem.sales_rate, + itemType: zohoItem.item_type, + status: zohoItem.status, + customFields: this.mapCustomFields(zohoItem) + }; + } + + // Map Zoho Books Invoice to standardized format + static mapBooksInvoice(zohoInvoice) { + return { + id: zohoInvoice.invoice_id, + invoiceNumber: zohoInvoice.invoice_number, + customerId: zohoInvoice.customer_id, + customerName: zohoInvoice.customer_name, + date: zohoInvoice.date, + dueDate: zohoInvoice.due_date, + total: zohoInvoice.total, + balance: zohoInvoice.balance, + status: zohoInvoice.status, + currency: zohoInvoice.currency_code, + customFields: this.mapCustomFields(zohoInvoice) + }; + } + + // Map Zoho Books Estimate to standardized format + static mapEstimate(zohoEstimate) { + return { + id: zohoEstimate.estimate_id, + estimateNumber: zohoEstimate.estimate_number, + customerId: zohoEstimate.customer_id, + customerName: zohoEstimate.customer_name, + date: zohoEstimate.date, + expiryDate: zohoEstimate.expiry_date, + total: zohoEstimate.total, + status: zohoEstimate.status, + currency: zohoEstimate.currency_code, + customFields: this.mapCustomFields(zohoEstimate) + }; + } + + // Map Zoho Books Bill to standardized format + static mapBill(zohoBill) { + return { + id: zohoBill.bill_id, + billNumber: zohoBill.bill_number, + vendorId: zohoBill.vendor_id, + vendorName: zohoBill.vendor_name, + date: zohoBill.date, + dueDate: zohoBill.due_date, + total: zohoBill.total, + balance: zohoBill.balance, + status: zohoBill.status, + currency: zohoBill.currency_code, + customFields: this.mapCustomFields(zohoBill) + }; + } + + // Map Zoho Books Expense to standardized format + static mapExpense(zohoExpense) { + return { + id: zohoExpense.expense_id, + expenseNumber: zohoExpense.expense_number, + accountId: zohoExpense.account_id, + accountName: zohoExpense.account_name, + date: zohoExpense.date, + amount: zohoExpense.amount, + description: zohoExpense.description, + status: zohoExpense.status, + currency: zohoExpense.currency_code, + customFields: this.mapCustomFields(zohoExpense) + }; + } + + // Map Zoho Books Bank Account to standardized format + static mapBankAccount(zohoBankAccount) { + return { + id: zohoBankAccount.account_id, + accountName: zohoBankAccount.account_name, + accountNumber: zohoBankAccount.account_number, + bankName: zohoBankAccount.bank_name, + accountType: zohoBankAccount.account_type, + balance: zohoBankAccount.balance, + currency: zohoBankAccount.currency_code, + customFields: this.mapCustomFields(zohoBankAccount) + }; + } + + // Map Zoho Books Bank Transaction to standardized format + static mapBankTransaction(zohoBankTransaction) { + return { + id: zohoBankTransaction.transaction_id, + accountId: zohoBankTransaction.account_id, + date: zohoBankTransaction.date, + amount: zohoBankTransaction.amount, + description: zohoBankTransaction.description, + type: zohoBankTransaction.type, + status: zohoBankTransaction.status, + customFields: this.mapCustomFields(zohoBankTransaction) + }; + } + // Map Zoho Projects Project to standardized format static mapProject(zohoProject) { return { @@ -176,13 +480,37 @@ class ZohoMapper { // Map multiple records using appropriate mapper static mapRecords(records, recordType) { const mapperMap = { + // CRM 'leads': this.mapLead, 'contacts': this.mapContact, 'deals': this.mapDeal, 'sales_orders': this.mapSalesOrder, 'purchase_orders': this.mapPurchaseOrder, 'invoices': this.mapInvoice, + // People 'employees': this.mapEmployee, + 'departments': this.mapDepartment, + 'timesheets': this.mapTimesheet, + 'leave_requests': this.mapLeaveRequest, + 'attendance': this.mapAttendance, + 'employee_forms': this.mapEmployeeForm, + 'attendance_entries': this.mapAttendanceEntry, + 'shift_configuration': this.mapShiftConfiguration, + 'leave_data': this.mapLeaveData, + 'goals_data': this.mapGoalsData, + 'performance_data': this.mapPerformanceData, + // Books + 'organizations': this.mapOrganization, + 'customers': this.mapCustomer, + 'vendors': this.mapVendor, + 'items': this.mapItem, + 'invoices': this.mapBooksInvoice, + 'estimates': this.mapEstimate, + 'bills': this.mapBill, + 'expenses': this.mapExpense, + 'bank_accounts': this.mapBankAccount, + 'bank_transactions': this.mapBankTransaction, + // Projects 'projects': this.mapProject, 'tasks': this.mapTask }; @@ -288,10 +616,153 @@ class ZohoMapper { }; break; - case 'sales_orders': - case 'purchase_orders': - case 'invoices': - // CRM response structure for sales orders, purchase orders, and invoices + + + case 'invoices': + // Handle both CRM and Books invoices - check response structure + if (zohoResponse.data && zohoResponse.info) { + // Books response structure + records = zohoResponse.data || []; + pageInfo = { + count: zohoResponse.info?.count || records.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + }; + } else { + // CRM response structure + records = zohoResponse.invoices || []; + pageInfo = { + count: zohoResponse.page_context?.count || records.length, + moreRecords: zohoResponse.page_context?.more_records || false, + page: zohoResponse.page_context?.page || 1 + }; + } + break; + + case 'contacts': + // Books response structure for contacts + records = zohoResponse.contacts || []; + pageInfo = { + count: zohoResponse.page_context?.count || records.length, + moreRecords: zohoResponse.page_context?.more_records || false, + page: zohoResponse.page_context?.page || 1 + }; + break; + + case 'vendors': + // Books response structure for vendors (filtered contacts) + records = zohoResponse.contacts || []; + pageInfo = { + count: zohoResponse.page_context?.count || records.length, + moreRecords: zohoResponse.page_context?.more_records || false, + page: zohoResponse.page_context?.page || 1 + }; + break; + + case 'sales_orders': + // Handle both CRM and Books sales orders - check response structure + if (zohoResponse.data && zohoResponse.info) { + // Books response structure + records = zohoResponse.data || []; + pageInfo = { + count: zohoResponse.info?.count || records.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + }; + } else { + // CRM response structure + records = zohoResponse.salesorders || []; + pageInfo = { + count: zohoResponse.page_context?.count || records.length, + moreRecords: zohoResponse.page_context?.more_records || false, + page: zohoResponse.page_context?.page || 1 + }; + } + break; + + case 'purchase_orders': + // Handle both CRM and Books purchase orders - check response structure + if (zohoResponse.data && zohoResponse.info) { + // Books response structure + records = zohoResponse.data || []; + pageInfo = { + count: zohoResponse.info?.count || records.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + }; + } else { + // CRM response structure + records = zohoResponse.purchaseorders || []; + pageInfo = { + count: zohoResponse.page_context?.count || records.length, + moreRecords: zohoResponse.page_context?.more_records || false, + page: zohoResponse.page_context?.page || 1 + }; + } + break; + + case 'employee_forms': + // Handle both CRM and Books sales orders - check response structure + + // Books response structure + records = zohoResponse.response.result || []; + pageInfo = { + count: zohoResponse.response.result?.count || records.length, + moreRecords: zohoResponse.response.result?.more_records || false, + page: zohoResponse.response.result?.page || 1 + }; + + + break; + + case 'departments': + case 'timesheets': + case 'leave_requests': + case 'attendance': + // People response structure + records = zohoResponse.data || []; + pageInfo = { + count: zohoResponse.info?.count || records.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + }; + break; + + case 'employee_forms': + case 'attendance_entries': + case 'shift_configuration': + case 'leave_data': + case 'goals_data': + case 'performance_data': + // People Forms API response structure + records = zohoResponse.data || []; + pageInfo = { + count: zohoResponse.info?.count || records.length, + moreRecords: zohoResponse.info?.more_records || false, + page: zohoResponse.info?.page || 1 + }; + break; + + case 'employee_detail': + // Single employee detail response + records = zohoResponse.data ? [zohoResponse.data] : []; + pageInfo = { + count: records.length, + moreRecords: false, + page: 1 + }; + break; + + case 'organizations': + case 'customers': + case 'items': + case 'estimates': + case 'bills': + case 'expenses': + case 'bank_accounts': + case 'bank_transactions': + case 'reports': + // Books response structure records = zohoResponse.data || []; pageInfo = { count: zohoResponse.info?.count || records.length, diff --git a/src/services/integration/integrationService.js b/src/services/integration/integrationService.js index 40f8503..be7972f 100644 --- a/src/services/integration/integrationService.js +++ b/src/services/integration/integrationService.js @@ -40,13 +40,33 @@ class IntegrationService { getMethodName(service, resource) { const resourceMap = { + // CRM 'leads': 'Leads', 'contacts': 'Contacts', 'deals': 'Deals', + 'sales_orders': 'SalesOrders', + 'purchase_orders': 'PurchaseOrders', + 'invoices': 'Invoices', + // People 'employees': 'Employees', + 'departments': 'Departments', + 'timesheets': 'Timesheets', + 'leave_requests': 'LeaveRequests', + 'attendance': 'Attendance', + // Books + 'organizations': 'Organizations', + 'customers': 'Customers', + 'vendors': 'Vendors', + 'items': 'Items', + 'estimates': 'Estimates', + 'bills': 'Bills', + 'expenses': 'Expenses', + 'bank_accounts': 'BankAccounts', + 'bank_transactions': 'BankTransactions', + 'reports': 'Reports', + // Projects 'projects': 'Projects', - 'tasks': 'Tasks', - 'timesheets': 'Timesheets' + 'tasks': 'Tasks' }; const resourceName = resourceMap[resource] || resource; @@ -167,6 +187,262 @@ class IntegrationService { } return await client.getInvoices(params); } + + // Zoho People specific methods + async getDepartments(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Departments are only available for Zoho provider'); + } + return await client.getDepartments(params); + } + + async getLeaveRequests(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Leave Requests are only available for Zoho provider'); + } + return await client.getLeaveRequests(params); + } + + async getAttendance(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Attendance is only available for Zoho provider'); + } + return await client.getAttendance(params); + } + + // Zoho Books specific methods + async getOrganizations(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Organizations are only available for Zoho provider'); + } + return await client.getOrganizations(params); + } + + async getCustomers(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Customers are only available for Zoho provider'); + } + return await client.getCustomers(params); + } + + async getVendors(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Vendors are only available for Zoho provider'); + } + return await client.getVendors(params); + } + + async getItems(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Items are only available for Zoho provider'); + } + return await client.getItems(params); + } + + async getEstimates(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Estimates are only available for Zoho provider'); + } + return await client.getEstimates(params); + } + + async getBills(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Bills are only available for Zoho provider'); + } + return await client.getBills(params); + } + + async getExpenses(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Expenses are only available for Zoho provider'); + } + return await client.getExpenses(params); + } + + async getBankAccounts(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Bank Accounts are only available for Zoho provider'); + } + return await client.getBankAccounts(params); + } + + async getBankTransactions(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Bank Transactions are only available for Zoho provider'); + } + return await client.getBankTransactions(params); + } + + async getReports(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Reports are only available for Zoho provider'); + } + return await client.getReports(params); + } + + async getBooksSalesOrders(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Books Sales Orders are only available for Zoho provider'); + } + return await client.getBooksSalesOrders(params); + } + + async getBooksPurchaseOrders(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Books Purchase Orders are only available for Zoho provider'); + } + return await client.getBooksPurchaseOrders(params); + } + + async getContacts(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Contacts are only available for Zoho provider'); + } + return await client.getContacts(params); + } + + // Zoho People Forms API methods + async getEmployeeForms(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Employee Forms are only available for Zoho provider'); + } + return await client.getEmployeeForms(params); + } + + async getEmployeeById(provider, recordId, formLinkName = 'employee') { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Employee Details are only available for Zoho provider'); + } + return await client.getEmployeeById(recordId, formLinkName); + } + + async getAttendanceEntries(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Attendance Entries are only available for Zoho provider'); + } + return await client.getAttendanceEntries(params); + } + + async getShiftConfiguration(provider, params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Shift Configuration is only available for Zoho provider'); + } + return await client.getShiftConfiguration(params); + } + + async getLeaveData(provider, formLinkName = 'leave', params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Leave Data is only available for Zoho provider'); + } + return await client.getLeaveData(formLinkName, params); + } + + async getGoalsData(provider, formLinkName = 'goals', params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Goals Data is only available for Zoho provider'); + } + return await client.getGoalsData(formLinkName, params); + } + + async getPerformanceData(provider, formLinkName = 'performance', params = {}) { + const client = this.clients[provider]; + if (!client) { + throw new Error(`Provider ${provider} not supported`); + } + if (provider !== 'zoho') { + throw new Error('Performance Data is only available for Zoho provider'); + } + return await client.getPerformanceData(formLinkName, params); + } } module.exports = IntegrationService;