Files
inventory/inventory-server/scripts/calculate-metrics.js

249 lines
9.4 KiB
JavaScript

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/product-metrics');
const calculateTimeAggregates = require('./metrics/time-aggregates');
const calculateFinancialMetrics = require('./metrics/financial-metrics');
const calculateVendorMetrics = require('./metrics/vendor-metrics');
const calculateCategoryMetrics = require('./metrics/category-metrics');
const calculateBrandMetrics = require('./metrics/brand-metrics');
const calculateSalesForecasts = require('./metrics/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);
});
}