const { outputProgress, formatElapsedTime, estimateRemaining, calculateRate, logError } = require('./utils/progress'); const { getConnection } = require('./utils/db'); async function calculateFinancialMetrics(startTime, totalProducts, processedCount = 0, isCancelled = false) { const connection = await getConnection(); let success = false; let processedOrders = 0; try { if (isCancelled) { outputProgress({ status: 'cancelled', operation: 'Financial metrics calculation cancelled', current: processedCount, total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: null, rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalProducts) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); return { processedProducts: processedCount, processedOrders: 0, processedPurchaseOrders: 0, success }; } // Get order count that will be processed const orderCount = await connection.query(` SELECT COUNT(*) as count FROM orders o WHERE o.canceled = false AND DATE(o.date) >= CURRENT_DATE - INTERVAL '12 months' `); processedOrders = parseInt(orderCount.rows[0].count); outputProgress({ status: 'running', operation: 'Starting financial metrics calculation', current: processedCount, total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalProducts), rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalProducts) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); // Calculate financial metrics with optimized query await connection.query(` WITH product_financials AS ( SELECT p.pid, p.cost_price * p.stock_quantity as inventory_value, SUM(o.quantity * o.price) as total_revenue, SUM(o.quantity * p.cost_price) as cost_of_goods_sold, SUM(o.quantity * (o.price - p.cost_price)) as gross_profit, MIN(o.date) as first_sale_date, MAX(o.date) as last_sale_date, EXTRACT(DAY FROM (MAX(o.date)::timestamp with time zone - MIN(o.date)::timestamp with time zone)) + 1 as calculation_period_days, COUNT(DISTINCT DATE(o.date)) as active_days FROM products p LEFT JOIN orders o ON p.pid = o.pid WHERE o.canceled = false AND DATE(o.date) >= CURRENT_DATE - INTERVAL '12 months' GROUP BY p.pid, p.cost_price, p.stock_quantity ) UPDATE product_metrics pm SET inventory_value = COALESCE(pf.inventory_value, 0), total_revenue = COALESCE(pf.total_revenue, 0), cost_of_goods_sold = COALESCE(pf.cost_of_goods_sold, 0), gross_profit = COALESCE(pf.gross_profit, 0), gmroi = CASE WHEN COALESCE(pf.inventory_value, 0) > 0 AND pf.active_days > 0 THEN (COALESCE(pf.gross_profit, 0) * (365.0 / pf.active_days)) / COALESCE(pf.inventory_value, 0) ELSE 0 END, last_calculated_at = CURRENT_TIMESTAMP FROM product_financials pf WHERE pm.pid = pf.pid `); processedCount = Math.floor(totalProducts * 0.65); outputProgress({ status: 'running', operation: 'Base financial metrics calculated, updating time aggregates', current: processedCount, total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalProducts), rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalProducts) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); if (isCancelled) return { processedProducts: processedCount, processedOrders, processedPurchaseOrders: 0, success }; // Update time-based aggregates with optimized query await connection.query(` WITH monthly_financials AS ( SELECT p.pid, EXTRACT(YEAR FROM o.date::timestamp with time zone) as year, EXTRACT(MONTH FROM o.date::timestamp with time zone) as month, p.cost_price * p.stock_quantity as inventory_value, SUM(o.quantity * (o.price - p.cost_price)) as gross_profit, COUNT(DISTINCT DATE(o.date)) as active_days, MIN(o.date) as period_start, MAX(o.date) as period_end FROM products p LEFT JOIN orders o ON p.pid = o.pid WHERE o.canceled = false GROUP BY p.pid, EXTRACT(YEAR FROM o.date::timestamp with time zone), EXTRACT(MONTH FROM o.date::timestamp with time zone), p.cost_price, p.stock_quantity ) UPDATE product_time_aggregates pta SET inventory_value = COALESCE(mf.inventory_value, 0), gmroi = CASE WHEN COALESCE(mf.inventory_value, 0) > 0 AND mf.active_days > 0 THEN (COALESCE(mf.gross_profit, 0) * (365.0 / mf.active_days)) / COALESCE(mf.inventory_value, 0) ELSE 0 END FROM monthly_financials mf WHERE pta.pid = mf.pid AND pta.year = mf.year AND pta.month = mf.month `); processedCount = Math.floor(totalProducts * 0.70); outputProgress({ status: 'running', operation: 'Time-based aggregates updated', current: processedCount, total: totalProducts, elapsed: formatElapsedTime(startTime), remaining: estimateRemaining(startTime, processedCount, totalProducts), rate: calculateRate(startTime, processedCount), percentage: ((processedCount / totalProducts) * 100).toFixed(1), timing: { start_time: new Date(startTime).toISOString(), end_time: new Date().toISOString(), elapsed_seconds: Math.round((Date.now() - startTime) / 1000) } }); // If we get here, everything completed successfully success = true; // Update calculate_status await connection.query(` INSERT INTO calculate_status (module_name, last_calculation_timestamp) VALUES ('financial_metrics', NOW()) ON CONFLICT (module_name) DO UPDATE SET last_calculation_timestamp = NOW() `); return { processedProducts: processedCount, processedOrders, processedPurchaseOrders: 0, success }; } catch (error) { success = false; logError(error, 'Error calculating financial metrics'); throw error; } finally { if (connection) { connection.release(); } } } module.exports = calculateFinancialMetrics;