const path = require('path'); // Change working directory to script directory process.chdir(path.dirname(__filename)); require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') }); // Add error handler for uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); // Add error handler for unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); const progress = require('./metrics/utils/progress'); console.log('Progress module loaded:', { modulePath: require.resolve('./metrics/utils/progress'), exports: Object.keys(progress), currentDir: process.cwd(), scriptDir: __dirname }); // Store progress functions in global scope to ensure availability global.formatElapsedTime = progress.formatElapsedTime; global.estimateRemaining = progress.estimateRemaining; global.calculateRate = progress.calculateRate; global.outputProgress = progress.outputProgress; global.clearProgress = progress.clearProgress; global.getProgress = progress.getProgress; global.logError = progress.logError; const { getConnection, closePool } = require('./metrics/utils/db'); const calculateProductMetrics = require('./metrics/core/product-metrics'); const calculateTimeAggregates = require('./metrics/utils/time-aggregates'); const calculateFinancialMetrics = require('./metrics/financial/financial-metrics'); const calculateVendorMetrics = require('./metrics/vendor/vendor-metrics'); const calculateCategoryMetrics = require('./metrics/category/category-metrics'); const calculateBrandMetrics = require('./metrics/brand/brand-metrics'); const calculateSalesForecasts = require('./metrics/forecasting/sales-forecasts'); // Set to 1 to skip product metrics and only calculate the remaining metrics const SKIP_PRODUCT_METRICS = 1; // Add cancel handler let isCancelled = false; function cancelCalculation() { isCancelled = true; global.clearProgress(); // Format as SSE event const event = { progress: { status: 'cancelled', operation: 'Calculation cancelled', current: 0, total: 0, elapsed: null, remaining: null, rate: 0, timestamp: Date.now() } }; process.stdout.write(JSON.stringify(event) + '\n'); process.exit(0); } // Handle SIGTERM signal for cancellation process.on('SIGTERM', cancelCalculation); // Update the main calculation function to use the new modular structure async function calculateMetrics() { let connection; const startTime = Date.now(); let processedCount = 0; let totalProducts = 0; try { // Add debug logging for the progress functions console.log('Debug - Progress functions:', { formatElapsedTime: typeof global.formatElapsedTime, estimateRemaining: typeof global.estimateRemaining, calculateRate: typeof global.calculateRate, startTime: startTime }); try { const elapsed = global.formatElapsedTime(startTime); console.log('Debug - formatElapsedTime test successful:', elapsed); } catch (err) { console.error('Debug - Error testing formatElapsedTime:', err); throw err; } isCancelled = false; connection = await getConnection(); try { global.outputProgress({ status: 'running', operation: 'Starting metrics calculation', current: 0, total: 100, elapsed: '0s', remaining: 'Calculating...', rate: 0, percentage: '0' }); // Get total number of products const [countResult] = await connection.query('SELECT COUNT(*) as total FROM products') .catch(err => { global.logError(err, 'Failed to count products'); throw err; }); totalProducts = countResult[0].total; if (!SKIP_PRODUCT_METRICS) { processedCount = await calculateProductMetrics(startTime, totalProducts); } else { console.log('Skipping product metrics calculation...'); processedCount = Math.floor(totalProducts * 0.6); global.outputProgress({ status: 'running', operation: 'Skipping product metrics calculation', current: processedCount, total: totalProducts, elapsed: global.formatElapsedTime(startTime), remaining: global.estimateRemaining(startTime, processedCount, totalProducts), rate: global.calculateRate(startTime, processedCount), percentage: '60' }); } // Calculate time-based aggregates processedCount = await calculateTimeAggregates(startTime, totalProducts, processedCount); // Calculate financial metrics processedCount = await calculateFinancialMetrics(startTime, totalProducts, processedCount); // Calculate vendor metrics processedCount = await calculateVendorMetrics(startTime, totalProducts, processedCount); // Calculate category metrics processedCount = await calculateCategoryMetrics(startTime, totalProducts, processedCount); // Calculate brand metrics processedCount = await calculateBrandMetrics(startTime, totalProducts, processedCount); // Calculate sales forecasts processedCount = await calculateSalesForecasts(startTime, totalProducts, processedCount); // Calculate ABC classification const [abcConfig] = await connection.query('SELECT a_threshold, b_threshold FROM abc_classification_config WHERE id = 1'); const abcThresholds = abcConfig[0] || { a_threshold: 20, b_threshold: 50 }; await connection.query(` WITH revenue_rankings AS ( SELECT product_id, total_revenue, PERCENT_RANK() OVER (ORDER BY COALESCE(total_revenue, 0) DESC) * 100 as revenue_percentile FROM product_metrics ), classification_update AS ( SELECT product_id, CASE WHEN revenue_percentile <= ? THEN 'A' WHEN revenue_percentile <= ? THEN 'B' ELSE 'C' END as abc_class FROM revenue_rankings ) UPDATE product_metrics pm JOIN classification_update cu ON pm.product_id = cu.product_id SET pm.abc_class = cu.abc_class, pm.last_calculated_at = NOW() `, [abcThresholds.a_threshold, abcThresholds.b_threshold]); // Final success message global.outputProgress({ status: 'complete', operation: 'Metrics calculation complete', current: totalProducts, total: totalProducts, elapsed: global.formatElapsedTime(startTime), remaining: '0s', rate: global.calculateRate(startTime, totalProducts), percentage: '100' }); // Clear progress file on successful completion global.clearProgress(); } catch (error) { if (isCancelled) { global.outputProgress({ status: 'cancelled', operation: 'Calculation cancelled', current: processedCount, total: totalProducts || 0, elapsed: global.formatElapsedTime(startTime), remaining: null, rate: global.calculateRate(startTime, processedCount), percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1) }); } else { global.outputProgress({ status: 'error', operation: 'Error: ' + error.message, current: processedCount, total: totalProducts || 0, elapsed: global.formatElapsedTime(startTime), remaining: null, rate: global.calculateRate(startTime, processedCount), percentage: ((processedCount / (totalProducts || 1)) * 100).toFixed(1) }); } throw error; } finally { if (connection) { connection.release(); } } } finally { // Close the connection pool when we're done await closePool(); } } // Export both functions and progress checker module.exports = calculateMetrics; module.exports.cancelCalculation = cancelCalculation; module.exports.getProgress = global.getProgress; // Run directly if called from command line if (require.main === module) { calculateMetrics().catch(error => { if (!error.message.includes('Operation cancelled')) { console.error('Error:', error); } process.exit(1); }); }