fcsc_ipi_backend/app/controllers/products.controller.js

440 lines
14 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 hsCode = req.body.hs_code;
if (!/^\d+$/.test(hsCode)) {
return res.status(400).json({error: "Invalid HS Code: Must be numeric only."});
}
if (String(hsCode).length > 10 && String(hsCode).length < 1 && hsCode === 0 ) {
return res.status(400).send({status: "failed", message: "Invalid HS Code: maximum length is 10 digits."});
}
if(hsCode === 0 || String(hsCode) === "0"){
return res.status(400).send({status: "failed", message: " must be numeric, 110 digits, and cannot be 0."});
}
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({
attributes: {
include: [
[
Sequelize.literal(
`(SELECT COUNT(*) FROM establishment_products AS EP WHERE EP.product_id = products.id)`
),
"mapped_establishment_count",
],
],
},
include: [
{
model: UnitMaster,
as: "unit",
attributes: ["uom"],
},
],
order: [
[Sequelize.literal("COALESCE(products.updated_at, products.created_at) DESC")],
],
});
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.uploadProductsFromCSV = async (req, res) => {
// try {
// // Check if file exists
// if (!req.file) {
// return res.status(400).send({ status: "failed", message: "No file uploaded" });
// }
// const filePath = req.file.path;
// // Validate file type
// if (!req.file.originalname.endsWith(".csv")) {
// fs.unlinkSync(filePath);
// return res.status(400).send({
// status: "failed",
// message: "Invalid file type. Only CSV files are allowed.",
// });
// }
// // Validate userId
// if (!req.body.userId || isNaN(req.body.userId)) {
// fs.unlinkSync(filePath);
// return res.status(400).send({
// status: "failed",
// message: "Invalid or missing userId. Must be an integer.",
// });
// }
// const results = [];
// const userId = parseInt(req.body.userId);
// console.log(userId, "userId");
// // Read CSV and clean headers/values
// fs.createReadStream(filePath)
// .pipe(csv())
// .on("data", (row) => {
// // Trim all keys and values to handle spaces in header names or values
// const cleanRow = {};
// for (const key in row) {
// cleanRow[key.trim()] = row[key] ? row[key].trim() : null;
// }
// results.push(cleanRow);
// })
// .on("end", async () => {
// try {
// // Check for empty CSV
// if (results.length === 0) {
// fs.unlinkSync(filePath);
// return res.status(400).send({
// status: "failed",
// message: "CSV file is empty or invalid.",
// });
// }
// console.log(results, "results");
// // Fetch existing products by hs_code
// const hsCodes = results.map((r) => r.hs_code);
// const existingProducts = await Product.findAll({
// where: { hs_code: hsCodes },
// });
// const existingHsCodes = existingProducts.map((p) => p.hs_code);
// const toInsert = [];
// const duplicates = [];
// // Process CSV rows
// for (const row of results) {
// const hsCode = row.hs_code?.trim();
// const productName = row.product_name?.trim();
// const description = row.hs_description?.trim();
// if (!hsCode || !productName) continue; // skip invalid rows
// if (existingHsCodes.includes(hsCode)) {
// duplicates.push(hsCode);
// } else {
// toInsert.push({
// hs_code: hsCode,
// product_name: productName,
// hs_description: description,
// created_by: userId,
// created_at: new Date(),
// });
// }
// }
// // Bulk insert new records
// let inserted = [];
// if (toInsert.length > 0) {
// inserted = await Product.bulkCreate(toInsert, { validate: true });
// }
// // Delete file after processing
// fs.unlinkSync(filePath);
// // Send success response
// return res.status(200).send({
// status: "success",
// message: `${inserted.length} products inserted successfully.`,
// inserted_count: inserted.length,
// duplicate_hs_codes: 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 });
// }
// };
exports.downloadProductSample = async (req, res) => {
try {
const filePath = path.join(__dirname, "../writable/uploads/sample_files/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 filePath = req.file.path;
if (!req.file.originalname.endsWith(".csv")) {
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)) {
fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "Invalid or missing User Id." });
}
const mode = req.body.mode?.toLowerCase() || "add";
if (mode !== "add") {
fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "Only 'Add Only' mode is supported currently." });
}
const results = [];
const userId = parseInt(req.user.id);
const stats = fs.statSync(filePath);
if (stats.size === 0) {
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 = {};
// Normalize headers and smart-map them
for (const key in row) {
const normalizedKey = key.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) || /^produc.*tname$/.test(normalizedKey)) {
mappedKey = "product_name";
} else if (/^unit[s]?$/.test(normalizedKey) || /measurement|uom|measure/i.test(normalizedKey)) {
mappedKey = "unit";
}
cleanRow[mappedKey] = row[key]?.trim() || null;
}
results.push(cleanRow);
})
.on("end", async () => {
try {
if (results.length === 0) {
fs.unlinkSync(filePath);
return res.status(400).send({ status: "failed", message: "CSV file is empty or invalid." });
}
const requiredCols = ["hs_code", "product_name", "unit"];
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) {
fs.unlinkSync(filePath);
let message = "";
if (missingCols.length > 0) message += `Missing required columns: ${missingCols.join(", ")}. `;
if (extraCols.length > 0) message += `Unexpected columns found: ${extraCols.join(", ")}. Only 'HS Code', 'Product Name', and 'Unit' are allowed.`;
return res.status(400).send({ status: "failed", message: message.trim() });
}
// Validate and normalize data
const normalizedRows = [];
const fileDuplicates = new Set();
const seenHsCodes = 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();
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 10 digits" });
continue;
}
const normalizedHs = hsCode.replace(/^0+/, "");
if (seenHsCodes.has(normalizedHs)) {
fileDuplicates.add(hsCode);
continue;
}
seenHsCodes.add(normalizedHs);
normalizedRows.push({ hsCode, productName, unit });
}
if (fileDuplicates.size > 0) {
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,
created_by: userId,
created_at: new Date(),
});
}
let inserted = [];
if (toInsert.length > 0) {
inserted = await Product.bulkCreate(toInsert, { validate: true });
}
fs.unlinkSync(filePath);
let finalStatus = "success";
let message = `${inserted.length} products inserted successfully.`;
if (errors.length > 0 || duplicates.length > 0) {
finalStatus = inserted.length > 0 ? "partial_success" : "failed";
if (finalStatus === "partial_success") {
message = `${inserted.length} products inserted, ${duplicates.length} duplicates skipped, ${errors.length} validation errors found.`;
} else {
message = `No products imported. ${duplicates.length} duplicates and ${errors.length} validation errors found.`;
}
}
return res.status(200).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 });
}
};