bulk read flow added and will trigger after first time / re-auth authetication

This commit is contained in:
yashwin-foxy 2025-09-26 17:13:57 +05:30
parent ec68e5ca39
commit 7352fec243
6 changed files with 604 additions and 21 deletions

View File

@ -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
};

View File

@ -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();

View File

@ -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(),

View File

@ -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<Array>} 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>} 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<number>} 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;

View File

@ -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');
}

View File

@ -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,13 +538,51 @@ 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);
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, {
status: 'completed',