154 lines
6.7 KiB
JavaScript
154 lines
6.7 KiB
JavaScript
const { outputProgress, formatElapsedTime, estimateRemaining, calculateRate, logError } = require('./utils/progress');
|
|
const { getConnection } = require('./utils/db');
|
|
|
|
async function calculateFinancialMetrics(startTime, totalProducts, processedCount, isCancelled = false) {
|
|
const connection = await getConnection();
|
|
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 processedCount;
|
|
}
|
|
|
|
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,
|
|
DATEDIFF(MAX(o.date), MIN(o.date)) + 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) >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
|
GROUP BY p.pid
|
|
)
|
|
UPDATE product_metrics pm
|
|
JOIN product_financials pf ON pm.pid = pf.pid
|
|
SET
|
|
pm.inventory_value = COALESCE(pf.inventory_value, 0),
|
|
pm.total_revenue = COALESCE(pf.total_revenue, 0),
|
|
pm.cost_of_goods_sold = COALESCE(pf.cost_of_goods_sold, 0),
|
|
pm.gross_profit = COALESCE(pf.gross_profit, 0),
|
|
pm.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,
|
|
pm.last_calculated_at = CURRENT_TIMESTAMP
|
|
`);
|
|
|
|
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 processedCount;
|
|
|
|
// Update time-based aggregates with optimized query
|
|
await connection.query(`
|
|
WITH monthly_financials AS (
|
|
SELECT
|
|
p.pid,
|
|
YEAR(o.date) as year,
|
|
MONTH(o.date) 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, YEAR(o.date), MONTH(o.date)
|
|
)
|
|
UPDATE product_time_aggregates pta
|
|
JOIN monthly_financials mf ON pta.pid = mf.pid
|
|
AND pta.year = mf.year
|
|
AND pta.month = mf.month
|
|
SET
|
|
pta.inventory_value = COALESCE(mf.inventory_value, 0),
|
|
pta.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,
|
|
pta.last_calculated_at = CURRENT_TIMESTAMP
|
|
`);
|
|
|
|
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)
|
|
}
|
|
});
|
|
|
|
return processedCount;
|
|
} catch (error) {
|
|
logError(error, 'Error calculating financial metrics');
|
|
throw error;
|
|
} finally {
|
|
if (connection) {
|
|
connection.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = calculateFinancialMetrics;
|