fcsc_ipi_backend/app/controllers/products.controller.js
2025-12-11 16:54:29 +05:30

398 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require("fs");
const csv = require("csv-parser");
const path = require("path");
const db = require("../models");
const { Sequelize } = require("sequelize");
const Product = db.Product;
const UnitMaster = db.UnitMaster;
exports.createProduct = async (req, res) => {
try {
const { product_name, hs_code, hs_description } = req.body;
if (hs_description && hs_description.length > 1000) {
return res.status(400).json({
status: "failed",
message: "HS Description is too long. Maximum allowed length is 1000 characters."
});
}
if (!/^\d+$/.test(hs_code)) {
return res.status(400).json({error: "Invalid HS Code: Must be numeric only."});
}
if (String(hs_code).length > 10 && String(hs_code).length < 1 && hs_code === 0 ) {
return res.status(400).send({status: "failed", message: "Invalid HS Code: maximum length is 10 digits."});
}
if(hs_code === 0 || String(hs_code) === "0"){
return res.status(400).send({status: "failed", message: " must be numeric, 110 digits, and cannot be 0."});
}
const productNameExists = await Product.findOne({
where: { product_name, is_active: true }
});
if (productNameExists) {
return res.status(400).json({
status: "failed",
message: `Product name '${product_name}' already exists.`
});
}
const hsCodeExists = await Product.findOne({
where: { hs_code, is_active: true }
});
if (hsCodeExists) {
return res.status(400).json({
status: "failed",
message: `HS Code '${hs_code}' already exists.`
});
}
req.body.created_by = req.user.id;
const data = await Product.create(req.body);
res.status(201).send({'status':"success",'message':"created successfully",'data': data });
} catch (error) {
res.status(500).send({'status':"failed",'message':error.message });
}
};
exports.getAllProducts = async (req, res) => {
try {
const data = await Product.findAll({
where: {
is_active: true
},
attributes: {
include: [
[
Sequelize.literal(
`(SELECT COUNT(*) FROM establishment_products AS EP WHERE EP.product_id = products.id)`
),
"mapped_establishment_count",
],
[
Sequelize.literal(`(SELECT name FROM admin_users au WHERE au.id = products.created_by)`),
"created_by_name"
]
],
},
include: [
{
model: UnitMaster,
as: "unit",
attributes: ["uom"],
},
],
order: [
["product_name", "ASC"]
],
});
res.status(200).send({
status: "success",
message: "Fetched successfully",
product_count: data.length,
data: data
});
} catch (error) {
res.status(500).send({ status: "failed", message: error.message });
}
};
exports.getProductById = async (req, res) => {
try {
const data = await Product.findByPk(req.params.id);
if (!data) res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" });
res.status(200).send({'status':"success",'message':"Fetched successfully",'data': data });
} catch (error) {
res.status(500).send({'status':"failed",'message':error.message });
}
};
exports.updateProduct = async (req, res) => {
try {
const object = {
...req.body,
updated_by: req.user.id,
updated_at: new Date()
};
const [updated] = await Product.update(object, { where: { id: req.params.id } });
if (!updated) res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" });
res.status(200).send({'status':"success",'message':"Updated successfully",'data': "" });
} catch (error) {
res.status(500).send({'status':"failed",'message':error.message });
}
};
exports.deleteProduct = async (req, res) => {
try {
const deleted = await Product.destroy({ where: { id: req.params.id } });
if (!deleted) res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" });
res.status(200).send({'status':"success",'message':"Deleted successfully",'data': "" });
} catch (error) {
res.status(500).send({'status':"failed",'message':error.message });
}
};
exports.downloadProductSample = async (req, res) => {
try {
const filePath = path.join(__dirname, "../downloads_csv/products_upload_sample.csv");
return res.download(filePath, "products_upload_sample.csv");
} catch (error) {
return res.status(500).send({ status: "failed", message: error.message });
}
};
exports.uploadProductsFromCSV = async (req, res) => {
try {
if (!req.file) {
return res.status(400).send({ status: "failed", message: "No file uploaded" });
}
const uploadedPath = path.resolve(req.file.path);
// Automatically detect the multer uploads folder
const uploadDir = path.resolve(path.dirname(uploadedPath));
// Validate the path stays inside multer's directory
if (!uploadedPath.startsWith(uploadDir)) {
if (fs.existsSync(uploadedPath)) fs.unlinkSync(uploadedPath);
return res.status(400).send({
status: "failed",
message: "Invalid file path detected."
});
}
// Use the real safe path
const filePath = uploadedPath;
/** END SAFE FIX --------------------------------------- */
// Validate file extension
if (!req.file.originalname.toLowerCase().endsWith(".csv")) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "Invalid file type. Only CSV allowed." });
}
if (!req.user.id || isNaN(req.user.id)) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "Invalid or missing User Id." });
}
const results = [];
const userId = parseInt(req.user.id);
const stats = fs.statSync(filePath);
if (stats.size === 0) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Uploaded file is empty.",
});
}
fs.createReadStream(filePath)
.pipe(csv())
.on("data", (row) => {
const cleanRow = {};
for (const key in row) {
let cleanedHeader = key
.replace(/\*/g, "")
.replace(/\(.*?\)/g, "")
.trim();
const normalizedKey = cleanedHeader.replace(/[\s\W]+/g, "_").trim().toLowerCase();
let mappedKey = normalizedKey;
if (/^hs.*code$/.test(normalizedKey) || normalizedKey === "hscode") {
mappedKey = "hs_code";
} else if (/^product.*name$/.test(normalizedKey)) {
mappedKey = "product_name";
} else if (/unit|measurement|uom|measure/i.test(normalizedKey)) {
mappedKey = "unit";
}else if (/desc(ription)?/i.test(normalizedKey)) { mappedKey = "description";}
cleanRow[mappedKey] = row[key]?.trim() || null;
}
results.push(cleanRow);
})
.on("end", async () => {
try {
if (results.length === 0) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "CSV file is empty or invalid." });
}
const requiredCols = ["hs_code", "product_name", "unit", "description"];
const headers = Object.keys(results[0]);
const missingCols = requiredCols.filter(col => !headers.includes(col));
const extraCols = headers.filter(col => !requiredCols.includes(col));
if (missingCols.length > 0 || extraCols.length > 0) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message:
`${missingCols.length ? `Missing columns: ${missingCols.join(", ")}. ` : ""}` +
`${extraCols.length ? `Unexpected columns: ${extraCols.join(", ")}.` : ""}`
});
}
const normalizedRows = [];
const seenHsCodes = new Set();
const fileDuplicates = new Set();
const errors = [];
for (let [index, row] of results.entries()) {
let hsCode = row.hs_code?.replace(/[-\s/]/g, "").trim();
const productName = row.product_name?.replace(/\s+/g, " ").trim();
const unit = row.unit?.trim().toLowerCase();
const description = row.description?.trim().toLowerCase() || "";
if (!hsCode || !productName) {
errors.push({ row: index + 1, error: "Missing required HS Code or Product Name" });
continue;
}
if (!/^\d+$/.test(hsCode)) {
errors.push({ row: index + 1, error: "HS Code must be numeric" });
continue;
}
if (hsCode.length > 10) {
errors.push({ row: index + 1, error: "HS Code must be max 10 digits" });
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) {
errors.push({ row: index + 1, error: "HS Description is too long. Maximum allowed length is 1000 characters." });
continue;
}
const normalizedHs = hsCode.replace(/^0+/, "");
if (seenHsCodes.has(normalizedHs)) {
fileDuplicates.add(hsCode);
continue;
}
seenHsCodes.add(normalizedHs);
normalizedRows.push({ hsCode, productName, unit, description });
}
if (fileDuplicates.size > 0) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(400).send({
status: "failed",
message: "Duplicate HS Codes found within file. Resolve and re-upload.",
duplicate_hs_codes_in_file: [...fileDuplicates]
});
}
const unitMasters = await UnitMaster.findAll({ attributes: ["id", "uom"] });
const unitMap = {};
unitMasters.forEach(u => (unitMap[u.uom.trim().toLowerCase()] = u.id));
const existingProducts = await Product.findAll({ attributes: ["hs_code"] });
const existingSet = new Set(existingProducts.map(p => p.hs_code.replace(/^0+/, "")));
const toInsert = [];
const duplicates = [];
for (const row of normalizedRows) {
const normalizedHs = row.hsCode.replace(/^0+/, "");
if (existingSet.has(normalizedHs)) {
duplicates.push(row.hsCode);
continue;
}
const unitId = unitMap[row.unit] || null;
if (!unitId) {
errors.push({ hs_code: row.hsCode, error: "Invalid unit" });
continue;
}
toInsert.push({
hs_code: row.hsCode,
product_name: row.productName,
unit_id: unitId,
hs_description: row.description,
created_by: userId,
created_at: new Date(),
});
}
let inserted = [];
if (toInsert.length > 0) {
inserted = await Product.bulkCreate(toInsert, { validate: true });
}
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
let finalStatus = "success";
let message = `${inserted.length} units inserted successfully.`;
let httpCode = 200;
if (errors.length === 0 && duplicates.length === 0) {
finalStatus = "success";
httpCode = 200;
}
else if (duplicates.length > 0 || errors.length > 0) {
finalStatus = "failed";
message = `No units imported. ${duplicates.length} duplicates and ${errors.length} errors found.`;
httpCode = 422;
}
else if (inserted.length === 0) {
finalStatus = "failed";
message = `No units imported. ${duplicates.length} duplicates and ${errors.length} errors found.`;
httpCode = 400;
}
return res.status(httpCode).send({
status: finalStatus,
message,
summary: {
total_records: results.length,
imported: inserted.length,
skipped: duplicates.length,
errors: errors,
},
duplicate_hs_codes_in_system: duplicates,
});
} catch (err) {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
return res.status(500).send({ status: "failed", message: err.message });
}
});
} catch (error) {
return res.status(500).send({ status: "failed", message: error.message });
}
};