From 7352fec2437515488336e2a06a77e17c5f551ce0 Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Fri, 26 Sep 2025 17:13:57 +0530 Subject: [PATCH] bulk read flow added and will trigger after first time / re-auth authetication --- src/api/controllers/integrationController.js | 20 +- src/api/controllers/reportsController.js | 448 ++++++++++++++++++ src/api/routes/integrationRoutes.js | 9 +- .../repositories/zohoBulkReadRepository.js | 41 +- src/integrations/zoho/client.js | 55 ++- src/integrations/zoho/handler.js | 52 +- 6 files changed, 604 insertions(+), 21 deletions(-) diff --git a/src/api/controllers/integrationController.js b/src/api/controllers/integrationController.js index 8233040..cfe1c0d 100644 --- a/src/api/controllers/integrationController.js +++ b/src/api/controllers/integrationController.js @@ -1091,6 +1091,24 @@ const scheduleBulkReadJobs = async (req, res) => { } } +async function getCrmCounts(req, res) { + try { + const { provider } = req.query; + + if (provider !== 'zoho') { + return res.status(400).json(failure('Only Zoho provider is supported for CRM counts', 'UNSUPPORTED_PROVIDER')); + } + + const zohoClient = new ZohoClient(req.user.uuid); + const counts = await zohoClient.getCrmCounts(); + + res.json(success('Zoho CRM module counts retrieved successfully', counts)); + } catch (error) { + console.error('Error getting CRM counts:', error); + res.status(500).json(failure(error.message, 'CRM_COUNTS_ERROR')); + } +} + module.exports = { getData, getServices, getResources, getPortals, getAllProjects, getAllProjectTasks, getAllProjectTaskLists, getAllProjectIssues, getAllProjectPhases, getSalesOrders, @@ -1099,5 +1117,5 @@ module.exports = { getExpenses, getBankAccounts, getBankTransactions, getReports, getBooksSalesOrders, getBooksPurchaseOrders, getContacts, getBooksContacts, getBooksInvoices, getEmployeeForms, getEmployeeById, getAttendanceEntries, getShiftConfiguration, getLeaveData, getGoalsData, getPerformanceData, - getUserReport, getLeaveTrackerReport, getHolidays, scheduleBulkReadJobs + getUserReport, getLeaveTrackerReport, getHolidays, scheduleBulkReadJobs, getCrmCounts }; diff --git a/src/api/controllers/reportsController.js b/src/api/controllers/reportsController.js index 82755b8..0eaf2f0 100644 --- a/src/api/controllers/reportsController.js +++ b/src/api/controllers/reportsController.js @@ -96,6 +96,22 @@ class ReportsController { purchaseOrders: purchaseOrdersData }); + // Add advanced financial KPIs + const advancedKPIs = this.calculateAdvancedFinancialKPIs({ + leads: leadsData, + tasks: tasksData, + contacts: contactsData, + accounts: accountsData, + deals: dealsData, + vendors: vendorsData, + invoices: invoicesData, + salesOrders: salesOrdersData, + purchaseOrders: purchaseOrdersData + }, dateFilter); + + // Merge advanced KPIs with existing KPIs + kpis.advancedFinancialKPIs = advancedKPIs; + return res.json(success('Comprehensive CRM KPIs retrieved successfully', kpis)); } catch (error) { console.error('Error fetching CRM KPIs:', error); @@ -811,6 +827,438 @@ class ReportsController { return {}; } + + /** + * Calculate advanced financial KPIs for comprehensive business analysis + */ + calculateAdvancedFinancialKPIs(data, dateFilter) { + const now = new Date(); + const oneYearAgo = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000); + const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + // Filter data based on date range + const filterByDate = (items, dateField = 'created_time') => { + if (!dateFilter || !dateFilter[dateField]) return items; + return items.filter(item => { + const itemDate = new Date(item[dateField]); + return itemDate >= dateFilter[dateField][Op.gte]; + }); + }; + + const filteredDeals = filterByDate(data.deals); + const filteredInvoices = filterByDate(data.invoices); + const filteredSalesOrders = filterByDate(data.salesOrders); + const filteredPurchaseOrders = filterByDate(data.purchaseOrders); + const filteredLeads = filterByDate(data.leads); + const filteredContacts = filterByDate(data.contacts); + const filteredTasks = filterByDate(data.tasks); + + // 1. Revenue Growth Rate (#1) + const revenueGrowthRate = this.calculateRevenueGrowthRate(filteredInvoices, filteredSalesOrders, oneYearAgo, sixMonthsAgo); + + // 2. Gross Margin (#2) + const grossMargin = this.calculateGrossMargin(filteredSalesOrders, filteredPurchaseOrders); + + // 3. Net Profit Margin (#3) + const netProfitMargin = this.calculateNetProfitMargin(filteredInvoices, filteredSalesOrders, filteredPurchaseOrders); + + // 4. Operating Cash Flow (#4) + const operatingCashFlow = this.calculateOperatingCashFlow(filteredInvoices, filteredPurchaseOrders); + + // 5. Cash Runway (#5) + const cashRunway = this.calculateCashRunway(filteredInvoices, filteredPurchaseOrders); + + // 6. Customer Acquisition Cost (CAC) (#6) + const customerAcquisitionCost = this.calculateCustomerAcquisitionCost(filteredLeads, filteredContacts, filteredTasks); + + // 7. Customer Lifetime Value (CLV) (#7) + const customerLifetimeValue = this.calculateCustomerLifetimeValue(filteredDeals, filteredInvoices, filteredContacts); + + // 8. LTV-to-CAC Ratio (#8) + const ltvToCacRatio = this.calculateLTVToCACRatio(customerLifetimeValue, customerAcquisitionCost); + + // 9. Net Revenue Retention (#9) + const netRevenueRetention = this.calculateNetRevenueRetention(filteredDeals, filteredInvoices, filteredContacts, oneYearAgo); + + // 10. Churn Rate (#10) + const churnRate = this.calculateChurnRate(filteredContacts, filteredDeals, oneYearAgo); + + // 11. Average Revenue Per Account (ARPA) (#11) + const averageRevenuePerAccount = this.calculateAverageRevenuePerAccount(filteredInvoices, filteredDeals, filteredContacts); + + // 12. Burn Multiple (#12) + const burnMultiple = this.calculateBurnMultiple(filteredPurchaseOrders, filteredInvoices, filteredSalesOrders); + + // 13. Sales Cycle Length (#13) + const salesCycleLength = this.calculateSalesCycleLength(filteredLeads, filteredTasks, filteredDeals); + + // 15. Net Promoter Score (NPS) (#15) - Placeholder as it requires customer feedback + const netPromoterScore = this.calculateNetPromoterScore(filteredContacts, filteredDeals); + + // 16. Days Sales Outstanding (DSO) (#16) + const daysSalesOutstanding = this.calculateDaysSalesOutstanding(filteredInvoices); + + // 17. Growth Efficiency Ratio (#17) + const growthEfficiencyRatio = this.calculateGrowthEfficiencyRatio(filteredDeals, filteredSalesOrders, filteredContacts, oneYearAgo); + + // 18. EBITDA (#18) - Simplified calculation + const ebitda = this.calculateEBITDA(filteredInvoices, filteredSalesOrders, filteredPurchaseOrders); + + return { + revenueGrowthRate, + grossMargin, + netProfitMargin, + operatingCashFlow, + cashRunway, + customerAcquisitionCost, + customerLifetimeValue, + ltvToCacRatio, + netRevenueRetention, + churnRate, + averageRevenuePerAccount, + burnMultiple, + salesCycleLength, + netPromoterScore, + daysSalesOutstanding, + growthEfficiencyRatio, + ebitda + }; + } + + // Individual KPI calculation methods + + calculateRevenueGrowthRate(invoices, salesOrders, oneYearAgo, sixMonthsAgo) { + const currentPeriodRevenue = this.getTotalRevenue(invoices, salesOrders, sixMonthsAgo); + const previousPeriodRevenue = this.getTotalRevenue(invoices, salesOrders, oneYearAgo, sixMonthsAgo); + + if (previousPeriodRevenue === 0) return { value: 0, percentage: '0%', trend: 'stable' }; + + const growthRate = ((currentPeriodRevenue - previousPeriodRevenue) / previousPeriodRevenue) * 100; + return { + value: growthRate, + percentage: `${growthRate.toFixed(2)}%`, + trend: growthRate > 0 ? 'growing' : growthRate < 0 ? 'declining' : 'stable', + currentPeriodRevenue, + previousPeriodRevenue + }; + } + + calculateGrossMargin(salesOrders, purchaseOrders) { + const totalRevenue = salesOrders.reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + const totalCOGS = purchaseOrders.reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + + if (totalRevenue === 0) return { value: 0, percentage: '0%' }; + + const grossMargin = ((totalRevenue - totalCOGS) / totalRevenue) * 100; + return { + value: grossMargin, + percentage: `${grossMargin.toFixed(2)}%`, + totalRevenue, + totalCOGS, + grossProfit: totalRevenue - totalCOGS + }; + } + + calculateNetProfitMargin(invoices, salesOrders, purchaseOrders) { + const totalRevenue = this.getTotalRevenue(invoices, salesOrders); + const totalCosts = purchaseOrders.reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + + if (totalRevenue === 0) return { value: 0, percentage: '0%' }; + + const netProfitMargin = ((totalRevenue - totalCosts) / totalRevenue) * 100; + return { + value: netProfitMargin, + percentage: `${netProfitMargin.toFixed(2)}%`, + totalRevenue, + totalCosts, + netProfit: totalRevenue - totalCosts + }; + } + + calculateOperatingCashFlow(invoices, purchaseOrders) { + const cashIn = invoices + .filter(invoice => invoice.status && invoice.status.toLowerCase().includes('paid')) + .reduce((sum, invoice) => sum + (parseFloat(invoice.total) || 0), 0); + + const cashOut = purchaseOrders + .filter(order => order.status && order.status.toLowerCase().includes('paid')) + .reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + + return { + cashIn, + cashOut, + netCashFlow: cashIn - cashOut, + cashFlowRatio: cashOut > 0 ? (cashIn / cashOut).toFixed(2) : 'N/A' + }; + } + + calculateCashRunway(invoices, purchaseOrders) { + const monthlyCashIn = this.getMonthlyAverage(invoices, 'total'); + const monthlyCashOut = this.getMonthlyAverage(purchaseOrders, 'total'); + + if (monthlyCashOut === 0) return { months: 'Infinite', status: 'positive' }; + + const runway = monthlyCashIn / monthlyCashOut; + return { + months: runway.toFixed(1), + status: runway > 12 ? 'healthy' : runway > 6 ? 'moderate' : 'critical', + monthlyCashIn, + monthlyCashOut + }; + } + + calculateCustomerAcquisitionCost(leads, contacts, tasks) { + const marketingSpend = tasks + .filter(task => task.subject && task.subject.toLowerCase().includes('marketing')) + .length * 100; // Assuming $100 per marketing task + + const newCustomers = contacts.filter(contact => { + const contactDate = new Date(contact.created_time); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + return contactDate >= thirtyDaysAgo; + }).length; + + if (newCustomers === 0) return { value: 0, status: 'no_new_customers' }; + + const cac = marketingSpend / newCustomers; + return { + value: cac, + marketingSpend, + newCustomers, + status: cac < 100 ? 'efficient' : cac < 500 ? 'moderate' : 'expensive' + }; + } + + calculateCustomerLifetimeValue(deals, invoices, contacts) { + const totalRevenue = this.getTotalRevenue(invoices, []); + const totalCustomers = contacts.length; + + if (totalCustomers === 0) return { value: 0, status: 'no_customers' }; + + const clv = totalRevenue / totalCustomers; + return { + value: clv, + totalRevenue, + totalCustomers, + status: clv > 1000 ? 'high_value' : clv > 500 ? 'medium_value' : 'low_value' + }; + } + + calculateLTVToCACRatio(clv, cac) { + if (cac.value === 0) return { ratio: 'N/A', status: 'no_cac_data' }; + + const ratio = clv.value / cac.value; + return { + ratio: ratio.toFixed(2), + clv: clv.value, + cac: cac.value, + status: ratio > 3 ? 'excellent' : ratio > 1 ? 'good' : 'poor' + }; + } + + calculateNetRevenueRetention(deals, invoices, contacts, oneYearAgo) { + const currentCustomers = contacts.length; + const recurringRevenue = invoices + .filter(invoice => { + const invoiceDate = new Date(invoice.created_time); + return invoiceDate >= oneYearAgo; + }) + .reduce((sum, invoice) => sum + (parseFloat(invoice.total) || 0), 0); + + if (currentCustomers === 0) return { percentage: '0%', status: 'no_customers' }; + + const nrr = (recurringRevenue / currentCustomers) * 100; + return { + percentage: `${nrr.toFixed(2)}%`, + recurringRevenue, + currentCustomers, + status: nrr > 100 ? 'growing' : nrr > 90 ? 'stable' : 'declining' + }; + } + + calculateChurnRate(contacts, deals, oneYearAgo) { + const totalCustomers = contacts.length; + const lostCustomers = deals + .filter(deal => { + const dealDate = new Date(deal.created_time); + return dealDate >= oneYearAgo && deal.stage && deal.stage.toLowerCase().includes('lost'); + }).length; + + if (totalCustomers === 0) return { percentage: '0%', status: 'no_customers' }; + + const churnRate = (lostCustomers / totalCustomers) * 100; + return { + percentage: `${churnRate.toFixed(2)}%`, + lostCustomers, + totalCustomers, + status: churnRate < 5 ? 'excellent' : churnRate < 10 ? 'good' : 'needs_attention' + }; + } + + calculateAverageRevenuePerAccount(invoices, deals, contacts) { + const totalRevenue = this.getTotalRevenue(invoices, []); + const totalAccounts = contacts.length; + + if (totalAccounts === 0) return { value: 0, status: 'no_accounts' }; + + const arpa = totalRevenue / totalAccounts; + return { + value: arpa, + totalRevenue, + totalAccounts, + status: arpa > 1000 ? 'high' : arpa > 500 ? 'medium' : 'low' + }; + } + + calculateBurnMultiple(purchaseOrders, invoices, salesOrders) { + const totalBurn = purchaseOrders.reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + const newARR = this.getTotalRevenue(invoices, salesOrders); + + if (newARR === 0) return { multiple: 'N/A', status: 'no_revenue' }; + + const multiple = totalBurn / newARR; + return { + multiple: multiple.toFixed(2), + totalBurn, + newARR, + status: multiple < 1 ? 'efficient' : multiple < 2 ? 'moderate' : 'inefficient' + }; + } + + calculateSalesCycleLength(leads, tasks, deals) { + const cycleLengths = []; + + deals.forEach(deal => { + const dealDate = new Date(deal.created_time); + const relatedLeads = leads.filter(lead => + lead.company === deal.account_name || + lead.email === deal.contact_name + ); + + if (relatedLeads.length > 0) { + const leadDate = new Date(relatedLeads[0].created_time); + const cycleLength = Math.ceil((dealDate - leadDate) / (1000 * 60 * 60 * 24)); + if (cycleLength > 0) cycleLengths.push(cycleLength); + } + }); + + if (cycleLengths.length === 0) return { averageDays: 0, status: 'no_data' }; + + const averageDays = cycleLengths.reduce((sum, days) => sum + days, 0) / cycleLengths.length; + return { + averageDays: Math.round(averageDays), + totalCycles: cycleLengths.length, + status: averageDays < 30 ? 'fast' : averageDays < 90 ? 'moderate' : 'slow' + }; + } + + calculateNetPromoterScore(contacts, deals) { + // Placeholder implementation - would need customer feedback data + const totalCustomers = contacts.length; + const promoters = deals + .filter(deal => deal.stage && deal.stage.toLowerCase().includes('closed won')) + .length; + + if (totalCustomers === 0) return { score: 0, status: 'no_customers' }; + + const nps = (promoters / totalCustomers) * 100; + return { + score: Math.round(nps), + promoters, + totalCustomers, + status: nps > 50 ? 'excellent' : nps > 0 ? 'good' : 'needs_improvement', + note: 'Based on deal closure rate - actual NPS requires customer surveys' + }; + } + + calculateDaysSalesOutstanding(invoices) { + const unpaidInvoices = invoices.filter(invoice => + invoice.status && !invoice.status.toLowerCase().includes('paid') + ); + + if (unpaidInvoices.length === 0) return { days: 0, status: 'no_outstanding' }; + + const totalOutstanding = unpaidInvoices.reduce((sum, invoice) => + sum + (parseFloat(invoice.total) || 0), 0); + + const averageDailySales = this.getTotalRevenue(invoices, []) / 365; + + if (averageDailySales === 0) return { days: 0, status: 'no_sales' }; + + const dso = totalOutstanding / averageDailySales; + return { + days: Math.round(dso), + totalOutstanding, + averageDailySales, + status: dso < 30 ? 'excellent' : dso < 60 ? 'good' : 'needs_attention' + }; + } + + calculateGrowthEfficiencyRatio(deals, salesOrders, contacts, oneYearAgo) { + const newRevenue = this.getTotalRevenue([], salesOrders, oneYearAgo); + const churnedCustomers = contacts.filter(contact => { + const contactDate = new Date(contact.created_time); + return contactDate < oneYearAgo; + }).length; + + const churnCost = churnedCustomers * 100; // Assuming $100 cost per churned customer + + if (churnCost === 0) return { ratio: 'N/A', status: 'no_churn' }; + + const ratio = newRevenue / churnCost; + return { + ratio: ratio.toFixed(2), + newRevenue, + churnCost, + churnedCustomers, + status: ratio > 2 ? 'efficient' : ratio > 1 ? 'moderate' : 'inefficient' + }; + } + + calculateEBITDA(invoices, salesOrders, purchaseOrders) { + const totalRevenue = this.getTotalRevenue(invoices, salesOrders); + const totalCosts = purchaseOrders.reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + + // Simplified EBITDA calculation (would need more detailed financial data) + const ebitda = totalRevenue - totalCosts; + + return { + value: ebitda, + totalRevenue, + totalCosts, + margin: totalRevenue > 0 ? ((ebitda / totalRevenue) * 100).toFixed(2) + '%' : '0%', + status: ebitda > 0 ? 'profitable' : 'loss' + }; + } + + // Helper methods + getTotalRevenue(invoices, salesOrders, startDate = null, endDate = null) { + let total = 0; + + const filterByDate = (items) => { + if (!startDate) return items; + return items.filter(item => { + const itemDate = new Date(item.created_time); + return itemDate >= startDate && (!endDate || itemDate < endDate); + }); + }; + + total += filterByDate(invoices).reduce((sum, invoice) => sum + (parseFloat(invoice.total) || 0), 0); + total += filterByDate(salesOrders).reduce((sum, order) => sum + (parseFloat(order.total) || 0), 0); + + return total; + } + + getMonthlyAverage(items, field) { + if (items.length === 0) return 0; + + const total = items.reduce((sum, item) => sum + (parseFloat(item[field]) || 0), 0); + const months = 12; // Assuming 12 months of data + + return total / months; + } } const reportsController = new ReportsController(); diff --git a/src/api/routes/integrationRoutes.js b/src/api/routes/integrationRoutes.js index 5742b43..c1a0c64 100644 --- a/src/api/routes/integrationRoutes.js +++ b/src/api/routes/integrationRoutes.js @@ -8,7 +8,7 @@ const { getExpenses, getBankAccounts, getBankTransactions, getReports, getBooksSalesOrders, getBooksPurchaseOrders, getContacts, getBooksContacts, getBooksInvoices, getEmployeeForms, getEmployeeById, getAttendanceEntries, getShiftConfiguration, getLeaveData, getGoalsData, getPerformanceData, - getUserReport, getLeaveTrackerReport, getHolidays, scheduleBulkReadJobs + getUserReport, getLeaveTrackerReport, getHolidays, scheduleBulkReadJobs, getCrmCounts } = require('../controllers/integrationController'); const auth = require('../middlewares/auth'); const ZohoHandler = require('../../integrations/zoho/handler'); @@ -158,6 +158,13 @@ const invoicesSchema = Joi.object({ router.get('/zoho/crm/invoices', auth, validate(invoicesSchema), getInvoices); router.get('/zoho/crm/contacts', auth, validate(invoicesSchema), getContacts); +// Get Zoho CRM counts for all modules +const crmCountsSchema = Joi.object({ + provider: Joi.string().valid('zoho').required() +}); + +router.get('/zoho/crm/counts', auth, validate(crmCountsSchema), getCrmCounts); + // Zoho People specific routes const departmentsSchema = Joi.object({ provider: Joi.string().valid('zoho').required(), diff --git a/src/data/repositories/zohoBulkReadRepository.js b/src/data/repositories/zohoBulkReadRepository.js index bb807e4..40e83da 100644 --- a/src/data/repositories/zohoBulkReadRepository.js +++ b/src/data/repositories/zohoBulkReadRepository.js @@ -41,15 +41,17 @@ class ZohoBulkReadRepository { * Bulk insert data for a specific module * @param {string} module - Module name * @param {Array} data - Array of records to insert + * @param {Object} transaction - Sequelize transaction (optional) * @returns {Promise} Inserted records */ - async bulkInsert(module, data) { + async bulkInsert(module, data, transaction = null) { try { const model = this.getModel(module); console.log(`💾 Bulk inserting ${data.length} records for ${module}`); const result = await model.bulkCreate(data, { - validate: true + validate: true, + transaction }); console.log(`✅ Successfully inserted ${result.length} records for ${module}`); @@ -64,20 +66,39 @@ class ZohoBulkReadRepository { * Clear existing data for a user and module * @param {string} userId - User UUID * @param {string} module - Module name - * @param {string} jobId - Bulk job ID (optional, used for logging) + * @param {Object} transaction - Sequelize transaction (optional) * @returns {Promise} Number of deleted records */ - async clearUserData(userId, module, jobId = null) { + async clearUserData(userId, module, transaction = null) { try { const model = this.getModel(module); - console.log(`🗑️ Clearing ALL existing data for user ${userId}, module ${module}${jobId ? ` (new job: ${jobId})` : ''}`); + console.log(`🗑️ Clearing ALL existing data for user ${userId}, module ${module}`); + + // First, let's see what records exist before deletion + const existingRecords = await model.findAll({ + where: { + user_uuid: userId, + provider: 'zoho' + }, + attributes: ['internal_id', 'zoho_id', 'user_uuid', 'provider', 'bulk_job_id'], + transaction + }); + + console.log(`🔍 Found ${existingRecords.length} existing records to delete:`); + existingRecords.slice(0, 5).forEach(record => { + console.log(` - ID: ${record.internal_id}, ZohoID: ${record.zoho_id}, JobID: ${record.bulk_job_id}`); + }); + if (existingRecords.length > 5) { + console.log(` ... and ${existingRecords.length - 5} more records`); + } const result = await model.destroy({ where: { user_uuid: userId, provider: 'zoho' - // Removed bulk_job_id filter to clear ALL data for the user and module - } + // NOTE: Intentionally NOT filtering by job_id to clear ALL data for user+module + }, + transaction }); console.log(`✅ Cleared ${result} existing records for ${module}`); @@ -121,9 +142,10 @@ class ZohoBulkReadRepository { * Get count of records for a user and module * @param {string} userId - User UUID * @param {string} module - Module name + * @param {Object} transaction - Sequelize transaction (optional) * @returns {Promise} Record count */ - async getUserDataCount(userId, module) { + async getUserDataCount(userId, module, transaction = null) { try { const model = this.getModel(module); @@ -131,7 +153,8 @@ class ZohoBulkReadRepository { where: { user_uuid: userId, provider: 'zoho' - } + }, + transaction }); return count; diff --git a/src/integrations/zoho/client.js b/src/integrations/zoho/client.js index e4e6ca1..fe8aa19 100644 --- a/src/integrations/zoho/client.js +++ b/src/integrations/zoho/client.js @@ -51,10 +51,8 @@ class ZohoClient { try { const response = await axios(url, config); - return response.data; } catch (error) { - // console.log('error in makeRequest',JSON.stringify(error)) if (error.response?.status === 401) { await this.refreshToken(); const newTokens = await this.getTokens(); @@ -130,6 +128,57 @@ class ZohoClient { return ZohoMapper.mapApiResponse(response, 'invoices'); } + async getAccounts(params = {}) { + const response = await this.makeRequest('/crm/v2/Accounts', { params }); + return ZohoMapper.mapApiResponse(response, 'accounts'); + } + + // Get counts for all CRM modules in a single call + async getCrmCounts() { + const modules = ['Accounts', 'Leads', 'Tasks', 'Invoices', 'Sales_Orders', 'Purchase_Orders', 'Deals', 'Contacts', 'Vendors']; + const counts = {}; + + // Use Promise.allSettled to handle all requests concurrently and continue even if some fail + const promises = modules.map(async (module) => { + try { + const response = await this.makeRequest(`/crm/v2.1/${module}/actions/count`); + return { module: module.toLowerCase(), count: response.count || 0, success: true }; + } catch (error) { + console.error(`Error getting count for ${module}:`, error.message); + return { module: module.toLowerCase(), count: 0, success: false, error: error.message }; + } + }); + + const results = await Promise.allSettled(promises); + + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + const { module, count, success, error } = result.value; + counts[module] = { + count, + success, + ...(error && { error }) + }; + } else { + const module = modules[index].toLowerCase(); + counts[module] = { + count: 0, + success: false, + error: result.reason?.message || 'Unknown error' + }; + } + }); + + return { + data: counts, + info: { + totalModules: modules.length, + successfulModules: Object.values(counts).filter(m => m.success).length, + failedModules: Object.values(counts).filter(m => !m.success).length + } + }; + } + // Zoho People methods async getEmployees(params = {}) { const response = await this.makeRequest('/people/api/v1/employees', { params }, 'people'); @@ -393,7 +442,7 @@ class ZohoClient { async getAllProjectIssues(portalId, params = {}) { const response = await this.makeRequest(`/api/v3/portal/${portalId}/issues`, { params }, 'projects'); - console.log('issues response i got',response) + // console.log('issues response i got',response) return ZohoMapper.mapApiResponse(response, 'issues'); } diff --git a/src/integrations/zoho/handler.js b/src/integrations/zoho/handler.js index 78b7749..e80d155 100644 --- a/src/integrations/zoho/handler.js +++ b/src/integrations/zoho/handler.js @@ -427,9 +427,9 @@ class ZohoHandler { // Verify and decode the JWT token using the same service const decoded = jwtService.verify(decryptedToken); - // Get user data from database using the user ID from token + // Get user data from database using the user UUID from token const userRepository = require('../../data/repositories/userRepository'); - const user = await userRepository.findById(decoded.id); + const user = await userRepository.findByUuid(decoded.uuid); if (!user) { throw new Error('User not found'); @@ -538,12 +538,50 @@ class ZohoHandler { const mappedData = csvService.parseCsvData(csvData, module, userId, job_id); console.log(`🔄 Mapped ${mappedData.length} records for database insertion`); - // Clear existing data for this job - await ZohoBulkReadRepository.clearUserData(userId, module, job_id); + // Use transaction to ensure atomicity of delete + insert operations + const sequelize = require('../../db/pool'); + const transaction = await sequelize.transaction(); + let insertedRecords = []; // Declare outside try block for scope - // Bulk insert data - const insertedRecords = await ZohoBulkReadRepository.bulkInsert(module, mappedData); - console.log(`✅ Successfully inserted ${insertedRecords.length} records`); + try { + // Clear existing data for this user and module (REPLACE strategy) + console.log(`🗑️ Starting data cleanup for user ${userId}, module ${module}`); + + // Check count before deletion + const beforeCount = await ZohoBulkReadRepository.getUserDataCount(userId, module, transaction); + console.log(`📊 Records before cleanup: ${beforeCount}`); + + const deletedCount = await ZohoBulkReadRepository.clearUserData(userId, module, transaction); + console.log(`🗑️ Deleted ${deletedCount} existing records for user ${userId}, module ${module}`); + + // Verify data is cleared by checking count + const remainingCount = await ZohoBulkReadRepository.getUserDataCount(userId, module, transaction); + console.log(`🔍 Remaining records after cleanup: ${remainingCount}`); + + if (remainingCount > 0) { + console.warn(`⚠️ Warning: ${remainingCount} records still exist after cleanup!`); + console.warn(`⚠️ This indicates the delete operation may not have worked properly`); + } + + // Bulk insert new data + console.log(`📥 Starting bulk insert of ${mappedData.length} new records`); + insertedRecords = await ZohoBulkReadRepository.bulkInsert(module, mappedData, transaction); + console.log(`✅ Successfully inserted ${insertedRecords.length} records`); + + // Verify final count + const finalCount = await ZohoBulkReadRepository.getUserDataCount(userId, module, transaction); + console.log(`📊 Final record count for user ${userId}, module ${module}: ${finalCount}`); + + // Commit transaction + await transaction.commit(); + console.log(`✅ Transaction committed successfully`); + + } catch (error) { + // Rollback transaction on error + await transaction.rollback(); + console.error(`❌ Transaction rolled back due to error:`, error.message); + throw error; + } // Update job status await ZohoBulkReadRepository.updateBulkReadJob(job_id, {