From 20344bc58955e9b8a470aae4aadfcaa8dd41de12 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Thu, 4 Jun 2026 10:50:29 +0530 Subject: [PATCH] GWM : product upload issue --- app/controllers/products.controller.js | 44 +++++++--- app/middleware/app.middleware.js | 19 +++- app/middleware/csvUpload.middleware.js | 116 +++++++++++++++++++++++++ app/routes/routes.js | 20 +++-- server.js | 12 ++- 5 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 app/middleware/csvUpload.middleware.js diff --git a/app/controllers/products.controller.js b/app/controllers/products.controller.js index d954c48..6155bd2 100644 --- a/app/controllers/products.controller.js +++ b/app/controllers/products.controller.js @@ -43,20 +43,32 @@ function parseWeightInIb(value) { return { ok: true, value: rounded }; } -/** Map unit_master by full name (uom) and short code (uom_short_name), case-insensitive. */ +/** Normalize unit text for case-insensitive matching (e.g. "numbers" → "numbers", " Numbers " → "numbers"). */ +function normalizeUnitKey(value) { + if (value === null || value === undefined) return ""; + const normalized = String(value).trim().replace(/\s+/g, " ").toLowerCase(); + if (!normalized || normalized === "not defined") return ""; + return normalized; +} + +/** Map unit_master by uom (full unit name), case-insensitive. */ function buildUnitLookupMap(unitMasters) { const unitMap = new Map(); for (const unit of unitMasters) { - if (unit.uom) { - unitMap.set(unit.uom.trim().toLowerCase(), unit.id); - } - if (unit.uom_short_name) { - unitMap.set(unit.uom_short_name.trim().toLowerCase(), unit.id); + const key = normalizeUnitKey(unit.uom); + if (key) { + unitMap.set(key, unit.id); } } return unitMap; } +function resolveUnitId(unitInput, unitMap) { + const key = normalizeUnitKey(unitInput); + if (!key) return null; + return unitMap.get(key) ?? null; +} + function normalizeCsvHeaderKey(key) { if (!key) return ""; const withoutBom = String(key).replace(/^\uFEFF/, ""); @@ -436,7 +448,11 @@ exports.uploadProductsFromCSV = async (req, res) => { if (!req.file) { return sendUploadResponse(res, 400, { status: "failed", - message: "No file uploaded", + message: + 'No CSV file attached. Use multipart form field "file" and select your .csv before submitting.', + hint: + 'In Swagger: open Try it out → pick hs-codes-sample.csv under field "file" → Execute.', + expected_field: "file", }); } @@ -547,7 +563,7 @@ exports.uploadProductsFromCSV = async (req, res) => { const productName = row.product_name ? row.product_name.replace(/\s+/g, " ").trim() : ""; - const unit = row.unit ? row.unit.trim().toLowerCase() : ""; + const unit = normalizeUnitKey(row.unit); const description = row.description ? row.description.trim() : ""; let weightInIb = null; @@ -649,10 +665,10 @@ exports.uploadProductsFromCSV = async (req, res) => { }); } - const unitMasters = await UnitMaster.findAll({ - attributes: ["id", "uom", "uom_short_name"], - where: { is_active: true }, - }); + const unitMasters = await UnitMaster.findAll({ + attributes: ["id", "uom"], + where: { is_active: true }, + }); const unitMap = buildUnitLookupMap(unitMasters); const existingProducts = await Product.findAll({ @@ -726,12 +742,12 @@ exports.uploadProductsFromCSV = async (req, res) => { for (let i = 0; i < normalizedRows.length; i++) { const row = normalizedRows[i]; - const unitId = unitMap.get(row.unit); + const unitId = resolveUnitId(row.unit, unitMap); if (!unitId) { validationErrors.push({ row: row.csvLineNumber, - error: `Invalid unit "${row.unit}". Use unit short code (e.g. NO) or full name (e.g. Numbers) from Unit Master.`, + error: `Invalid unit "${row.unit}". Use the unit name (uom) from Unit Master (e.g. Numbers, Kilogram). Matching is case-insensitive.`, }); continue; } diff --git a/app/middleware/app.middleware.js b/app/middleware/app.middleware.js index 0f18016..31d7a40 100644 --- a/app/middleware/app.middleware.js +++ b/app/middleware/app.middleware.js @@ -2,10 +2,23 @@ require("dotenv").config(); module.exports = function (req, res, next) { - const appSignature = req.headers["app_signature"]; + const appSignature = ( + req.headers["app_signature"] || req.headers["x-app-signature"] || "" + ).trim(); + const expectedSignature = (process.env.APP_SIGNATURE || "").trim(); - if (!appSignature || appSignature !== process.env.APP_SIGNATURE) { - return res.status(403).json({ message: "Access denied, invalid or missing APP_SIGNATURE" }); + if (!appSignature) { + return res.status(403).json({ + status: "failed", + message: "Missing APP_SIGNATURE header.", + }); + } + + if (appSignature !== expectedSignature) { + return res.status(403).json({ + status: "failed", + message: "Invalid APP_SIGNATURE. Use the exact value from server .env.", + }); } next(); // allow request to continue diff --git a/app/middleware/csvUpload.middleware.js b/app/middleware/csvUpload.middleware.js new file mode 100644 index 0000000..d79098e --- /dev/null +++ b/app/middleware/csvUpload.middleware.js @@ -0,0 +1,116 @@ +const multer = require("multer"); +const { UPLOAD_DIR } = require("../config/upload.config"); + +const CSV_UPLOAD_FIELD = "file"; + +const upload = multer({ + dest: UPLOAD_DIR, + limits: { fileSize: 10 * 1024 * 1024 }, +}); + +function respondCsvUploadError(res, statusCode, message, extra = {}) { + return res.status(statusCode).json({ + status: "failed", + message, + ...extra, + }); +} + +/** + * Parses multipart upload and sets req.file from form field "file". + * Returns helpful errors when Swagger/Postman sends the wrong shape. + */ +function handleCsvUpload(req, res, next) { + upload.any()(req, res, (err) => { + if (err) { + if (err.code === "LIMIT_FILE_SIZE") { + return respondCsvUploadError( + res, + 400, + "CSV file is too large. Maximum allowed size is 10 MB." + ); + } + if (err.code === "LIMIT_UNEXPECTED_FILE") { + return respondCsvUploadError( + res, + 400, + `Unexpected file field "${err.field}". Use form field name "${CSV_UPLOAD_FIELD}" for the CSV.`, + { expected_field: CSV_UPLOAD_FIELD, received_field: err.field } + ); + } + return respondCsvUploadError( + res, + 400, + "Could not read uploaded file. Please upload a valid .csv file.", + { detail: err.message } + ); + } + + const files = Array.isArray(req.files) ? req.files : []; + + if (files.length === 0) { + const contentType = req.headers["content-type"] || ""; + + if (!contentType.includes("multipart/form-data")) { + return respondCsvUploadError( + res, + 400, + "CSV upload must use multipart/form-data, not JSON.", + { + hint: `Send your .csv in form field "${CSV_UPLOAD_FIELD}". In Swagger: choose the file under "${CSV_UPLOAD_FIELD}" before Execute.`, + expected_field: CSV_UPLOAD_FIELD, + received_content_type: contentType || "(missing)", + } + ); + } + + return respondCsvUploadError( + res, + 400, + `No CSV file received. Attach a .csv file to form field "${CSV_UPLOAD_FIELD}".`, + { + hint: + 'In Swagger UI: click "Try it out" → use the file picker for "' + + CSV_UPLOAD_FIELD + + '" (e.g. hs-codes-sample.csv) → then Execute. Do not leave the file field empty.', + expected_field: CSV_UPLOAD_FIELD, + example_curl: + 'curl -F "file=@hs-codes-sample.csv;type=text/csv" ...', + } + ); + } + + const csvFile = files.find((f) => f.fieldname === CSV_UPLOAD_FIELD); + + if (!csvFile) { + const receivedFields = [...new Set(files.map((f) => f.fieldname))]; + return respondCsvUploadError( + res, + 400, + `CSV must use form field "${CSV_UPLOAD_FIELD}", but received: ${receivedFields.join(", ")}.`, + { + hint: `Change the form field name to "${CSV_UPLOAD_FIELD}" and upload again.`, + expected_field: CSV_UPLOAD_FIELD, + received_fields: receivedFields, + } + ); + } + + if (files.length > 1) { + return respondCsvUploadError( + res, + 400, + "Only one CSV file can be uploaded per request.", + { + hint: `Send a single file in field "${CSV_UPLOAD_FIELD}".`, + received_file_count: files.length, + } + ); + } + + req.file = csvFile; + return next(); + }); +} + +module.exports = { handleCsvUpload, CSV_UPLOAD_FIELD }; diff --git a/app/routes/routes.js b/app/routes/routes.js index 2fa06ee..9a054fa 100644 --- a/app/routes/routes.js +++ b/app/routes/routes.js @@ -27,6 +27,7 @@ const multer = require("multer"); const { UPLOAD_DIR } = require("../config/upload.config"); const upload = multer({ dest: UPLOAD_DIR }); +const { handleCsvUpload } = require("../middleware/csvUpload.middleware"); @@ -1415,29 +1416,36 @@ router.delete("/products/:id",[verifySignature, verifyToken], productController. * description: This API accepts CSV file and inserts multiple products in bulk. CSV header columns must match Product table columns. * tags: [Products] * security: - * - appSignature: [] - * - CSRF: [] - * cookieAuth: [] # or bearerAuth: [] if you use Authorization header + * - bearerAuth: [] + * appSignature: [] * requestBody: * required: true * content: * multipart/form-data: * schema: * type: object + * required: + * - file * properties: * file: * type: string * format: binary - * description: CSV file to upload + * description: CSV file — form field name must be "file" * responses: * 201: * description: Products uploaded successfully * 400: - * description: No file uploaded + * description: Missing file, wrong form field, or validation error + * 403: + * description: Missing or invalid APP_SIGNATURE or auth token * 500: * description: Server error */ -router.post("/products/uploadCSV",[ verifySignature, verifyToken, upload.single("file")], productController.uploadProductsFromCSV); +router.post( + "/products/uploadCSV", + [verifySignature, verifyToken, handleCsvUpload], + productController.uploadProductsFromCSV +); /** diff --git a/server.js b/server.js index fb8abc0..8acfa56 100644 --- a/server.js +++ b/server.js @@ -251,7 +251,10 @@ app.use((req, res, next) => { "/api/forgot-password/verify-otp", "/api/csrf-token", "/api/auth/request-otp", - "/api/auth/verify-otp" + "/api/auth/verify-otp", + "/api/products/uploadCSV", + "/api/establishments/uploadCSV", + "/api/unit_master/uploadCSV", ]; if (csrfExcludedPaths.includes(req.path)) { @@ -281,6 +284,13 @@ app.use((err, req, res, next) => { message: "Invalid or missing CSRF token", }); } + if (err.name === "MulterError") { + return res.status(400).json({ + status: "failed", + message: err.message, + hint: 'Upload the CSV using form field "file".', + }); + } next(err); });