GWM : product upload issue

This commit is contained in:
Gowtham M 2026-06-04 10:50:29 +05:30
parent ca936f8f7e
commit 20344bc589
5 changed files with 187 additions and 24 deletions

View File

@ -43,20 +43,32 @@ function parseWeightInIb(value) {
return { ok: true, value: rounded }; 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) { function buildUnitLookupMap(unitMasters) {
const unitMap = new Map(); const unitMap = new Map();
for (const unit of unitMasters) { for (const unit of unitMasters) {
if (unit.uom) { const key = normalizeUnitKey(unit.uom);
unitMap.set(unit.uom.trim().toLowerCase(), unit.id); if (key) {
} unitMap.set(key, unit.id);
if (unit.uom_short_name) {
unitMap.set(unit.uom_short_name.trim().toLowerCase(), unit.id);
} }
} }
return unitMap; return unitMap;
} }
function resolveUnitId(unitInput, unitMap) {
const key = normalizeUnitKey(unitInput);
if (!key) return null;
return unitMap.get(key) ?? null;
}
function normalizeCsvHeaderKey(key) { function normalizeCsvHeaderKey(key) {
if (!key) return ""; if (!key) return "";
const withoutBom = String(key).replace(/^\uFEFF/, ""); const withoutBom = String(key).replace(/^\uFEFF/, "");
@ -436,7 +448,11 @@ exports.uploadProductsFromCSV = async (req, res) => {
if (!req.file) { if (!req.file) {
return sendUploadResponse(res, 400, { return sendUploadResponse(res, 400, {
status: "failed", 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 const productName = row.product_name
? row.product_name.replace(/\s+/g, " ").trim() ? 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() : ""; const description = row.description ? row.description.trim() : "";
let weightInIb = null; let weightInIb = null;
@ -649,10 +665,10 @@ exports.uploadProductsFromCSV = async (req, res) => {
}); });
} }
const unitMasters = await UnitMaster.findAll({ const unitMasters = await UnitMaster.findAll({
attributes: ["id", "uom", "uom_short_name"], attributes: ["id", "uom"],
where: { is_active: true }, where: { is_active: true },
}); });
const unitMap = buildUnitLookupMap(unitMasters); const unitMap = buildUnitLookupMap(unitMasters);
const existingProducts = await Product.findAll({ const existingProducts = await Product.findAll({
@ -726,12 +742,12 @@ exports.uploadProductsFromCSV = async (req, res) => {
for (let i = 0; i < normalizedRows.length; i++) { for (let i = 0; i < normalizedRows.length; i++) {
const row = normalizedRows[i]; const row = normalizedRows[i];
const unitId = unitMap.get(row.unit); const unitId = resolveUnitId(row.unit, unitMap);
if (!unitId) { if (!unitId) {
validationErrors.push({ validationErrors.push({
row: row.csvLineNumber, 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; continue;
} }

View File

@ -2,10 +2,23 @@ require("dotenv").config();
module.exports = function (req, res, next) { 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) { if (!appSignature) {
return res.status(403).json({ message: "Access denied, invalid or missing APP_SIGNATURE" }); 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 next(); // allow request to continue

View File

@ -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 };

View File

@ -27,6 +27,7 @@ const multer = require("multer");
const { UPLOAD_DIR } = require("../config/upload.config"); const { UPLOAD_DIR } = require("../config/upload.config");
const upload = multer({ dest: UPLOAD_DIR }); 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. * description: This API accepts CSV file and inserts multiple products in bulk. CSV header columns must match Product table columns.
* tags: [Products] * tags: [Products]
* security: * security:
* - appSignature: [] * - bearerAuth: []
* - CSRF: [] * appSignature: []
* cookieAuth: [] # or bearerAuth: [] if you use Authorization header
* requestBody: * requestBody:
* required: true * required: true
* content: * content:
* multipart/form-data: * multipart/form-data:
* schema: * schema:
* type: object * type: object
* required:
* - file
* properties: * properties:
* file: * file:
* type: string * type: string
* format: binary * format: binary
* description: CSV file to upload * description: CSV file form field name must be "file"
* responses: * responses:
* 201: * 201:
* description: Products uploaded successfully * description: Products uploaded successfully
* 400: * 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: * 500:
* description: Server error * description: Server error
*/ */
router.post("/products/uploadCSV",[ verifySignature, verifyToken, upload.single("file")], productController.uploadProductsFromCSV); router.post(
"/products/uploadCSV",
[verifySignature, verifyToken, handleCsvUpload],
productController.uploadProductsFromCSV
);
/** /**

View File

@ -251,7 +251,10 @@ app.use((req, res, next) => {
"/api/forgot-password/verify-otp", "/api/forgot-password/verify-otp",
"/api/csrf-token", "/api/csrf-token",
"/api/auth/request-otp", "/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)) { if (csrfExcludedPaths.includes(req.path)) {
@ -281,6 +284,13 @@ app.use((err, req, res, next) => {
message: "Invalid or missing CSRF token", 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); next(err);
}); });