From 6724b1376587580b431bda2038192c65dd3ef8b2 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 4 Nov 2025 12:22:13 +0530 Subject: [PATCH] GWM : product upload and get previous quarter data --- app/controllers/products.controller.js | 51 ++++++++++++ app/controllers/submission.controller.js | 45 ++++++++++ app/routes/routes.js | 100 +++++++++++++++++++++++ package-lock.json | 12 +++ package.json | 1 + 5 files changed, 209 insertions(+) diff --git a/app/controllers/products.controller.js b/app/controllers/products.controller.js index b017c56..1553d94 100644 --- a/app/controllers/products.controller.js +++ b/app/controllers/products.controller.js @@ -1,5 +1,9 @@ const db = require("../models"); const Product = db.Product; +const fs = require("fs"); +const csv = require("csv-parser"); +const path = require("path"); + exports.createProduct = async (req, res) => { try { @@ -66,3 +70,50 @@ exports.deleteProduct = async (req, res) => { res.status(500).send({'status':"failed",'message':err.message }); } }; + +exports.uploadProductsFromCSV = async (req, res) => { + try { + if (!req.file) { + return res.status(400).send({ status: "failed", message: "No file uploaded" }); + } + + const results = []; + const filePath = req.file.path; + + // Read CSV and store rows + fs.createReadStream(filePath) + .pipe(csv()) + .on("data", (row) => { + results.push(row); + }) + .on("end", async () => { + try { + // Insert all rows into Product table + const inserted = await Product.bulkCreate(results, { validate: true }); + fs.unlinkSync(filePath); // delete file after processing + + res.status(201).send({ + status: "success", + message: `${inserted.length} products inserted successfully`, + data: inserted, + }); + } catch (dbErr) { + res.status(500).send({ + status: "failed", + message: dbErr.message, + }); + } + }); + } catch (error) { + res.status(500).send({ status: "failed", message: error.message }); + } +}; + +exports.downloadProductSample = async (req, res) => { + try { + const filePath = path.join(__dirname, "../writable/uploads/sample_files/products_upload_sample.csv"); + return res.download(filePath, "products_upload_sample.csv"); + } catch (err) { + return res.status(500).send({ status: "failed", message: err.message }); + } +}; diff --git a/app/controllers/submission.controller.js b/app/controllers/submission.controller.js index dc5603a..5cf4eb0 100644 --- a/app/controllers/submission.controller.js +++ b/app/controllers/submission.controller.js @@ -526,3 +526,48 @@ exports.getQuarterPeriods = async (req, res) => { return res.status(500).json({ status: "failed", message: err.message }); } }; + +exports.getPreviousForecastData = async (req, res) => { + try { + + const { establishment_id, quarter, year, product_id } = req.query; + + if(!establishment_id || !quarter || !year || !product_id){ + return res.status(400).json({ status:"failed", message:"Missing required query params" }); + } + + // find submission id + const submission = await Submission.findOne({ + where : { establishment_id, quarter, year }, + }); + + if(!submission){ + return res.status(404).json({ status:"failed", message:"No previous submission found" }); + } + + // find submission product row + const submissionProduct = await SubmissionProduct.findOne({ + where : { + submission_id : submission.id, + product_id + }, + include:[ + { + model: Product, + as: "product", + attributes:["product_name","hs_code"] + } + ] + }); + + if(!submissionProduct){ + return res.status(404).json({ status:"failed", message:"No previous product row found" }); + } + + return res.status(200).json({ status:"success", data: submissionProduct }); + + } catch (err) { + return res.status(500).json({ status:"failed", message: err.message }); + } +}; + diff --git a/app/routes/routes.js b/app/routes/routes.js index f53cf7c..df050c7 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -18,8 +18,15 @@ const dashboardController = require("../controllers/dashboard.controller"); const notificationTemplateController = require("../controllers/notificationTemplate.controller"); const quarterlyWindowsController = require("../controllers/quarterlyWindowsConfiguration.controller"); +const path = require("path"); +const multer = require("multer"); +const upload = multer({ dest: "../writable/uploads/products_bulk_uploads_files" }); + + + + /** * @swagger * tags: @@ -1015,6 +1022,62 @@ router.put("/products/:id",[verifySignature, verifyToken], productController.upd */ router.delete("/products/:id",[verifySignature, verifyToken], productController.deleteProduct); +/** + * @swagger + * /api/products/uploadCSV: + * post: + * summary: Upload products in bulk using CSV file + * description: This API accepts CSV file and inserts multiple products in bulk. CSV header columns must match Product table columns. + * tags: [Products] + * security: + * - appSignature: [] + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * description: CSV file to upload + * responses: + * 201: + * description: Products uploaded successfully + * 400: + * description: No file uploaded + * 500: + * description: Server error + */ +router.post("/products/uploadCSV",[ verifySignature, verifyToken, upload.single("file")], productController.uploadProductsFromCSV); + + +/** + * @swagger + * /api/products/download-sample: + * get: + * summary: Download sample CSV template for bulk product upload + * tags: [Products] + * responses: + * 200: + * description: Sample CSV file downloaded successfully + * content: + * text/csv: + * schema: + * type: string + * format: binary + * 404: + * description: File not found + * 500: + * description: Server error + */ +router.get("/download-sample-product-upload-file",[verifySignature, verifyToken], productController.downloadProductSample); + + + + @@ -1964,6 +2027,43 @@ router.put("/approveOrRejectSubmission/:id",[verifySignature, verifyToken], subm */ router.post("/getQuarterPeriods",[verifySignature, verifyToken], submissionController.getQuarterPeriods); +/** + * @swagger + * /api/submissions/getPreviousForecastData: + * get: + * summary: Get Previous and Forecast Data based on Establishment + Quarter + Year + Product + * tags: [Submissions] + * security: + * - appSignature: [] + * - bearerAuth: [] + * parameters: + * - in: query + * name: establishment_id + * required: true + * schema: { type: integer } + * - in: query + * name: quarter + * required: true + * schema: { type: string, enum: [Q1, Q2, Q3, Q4] } + * - in: query + * name: year + * required: true + * schema: { type: integer } + * - in: query + * name: product_id + * required: true + * schema: { type: integer } + * responses: + * 200: + * description: Previous forecast data fetched successfully + * 404: + * description: Not found + * 500: + * description: Server error + */ +router.get("/submissions/getPreviousForecastData", submissionController.getPreviousForecastData); + + diff --git a/package-lock.json b/package-lock.json index c4b01d8..29a980c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "csv-parser": "^3.2.0", "dotenv": "^17.2.3", "exceljs": "^4.4.0", "express": "^5.1.0", @@ -724,6 +725,17 @@ "node": ">= 10" } }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", diff --git a/package.json b/package.json index 090bc5c..3269ed0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "csv-parser": "^3.2.0", "dotenv": "^17.2.3", "exceljs": "^4.4.0", "express": "^5.1.0",