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 };
}
/** 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;
}

View File

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

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 = 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
);
/**

View File

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