From 01d5c7a1c060596fae6496b7b964c5ab67b117e5 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 10 Nov 2025 17:53:05 +0530 Subject: [PATCH] Geetha : unitmaster file upload --- app/controllers/unitMasterController.js | 231 ++++++++++++++++++++++++ app/routes/routes.js | 34 ++++ 2 files changed, 265 insertions(+) diff --git a/app/controllers/unitMasterController.js b/app/controllers/unitMasterController.js index 53f77ef..1baa530 100644 --- a/app/controllers/unitMasterController.js +++ b/app/controllers/unitMasterController.js @@ -2,6 +2,8 @@ const db = require("../models"); const { Op } = require("sequelize"); const { Sequelize } = require("sequelize"); const { UnitMaster } = require("../models"); +const fs = require("fs"); +const csv = require("csv-parser"); // Create new Unit exports.createUnit = async (req, res) => { @@ -72,3 +74,232 @@ exports.deleteUnit = async (req, res) => { res.status(500).json({ status: "error", message: err.message }); } }; + + +exports.uploadUnitMasterFromCSV = async (req, res) => { + try { + if (!req.file) { + return res.status(400).send({ + status: "failed", + message: "No file uploaded.", + }); + } + + const filePath = req.file.path; + + // Validate file type + if (!req.file.originalname.endsWith(".csv")) { + fs.unlinkSync(filePath); + return res.status(400).send({ + status: "failed", + message: "Invalid file type. Only CSV allowed.", + }); + } + + // Validate created_by + if (!req.body.created_by || isNaN(req.body.created_by)) { + fs.unlinkSync(filePath); + return res.status(400).send({ + status: "failed", + message: "Invalid or missing created_by.", + }); + } + + // Check if file is empty + const stats = fs.statSync(filePath); + if (stats.size === 0) { + fs.unlinkSync(filePath); + return res.status(400).send({ + status: "failed", + message: "Uploaded file is empty.", + }); + } + + const results = []; + fs.createReadStream(filePath) + .pipe(csv()) + .on("data", (row) => results.push(row)) + .on("end", async () => { + try { + if (results.length === 0) { + fs.unlinkSync(filePath); + return res.status(400).send({ + status: "failed", + message: "CSV file is empty or invalid.", + }); + } + + const requiredCols = ["Display Name", "Unit Name", "Description"]; + const headers = Object.keys(results[0] || {}); + + //Check for missing or extra columns + const missingCols = requiredCols.filter((col) => !headers.includes(col)); + const extraCols = headers.filter((col) => !requiredCols.includes(col)); + + if (missingCols.length > 0 || extraCols.length > 0) { + fs.unlinkSync(filePath); + let msg = ""; + if (missingCols.length > 0) + msg += `Missing required columns: ${missingCols.join(", ")}. `; + if (extraCols.length > 0) + msg += `Unexpected columns found: ${extraCols.join(", ")}. Only 'Display Name', 'Unit Name', and 'Description' are allowed.`; + return res.status(400).send({ status: "failed", message: msg.trim() }); + } + + const inserted = []; + const duplicates = []; // DB duplicates + const errors = []; + + //Detect duplicates within uploaded file + const seenShortNames = new Set(); + const seenUnitNames = new Set(); + const fileDuplicates = []; + + for (const [index, row] of results.entries()) { + const uomShort = row["Display Name"]?.trim().toUpperCase(); + const uom = row["Unit Name"]?.trim(); + const description = row["Description"]?.trim(); + + if (!uomShort || !uom) { + errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." }); + continue; + } + + if (!/^[A-Z]+$/i.test(uomShort)) { + errors.push({ row: index + 1, reason: "Display Name must contain only alphabetic characters (A–Z)." }); + continue; + } + + if (uomShort.length > 5) { + errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." }); + continue; + } + + const shortKey = uomShort.toLowerCase(); + const uomKey = uom.toLowerCase(); + + if (seenShortNames.has(shortKey) || seenUnitNames.has(uomKey)) { + fileDuplicates.push({ row: index + 1, uom_short_name: uomShort, uom }); + continue; + } + + seenShortNames.add(shortKey); + seenUnitNames.add(uomKey); + } + + // Stop if file has duplicate rows + if (fileDuplicates.length > 0) { + fs.unlinkSync(filePath); + return res.status(400).send({ + status: "failed", + message: "Duplicate entries found within uploaded file.", + duplicate_rows: fileDuplicates, + }); + } + + // Process each row and check against DB + for (const [index, row] of results.entries()) { + try { + const uomShort = row["Display Name"]?.trim().toUpperCase(); + const uom = row["Unit Name"]?.trim(); + const description = row["Description"]?.trim(); + + if (!uomShort || !uom) { + errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." }); + continue; + } + + if (uomShort.length > 5) { + errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." }); + continue; + } + + //Check for existing record (duplicate) in DB + const existing = await UnitMaster.findOne({ + where: { + [Op.or]: [ + Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("uom_short_name")), + uomShort.toLowerCase() + ), + Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("uom")), + uom.toLowerCase() + ), + ], + }, + }); + + if (existing) { + duplicates.push({ + id: existing.id, + uom_short_name: existing.uom_short_name, + uom: existing.uom, + }); + continue; + } + + // Insert record + const newUnit = await UnitMaster.create({ + uom_short_name: uomShort, + uom: uom, + description: description || null, + created_by: parseInt(req.body.created_by), + created_at: new Date(), + }); + + inserted.push(newUnit); + } catch (err) { + errors.push({ + row: index + 1, + reason: err.message, + }); + } + } + + // Cleanup file + fs.unlinkSync(filePath); + + // Final response summary + let finalStatus = "success"; + let message = `${inserted.length} units inserted successfully.`; + + if (errors.length > 0 || duplicates.length > 0) { + finalStatus = inserted.length > 0 ? "partial_success" : "failed"; + if (finalStatus === "partial_success") { + message = `${inserted.length} units inserted, ${duplicates.length} duplicates skipped, ${errors.length} validation errors found.`; + } else { + message = `No units imported. ${duplicates.length} duplicates and ${errors.length} validation errors found.`; + } + } + + return res.status(200).send({ + status: finalStatus, + message, + summary: { + total_records: results.length, + imported: inserted.length, + skipped: duplicates.length, + errors: errors.length, + }, + duplicates, + error_details: errors, + }); + } catch (err) { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + return res.status(500).send({ + status: "failed", + message: err.message, + }); + } + }); + } catch (error) { + console.error("Error uploading Unit Master CSV:", error); + if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); + return res.status(500).send({ + status: "failed", + message: "Error processing CSV file.", + error: error.message, + }); + } +}; diff --git a/app/routes/routes.js b/app/routes/routes.js index 8a7c338..4a3d7fa 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -1774,6 +1774,40 @@ router.put("/unit_master/:id",[verifySignature, verifyToken], unitMasterControll */ router.delete("/unit_master/:id",[verifySignature, verifyToken], unitMasterController.deleteUnit); +/** + * @swagger + * /api/unit-master/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: [Unit Master] + * security: + * - appSignature: [] + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * description: CSV file to upload + * created_by: + * type: integer + * description: ID of the user performing the upload + * responses: + * 201: + * description: Unit Master file uploaded successfully + * 400: + * description: No file uploaded + * 500: + * description: Server error + */ +router.post("/unit-master/uploadCSV",[ verifySignature, verifyToken, upload.single("file")], unitMasterController.uploadUnitMasterFromCSV); +