diff --git a/app/controllers/test.controller.js b/app/controllers/test.controller.js new file mode 100644 index 0000000..7d88933 --- /dev/null +++ b/app/controllers/test.controller.js @@ -0,0 +1,243 @@ +const db = require("../models"); + +const VALID_QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +async function createSubmissionWithProducts(transaction, payload) { + const { year, establishment_id, product_id, data } = payload; + + for (const row of data) { + const { quarter, qty, cost } = row; + + await db.sequelize.query( + ` + INSERT INTO submission ( + establishment_id, quarter, year, + edit_request, edit_access, status, + created_by, created_at, updated_at, + approve_reject_status + ) + SELECT ?, ?, ?, 0, 0, 'Approved', 1, + CASE ? + WHEN 'Q1' THEN CONCAT(?, '-03-15 10:00:00') + WHEN 'Q2' THEN CONCAT(?, '-06-15 10:00:00') + WHEN 'Q3' THEN CONCAT(?, '-09-15 10:00:00') + WHEN 'Q4' THEN CONCAT(?, '-12-15 10:00:00') + END, + CASE ? + WHEN 'Q1' THEN CONCAT(?, '-03-15 10:00:00') + WHEN 'Q2' THEN CONCAT(?, '-06-15 10:00:00') + WHEN 'Q3' THEN CONCAT(?, '-09-15 10:00:00') + WHEN 'Q4' THEN CONCAT(?, '-12-15 10:00:00') + END, + 1 + WHERE NOT EXISTS ( + SELECT 1 + FROM submission + WHERE establishment_id = ? + AND quarter = ? + AND year = ? + ) + `, + { + replacements: [ + establishment_id, + quarter, + year, + quarter, + year, + year, + year, + year, + quarter, + year, + year, + year, + year, + establishment_id, + quarter, + year, + ], + transaction, + } + ); + + const submissionRows = await db.sequelize.query( + ` + SELECT id + FROM submission + WHERE establishment_id = ? + AND quarter = ? + AND year = ? + LIMIT 1 + `, + { + replacements: [establishment_id, quarter, year], + type: db.Sequelize.QueryTypes.SELECT, + transaction, + } + ); + + if (!submissionRows.length) { + throw new Error(`Submission not found for ${quarter}-${year}`); + } + + const submission_id = submissionRows[0].id; + + await db.sequelize.query( + ` + INSERT INTO submission_products ( + submission_id, product_id, unit_id, + annual_installed_capacity, + + previous_quantity_period_one, + previous_quantity_period_two, + previous_quantity_period_three, + + previous_cost_period_one, + previous_cost_period_two, + previous_cost_period_three, + + current_quantity_period_one, + current_quantity_period_two, + current_quantity_period_three, + + current_cost_period_one, + current_cost_period_two, + current_cost_period_three, + + forecast_quantity_period_one, + forecast_quantity_period_two, + forecast_quantity_period_three, + + forecast_cost_period_one, + forecast_cost_period_two, + forecast_cost_period_three, + + previous_quantity, previous_cost, + current_quantity, current_cost, + forecast_quantity, forecast_cost, + + remarks, created_by, created_at, updated_at, is_active + ) + SELECT ?, ?, 1, 50000, + 0, 0, 0, + 0, 0, 0, + ?, ?, ?, + ?, ?, ?, + 0, 0, 0, + 0, 0, 0, + 0, 0, + ?, ?, + 0, 0, + CONCAT(?, ' current only'), + 1, NOW(), NOW(), 1 + WHERE NOT EXISTS ( + SELECT 1 + FROM submission_products + WHERE submission_id = ? + AND product_id = ? + ) + `, + { + replacements: [ + submission_id, + product_id, + qty / 3, + qty / 3, + qty / 3, + cost / 3, + cost / 3, + cost / 3, + qty, + cost, + quarter, + submission_id, + product_id, + ], + transaction, + } + ); + } +} + +exports.createSubmissionWithProducts = async (req, res) => { + const payloadList = Array.isArray(req.body) ? req.body : [req.body]; + + if (!payloadList.length) { + return res.status(400).json({ + success: false, + message: "Request body must be a non-empty array", + }); + } + + const invalidItem = payloadList.find( + (item) => + !item || + !item.year || + !item.establishment_id || + !item.product_id || + !Array.isArray(item.data) || + item.data.length === 0 + ); + if (invalidItem) { + return res.status(400).json({ + success: false, + message: + "Each payload item must contain year, establishment_id, product_id and non-empty data array", + }); + } + + const invalidQuarterItem = payloadList.find((item) => + item.data.some((row) => !VALID_QUARTERS.includes(row?.quarter)) + ); + if (invalidQuarterItem) { + const invalidQuarterRow = invalidQuarterItem.data.find( + (row) => !VALID_QUARTERS.includes(row?.quarter) + ); + return res.status(400).json({ + success: false, + message: `Invalid quarter '${invalidQuarterRow.quarter}'. Allowed: Q1, Q2, Q3, Q4`, + }); + } + + const invalidNumberItem = payloadList.find((item) => + item.data.some((row) => Number.isNaN(Number(row?.qty)) || Number.isNaN(Number(row?.cost))) + ); + if (invalidNumberItem) { + return res.status(400).json({ + success: false, + message: "Each data row in each payload item must contain numeric qty and cost values", + }); + } + + const transaction = await db.sequelize.transaction(); + try { + for (const item of payloadList) { + await createSubmissionWithProducts(transaction, { + year: Number(item.year), + establishment_id: Number(item.establishment_id), + product_id: Number(item.product_id), + data: item.data.map((row) => ({ + quarter: row.quarter, + qty: Number(row.qty), + cost: Number(row.cost), + })), + }); + } + + await transaction.commit(); + + return res.status(201).json({ + success: true, + message: "Submission and submission products processed successfully", + total_payload_items: payloadList.length, + }); + } catch (error) { + await transaction.rollback(); + return res.status(500).json({ + success: false, + message: "Failed to process submission data", + error: error.message, + }); + } +}; diff --git a/app/models/product.model.js b/app/models/product.model.js index f361973..cb7d0d5 100644 --- a/app/models/product.model.js +++ b/app/models/product.model.js @@ -42,6 +42,10 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.INTEGER, allowNull: true, }, + isic_code: { + type: DataTypes.INTEGER, + allowNull: true, + }, is_active: { type: DataTypes.BOOLEAN, defaultValue: true, diff --git a/app/routes/routes.js b/app/routes/routes.js index d7d2396..dc962b8 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -19,6 +19,7 @@ const notificationTemplateController = require("../controllers/notificationTempl const quarterlyWindowsController = require("../controllers/quarterlyWindowsConfiguration.controller"); const calculationController = require("../controllers/calculation.controller"); const ManufacturingIpiController = require("../controllers/manufacturingIPI.controller") +const testController = require("../controllers/test.controller"); const fs = require("fs"); const path = require("path"); const multer = require("multer"); @@ -530,6 +531,101 @@ router.put("/admin_users/:id/change-password",[verifySignature, verifyToken], ad router.get("/testEmail", establishmentController.testEmail); +/** + * @swagger + * /api/test/submissions-with-products: + * post: + * summary: Create test submission and submission-products for multiple quarters + * tags: [Submissions] + * security: + * - appSignature: [] + * - CSRF: [] + * cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * required: + * - year + * - establishment_id + * - product_id + * - data + * properties: + * year: + * type: integer + * example: 2022 + * establishment_id: + * type: integer + * example: 2 + * product_id: + * type: integer + * example: 3 + * data: + * type: array + * items: + * type: object + * required: + * - quarter + * - qty + * - cost + * properties: + * quarter: + * type: string + * enum: [Q1, Q2, Q3, Q4] + * example: Q1 + * qty: + * type: number + * example: 456573559 + * cost: + * type: number + * example: 40566560717 + * example: + * - year: 2022 + * establishment_id: 2 + * product_id: 3 + * data: + * - quarter: Q1 + * qty: 456573559 + * cost: 40566560717 + * - quarter: Q2 + * qty: 8755776 + * cost: 7654656 + * - quarter: Q3 + * qty: 7465465 + * cost: 865454 + * - quarter: Q4 + * qty: 656465465 + * cost: 98776 + * - year: 2022 + * establishment_id: 2 + * product_id: 3 + * data: + * - quarter: Q1 + * qty: 456573559 + * cost: 40566560717 + * - quarter: Q2 + * qty: 8755776 + * cost: 7654656 + * - quarter: Q3 + * qty: 7465465 + * cost: 865454 + * - quarter: Q4 + * qty: 656465465 + * cost: 98776 + * responses: + * 201: + * description: Submission and products processed successfully + * 400: + * description: Validation error + * 500: + * description: Server error + */ +router.post("/test/submissions-with-products",[verifySignature], testController.createSubmissionWithProducts); + /** diff --git a/app/services/ipi_calculation_service.js b/app/services/ipi_calculation_service.js index dca31d3..eb324d2 100644 --- a/app/services/ipi_calculation_service.js +++ b/app/services/ipi_calculation_service.js @@ -309,85 +309,34 @@ class IPICalculationService { const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - // Aggregate by establishment ISIC4 for the target month-period. - // This keeps grouping driven by establishment isic_code as requested. + // ISIC mapping source is now products.isic_code. + // Aggregate weighted item indices to ISIC 4-digit using product master mapping. const query = ` - WITH submission_period AS ( - SELECT - s.establishment_id, - sp.product_id, - LEFT(e.isic_code, 4) AS isic_4digit_code, - SUM( - CASE - WHEN ? BETWEEN 1 AND 3 THEN - CASE - WHEN ? = 1 THEN COALESCE(sp.current_quantity_period_one, 0) - WHEN ? = 2 THEN COALESCE(sp.current_quantity_period_two, 0) - ELSE COALESCE(sp.current_quantity_period_three, 0) - END - WHEN ? BETWEEN 4 AND 6 THEN - CASE - WHEN ? = 4 THEN COALESCE(sp.current_quantity_period_one, 0) - WHEN ? = 5 THEN COALESCE(sp.current_quantity_period_two, 0) - ELSE COALESCE(sp.current_quantity_period_three, 0) - END - WHEN ? BETWEEN 7 AND 9 THEN - CASE - WHEN ? = 7 THEN COALESCE(sp.current_quantity_period_one, 0) - WHEN ? = 8 THEN COALESCE(sp.current_quantity_period_two, 0) - ELSE COALESCE(sp.current_quantity_period_three, 0) - END - ELSE - CASE - WHEN ? = 10 THEN COALESCE(sp.current_quantity_period_one, 0) - WHEN ? = 11 THEN COALESCE(sp.current_quantity_period_two, 0) - ELSE COALESCE(sp.current_quantity_period_three, 0) - END - END - ) AS current_production - FROM submission_products sp - JOIN submission s ON s.id = sp.submission_id - JOIN establishments e ON e.id = s.establishment_id - WHERE s.year = ? - AND s.quarter = CASE - WHEN ? BETWEEN 1 AND 3 THEN 'Q1' - WHEN ? BETWEEN 4 AND 6 THEN 'Q2' - WHEN ? BETWEEN 7 AND 9 THEN 'Q3' - ELSE 'Q4' - END - AND s.status = 'Approved' - AND sp.is_active = 1 - AND e.isic_code IS NOT NULL - AND e.isic_code != '' - GROUP BY s.establishment_id, sp.product_id, LEFT(e.isic_code, 4) - ) SELECT - spd.isic_4digit_code, + LEFT(CAST(p.isic_code AS CHAR), 4) AS isic_4digit_code, SUM(COALESCE(p.weight_in_ib, 0)) AS total_weight, SUM( COALESCE(p.weight_in_ib, 0) * - ((spd.current_production / byp.avg_by_production) * 100) + ili.item_index ) AS weighted_index_sum, ( SUM( COALESCE(p.weight_in_ib, 0) * - ((spd.current_production / byp.avg_by_production) * 100) + ili.item_index ) / NULLIF(SUM(COALESCE(p.weight_in_ib, 0)), 0) ) AS isic_4digit_index - FROM submission_period spd - JOIN products p ON p.id = spd.product_id - JOIN base_year_production byp ON byp.product_id = spd.product_id - WHERE byp.avg_by_production > 0 + FROM item_level_indices ili + JOIN products p ON p.id = ili.product_id + WHERE ili.year = ? + AND ili.month = ? + AND p.isic_code IS NOT NULL AND p.weight_in_ib IS NOT NULL AND p.weight_in_ib > 0 - GROUP BY spd.isic_4digit_code + GROUP BY LEFT(CAST(p.isic_code AS CHAR), 4) `; - const [isic4Results] = await connection.query(query, [ - month, month, month, month, month, month, month, month, month, month, month, - year, month, month, month - ]); + const [isic4Results] = await connection.query(query, [year, month]); for (const result of isic4Results) { await connection.query(