GWM : product upload issue
This commit is contained in:
parent
ca936f8f7e
commit
20344bc589
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
116
app/middleware/csvUpload.middleware.js
Normal file
116
app/middleware/csvUpload.middleware.js
Normal 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 };
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
12
server.js
12
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);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user