Csv file changes added
This commit is contained in:
parent
180f34d483
commit
87f9b87a0d
@ -217,7 +217,12 @@ const filePath = uploadedPath;
|
|||||||
const cleanRow = {};
|
const cleanRow = {};
|
||||||
|
|
||||||
for (const key in row) {
|
for (const key in row) {
|
||||||
const normalizedKey = key.replace(/[\s\W]+/g, "_").trim().toLowerCase();
|
let cleanedHeader = key
|
||||||
|
.replace(/\*/g, "")
|
||||||
|
.replace(/\(.*?\)/g, "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const normalizedKey = cleanedHeader.replace(/[\s\W]+/g, "_").trim().toLowerCase();
|
||||||
|
|
||||||
let mappedKey = normalizedKey;
|
let mappedKey = normalizedKey;
|
||||||
if (/^hs.*code$/.test(normalizedKey) || normalizedKey === "hscode") {
|
if (/^hs.*code$/.test(normalizedKey) || normalizedKey === "hscode") {
|
||||||
@ -281,7 +286,10 @@ const filePath = uploadedPath;
|
|||||||
errors.push({ row: index + 1, error: "HS Code must be max 10 digits" });
|
errors.push({ row: index + 1, error: "HS Code must be max 10 digits" });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (productName.length > 1000) {
|
||||||
|
errors.push({ row: index + 1, error: "Product Name is too long. Maximum allowed length is 1000 characters." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (description.length > 1000) {
|
if (description.length > 1000) {
|
||||||
errors.push({ row: index + 1, error: "HS Description is too long. Maximum allowed length is 1000 characters." });
|
errors.push({ row: index + 1, error: "HS Description is too long. Maximum allowed length is 1000 characters." });
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -209,7 +209,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
let results = [];
|
||||||
|
|
||||||
fs.createReadStream(filePath)
|
fs.createReadStream(filePath)
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
@ -224,24 +224,68 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Expected columns
|
// -------------------------------------------------------------------
|
||||||
const requiredCols = ["Unit Name", "Description"];
|
// NORMALIZE HEADERS (Very Important!)
|
||||||
const headers = Object.keys(results[0] || {});
|
// -------------------------------------------------------------------
|
||||||
const missingCols = requiredCols.filter((col) => !headers.includes(col));
|
const normalizeHeader = (h) => {
|
||||||
const extraCols = headers.filter((col) => !requiredCols.includes(col));
|
return h
|
||||||
|
.replace(/\*/g, "") // remove *
|
||||||
|
.replace(/\(.*?\)/g, "") // remove (Mandatory)
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s\W]+/g, "_") // spaces & special chars -> _
|
||||||
|
.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Required normalized fields
|
||||||
|
const mappedRequiredCols = {
|
||||||
|
unit_name: "Unit Name",
|
||||||
|
description: "Description",
|
||||||
|
};
|
||||||
|
|
||||||
|
const incomingHeaders = Object.keys(results[0] || {});
|
||||||
|
const normalizedIncoming = incomingHeaders.map(h => normalizeHeader(h));
|
||||||
|
|
||||||
|
// Check missing columns
|
||||||
|
const missingCols = Object.keys(mappedRequiredCols).filter(
|
||||||
|
req => !normalizedIncoming.includes(req)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check unexpected columns
|
||||||
|
const extraCols = normalizedIncoming.filter(
|
||||||
|
col => !Object.keys(mappedRequiredCols).includes(col)
|
||||||
|
);
|
||||||
|
|
||||||
if (missingCols.length > 0 || extraCols.length > 0) {
|
if (missingCols.length > 0 || extraCols.length > 0) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
|
||||||
let msg = "";
|
let msg = "";
|
||||||
if (missingCols.length > 0)
|
if (missingCols.length > 0)
|
||||||
msg += `Missing required columns: ${missingCols.join(", ")}. `;
|
msg += `Missing required columns: ${missingCols.map(c => mappedRequiredCols[c]).join(", ")}. `;
|
||||||
|
|
||||||
if (extraCols.length > 0)
|
if (extraCols.length > 0)
|
||||||
msg += `Unexpected columns found: ${extraCols.join(
|
msg += `Unexpected columns found: ${extraCols.join(", ")}. Only 'Unit Name' and 'Description' are allowed.`;
|
||||||
", "
|
|
||||||
)}. Only 'Unit Name' and 'Description' are allowed.`;
|
return res.status(400).send({
|
||||||
return res.status(400).send({ status: "failed", message: msg.trim() });
|
status: "failed",
|
||||||
|
message: msg.trim(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remap row keys to clean headers
|
||||||
|
results = results.map(row => {
|
||||||
|
const newRow = {};
|
||||||
|
for (const key in row) {
|
||||||
|
const normalized = normalizeHeader(key);
|
||||||
|
const mapped = mappedRequiredCols[normalized];
|
||||||
|
if (mapped) newRow[mapped] = row[key];
|
||||||
|
}
|
||||||
|
return newRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// VALIDATION AND PROCESSING
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
const inserted = [];
|
const inserted = [];
|
||||||
const duplicates = [];
|
const duplicates = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
@ -251,7 +295,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
|
|
||||||
const generateShortName = (unitName, existingShorts = new Set()) => {
|
const generateShortName = (unitName, existingShorts = new Set()) => {
|
||||||
const cleaned = unitName.replace(/[^a-zA-Z]/g, "").toUpperCase();
|
const cleaned = unitName.replace(/[^a-zA-Z]/g, "").toUpperCase();
|
||||||
// Predefined abbreviations
|
|
||||||
const abbreviationMap = {
|
const abbreviationMap = {
|
||||||
METER: "MT",
|
METER: "MT",
|
||||||
METRE: "MT",
|
METRE: "MT",
|
||||||
@ -272,54 +316,59 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
UNIT: "UNT",
|
UNIT: "UNT",
|
||||||
};
|
};
|
||||||
|
|
||||||
//if Unit Name is ≤ 5 characters, just use it in uppercase
|
|
||||||
if (cleaned.length <= 5) return cleaned;
|
if (cleaned.length <= 5) return cleaned;
|
||||||
// Otherwise, use abbreviation map or generate automatically
|
|
||||||
let shortName = abbreviationMap[cleaned] || cleaned.substring(0, 3);
|
let shortName = abbreviationMap[cleaned] || cleaned.substring(0, 3);
|
||||||
|
|
||||||
if (shortName.length < 2) shortName = shortName.padEnd(2, "X");
|
if (shortName.length < 2) shortName = shortName.padEnd(2, "X");
|
||||||
// Random 2-letter suffix for uniqueness
|
|
||||||
const randomLetters = () =>
|
const randomLetters = () =>
|
||||||
Array.from({ length: 2 }, () =>
|
Array.from({ length: 2 }, () =>
|
||||||
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
).join("");
|
).join("");
|
||||||
|
|
||||||
let finalShort = `${shortName}${randomLetters()}`;
|
let final = `${shortName}${randomLetters()}`;
|
||||||
while (existingShorts.has(finalShort.toLowerCase())) {
|
while (existingShorts.has(final.toLowerCase())) {
|
||||||
finalShort = `${shortName}${randomLetters()}`;
|
final = `${shortName}${randomLetters()}`;
|
||||||
}
|
}
|
||||||
return finalShort.substring(0, 5);
|
|
||||||
|
return final.substring(0, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
//Validate & prepare records
|
// Row validation
|
||||||
for (const [index, row] of results.entries()) {
|
for (const [i, row] of results.entries()) {
|
||||||
const uom = row["Unit Name"]?.trim();
|
const uom = row["Unit Name"]?.trim();
|
||||||
const description = row["Description"]?.trim() || null;
|
const description = row["Description"]?.trim() || null;
|
||||||
|
|
||||||
|
if (description.length > 1000) {
|
||||||
|
errors.push({ row: i + 1, reason: "HS Description is too long. Maximum allowed length is 1000 characters." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!uom) {
|
if (!uom) {
|
||||||
errors.push({ row: index + 1, reason: "Missing 'Unit Name'." });
|
errors.push({ row: i + 1, reason: "Missing 'Unit Name'." });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uomKey = uom.toLowerCase();
|
const uomKey = uom.toLowerCase();
|
||||||
if (seenUnitNames.has(uomKey)) {
|
if (seenUnitNames.has(uomKey)) {
|
||||||
fileDuplicates.push({
|
fileDuplicates.push({
|
||||||
row: index + 1,
|
row: i + 1,
|
||||||
reason: "Duplicate Unit Name in file.",
|
reason: "Duplicate Unit Name in file.",
|
||||||
uom,
|
uom,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uomShort = generateShortName(uom, seenShortNames);
|
const uomShort = generateShortName(uom, seenShortNames);
|
||||||
|
|
||||||
seenUnitNames.add(uomKey);
|
seenUnitNames.add(uomKey);
|
||||||
seenShortNames.add(uomShort.toLowerCase());
|
seenShortNames.add(uomShort.toLowerCase());
|
||||||
results[index]._generatedShort = uomShort;
|
|
||||||
results[index]._description = description;
|
results[i]._generatedShort = uomShort;
|
||||||
|
results[i]._description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop if file has internal duplicates
|
|
||||||
if (fileDuplicates.length > 0) {
|
if (fileDuplicates.length > 0) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
@ -329,10 +378,10 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Insert into DB with safe duplicate handling
|
// Insert into DB
|
||||||
for (const [index, row] of results.entries()) {
|
for (const [i, row] of results.entries()) {
|
||||||
try {
|
try {
|
||||||
const uom = row["Unit Name"]?.trim();
|
const uom = row["Unit Name"].trim();
|
||||||
let uomShort = row._generatedShort;
|
let uomShort = row._generatedShort;
|
||||||
const description = row._description;
|
const description = row._description;
|
||||||
|
|
||||||
@ -352,34 +401,12 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Skip if same Unit Name already exists
|
duplicates.push({
|
||||||
if (existing.uom.toLowerCase() === uom.toLowerCase()) {
|
id: existing.id,
|
||||||
duplicates.push({
|
uom: existing.uom,
|
||||||
id: existing.id,
|
uom_short_name: existing.uom_short_name,
|
||||||
uom: existing.uom,
|
});
|
||||||
uom_short_name: existing.uom_short_name,
|
continue;
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Safe regeneration using ternary inside while loop
|
|
||||||
let newShort = uomShort;
|
|
||||||
let existsInDb;
|
|
||||||
|
|
||||||
do {
|
|
||||||
existsInDb = await UnitMaster.findOne({
|
|
||||||
where: Sequelize.where(
|
|
||||||
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
|
|
||||||
newShort.toLowerCase()
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
newShort = existsInDb
|
|
||||||
? `${uomShort}${Math.floor(Math.random() * 1000)}`.substring(0, 5)
|
|
||||||
: newShort;
|
|
||||||
} while (existsInDb);
|
|
||||||
|
|
||||||
uomShort = newShort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUnit = await UnitMaster.create({
|
const newUnit = await UnitMaster.create({
|
||||||
@ -391,13 +418,18 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
inserted.push(newUnit);
|
inserted.push(newUnit);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push({ row: index + 1, reason: err.message });
|
errors.push({ row: i + 1, reason: err.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// FINAL RESPONSE
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
let finalStatus = "success";
|
let finalStatus = "success";
|
||||||
let message = `${inserted.length} units inserted successfully.`;
|
let message = `${inserted.length} units inserted successfully.`;
|
||||||
|
|
||||||
@ -421,6 +453,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
duplicates,
|
duplicates,
|
||||||
error_details: errors,
|
error_details: errors,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||||
return res.status(500).send({
|
return res.status(500).send({
|
||||||
@ -431,7 +464,9 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading Unit Master CSV:", error);
|
console.error("Error uploading Unit Master CSV:", error);
|
||||||
if (req.file && fs.existsSync(path.resolve(req.file.path))) fs.unlinkSync(path.resolve(req.file.path));
|
if (req.file && fs.existsSync(path.resolve(req.file.path)))
|
||||||
|
fs.unlinkSync(path.resolve(req.file.path));
|
||||||
|
|
||||||
return res.status(500).send({
|
return res.status(500).send({
|
||||||
status: "failed",
|
status: "failed",
|
||||||
message: "Error processing CSV file.",
|
message: "Error processing CSV file.",
|
||||||
@ -440,15 +475,6 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// exports.downloadUnitMasterFile = async (req, res) => {
|
|
||||||
// try {
|
|
||||||
// const filePath = path.join(__dirname, "../uploads/unit_master_sample.csv");
|
|
||||||
// return res.download(filePath, "unit_master_sample.csv");
|
|
||||||
// } catch (error) {
|
|
||||||
// return res.status(500).send({ status: "failed", message: error.message });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
exports.downloadUnitMasterFile = async (req, res) => {
|
exports.downloadUnitMasterFile = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const safeBasePath = path.resolve(__dirname, "../downloads_csv");
|
const safeBasePath = path.resolve(__dirname, "../downloads_csv");
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
HS Code,Product Name,Unit,Description
|
HS Code * (Mandatory),Product Name * (Mandatory),Unit * (Mandatory),Description
|
||||||
|
|||||||
|
@ -1 +1 @@
|
|||||||
Unit Name,Description
|
Unit Name * (Mandatory),Description * (Mandatory)
|
||||||
|
|||||||
|
@ -31,6 +31,11 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
description: {
|
description: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
len: {
|
||||||
|
args: [0, 1000],
|
||||||
|
msg: "Description is too long. Maximum allowed length is 1000 characters."
|
||||||
|
}}
|
||||||
},
|
},
|
||||||
is_active: {
|
is_active: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user