GWM : iip calculation

This commit is contained in:
Gowtham M 2026-04-29 10:39:44 +05:30
parent 5ffb872c3a
commit 7be44b562d
11 changed files with 3049 additions and 316 deletions

View File

@ -94,7 +94,7 @@ exports.calculate_base_year = async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: `Base year ${baseYear} production calculated successfully`, message: `Base year ${baseYear} production calculated successfully (quarter-based)`,
data: result data: result
}); });
@ -109,34 +109,32 @@ exports.calculate_base_year = async (req, res) => {
}; };
// 2. Calculate IPI for Specific Month // 2. Calculate IPI for Specific Quarter
exports.calculate_month = async (req, res) => { exports.calculate_month = async (req, res) => {
try { try {
// const { year, month } = req.body; const { year, quarter } = req.body;
const year = 2026;
const month = 3;
if (!year || !month) { if (!year || !quarter) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Year and month are required' message: 'Year and quarter are required'
}); });
} }
if (month < 1 || month > 12) { if (!['Q1', 'Q2', 'Q3', 'Q4'].includes(quarter)) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Month must be between 1 and 12' message: 'Quarter must be one of Q1, Q2, Q3, Q4'
}); });
} }
const service = new IPICalculationService(dbConfig); const service = new IPICalculationService(dbConfig);
const result = await service.runCompleteCalculation(year, month); const result = await service.runCompleteCalculation(year, quarter);
await service.close(); await service.close();
res.json({ res.json({
success: true, success: true,
message: `IPI calculated for ${year}-${String(month).padStart(2, '0')}`, message: `IPI calculated for ${year}-${quarter}`,
data: result data: result
}); });
@ -151,7 +149,7 @@ exports.calculate_month = async (req, res) => {
}; };
// 3. Calculate IPI for Multiple Months // 3. Calculate IPI for quarter
exports.calculate_quarter = async (req, res) => { exports.calculate_quarter = async (req, res) => {
try { try {
@ -164,39 +162,19 @@ exports.calculate_quarter = async (req, res) => {
}); });
} }
// Quarter to month mapping if (!['Q1', 'Q2', 'Q3', 'Q4'].includes(quarter)) {
const quarterMap = {
Q1: { startMonth: 1, endMonth: 3 },
Q2: { startMonth: 4, endMonth: 6 },
Q3: { startMonth: 7, endMonth: 9 },
Q4: { startMonth: 10, endMonth: 12 },
};
const selectedQuarter = quarterMap[quarter];
if (!selectedQuarter) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Invalid quarter. Accepted values: Q1, Q2, Q3, Q4' message: 'Invalid quarter. Accepted values: Q1, Q2, Q3, Q4'
}); });
} }
const { startMonth, endMonth } = selectedQuarter;
const service = new IPICalculationService(dbConfig); const service = new IPICalculationService(dbConfig);
const results = []; const result = await service.runCompleteCalculation(year, quarter);
for (let month = startMonth; month <= endMonth; month++) {
const result = await service.runCompleteCalculation(year, month);
results.push({
month,
...result
});
}
await service.close(); await service.close();
res.json({ success: true, message: `IPI calculated for ${year} months ${startMonth}-${endMonth}`, data: results }); res.json({ success: true, message: `IPI calculated for ${year} ${quarter}`, data: result });
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -289,7 +267,7 @@ exports.survey_auto_submit = async (req, res) => {
exports.getCalculationLogsByYearMonth = async (req, res) => { exports.getCalculationLogsByYearMonth = async (req, res) => {
try { try {
const { year, month } = req.query; const { year, quarter, month } = req.query;
if (!year) { if (!year) {
return res.status(400).json({ return res.status(400).json({
@ -297,16 +275,18 @@ exports.getCalculationLogsByYearMonth = async (req, res) => {
}); });
} }
if (!month) { const resolvedQuarter = quarter || (month ? (month <= 3 ? "Q1" : month <= 6 ? "Q2" : month <= 9 ? "Q3" : "Q4") : null);
if (!resolvedQuarter) {
return res.status(400).json({ return res.status(400).json({
message: "Month is required", message: "Quarter (or month to derive quarter) is required",
}); });
} }
const logs = await CalculationLog.findAll({ const logs = await CalculationLog.findAll({
where: { where: {
reference_year: year, reference_year: year,
reference_month: month, reference_month: resolvedQuarter,
}, },
order: [["started_at", "DESC"]], order: [["started_at", "DESC"]],
}); });

View File

@ -37,6 +37,14 @@ const formatNumber = (value) => {
return parsed.toFixed(2); return parsed.toFixed(2);
}; };
const monthToQuarter = (month) => {
if (month >= 1 && month <= 3) return "Q1";
if (month >= 4 && month <= 6) return "Q2";
if (month >= 7 && month <= 9) return "Q3";
if (month >= 10 && month <= 12) return "Q4";
return null;
};
exports.renderPage = async (req, res) => { exports.renderPage = async (req, res) => {
const now = new Date(); const now = new Date();
const year = Number(req.query.year) || now.getFullYear(); const year = Number(req.query.year) || now.getFullYear();
@ -61,7 +69,8 @@ exports.getData = async (req, res) => {
}); });
} }
const whereClause = { year, month }; const quarter = monthToQuarter(month);
const whereClause = { year, quarter };
const [manufacturing, isic2, isic3, isic4, itemCount, logs] = await Promise.all([ const [manufacturing, isic2, isic3, isic4, itemCount, logs] = await Promise.all([
ManufacturingIpi.findOne({ where: whereClause, raw: true }), ManufacturingIpi.findOne({ where: whereClause, raw: true }),
@ -69,36 +78,21 @@ exports.getData = async (req, res) => {
Isic3DigitIndices.findAll({ where: whereClause, order: [["isic_3digit_code", "ASC"]], raw: true }), Isic3DigitIndices.findAll({ where: whereClause, order: [["isic_3digit_code", "ASC"]], raw: true }),
Isic4DigitIndices.findAll({ where: whereClause, order: [["isic_4digit_code", "ASC"]], raw: true }), Isic4DigitIndices.findAll({ where: whereClause, order: [["isic_4digit_code", "ASC"]], raw: true }),
db.sequelize.query( db.sequelize.query(
`SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND month = ?`, `SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND quarter = ?`,
{ replacements: [year, month], type: db.Sequelize.QueryTypes.SELECT } { replacements: [year, quarter], type: db.Sequelize.QueryTypes.SELECT }
), ),
CalculationLog.findAll({ CalculationLog.findAll({
where: { reference_year: year, reference_month: month }, where: { reference_year: year, reference_month: quarter },
order: [["started_at", "DESC"]], order: [["started_at", "DESC"]],
raw: true, raw: true,
}), })
db.sequelize.query(
`SELECT product_id, product_hs_code, base_year, avg_by_production, jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, \`dec\`
FROM base_year_production
WHERE base_year = 2022
ORDER BY product_id
LIMIT 25`,
{ type: db.Sequelize.QueryTypes.SELECT }
),
db.sequelize.query(
`SELECT product_id, product_hs_code, year, month, month_name, production_quantity, unit
FROM monthly_production
WHERE year = ? AND month = ?
ORDER BY product_id
LIMIT 50`,
{ replacements: [year, month], type: db.Sequelize.QueryTypes.SELECT }
),
]); ]);
return res.json({ return res.json({
success: true, success: true,
year, year,
month, month,
quarter,
summary: { summary: {
item_count: Number(itemCount[0]?.total || 0), item_count: Number(itemCount[0]?.total || 0),
isic4_count: isic4.length, isic4_count: isic4.length,
@ -149,6 +143,7 @@ exports.getTableData = async (req, res) => {
const table = String(req.query.table || ""); const table = String(req.query.table || "");
const year = Number(req.query.year); const year = Number(req.query.year);
const month = Number(req.query.month); const month = Number(req.query.month);
const quarter = monthToQuarter(month);
const baseYear = Number(req.query.baseYear) || 2022; const baseYear = Number(req.query.baseYear) || 2022;
const page = Math.max(1, Number(req.query.page) || 1); const page = Math.max(1, Number(req.query.page) || 1);
const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50)); const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50));
@ -157,14 +152,14 @@ exports.getTableData = async (req, res) => {
const queryConfig = { const queryConfig = {
monthly: { monthly: {
whereSql: `year = ? AND month = ?`, whereSql: `year = ? AND quarter = ?`,
whereParams: [year, month], whereParams: [year, quarter],
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ? OR month_name LIKE ?)` : "", searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ? OR quarter_name LIKE ?)` : "",
searchParams: search ? [`%${search}%`, `%${search}%`, `%${search}%`] : [], searchParams: search ? [`%${search}%`, `%${search}%`, `%${search}%`] : [],
countSql: `SELECT COUNT(*) AS total FROM monthly_production WHERE year = ? AND month = ?`, countSql: `SELECT COUNT(*) AS total FROM monthly_production WHERE year = ? AND quarter = ?`,
dataSql: `SELECT product_id, product_hs_code, year, month, month_name, production_quantity, unit dataSql: `SELECT product_id, product_hs_code, year, quarter, quarter_name, production_quantity, unit
FROM monthly_production FROM monthly_production
WHERE year = ? AND month = ?`, WHERE year = ? AND quarter = ?`,
orderSql: ` ORDER BY product_id`, orderSql: ` ORDER BY product_id`,
}, },
baseyear: { baseyear: {
@ -173,20 +168,20 @@ exports.getTableData = async (req, res) => {
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "", searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "",
searchParams: search ? [`%${search}%`, `%${search}%`] : [], searchParams: search ? [`%${search}%`, `%${search}%`] : [],
countSql: `SELECT COUNT(*) AS total FROM base_year_production WHERE base_year = ?`, countSql: `SELECT COUNT(*) AS total FROM base_year_production WHERE base_year = ?`,
dataSql: `SELECT product_id, product_hs_code, base_year, avg_by_production, jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, \`dec\` dataSql: `SELECT product_id, product_hs_code, base_year, avg_by_production, q1, q2, q3, q4
FROM base_year_production FROM base_year_production
WHERE base_year = ?`, WHERE base_year = ?`,
orderSql: ` ORDER BY product_id`, orderSql: ` ORDER BY product_id`,
}, },
items: { items: {
whereSql: `year = ? AND month = ?`, whereSql: `year = ? AND quarter = ?`,
whereParams: [year, month], whereParams: [year, quarter],
searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "", searchSql: search ? ` AND (CAST(product_id AS CHAR) LIKE ? OR CAST(product_hs_code AS CHAR) LIKE ?)` : "",
searchParams: search ? [`%${search}%`, `%${search}%`] : [], searchParams: search ? [`%${search}%`, `%${search}%`] : [],
countSql: `SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND month = ?`, countSql: `SELECT COUNT(*) AS total FROM item_level_indices WHERE year = ? AND quarter = ?`,
dataSql: `SELECT product_id, product_hs_code, current_production, base_year_avg_production, production_relative, item_index dataSql: `SELECT product_id, product_hs_code, current_production, base_year_avg_production, production_relative, item_index
FROM item_level_indices FROM item_level_indices
WHERE year = ? AND month = ?`, WHERE year = ? AND quarter = ?`,
orderSql: ` ORDER BY product_id`, orderSql: ` ORDER BY product_id`,
}, },
}; };
@ -195,7 +190,7 @@ exports.getTableData = async (req, res) => {
return res.status(400).json({ success: false, message: "Invalid table parameter" }); return res.status(400).json({ success: false, message: "Invalid table parameter" });
} }
if ((table === "monthly" || table === "items") && (!year || !month || month < 1 || month > 12)) { if ((table === "monthly" || table === "items") && (!year || !month || month < 1 || month > 12 || !quarter)) {
return res.status(400).json({ success: false, message: "Valid year and month are required" }); return res.status(400).json({ success: false, message: "Valid year and month are required" });
} }
@ -216,7 +211,7 @@ exports.getTableData = async (req, res) => {
const copy = { ...row }; const copy = { ...row };
Object.keys(copy).forEach((k) => { Object.keys(copy).forEach((k) => {
if ( if (
["production_quantity", "avg_by_production", "current_production", "base_year_avg_production", "production_relative", "item_index", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"].includes(k) ["production_quantity", "avg_by_production", "current_production", "base_year_avg_production", "production_relative", "item_index", "q1", "q2", "q3", "q4"].includes(k)
) { ) {
copy[k] = formatNumber(copy[k]); copy[k] = formatNumber(copy[k]);
} }
@ -245,8 +240,9 @@ exports.runMonthCalculation = async (req, res) => {
try { try {
const year = Number(req.body.year); const year = Number(req.body.year);
const month = Number(req.body.month); const month = Number(req.body.month);
const quarter = monthToQuarter(month);
if (!year || !month || month < 1 || month > 12) { if (!year || !month || month < 1 || month > 12 || !quarter) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Valid year and month are required", message: "Valid year and month are required",
@ -254,12 +250,12 @@ exports.runMonthCalculation = async (req, res) => {
} }
const service = new IPICalculationService(dbConfig); const service = new IPICalculationService(dbConfig);
const result = await service.runCompleteCalculation(year, month); const result = await service.runCompleteCalculation(year, quarter);
await service.close(); await service.close();
return res.json({ return res.json({
success: true, success: true,
message: `Calculation completed for ${year}-${String(month).padStart(2, "0")}`, message: `Calculation completed for ${year}-${quarter}`,
data: result, data: result,
}); });
} catch (error) { } catch (error) {
@ -275,8 +271,9 @@ exports.getRunStatus = async (req, res) => {
try { try {
const year = Number(req.query.year); const year = Number(req.query.year);
const month = Number(req.query.month); const month = Number(req.query.month);
const quarter = monthToQuarter(month);
if (!year || !month || month < 1 || month > 12) { if (!year || !month || month < 1 || month > 12 || !quarter) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Valid year and month are required", message: "Valid year and month are required",
@ -286,7 +283,7 @@ exports.getRunStatus = async (req, res) => {
const logs = await CalculationLog.findAll({ const logs = await CalculationLog.findAll({
where: { where: {
reference_year: year, reference_year: year,
reference_month: month, reference_month: quarter,
}, },
order: [["started_at", "ASC"]], order: [["started_at", "ASC"]],
raw: true, raw: true,
@ -314,6 +311,7 @@ exports.getRunStatus = async (req, res) => {
success: true, success: true,
year, year,
month, month,
quarter,
started: startedAny, started: startedAny,
isFinished, isFinished,
completedCount, completedCount,

View File

@ -61,27 +61,27 @@ exports.getAllManufacturingIndexDetails = async (req, res) => {
exports.getManufacturingMonthlyOverviewByYearMonth = async (req, res) => { exports.getManufacturingMonthlyOverviewByYearMonth = async (req, res) => {
try { try {
const { year, month } = req.query; const { year, month, quarter } = req.query;
// Validate parameters exist // Validate parameters exist
if (!year || !month) { if (!year || (!month && !quarter)) {
logger.warn('getManufacturingMonthlyOverview API: Missing required parameters'); logger.warn('getManufacturingMonthlyOverview API: Missing required parameters');
return res.status(400).send({ return res.status(400).send({
status: "failed", status: "failed",
message: "Year and month are required" message: "Year and quarter (or month to derive quarter) are required"
}); });
} }
// Sanitize and validate input - prevent SQL injection // Sanitize and validate input - prevent SQL injection
const yearInt = parseInt(year, 10); const yearInt = parseInt(year, 10);
const monthInt = parseInt(month, 10); const monthInt = month ? parseInt(month, 10) : null;
// Validate that parsing was successful // Validate that parsing was successful
if (isNaN(yearInt) || isNaN(monthInt)) { if (isNaN(yearInt) || (month && isNaN(monthInt))) {
logger.warn(`getManufacturingMonthlyOverview API: Invalid input - year: ${year}, month: ${month}`); logger.warn(`getManufacturingMonthlyOverview API: Invalid input - year: ${year}, month: ${month}, quarter: ${quarter}`);
return res.status(400).send({ return res.status(400).send({
status: "failed", status: "failed",
message: "Year and month must be valid numbers" message: "Year/month must be valid numbers"
}); });
} }
@ -93,18 +93,25 @@ exports.getManufacturingMonthlyOverviewByYearMonth = async (req, res) => {
}); });
} }
// Validate month range if (month && (monthInt < 1 || monthInt > 12)) {
if (monthInt < 1 || monthInt > 12) {
return res.status(400).send({ return res.status(400).send({
status: "failed", status: "failed",
message: "Month must be between 1 and 12" message: "Month must be between 1 and 12"
}); });
} }
const derivedQuarter = quarter || (monthInt <= 3 ? "Q1" : monthInt <= 6 ? "Q2" : monthInt <= 9 ? "Q3" : "Q4");
if (!["Q1", "Q2", "Q3", "Q4"].includes(derivedQuarter)) {
return res.status(400).send({
status: "failed",
message: "Quarter must be one of Q1, Q2, Q3, Q4"
});
}
// Common where clause // Common where clause
const whereClause = { const whereClause = {
year: yearInt, year: yearInt,
month: monthInt quarter: derivedQuarter
}; };
// Fetch data from all tables in parallel // Fetch data from all tables in parallel
@ -132,7 +139,7 @@ exports.getManufacturingMonthlyOverviewByYearMonth = async (req, res) => {
// Check if manufacturing data exists (main data) // Check if manufacturing data exists (main data)
if (!manufacturingData) { if (!manufacturingData) {
logger.info(`getManufacturingMonthlyOverview API: No data found for year ${yearInt}, month ${monthInt}`); logger.info(`getManufacturingMonthlyOverview API: No data found for year ${yearInt}, quarter ${derivedQuarter}`);
return res.status(404).send({ return res.status(404).send({
status: "failed", status: "failed",
message: "No data found for the specified period" message: "No data found for the specified period"
@ -183,7 +190,7 @@ exports.getManufacturingMonthlyOverviewByYearMonth = async (req, res) => {
}; };
}); });
logger.info(`getManufacturingMonthlyOverview API: Successfully fetched data for year ${yearInt}, month ${monthInt}`); logger.info(`getManufacturingMonthlyOverview API: Successfully fetched data for year ${yearInt}, quarter ${derivedQuarter}`);
res.status(200).send({ res.status(200).send({
status: "success", status: "success",

View File

@ -22,11 +22,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
month: { quarter: {
type: DataTypes.INTEGER, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
month_name: { quarter_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },

View File

@ -22,11 +22,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
month: { quarter: {
type: DataTypes.INTEGER, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
month_name: { quarter_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },

View File

@ -22,11 +22,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
month: { quarter: {
type: DataTypes.INTEGER, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
month_name: { quarter_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },

View File

@ -9,11 +9,11 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
}, },
month: { quarter: {
type: DataTypes.INTEGER, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
month_name: { quarter_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },

View File

@ -10,7 +10,7 @@ class IPICalculationService {
} }
// STEP 1: Calculate and Store Base Year Average Production (2022) // STEP 1: Calculate and Store Base Year Average Production (Quarter-based)
async calculateBaseYearProduction(baseYear = 2022, forceRecalculate = false) { async calculateBaseYearProduction(baseYear = 2022, forceRecalculate = false) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
@ -33,38 +33,22 @@ class IPICalculationService {
// Log calculation start // Log calculation start
logId = await this.logRecord( 'base_year', baseYear, null, 'Started', null, 0, null, connection); logId = await this.logRecord( 'base_year', baseYear, null, 'Started', null, 0, null, connection);
// Your existing query to calculate base year production // Quarter-based base year production using submission_products.current_quantity
const query = ` const query = `
SELECT SELECT
p.id AS product_id, p.id AS product_id,
p.hs_code AS product_hs_code, p.hs_code AS product_hs_code,
u.uom_short_name AS unit, u.uom_short_name AS unit,
SUM(CASE WHEN s.quarter = 'Q1' THEN sp.current_quantity_period_one END) AS Jan, SUM(CASE WHEN s.quarter = 'Q1' THEN COALESCE(sp.current_quantity, 0) END) AS q1,
SUM(CASE WHEN s.quarter = 'Q1' THEN sp.current_quantity_period_two END) AS Feb, SUM(CASE WHEN s.quarter = 'Q2' THEN COALESCE(sp.current_quantity, 0) END) AS q2,
SUM(CASE WHEN s.quarter = 'Q1' THEN sp.current_quantity_period_three END) AS Mar, SUM(CASE WHEN s.quarter = 'Q3' THEN COALESCE(sp.current_quantity, 0) END) AS q3,
SUM(CASE WHEN s.quarter = 'Q2' THEN sp.current_quantity_period_one END) AS Apr, SUM(CASE WHEN s.quarter = 'Q4' THEN COALESCE(sp.current_quantity, 0) END) AS q4,
SUM(CASE WHEN s.quarter = 'Q2' THEN sp.current_quantity_period_two END) AS May,
SUM(CASE WHEN s.quarter = 'Q2' THEN sp.current_quantity_period_three END) AS Jun,
SUM(CASE WHEN s.quarter = 'Q3' THEN sp.current_quantity_period_one END) AS Jul,
SUM(CASE WHEN s.quarter = 'Q3' THEN sp.current_quantity_period_two END) AS Aug,
SUM(CASE WHEN s.quarter = 'Q3' THEN sp.current_quantity_period_three END) AS Sep,
SUM(CASE WHEN s.quarter = 'Q4' THEN sp.current_quantity_period_one END) AS Oct,
SUM(CASE WHEN s.quarter = 'Q4' THEN sp.current_quantity_period_two END) AS Nov,
SUM(CASE WHEN s.quarter = 'Q4' THEN sp.current_quantity_period_three END) AS \`Dec\`,
ROUND(( ROUND((
COALESCE(SUM(CASE WHEN s.quarter='Q1' THEN sp.current_quantity_period_one END),0) + COALESCE(SUM(CASE WHEN s.quarter='Q1' THEN sp.current_quantity END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q1' THEN sp.current_quantity_period_two END),0) + COALESCE(SUM(CASE WHEN s.quarter='Q2' THEN sp.current_quantity END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q1' THEN sp.current_quantity_period_three END),0) + COALESCE(SUM(CASE WHEN s.quarter='Q3' THEN sp.current_quantity END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q2' THEN sp.current_quantity_period_one END),0) + COALESCE(SUM(CASE WHEN s.quarter='Q4' THEN sp.current_quantity END),0)
COALESCE(SUM(CASE WHEN s.quarter='Q2' THEN sp.current_quantity_period_two END),0) + ) / 4, 2) AS avg_by_production
COALESCE(SUM(CASE WHEN s.quarter='Q2' THEN sp.current_quantity_period_three END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q3' THEN sp.current_quantity_period_one END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q3' THEN sp.current_quantity_period_two END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q3' THEN sp.current_quantity_period_three END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q4' THEN sp.current_quantity_period_one END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q4' THEN sp.current_quantity_period_two END),0) +
COALESCE(SUM(CASE WHEN s.quarter='Q4' THEN sp.current_quantity_period_three END),0)
) / 12, 2) AS avg_by_production
FROM submission_products sp FROM submission_products sp
JOIN submission s ON s.id = sp.submission_id JOIN submission s ON s.id = sp.submission_id
JOIN products p ON p.id = sp.product_id JOIN products p ON p.id = sp.product_id
@ -84,15 +68,11 @@ class IPICalculationService {
for (const product of products) { for (const product of products) {
await connection.query( await connection.query(
`INSERT INTO base_year_production `INSERT INTO base_year_production
(product_id, product_hs_code, base_year, unit, jan, feb, mar, apr, may, jun, (product_id, product_hs_code, base_year, unit, q1, q2, q3, q4, avg_by_production)
jul, aug, sep, oct, nov, \`dec\`, avg_by_production) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
product.product_id, product.product_hs_code, baseYear, product.unit, product.product_id, product.product_hs_code, baseYear, product.unit,
product.Jan || 0, product.Feb || 0, product.Mar || 0, product.q1 || 0, product.q2 || 0, product.q3 || 0, product.q4 || 0,
product.Apr || 0, product.May || 0, product.Jun || 0,
product.Jul || 0, product.Aug || 0, product.Sep || 0,
product.Oct || 0, product.Nov || 0, product.Dec || 0,
product.avg_by_production product.avg_by_production
] ]
); );
@ -128,38 +108,23 @@ class IPICalculationService {
} }
// STEP 2: Aggregate Monthly Production // STEP 2: Aggregate Quarterly Production
async aggregateMonthlyProduction(year, month) { async aggregateQuarterlyProduction(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`aggregateMonthlyProduction - Aggregating production for ${year}-${month}...`); logger.info(`aggregateQuarterlyProduction - Aggregating production for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'monthly_production', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('monthly_production', year, quarter, 'Started', null, 0, null, connection);
const validQuarters = new Set(['Q1', 'Q2', 'Q3', 'Q4']);
// Determine quarter and period based on month if (!validQuarters.has(quarter)) {
const quarterMap = { throw new Error('Invalid quarter. Accepted values: Q1, Q2, Q3, Q4');
1: { quarter: 'Q1', period: 'current_quantity_period_one' }, }
2: { quarter: 'Q1', period: 'current_quantity_period_two' },
3: { quarter: 'Q1', period: 'current_quantity_period_three' },
4: { quarter: 'Q2', period: 'current_quantity_period_one' },
5: { quarter: 'Q2', period: 'current_quantity_period_two' },
6: { quarter: 'Q2', period: 'current_quantity_period_three' },
7: { quarter: 'Q3', period: 'current_quantity_period_one' },
8: { quarter: 'Q3', period: 'current_quantity_period_two' },
9: { quarter: 'Q3', period: 'current_quantity_period_three' },
10: { quarter: 'Q4', period: 'current_quantity_period_one' },
11: { quarter: 'Q4', period: 'current_quantity_period_two' },
12: { quarter: 'Q4', period: 'current_quantity_period_three' }
};
const { quarter, period } = quarterMap[month];
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Aggregate production by product // Aggregate production by product
const query = ` const query = `
@ -167,7 +132,7 @@ class IPICalculationService {
p.id AS product_id, p.id AS product_id,
p.hs_code AS product_hs_code, p.hs_code AS product_hs_code,
u.uom_short_name AS unit, u.uom_short_name AS unit,
SUM(sp.${period}) AS production_quantity SUM(COALESCE(sp.current_quantity, 0)) AS production_quantity
FROM submission_products sp FROM submission_products sp
JOIN submission s ON s.id = sp.submission_id JOIN submission s ON s.id = sp.submission_id
JOIN products p ON p.id = sp.product_id JOIN products p ON p.id = sp.product_id
@ -184,34 +149,35 @@ class IPICalculationService {
for (const product of products) { for (const product of products) {
await connection.query( await connection.query(
`INSERT INTO monthly_production `INSERT INTO monthly_production
(product_id, product_hs_code, year, month, month_name, (product_id, product_hs_code, year, quarter, quarter_name,
production_quantity, unit) production_quantity, unit)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
production_quantity=VALUES(production_quantity), production_quantity=VALUES(production_quantity),
quarter_name=VALUES(quarter_name),
unit=VALUES(unit)`, unit=VALUES(unit)`,
[ [
product.product_id, product.product_hs_code, year, month, product.product_id, product.product_hs_code, year, quarter,
monthNames[month], product.production_quantity || 0, product.unit quarter, product.production_quantity || 0, product.unit
] ]
); );
} }
// Update log // Update log
await this.logRecord('monthly_production', year, month, 'Completed', logId, products.length, null,connection); await this.logRecord('monthly_production', year, quarter, 'Completed', logId, products.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`aggregateMonthlyProduction - Monthly production aggregated: ${products.length} products`); logger.info(`aggregateQuarterlyProduction - Quarterly production aggregated: ${products.length} products`);
return { success: true, productsProcessed: products.length }; return { success: true, productsProcessed: products.length };
} catch (error) { } catch (error) {
await connection.rollback(); await connection.rollback();
console.error('Error aggregating monthly production:', error); console.error('Error aggregating monthly production:', error);
logger.error(`aggregateMonthlyProduction - Error aggregating monthly production: ${error}`); logger.error(`aggregateQuarterlyProduction - Error aggregating quarterly production: ${error}`);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('monthly_production', year, month, 'Failed', logId, 0, error.message); await this.logRecord('monthly_production', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -221,18 +187,16 @@ class IPICalculationService {
// STEP 3: Calculate Item Level Indices // STEP 3: Calculate Item Level Indices
async calculateItemLevelIndices(year, month) { async calculateItemLevelIndices(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`calculateItemLevelIndices - Calculating item level indices for ${year}-${month}...`); logger.info(`calculateItemLevelIndices - Calculating item level indices for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'item_level_indices', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('item_level_indices', year, quarter, 'Started', null, 0, null, connection);
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Calculate indices: Ri = Current Production / Base Year Avg Production // Calculate indices: Ri = Current Production / Base Year Avg Production
// Ii = Ri × 100 // Ii = Ri × 100
@ -246,17 +210,17 @@ class IPICalculationService {
((mp.production_quantity / byp.avg_by_production) * 100) AS item_index ((mp.production_quantity / byp.avg_by_production) * 100) AS item_index
FROM monthly_production mp FROM monthly_production mp
JOIN base_year_production byp ON byp.product_id = mp.product_id JOIN base_year_production byp ON byp.product_id = mp.product_id
WHERE mp.year = ? AND mp.month = ? WHERE mp.year = ? AND mp.quarter = ?
AND byp.avg_by_production > 0 AND byp.avg_by_production > 0
`; `;
const [items] = await connection.query(query, [year, month]); const [items] = await connection.query(query, [year, quarter]);
// Insert item level indices // Insert item level indices
for (const item of items) { for (const item of items) {
await connection.query( await connection.query(
`INSERT INTO item_level_indices `INSERT INTO item_level_indices
(product_id, product_hs_code, year, month, month_name, (product_id, product_hs_code, year, quarter, quarter_name,
current_production, base_year_avg_production, current_production, base_year_avg_production,
production_relative, item_index) production_relative, item_index)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
@ -266,7 +230,7 @@ class IPICalculationService {
production_relative=VALUES(production_relative), production_relative=VALUES(production_relative),
item_index=VALUES(item_index)`, item_index=VALUES(item_index)`,
[ [
item.product_id, item.product_hs_code, year, month, monthNames[month], item.product_id, item.product_hs_code, year, quarter, quarter,
item.current_production, item.base_year_avg_production, item.current_production, item.base_year_avg_production,
item.production_relative, item.item_index item.production_relative, item.item_index
] ]
@ -274,7 +238,7 @@ class IPICalculationService {
} }
// Update log // Update log
await this.logRecord('item_level_indices', year, month, 'Completed', logId, items.length, null,connection); await this.logRecord('item_level_indices', year, quarter, 'Completed', logId, items.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateItemLevelIndices - Item level indices calculated: ${items.length} items`); logger.info(`calculateItemLevelIndices - Item level indices calculated: ${items.length} items`);
@ -286,7 +250,7 @@ class IPICalculationService {
logger.error(`calculateItemLevelIndices - Error calculating item indices: ${error}`); logger.error(`calculateItemLevelIndices - Error calculating item indices: ${error}`);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('item_level_indices', year, month, 'Failed', logId, 0, error.message); await this.logRecord('item_level_indices', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -296,52 +260,65 @@ class IPICalculationService {
// STEP 4: Calculate ISIC 4-Digit Level Indices // STEP 4: Calculate ISIC 4-Digit Level Indices
async calculateISIC4DigitIndices(year, month) { async calculateISIC4DigitIndices(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`calculateISIC4DigitIndices - Calculating ISIC 4-digit indices for ${year}-${month}...`); logger.info(`calculateISIC4DigitIndices - Calculating ISIC 4-digit indices for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'isic_4digit_indices', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('isic_4digit_indices', year, quarter, 'Started', null, 0, null, connection);
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // ISIC 4-digit grouping uses establishments.isic_code (via approved submissions).
// One establishment per product-period is chosen (highest current_quantity, then submission id)
// ISIC mapping source is now products.isic_code. // so item_index and weight_in_ib are not multiplied when several establishments report the same product.
// Aggregate weighted item indices to ISIC 4-digit using product master mapping.
const query = ` const query = `
WITH product_isic_from_establishment AS (
SELECT SELECT
LEFT(CAST(p.isic_code AS CHAR), 4) AS isic_4digit_code, ili.product_id,
ili.item_index,
LEFT(TRIM(CAST(e.isic_code AS CHAR)), 4) AS isic_4digit_code,
ROW_NUMBER() OVER (
PARTITION BY ili.product_id
ORDER BY COALESCE(sp.current_quantity, 0) DESC, s.id ASC
) AS rn
FROM item_level_indices ili
JOIN submission_products sp ON sp.product_id = ili.product_id
JOIN submission s ON s.id = sp.submission_id
AND s.year = ?
AND s.quarter = ?
AND s.status = 'Approved'
JOIN establishments e ON e.id = s.establishment_id
WHERE ili.year = ?
AND ili.quarter = ?
AND sp.is_active = 1
AND e.isic_code IS NOT NULL
AND TRIM(CAST(e.isic_code AS CHAR)) <> ''
)
SELECT
pie.isic_4digit_code,
SUM(COALESCE(p.weight_in_ib, 0)) AS total_weight, SUM(COALESCE(p.weight_in_ib, 0)) AS total_weight,
SUM( SUM(COALESCE(p.weight_in_ib, 0) * pie.item_index) AS weighted_index_sum,
COALESCE(p.weight_in_ib, 0) *
ili.item_index
) AS weighted_index_sum,
( (
SUM( SUM(COALESCE(p.weight_in_ib, 0) * pie.item_index) /
COALESCE(p.weight_in_ib, 0) *
ili.item_index
) /
NULLIF(SUM(COALESCE(p.weight_in_ib, 0)), 0) NULLIF(SUM(COALESCE(p.weight_in_ib, 0)), 0)
) AS isic_4digit_index ) AS isic_4digit_index
FROM item_level_indices ili FROM product_isic_from_establishment pie
JOIN products p ON p.id = ili.product_id JOIN products p ON p.id = pie.product_id
WHERE ili.year = ? WHERE pie.rn = 1
AND ili.month = ?
AND p.isic_code IS NOT NULL
AND p.weight_in_ib IS NOT NULL AND p.weight_in_ib IS NOT NULL
AND p.weight_in_ib > 0 AND p.weight_in_ib > 0
GROUP BY LEFT(CAST(p.isic_code AS CHAR), 4) GROUP BY pie.isic_4digit_code
`; `;
const [isic4Results] = await connection.query(query, [year, month]); const [isic4Results] = await connection.query(query, [year, quarter, year, quarter]);
for (const result of isic4Results) { for (const result of isic4Results) {
await connection.query( await connection.query(
`INSERT INTO isic_4digit_indices `INSERT INTO isic_4digit_indices
(isic_4digit_code, year, month, month_name, total_weight, (isic_4digit_code, year, quarter, quarter_name, total_weight,
weighted_index_sum, isic_4digit_index) weighted_index_sum, isic_4digit_index)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@ -349,7 +326,7 @@ class IPICalculationService {
weighted_index_sum=VALUES(weighted_index_sum), weighted_index_sum=VALUES(weighted_index_sum),
isic_4digit_index=VALUES(isic_4digit_index)`, isic_4digit_index=VALUES(isic_4digit_index)`,
[ [
result.isic_4digit_code, year, month, monthNames[month], result.isic_4digit_code, year, quarter, quarter,
result.total_weight, result.total_weight,
result.weighted_index_sum, result.weighted_index_sum,
result.isic_4digit_index result.isic_4digit_index
@ -358,7 +335,7 @@ class IPICalculationService {
} }
// Update log // Update log
await this.logRecord('isic_4digit_indices', year, month, 'Completed', logId, isic4Results.length, null,connection); await this.logRecord('isic_4digit_indices', year, quarter, 'Completed', logId, isic4Results.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateISIC4DigitIndices - ISIC 4-digit indices calculated: ${isic4Results.length} codes`); logger.info(`calculateISIC4DigitIndices - ISIC 4-digit indices calculated: ${isic4Results.length} codes`);
@ -370,7 +347,7 @@ class IPICalculationService {
logger.error(`calculateISIC4DigitIndices - Error calculating ISIC 4-digit indices: ${error} `); logger.error(`calculateISIC4DigitIndices - Error calculating ISIC 4-digit indices: ${error} `);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('isic_4digit_indices', year, month, 'Failed', logId, 0, error.message); await this.logRecord('isic_4digit_indices', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -380,18 +357,16 @@ class IPICalculationService {
// STEP 5: Calculate ISIC 3-Digit Level Indices // STEP 5: Calculate ISIC 3-Digit Level Indices
async calculateISIC3DigitIndices(year, month) { async calculateISIC3DigitIndices(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`calculateISIC3DigitIndices - Calculating ISIC 3-digit indices for ${year}-${month}...`); logger.info(`calculateISIC3DigitIndices - Calculating ISIC 3-digit indices for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'isic_3digit_indices', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('isic_3digit_indices', year, quarter, 'Started', null, 0, null, connection);
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const query = ` const query = `
SELECT SELECT
@ -400,16 +375,16 @@ class IPICalculationService {
SUM(total_weight * isic_4digit_index) AS weighted_index_sum, SUM(total_weight * isic_4digit_index) AS weighted_index_sum,
(SUM(total_weight * isic_4digit_index) / SUM(total_weight)) AS isic_3digit_index (SUM(total_weight * isic_4digit_index) / SUM(total_weight)) AS isic_3digit_index
FROM isic_4digit_indices FROM isic_4digit_indices
WHERE year = ? AND month = ? WHERE year = ? AND quarter = ?
GROUP BY LEFT(isic_4digit_code, 3) GROUP BY LEFT(isic_4digit_code, 3)
`; `;
const [isic3Results] = await connection.query(query, [year, month]); const [isic3Results] = await connection.query(query, [year, quarter]);
for (const result of isic3Results) { for (const result of isic3Results) {
await connection.query( await connection.query(
`INSERT INTO isic_3digit_indices `INSERT INTO isic_3digit_indices
(isic_3digit_code, year, month, month_name, total_weight, (isic_3digit_code, year, quarter, quarter_name, total_weight,
weighted_index_sum, isic_3digit_index) weighted_index_sum, isic_3digit_index)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@ -417,7 +392,7 @@ class IPICalculationService {
weighted_index_sum=VALUES(weighted_index_sum), weighted_index_sum=VALUES(weighted_index_sum),
isic_3digit_index=VALUES(isic_3digit_index)`, isic_3digit_index=VALUES(isic_3digit_index)`,
[ [
result.isic_3digit_code, year, month, monthNames[month], result.isic_3digit_code, year, quarter, quarter,
result.total_weight, result.weighted_index_sum, result.total_weight, result.weighted_index_sum,
result.isic_3digit_index result.isic_3digit_index
] ]
@ -425,7 +400,7 @@ class IPICalculationService {
} }
// Update log // Update log
await this.logRecord('isic_3digit_indices', year, month, 'Completed', logId, isic3Results.length, null,connection); await this.logRecord('isic_3digit_indices', year, quarter, 'Completed', logId, isic3Results.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateISIC3DigitIndices - ISIC 3-digit indices calculated: ${isic3Results.length} codes`); logger.info(`calculateISIC3DigitIndices - ISIC 3-digit indices calculated: ${isic3Results.length} codes`);
@ -437,7 +412,7 @@ class IPICalculationService {
logger.error(`calculateISIC3DigitIndices - Error calculating ISIC 3-digit indices: ${error} `); logger.error(`calculateISIC3DigitIndices - Error calculating ISIC 3-digit indices: ${error} `);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('isic_3digit_indices', year, month, 'Failed', logId, 0, error.message); await this.logRecord('isic_3digit_indices', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -447,18 +422,16 @@ class IPICalculationService {
// STEP 6: Calculate ISIC 2-Digit Level Indices // STEP 6: Calculate ISIC 2-Digit Level Indices
async calculateISIC2DigitIndices(year, month) { async calculateISIC2DigitIndices(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`calculateISIC2DigitIndices - Calculating ISIC 2-digit indices for ${year}-${month}...`); logger.info(`calculateISIC2DigitIndices - Calculating ISIC 2-digit indices for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'isic_2digit_indices', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('isic_2digit_indices', year, quarter, 'Started', null, 0, null, connection);
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const query = ` const query = `
SELECT SELECT
@ -467,16 +440,16 @@ class IPICalculationService {
SUM(total_weight * isic_3digit_index) AS weighted_index_sum, SUM(total_weight * isic_3digit_index) AS weighted_index_sum,
(SUM(total_weight * isic_3digit_index) / SUM(total_weight)) AS isic_2digit_index (SUM(total_weight * isic_3digit_index) / SUM(total_weight)) AS isic_2digit_index
FROM isic_3digit_indices FROM isic_3digit_indices
WHERE year = ? AND month = ? WHERE year = ? AND quarter = ?
GROUP BY LEFT(isic_3digit_code, 2) GROUP BY LEFT(isic_3digit_code, 2)
`; `;
const [isic2Results] = await connection.query(query, [year, month]); const [isic2Results] = await connection.query(query, [year, quarter]);
for (const result of isic2Results) { for (const result of isic2Results) {
await connection.query( await connection.query(
`INSERT INTO isic_2digit_indices `INSERT INTO isic_2digit_indices
(isic_2digit_code, year, month, month_name, total_weight, (isic_2digit_code, year, quarter, quarter_name, total_weight,
weighted_index_sum, isic_2digit_index) weighted_index_sum, isic_2digit_index)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@ -484,7 +457,7 @@ class IPICalculationService {
weighted_index_sum=VALUES(weighted_index_sum), weighted_index_sum=VALUES(weighted_index_sum),
isic_2digit_index=VALUES(isic_2digit_index)`, isic_2digit_index=VALUES(isic_2digit_index)`,
[ [
result.isic_2digit_code, year, month, monthNames[month], result.isic_2digit_code, year, quarter, quarter,
result.total_weight, result.weighted_index_sum, result.total_weight, result.weighted_index_sum,
result.isic_2digit_index result.isic_2digit_index
] ]
@ -492,7 +465,7 @@ class IPICalculationService {
} }
// Update log // Update log
await this.logRecord('isic_2digit_indices', year, month, 'Completed', logId, isic2Results.length, null,connection); await this.logRecord('isic_2digit_indices', year, quarter, 'Completed', logId, isic2Results.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateISIC2DigitIndices - ISIC 2-digit indices calculated: ${isic2Results.length} codes`); logger.info(`calculateISIC2DigitIndices - ISIC 2-digit indices calculated: ${isic2Results.length} codes`);
@ -504,7 +477,7 @@ class IPICalculationService {
logger.error(`calculateISIC2DigitIndices - Error calculating ISIC 2-digit indices: ${error}`); logger.error(`calculateISIC2DigitIndices - Error calculating ISIC 2-digit indices: ${error}`);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('isic_2digit_indices', year, month, 'Failed', logId, 0, error.message); await this.logRecord('isic_2digit_indices', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -514,18 +487,16 @@ class IPICalculationService {
// STEP 7: Calculate Manufacturing IPI (Headline Index) // STEP 7: Calculate Manufacturing IPI (Headline Index)
async calculateManufacturingIPI(year, month) { async calculateManufacturingIPI(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
let logId = null; let logId = null;
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(`calculateManufacturingIPI - Calculating Manufacturing indices for ${year}-${month}...`); logger.info(`calculateManufacturingIPI - Calculating Manufacturing indices for ${year}-${quarter}...`);
// Log calculation start // Log calculation start
logId = await this.logRecord( 'manufacturing_ipi', year, month, 'Started', null, 0, null, connection); logId = await this.logRecord('manufacturing_ipi', year, quarter, 'Started', null, 0, null, connection);
const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Calculate Manufacturing IPI // Calculate Manufacturing IPI
const query = ` const query = `
@ -534,10 +505,10 @@ class IPICalculationService {
SUM(total_weight * isic_2digit_index) AS weighted_index_sum, SUM(total_weight * isic_2digit_index) AS weighted_index_sum,
(SUM(total_weight * isic_2digit_index) / SUM(total_weight)) AS manufacturing_index (SUM(total_weight * isic_2digit_index) / SUM(total_weight)) AS manufacturing_index
FROM isic_2digit_indices FROM isic_2digit_indices
WHERE year = ? AND month = ? WHERE year = ? AND quarter = ?
`; `;
const [result] = await connection.query(query, [year, month]); const [result] = await connection.query(query, [year, quarter]);
// If no data found (or aggregate returns NULLs), skip insert to avoid NOT NULL DB errors // If no data found (or aggregate returns NULLs), skip insert to avoid NOT NULL DB errors
if ( if (
@ -547,47 +518,50 @@ class IPICalculationService {
result[0].weighted_index_sum == null || result[0].weighted_index_sum == null ||
result[0].manufacturing_index == null result[0].manufacturing_index == null
) { ) {
await this.logRecord('manufacturing_ipi', year, month, 'Completed', logId, 0, null, connection); await this.logRecord('manufacturing_ipi', year, quarter, 'Completed', logId, 0, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateManufacturingIPI - No valid ISIC 2-digit aggregates found for ${year}-${month}`); logger.info(`calculateManufacturingIPI - No valid ISIC 2-digit aggregates found for ${year}-${quarter}`);
return { return {
success: true, success: true,
index: null, index: null,
qoqChange: null,
momChange: null, momChange: null,
yoyChange: null, yoyChange: null,
message: "No IPI data found for the given year and month" message: "No IPI data found for the given year and quarter"
}; };
} }
const ipiData = result[0]; const ipiData = result[0];
// Calculate MoM and YoY changes // Calculate QoQ and YoY changes
let momChange = null; let qoqChange = null;
let yoyChange = null; let yoyChange = null;
// Get previous month index for MoM // Previous quarter (QoQ)
const prevMonth = month === 1 ? 12 : month - 1; const quarterOrder = ['Q1', 'Q2', 'Q3', 'Q4'];
const prevYear = month === 1 ? year - 1 : year; const quarterIdx = quarterOrder.indexOf(quarter);
const prevQuarter = quarterIdx === 0 ? 'Q4' : quarterOrder[quarterIdx - 1];
const prevYear = quarterIdx === 0 ? year - 1 : year;
const [prevMonthData] = await connection.query( const [prevQuarterData] = await connection.query(
`SELECT manufacturing_index FROM manufacturing_ipi `SELECT manufacturing_index FROM manufacturing_ipi
WHERE year = ? AND month = ?`, WHERE year = ? AND quarter = ?`,
[prevYear, prevMonth] [prevYear, prevQuarter]
); );
if (prevMonthData.length > 0 && prevMonthData[0].manufacturing_index) { if (prevQuarterData.length > 0 && prevQuarterData[0].manufacturing_index) {
const prevIndex = prevMonthData[0].manufacturing_index; const prevIndex = prevQuarterData[0].manufacturing_index;
if (Number(prevIndex) !== 0) { if (Number(prevIndex) !== 0) {
momChange = ((ipiData.manufacturing_index - prevIndex) / prevIndex) * 100; qoqChange = ((ipiData.manufacturing_index - prevIndex) / prevIndex) * 100;
} }
} }
// Get same month last year for YoY // Get same quarter last year for YoY
const [lastYearData] = await connection.query( const [lastYearData] = await connection.query(
`SELECT manufacturing_index FROM manufacturing_ipi `SELECT manufacturing_index FROM manufacturing_ipi
WHERE year = ? AND month = ?`, WHERE year = ? AND quarter = ?`,
[year - 1, month] [year - 1, quarter]
); );
if (lastYearData.length > 0 && lastYearData[0].manufacturing_index) { if (lastYearData.length > 0 && lastYearData[0].manufacturing_index) {
@ -597,13 +571,13 @@ class IPICalculationService {
} }
} }
// Reference date is the first day of the month const quarterStartMonth = { Q1: '01', Q2: '04', Q3: '07', Q4: '10' };
const referenceDate = `${year}-${String(month).padStart(2, '0')}-01`; const referenceDate = `${year}-${quarterStartMonth[quarter]}-01`;
// Insert Manufacturing IPI // Insert Manufacturing IPI
await connection.query( await connection.query(
`INSERT INTO manufacturing_ipi `INSERT INTO manufacturing_ipi
(year, month, month_name, reference_date, total_weight, (year, quarter, quarter_name, reference_date, total_weight,
weighted_index_sum, manufacturing_index, mom_change, yoy_change, status, generated_on) weighted_index_sum, manufacturing_index, mom_change, yoy_change, status, generated_on)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Completed',?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Completed',?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
@ -615,27 +589,28 @@ class IPICalculationService {
status=VALUES(status), status=VALUES(status),
generated_on = VALUES(generated_on)`, generated_on = VALUES(generated_on)`,
[ [
year, month, monthNames[month], referenceDate, year, quarter, quarter, referenceDate,
ipiData.total_weight, ipiData.weighted_index_sum, ipiData.total_weight, ipiData.weighted_index_sum,
ipiData.manufacturing_index, momChange, yoyChange , new Date() ipiData.manufacturing_index, qoqChange, yoyChange , new Date()
] ]
); );
// Update log // Update log
await this.logRecord('manufacturing_ipi', year, month, 'Completed', logId, result.length, null,connection); await this.logRecord('manufacturing_ipi', year, quarter, 'Completed', logId, result.length, null, connection);
await connection.commit(); await connection.commit();
logger.info(`calculateManufacturingIPI - Manufacturing IPI calculated: ${ipiData.manufacturing_index}`); logger.info(`calculateManufacturingIPI - Manufacturing IPI calculated: ${ipiData.manufacturing_index}`);
logger.info(`calculateManufacturingIPI - MoM: ${momChange ? momChange.toFixed(2) + '%' : 'N/A'}`); logger.info(`calculateManufacturingIPI - QoQ: ${qoqChange ? qoqChange.toFixed(2) + '%' : 'N/A'}`);
logger.info(`calculateManufacturingIPI - YoY: ${yoyChange ? yoyChange.toFixed(2) + '%' : 'N/A'}`); logger.info(`calculateManufacturingIPI - YoY: ${yoyChange ? yoyChange.toFixed(2) + '%' : 'N/A'}`);
return { return {
success: true, success: true,
index: ipiData.manufacturing_index, index: ipiData.manufacturing_index,
momChange, qoqChange,
momChange: qoqChange,
yoyChange yoyChange
}; };
@ -645,7 +620,7 @@ class IPICalculationService {
logger.error(`calculateManufacturingIPI - Error calculating Manufacturing IPI: ${error}`); logger.error(`calculateManufacturingIPI - Error calculating Manufacturing IPI: ${error}`);
// Update log OUTSIDE the failed transaction // Update log OUTSIDE the failed transaction
if (logId) { if (logId) {
await this.logRecord('manufacturing_ipi', year, month, 'Failed', logId, 0, error.message); await this.logRecord('manufacturing_ipi', year, quarter, 'Failed', logId, 0, error.message);
} }
throw error; throw error;
} finally { } finally {
@ -655,28 +630,28 @@ class IPICalculationService {
// MASTER FUNCTION: Run Complete Calculation Pipeline // MASTER FUNCTION: Run Complete Calculation Pipeline
async runCompleteCalculation(year, month) { async runCompleteCalculation(year, quarter) {
logger.info(`**************************** Starting IPI Calculation for ${year}-${month} ****************************`); logger.info(`**************************** Starting IPI Calculation for ${year}-${quarter} ****************************`);
try { try {
// Step 1: Aggregate monthly production // Step 1: Aggregate quarterly production
await this.aggregateMonthlyProduction(year, month); await this.aggregateQuarterlyProduction(year, quarter);
// Step 2: Calculate item level indices // Step 2: Calculate item level indices
await this.calculateItemLevelIndices(year, month); await this.calculateItemLevelIndices(year, quarter);
// Step 3: Calculate ISIC 4-digit indices // Step 3: Calculate ISIC 4-digit indices
await this.calculateISIC4DigitIndices(year, month); await this.calculateISIC4DigitIndices(year, quarter);
// Step 4: Calculate ISIC 3-digit indices // Step 4: Calculate ISIC 3-digit indices
await this.calculateISIC3DigitIndices(year, month); await this.calculateISIC3DigitIndices(year, quarter);
// Step 5: Calculate ISIC 2-digit indices // Step 5: Calculate ISIC 2-digit indices
await this.calculateISIC2DigitIndices(year, month); await this.calculateISIC2DigitIndices(year, quarter);
// Step 6: Calculate Manufacturing IPI // Step 6: Calculate Manufacturing IPI
const result = await this.calculateManufacturingIPI(year, month); const result = await this.calculateManufacturingIPI(year, quarter);
logger.info(`**************************** IPI Calculation Completed Successfully ****************************`); logger.info(`**************************** IPI Calculation Completed Successfully ****************************`);
@ -746,7 +721,7 @@ async logRecord(calculation_type, reference_year, reference_month = null, status
product_id, product_id,
product_hs_code, product_hs_code,
avg_by_production, avg_by_production,
jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, \`dec\` q1, q2, q3, q4
FROM base_year_production FROM base_year_production
WHERE base_year = ? WHERE base_year = ?
ORDER BY product_id ORDER BY product_id
@ -780,46 +755,46 @@ async logRecord(calculation_type, reference_year, reference_month = null, status
// HELPER: Clear All Calculated Data for a Period // HELPER: Clear All Calculated Data for a Period
async clearCalculatedData(year, month) { async clearCalculatedData(year, quarter) {
const connection = await this.pool.getConnection(); const connection = await this.pool.getConnection();
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
logger.info(` Clearing calculated data for ${year}-${month} `); logger.info(` Clearing calculated data for ${year}-${quarter} `);
await connection.query( await connection.query(
`DELETE FROM monthly_production WHERE year = ? AND month = ?`, `DELETE FROM monthly_production WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.query( await connection.query(
`DELETE FROM item_level_indices WHERE year = ? AND month = ?`, `DELETE FROM item_level_indices WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.query( await connection.query(
`DELETE FROM isic_4digit_indices WHERE year = ? AND month = ?`, `DELETE FROM isic_4digit_indices WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.query( await connection.query(
`DELETE FROM isic_3digit_indices WHERE year = ? AND month = ?`, `DELETE FROM isic_3digit_indices WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.query( await connection.query(
`DELETE FROM isic_2digit_indices WHERE year = ? AND month = ?`, `DELETE FROM isic_2digit_indices WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.query( await connection.query(
`DELETE FROM manufacturing_ipi WHERE year = ? AND month = ?`, `DELETE FROM manufacturing_ipi WHERE year = ? AND quarter = ?`,
[year, month] [year, quarter]
); );
await connection.commit(); await connection.commit();
logger.info(` Data cleared for ${year}-${month} `); logger.info(` Data cleared for ${year}-${quarter} `);
return { success: true }; return { success: true };

View File

@ -107,31 +107,12 @@ class AutomatedSchedulerService {
console.log(`[${new Date().toISOString()}] Starting calculate_quarter for ${year} ${quarter}`); console.log(`[${new Date().toISOString()}] Starting calculate_quarter for ${year} ${quarter}`);
try { try {
// Quarter to month mapping if (!['Q1', 'Q2', 'Q3', 'Q4'].includes(quarter)) {
const quarterMap = {
Q1: { startMonth: 1, endMonth: 3 },
Q2: { startMonth: 4, endMonth: 6 },
Q3: { startMonth: 7, endMonth: 9 },
Q4: { startMonth: 10, endMonth: 12 },
};
const selectedQuarter = quarterMap[quarter];
if (!selectedQuarter) {
throw new Error('Invalid quarter. Accepted values: Q1, Q2, Q3, Q4'); throw new Error('Invalid quarter. Accepted values: Q1, Q2, Q3, Q4');
} }
const { startMonth, endMonth } = selectedQuarter;
const service = new IPICalculationService(dbConfig); const service = new IPICalculationService(dbConfig);
const results = []; const result = await service.runCompleteCalculation(year, quarter);
for (let month = startMonth; month <= endMonth; month++) {
const result = await service.runCompleteCalculation(year, month);
results.push({
month,
...result
});
}
await service.close(); await service.close();
@ -139,8 +120,8 @@ class AutomatedSchedulerService {
return { return {
success: true, success: true,
message: `IPI calculated for ${year} months ${startMonth}-${endMonth}`, message: `IPI calculated for ${year} ${quarter}`,
data: results data: result
}; };
} catch (error) { } catch (error) {
console.error(`[${new Date().toISOString()}] Error in calculate_quarter:`, error); console.error(`[${new Date().toISOString()}] Error in calculate_quarter:`, error);

View File

@ -44,7 +44,7 @@
<div class="wrap"> <div class="wrap">
<div class="card"> <div class="card">
<h2 style="margin:0 0 8px 0;">IPI Calculation Demo Dashboard</h2> <h2 style="margin:0 0 8px 0;">IPI Calculation Demo Dashboard</h2>
<div class="muted">Use Swagger to trigger calculation first, then view data here by year/month.</div> <div class="muted">Use Swagger to trigger calculation first, then view data by year + month (month is mapped to quarter).</div>
<div class="row" style="margin-top:12px;"> <div class="row" style="margin-top:12px;">
<label>Year <input id="year" type="number" value="<%= defaultYear %>" min="2000" max="2100" /></label> <label>Year <input id="year" type="number" value="<%= defaultYear %>" min="2000" max="2100" /></label>
<label>Month <input id="month" type="number" value="<%= defaultMonth %>" min="1" max="12" /></label> <label>Month <input id="month" type="number" value="<%= defaultMonth %>" min="1" max="12" /></label>
@ -252,8 +252,8 @@
{ key: "product_id", label: "Product ID" }, { key: "product_id", label: "Product ID" },
{ key: "product_hs_code", label: "HS Code" }, { key: "product_hs_code", label: "HS Code" },
{ key: "year", label: "Year" }, { key: "year", label: "Year" },
{ key: "month", label: "Month" }, { key: "quarter", label: "Quarter" },
{ key: "month_name", label: "Month Name" }, { key: "quarter_name", label: "Quarter Name" },
{ key: "production_quantity", label: "Production Qty" }, { key: "production_quantity", label: "Production Qty" },
{ key: "unit", label: "Unit" }, { key: "unit", label: "Unit" },
]; ];
@ -264,10 +264,10 @@
{ key: "product_hs_code", label: "HS Code" }, { key: "product_hs_code", label: "HS Code" },
{ key: "base_year", label: "Base Year" }, { key: "base_year", label: "Base Year" },
{ key: "avg_by_production", label: "Avg Base Year Prod." }, { key: "avg_by_production", label: "Avg Base Year Prod." },
{ key: "jan", label: "Jan" }, { key: "feb", label: "Feb" }, { key: "mar", label: "Mar" }, { key: "q1", label: "Q1" },
{ key: "apr", label: "Apr" }, { key: "may", label: "May" }, { key: "jun", label: "Jun" }, { key: "q2", label: "Q2" },
{ key: "jul", label: "Jul" }, { key: "aug", label: "Aug" }, { key: "sep", label: "Sep" }, { key: "q3", label: "Q3" },
{ key: "oct", label: "Oct" }, { key: "nov", label: "Nov" }, { key: "dec", label: "Dec" }, { key: "q4", label: "Q4" },
]; ];
} }
return [ return [

File diff suppressed because one or more lines are too long