fcsc_ipi_backend/app/controllers/establishment.controller.js
2025-12-11 13:10:21 +05:30

1623 lines
53 KiB
JavaScript

const db = require("../models");
const bcrypt = require("bcryptjs");
const Establishment = db.Establishment;
const EstablishmentUser = db.EstablishmentUser;
const EstablishmentPasswordResetRequest = db.EstablishmentPasswordResetRequest;
const EstablishmentProduct = db.EstablishmentProduct;
const CityTown = db.CityTown;
const Emirate = db.Emirate;
const user = db.user;
const Product = db.Product;
const ExcelJS = require("exceljs");
const { Op, Sequelize } = require("sequelize");
const { sendEmail } = require("../services/emailHelper");
const { sendEmailService } = require("../services/email.service");
const logger = require("../services/logger");
const fs = require("fs");
const csv = require("csv-parser");
const path = require("path");
const { version } = require("os");
const sequelize = db.sequelize;
const { sanitizeForLog } = require("../utils/sanitize");
exports.testEmail = async (req, res) => {
placeHolderData = {
contact_name : 'Gowtham',
portal_url : process.env.FE_BASE_URL,
username: 'test_user',
password: 'Test@123!',
support_email : process.env.SUPPORT_EMAIL,
support_phone : process.env.SUPPORT_PHONE,
}
await sendEmailService('gowthamceline46@gmail.com', 'establishment_user_creation_to_user', placeHolderData);
};
exports.createEstablishment = async (req, res) => {
try {
// convert empty "" → null BEFORE destructure
[
"establishment_code",
"factory_name",
"permanent_factory_code",
"industry_code",
"industry_code_production",
"license_number",
"isic_code"
].forEach(key => {
if (req.body[key] === "") req.body[key] = null;
});
const {
establishment_code,
factory_name,
permanent_factory_code,
industry_code,
industry_code_production,
industry_code_mismatch_remarks,
license_number,
isic_code,
description,
// Establishment Contact Details
establishment_address,
establishment_city_town_id,
establishment_emirate_id,
establishment_postal_code,
establishment_po_box,
establishment_makani_number,
establishment_contact_person_name,
establishment_contact_person_designation,
establishment_mobile_number,
establishment_contact_email,
establishment_website,
// Corporate Details
corporate_same_as_establishment,
corporate_name,
corporate_address,
corporate_city_town_id,
corporate_emirate_id,
corporate_postal_code,
corporate_po_box,
corporate_makani_number,
corporate_contact_person_name,
corporate_contact_person_designation,
corporate_mobile_number,
corporate_email,
corporate_website,
emirati_male,
emirati_female,
non_emirati_male,
non_emirati_female,
total_emirati,
total_employees,
created_by,
establishment_user,
establishment_products
} = req.body;
// Basic Validation
if (!establishment_code || !factory_name || !establishment_user) {
return res.status(400).send({
status: "failed",
message: "Missing required fields: establishment_code, factory_name, email, establishment_user",
});
}
if (!establishment_city_town_id || !corporate_city_town_id) {
return res.status(400).send({
status: "failed",
message: "Missing required field: city/town",
});
}
if (!Array.isArray(establishment_products) || establishment_products.length === 0) {
return res.status(400).send({
status: "failed",
message: "Please select at least one product."
});
}
for (const item of establishment_products) {
const id = Number(item.product_id);
if (!id || isNaN(id) || id <= 1) {
return res.status(400).send({
status: "failed",
message: "Invalid product_id. product_id must be a number greater than 1 and cannot be 0."
});
}
}
// Check if establishment already exists
const existingEstablishment = await Establishment.findOne({where: { establishment_code }, });
if (existingEstablishment) {
return res.status(400).send({status: "failed",message: "Establishment code already exists", });
}
// Create establishment record
const establishment = await Establishment.create({
establishment_code,
factory_name,
permanent_factory_code: permanent_factory_code || null,
industry_code: industry_code || null,
industry_code_production: industry_code_production || null,
industry_code_mismatch_remarks,
license_number,
isic_code,
description,
establishment_address,
establishment_city_town_id,
establishment_emirate_id,
establishment_postal_code,
establishment_po_box,
establishment_makani_number,
establishment_contact_person_name,
establishment_contact_person_designation,
establishment_mobile_number,
establishment_contact_email,
establishment_website,
corporate_same_as_establishment,
corporate_name,
corporate_address,
corporate_city_town_id,
corporate_emirate_id,
corporate_postal_code,
corporate_po_box,
corporate_makani_number,
corporate_contact_person_name,
corporate_contact_person_designation,
corporate_mobile_number,
corporate_email,
corporate_website,
emirati_male,
emirati_female,
non_emirati_male,
non_emirati_female,
total_emirati,
total_employees,
created_by: req.body.created_by || req.user.id,
created_at: new Date(),
});
// Hash password
const hashedPassword = await bcrypt.hash(establishment_user.password, 10);
// Create linked Establishment User
const user = await EstablishmentUser.create({
establishment_id: establishment.id,
name: establishment_user.name,
email: establishment_user.email,
password: hashedPassword,
created_by: req.body.created_by || req.user.id,
});
//send email to user
placeHolderData = {
contact_name : sanitizeForLog(establishment_user.name),
portal_url : process.env.FE_BASE_URL,
username : sanitizeForLog(establishment_user.email),
password : establishment_user.password,
support_email : process.env.SUPPORT_EMAIL,
support_phone : process.env.SUPPORT_PHONE,
}
await sendEmailService(sanitizeForLog(establishment_user.email), 'establishment_user_creation_to_user', placeHolderData);
// insert establishment_products
// if (Array.isArray(establishment_products) && establishment_products.length > 0) {
// // remove duplicates from request itself
// let uniqueProducts = [...new Set(establishment_products.map(x => x.product_id))];
// // now check which already exist for this establishment
// const existing = await EstablishmentProduct.findAll({
// where: {
// establishment_id: establishment.id,
// product_id: uniqueProducts
// },
// attributes: ['product_id']
// });
// const existingIds = existing.map(x => x.product_id);
// // filter only new ones (not existing)
// const newProducts = uniqueProducts
// .filter(pid => !existingIds.includes(pid))
// .map(pid => ({
// establishment_id: establishment.id,
// product_id: pid,
// created_at: new Date(),
// }));
// // only insert if have new ones
// if (newProducts.length) {
// await EstablishmentProduct.bulkCreate(newProducts);
// }
// }
// insert establishment_products
if (Array.isArray(establishment_products) && establishment_products.length > 0)
{
// remove duplicates inside request
const uniqueProducts = establishment_products.reduce((acc, item) => {
if (!acc.some(p => p.product_id === item.product_id)) {
acc.push(item);
}
return acc;
}, []);
// find already existing products
const existing = await EstablishmentProduct.findAll({
where: {
establishment_id: establishment.id,
product_id: uniqueProducts.map(x => x.product_id)
},
attributes: ["product_id"]
});
const existingIds = existing.map(x => x.product_id);
// filter only NEW ones and include audit fields
const insertData = uniqueProducts
.filter(x => !existingIds.includes(x.product_id))
.map(x => ({
establishment_id: establishment.id,
product_id: x.product_id,
created_by: x.created_by || null,
action_done_by: x.action_done_by || "admin_user",
created_at: new Date()
}));
if (insertData.length > 0) {
await EstablishmentProduct.bulkCreate(insertData);
}
}
//Return success response
return res.status(201).send({
status: "success",
message: "Establishment and linked user created successfully",
data: {
establishment,
user: {
id: user.id,
name: user.name,
email: user.email,
},
},
});
} catch (err) {
if (err.name === "SequelizeUniqueConstraintError") {
// extract exact field
const field = err.errors[0].path; // <-- this gives the column name
return res.status(400).json({
status: "failed",
message: `${field} already exists`
});
}
return res.status(500).send({status: "failed",message: err.message || "Internal server error",});
}
};
exports.getAllEstablishments = async (req, res) => {
try {
let {
page = 1,
limit = 10,
search = "",
emirate_id,
isic_code,
status,
sort_by = "created_at",
sort_order = "DESC",
export: exportType, // detect ?export=excel
} = req.query;
page = parseInt(page);
limit = parseInt(limit);
const offset = (page - 1) * limit;
// Build dynamic where clause
const whereClause = {};
// Filters
if (status) whereClause.is_active = status === "active" ? 1 : 0;
if (isic_code) whereClause.isic_code = isic_code;
if (emirate_id) whereClause.establishment_emirate_id = emirate_id;
// Search by name, establishment_code, or contact emails
if (search) {
whereClause[Op.or] = [
{ factory_name: { [Op.like]: `%${search}%` } },
{ establishment_code: { [Op.like]: `%${search}%` } },
{ establishment_contact_email: { [Op.like]: `%${search}%` } },
{ corporate_email: { [Op.like]: `%${search}%` } },
];
}
// Fetch Data
const data = await Establishment.findAll({
where: whereClause,
attributes: {
include: [
[
Sequelize.literal(`(
SELECT COUNT(*)
FROM establishment_products ep
WHERE ep.establishment_id = establishments.id
)`),
"product_count"
],
[
Sequelize.literal(`(
SELECT name
FROM admin_users au
WHERE au.id = establishments.created_by
)`),
"created_by_name"
]
]
},
include: [
{ model: CityTown, as: "establishment_city", attributes: ["name"] },
{ model: Emirate, as: "establishment_emirate", attributes: ["name"] },
{ model: CityTown, as: "corporate_city", attributes: ["name"] },
{ model: Emirate, as: "corporate_emirate", attributes: ["name"] },
{ model: user, as: "created_user", attributes: ["name"] },
],
order: [[sort_by, sort_order]],
...(exportType ? {} : { limit, offset }),
});
// If export = excel → generate file
if (exportType && exportType.toLowerCase() === "excel") {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Establishments");
// 🧩 Define headers
worksheet.columns = [
{ header: "ID", key: "id", width: 10 },
{ header: "Establishment Name", key: "factory_name", width: 30 },
{ header: "Contact Name", key: "establishment_contact_person_name", width: 30 },
{ header: "Emirate", key: "emirate_name", width: 20 },
{ header: "ISIC Code", key: "isic_code", width: 20 },
{ header: "Establishment ID", key: "establishment_code", width: 20 },
{ header: "Total Employees", key: "total_employees", width: 20 },
{ header: "Created By", key: "created_user", width: 20 },
{ header: "Created On", key: "created_at", width: 20 },
{ header: "Last Updated", key: "updated_at", width: 20 },
{ header: "Status", key: "status", width: 15 },
// { header: "City/Town", key: "city_name", width: 20 },
// { header: "Contact Email", key: "contact_email", width: 30 },
// { header: "Corporate Email", key: "corporate_email", width: 30 },
];
// 🧠 Format data
data.forEach((item) => {
worksheet.addRow({
id: item.id,
factory_name: item.factory_name,
establishment_contact_person_name: item.establishment_contact_person_name,
emirate_name: item.establishment_emirate?.name || "-",
isic_code: item.isic_code || "-",
establishment_code: item.establishment_code,
total_employees: item.total_employees,
created_user: item.created_user?.name || "-",
created_at: new Date(item.created_at).toLocaleDateString(),
updated_at: new Date(item.updated_at).toLocaleDateString(),
status: item.is_active ? "Active" : "Inactive",
// city_name: item.establishment_city?.name || "-",
// contact_email: item.establishment_contact_email || "-",
// corporate_email: item.corporate_email || "-",
});
});
// 🖋️ Styling header
worksheet.getRow(1).eachCell((cell) => {
cell.font = { bold: true };
cell.alignment = { horizontal: "center" };
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
// 📤 Send as Excel file
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
res.setHeader(
"Content-Disposition",
`attachment; filename=Establishments_${Date.now()}.xlsx`
);
await workbook.xlsx.write(res);
return res.end();
}
// Otherwise return JSON (paginated)
const totalCount = await Establishment.count({ where: whereClause });
return res.status(200).json({
status: "success",
message: "Fetched establishments successfully",
data,
pagination: {
total_records: totalCount,
current_page: page,
total_pages: Math.ceil(totalCount / limit),
limit,
},
});
// return res.status(200).json({
// status: "success",
// message: "Fetched establishments successfully",
// data: rows,
// pagination: {
// total_records: count,
// current_page: page,
// total_pages: Math.ceil(count / limit),
// limit,
// },
// });
} catch (err) {
console.error(err);
return res.status(500).json({
status: "failed",
message: err.message || "Internal server error",
});
}
};
// Get one establishment
exports.getEstablishmentById = async (req, res) => {
try {
const data = await Establishment.findByPk(req.params.id, {
include: [
{
model: EstablishmentUser,
as: "users",
attributes: ["id", "name", "email", "gender", "is_active"],
},
{
model: CityTown,
as: "establishment_city",
attributes: ["name"],
},
{
model: Emirate,
as: "establishment_emirate",
attributes: ["name"],
},{
model: CityTown,
as: "corporate_city",
attributes: ["name"],
},
{
model: Emirate,
as: "corporate_emirate",
attributes: ["name"],
},
{
model: user,
as: "created_user",
attributes: ["name"],
},
{
model: EstablishmentProduct,
as: "establishment_products",
where: { is_active: 1 },
attributes: [
"id",
"establishment_id",
"product_id",
"action_done_by",
[
Sequelize.literal(`
CASE
WHEN establishment_products.action_done_by = 'admin_users' THEN
(SELECT name FROM admin_users WHERE admin_users.id = establishment_products.created_by)
WHEN establishment_products.action_done_by = 'establishment_users' THEN
(SELECT name FROM establishment_users WHERE establishment_users.id = establishment_products.created_by)
ELSE NULL
END
`),
"created_user"
]
],
include: [
{
model: Product,
as: "product",
attributes: ["product_name", "hs_code", "hs_description"],
},
],
}
],
});
if (!data) return res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" });
return res.status(200).send({'status':"success",'message':"Fetched successfully",'data': data });
} catch (err) {
return res.status(500).send({'status':"failed",'message':err.message });
}
};
// Update establishment
exports.updateEstablishment = async (req, res) => {
try {
// convert empty "" → null BEFORE destructure
[
"establishment_code",
"factory_name",
"permanent_factory_code",
"industry_code",
"industry_code_production",
"license_number",
"isic_code"
].forEach(key => {
if (req.body[key] === "") req.body[key] = null;
});
const { id } = req.params;
const { establishment_products, establishment_user, ...estData } = req.body;
const user = await EstablishmentUser.findOne({where:{establishment_id:id}});
// console.log("Establishment User email: ",user.email)
// if (user?.email === 'bhavinkumar.chandulal@fcsc.gov.ae') {
// return res.status(403).json({
// status: "error",
// message: "You cannot modify Super Admin data."
// });
// }
const resolveActionDoneBy = (item) => {
if (item?.action_done_by) return item.action_done_by;
if (req.user.role === "Admin") return "admin_users";
return "establishment_users";
};
const establishment = await Establishment.findByPk(id);
if (!establishment) {
return res.status(404).json({ status: "failed", message: "Record not found" });
}
// check duplicate establishment_code
if (estData.establishment_code) {
const exists = await Establishment.findOne({
where: { establishment_code: estData.establishment_code, id: { [Op.ne]: id } }
});
if (exists) {
return res.status(400).json({ status:"failed", message:"establishment_code already exists" });
}
}
estData.updated_by = estData.updated_by || req.user.id;
estData.updated_at = estData.updated_at || new Date();
// update establishment
await establishment.update(estData);
if (req.body.is_active === true || req.body.is_active === false) {
await EstablishmentUser.update(
{
is_active : req.body.is_active,
updated_by: req.user.id,
updated_at: new Date()
},
{ where: { establishment_id: id } }
);
await EstablishmentProduct.update(
{
is_active : req.body.is_active,
updated_by: req.user.id,
updated_at: new Date()
},
{ where: { establishment_id: id } }
);
}
// update user if needed
if (establishment_user) {
if (user) {
let updateUser = {
name: establishment_user.name,
email: establishment_user.email
};
if (establishment_user.password) {
updateUser.password = await bcrypt.hash(establishment_user.password,10);
}
updateUser.updated_by = updateUser.updated_by || req.user.id;
updateUser.updated_at = updateUser.updated_at || new Date();
await user.update(updateUser);
}
}
// update establishment products
// if (Array.isArray(establishment_products)) {
// let incomingIds = establishment_products.map(e => e.product_id);
// const oldProducts = await EstablishmentProduct.findAll({ where:{ establishment_id:id } });
// const oldIds = oldProducts.map(e => e.product_id);
// // remove products not in new list
// const removeIds = oldIds.filter(v => !incomingIds.includes(v));
// if (removeIds.length) {
// await EstablishmentProduct.destroy({ where:{ establishment_id:id, product_id:removeIds } });
// }
// // add only new ones
// const newIds = incomingIds.filter(v => !oldIds.includes(v));
// for (let pid of newIds) {
// await EstablishmentProduct.create({ establishment_id:id, product_id:pid, created_by: req.user.id});
// }
// }
if (Array.isArray(establishment_products)) {
const incoming = establishment_products;
const incomingIds = incoming.map(e => e.product_id);
const oldProducts = await EstablishmentProduct.findAll({
where: { establishment_id: id }
});
const oldIds = oldProducts.map(e => e.product_id);
// SOFT-REMOVE products not present anymore
const removeIds = oldIds.filter(v => !incomingIds.includes(v));
if (removeIds.length > 0) {
// find matching incoming data for audit fields
const removedData = incoming.find(
p => removeIds.includes(p.product_id)
);
await EstablishmentProduct.update(
{
is_active: 0,
updated_by: removedData?.updated_by ,
// action_done_by: removedData?.action_done_by || null,
action_done_by: req.user.role === "Admin" ? "admin_users" : "establishment_users",
updated_at: new Date()
},
{
where: {
establishment_id: id,
product_id: removeIds
}
}
);
}
// ADD new active products
const newItems = incoming.filter(p => !oldIds.includes(p.product_id));
for (const item of newItems) {
await EstablishmentProduct.create({
establishment_id: id,
product_id: item.product_id,
is_active: 1,
created_by: item.created_by || null,
// action_done_by: item.action_done_by,
action_done_by: resolveActionDoneBy(item),
created_at: new Date()
});
}
// UPDATE existing product metadata
const updateItems = incoming.filter(p => oldIds.includes(p.product_id));
for (const item of updateItems) {
await EstablishmentProduct.update(
{
is_active: 1,
updated_by: item.updated_by || null,
// action_done_by: item.action_done_by,
action_done_by: resolveActionDoneBy(item),
updated_at: new Date()
},
{
where: {
establishment_id: id,
product_id: item.product_id
}
}
);
}
}
return res.json({ status:"success", message:"Updated successfully" , data:"" });
} catch(err) {
if (err.name === "SequelizeUniqueConstraintError") {
const field = err.errors[0].path;
return res.status(400).json({ status:"failed", message:`${field} already exists` });
}
return res.status(500).json({ status:"failed", message: err.message });
}
};
// exports.updateEstablishment = async (req, res) => {
// try {
// const [updated] = await Establishment.update(req.body, {
// where: { id: req.params.id },
// });
// if (!updated){ return res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" }); }
// return res.status(200).send({'status':"success",'message':"Updated successfully",'data': "" });
// } catch (err) {
// return res.status(500).send({'status':"failed",'message':err.message });
// }
// };
// Delete establishment
exports.deleteEstablishment = async (req, res) => {
try {
const deleted = await Establishment.destroy({ where: { id: req.params.id } });
if (!deleted) return res.status(404).send({'status':"failed",'message':"Record Not found",'data': "" });
return res.status(200).send({'status':"success",'message':"Deleted successfully",'data': "" });
} catch (err) {
return res.status(500).send({'status':"failed",'message':err.message });
}
};
exports.getAllEmirates = async (req, res) => {
try {
const emirates = await Emirate.findAll({
attributes: ["id", "name", "code"],
order: [["name", "ASC"]],
});
return res.status(200).send({status: "success", message: "Emirates fetched successfully", data: emirates,});
} catch (err) {
return res.status(500).send({status: "failed",message: err.message || "Internal server error", });
}
};
exports.getAllCityTowns = async (req, res) => {
try {
const { emirate_id } = req.query;
const whereClause = {};
if (emirate_id) {
whereClause.emirate_id = emirate_id;
}
const cities = await CityTown.findAll({
where: whereClause,
attributes: ["id", "name", "emirate_id"],
order: [["name", "ASC"]],
});
return res.status(200).send({status: "success", message: "City/Town fetched successfully", data: cities, });
} catch (err) {
return res.status(500).send({ status: "failed", message: err.message || "Internal server error", });
}
};
exports.getAllRequests = async (req, res) => {
try {
const requests = await EstablishmentPasswordResetRequest.findAll({
order: [["created_at", "DESC"]],
});
res.status(200).json({
status: "success",
data: requests,
});
} catch (err) {
res.status(500).json({ status: "failed", message: err.message });
}
};
exports.createRequest = async (req, res) => {
try {
const {
establishment_name,
establishment_code,
registered_email,
contact_person_name,
contact_phone,
additional_notes,
} = req.body;
// Validate input
if (!establishment_name || !establishment_code || !registered_email) {
return res.status(400).json({
status: "failed",
message: "establishment_name, establishment_code, and registered_email are required",
});
}
// Find user by email
const user = await EstablishmentUser.findOne({
where: { email: registered_email },
});
if (!user)
return res.status(404).json({ status: "failed", message: "Registered email not found" });
// Find establishment by code
const establishment = await Establishment.findOne({
where: { code: establishment_code },
});
if (!establishment)
return res.status(404).json({ status: "failed", message: "Invalid establishment code" });
// Verify both match
if (user.establishment_id !== establishment.id)
return res.status(400).json({
status: "failed",
message: "Establishment mismatch between code and email",
});
// Create reset request
const newRequest = await EstablishmentPasswordResetRequest.create({
establishment_name,
establishment_code,
registered_email,
contact_person_name,
contact_phone,
additional_notes,
establishment_user_id: user.id,
establishment_id: establishment.id,
created_by: req.user.id,
});
// Trigger Email
const subject = "Password Reset Request Received";
const body = `
Dear ${contact_person_name || establishment_name},
We have received your password reset request for establishment "${establishment_name}".
Our team will verify and get back to you shortly.
Regards,
Support Team
`;
await sendEmail(registered_email, subject, body);
res.status(201).json({
status: "success",
message: "Password reset request created and email sent successfully",
data: newRequest,
});
} catch (err) {
console.error(err);
res.status(500).json({ status: "failed", message: err.message });
}
};
exports.forgotPasswordRequestOTP = async (req, res) => {
try {
const { user_type , establishment_name, establishment_code, registered_email } = req.body;
if(user_type == 'establishment_user')
{
// Validate inputs
if (!establishment_name || !establishment_code || !registered_email)
return res.status(400).json({ status: "failed", message: "All fields are required" });
// Find establishment and user
const establishment = await Establishment.findOne({ where: { establishment_code } });
if (!establishment)
return res.status(404).json({ status: "failed", message: "Establishment not found" });
const user = await EstablishmentUser.findOne({
where: { email: registered_email, establishment_id: establishment.id },
});
if (!user)
return res.status(404).json({ status: "failed", message: "User not found for this establishment" });
// Generate 6-digit OTP
const otp = Math.floor(100000 + Math.random() * 900000).toString();
// Hash OTP before saving (security)
const hashedOtp = await bcrypt.hash(otp, 10);
// Store OTP in user record
await user.update({
reset_otp: hashedOtp,
reset_otp_expires_at: new Date(Date.now() + 10 * 60 * 1000), // valid for 10 mins
});
// Send OTP email
await sendEmail(
registered_email,
"Password Reset OTP",
`<p>Dear ${user.name},</p><p>Your OTP for password reset is <b>${otp}</b>. It is valid for 10 minutes.</p>`
);
}else{
const adminUser = await user.findOne({where: { email: registered_email },});
if (!adminUser)
return res.status(404).json({ status: "failed", message: "Admin user not found" });
// Generate 6-digit OTP
const otp = Math.floor(100000 + Math.random() * 900000).toString();
// Hash OTP before saving (security)
const hashedOtp = await bcrypt.hash(otp, 10);
// Store OTP in user record
await adminUser.update({
reset_otp: hashedOtp,
reset_otp_expires_at: new Date(Date.now() + 10 * 60 * 1000), // valid for 10 mins
});
// Send OTP email
await sendEmail(
registered_email,
"Password Reset OTP",
`<p>Dear ${adminUser.name},</p><p>Your OTP for password reset is <b>${otp}</b>. It is valid for 10 minutes.</p>`
);
}
const safeEmail = sanitizeForLog(registered_email);
logger.info(`OTP sent email: ${safeEmail}`);
return res.status(200).json({
status: "success",
message: "OTP sent successfully to registered email",
});
} catch (err) {
logger.error(`Error in forgotPasswordRequestOTP: ${err.message}`);
return res.status(500).json({ status: "failed", message: err.message });
}
};
exports.forgotPasswordVerifyOTP = async (req, res) => {
try {
const { user_type , registered_email, otp, password, confirm_password } = req.body;
if (!registered_email || !otp || !password || !confirm_password)
return res.status(400).json({ status: "failed", message: "All fields are required" });
if (password !== confirm_password)
return res.status(400).json({ status: "failed", message: "Passwords do not match" });
if(user_type == 'establishment_user')
{
const user = await EstablishmentUser.findOne({ where: { email: registered_email } });
if (!user || !user.reset_otp)
return res.status(404).json({ status: "failed", message: "verification code not found or invalid user" });
// Check OTP expiry
if (new Date() > new Date(user.reset_otp_expires_at))
return res.status(400).json({ status: "failed", message: "Verification code has expired. Please request a new one" });
// Compare OTP
const isOtpValid = await bcrypt.compare(otp, user.reset_otp);
if (!isOtpValid)
return res.status(400).json({ status: "failed", message: "Invalid verification code" });
// Update password
const hashedPassword = await bcrypt.hash(password, 10);
await user.update({
password: hashedPassword,
reset_otp: null,
reset_otp_expires_at: null,
});
}else{
const adminUser = await user.findOne({ where: { email: registered_email } });
if (!adminUser || !adminUser.reset_otp)
return res.status(404).json({ status: "failed", message: "verification code not found or invalid user" });
// Check OTP expiry
if (new Date() > new Date(adminUser.reset_otp_expires_at))
return res.status(400).json({ status: "failed", message: "Verification code has expired. Please request a new one" });
// Compare OTP
const isOtpValid = await bcrypt.compare(otp, adminUser.reset_otp);
if (!isOtpValid)
return res.status(400).json({ status: "failed", message: "Invalid verification code" });
// Update password
const hashedPassword = await bcrypt.hash(password, 10);
await adminUser.update({
password: hashedPassword,
reset_otp: null,
reset_otp_expires_at: null,
});
}
const safeEmail = sanitizeForLog(registered_email);
logger.info(`Password reset successful for user: ${safeEmail}`);
return res.status(200).json({
status: "success",
message: "Password reset successfully",
});
} catch (err) {
logger.error(`Error in forgotPasswordVerifyOTP: ${err.message}`);
return res.status(500).json({ status: "failed", message: err.message });
}
};
const GENERIC_ERROR_MSG =
"Invalid data. Please check the instructions given and upload again.";
exports.establishmentBulkUpload = async (req, res) => {
let transaction = null;
let filePath = null;
try {
if (!req.file) {
return res.status(400).send({
status: "failed",
message: "No file uploaded."
});
}
// ---------------------------
// SAFE PATH HANDLING (Scanner-friendly)
// ---------------------------
// Resolve multer's actual saved file path
const uploadedPath = path.resolve(req.file.path);
// Derive the directory multer actually used
const multerUploadDir = path.resolve(path.dirname(uploadedPath));
// Optionally, a configured upload dir (if you set one in your app)
// We prefer the multer directory (so mismatched configs don't break).
const configuredUploadsDir = path.resolve(process.env.UPLOAD_DIR || path.join(__dirname, "../../uploads"));
// Use the directory that actually contains the uploaded file (prefer multer's)
const baseUploadsDir = multerUploadDir || configuredUploadsDir;
// Ensure uploadedPath is inside baseUploadsDir using path.relative (cross-platform safe)
const relative = path.relative(baseUploadsDir, uploadedPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
// not inside the uploads directory -> possible traversal / mismatch
if (fs.existsSync(uploadedPath)) {
try { fs.unlinkSync(uploadedPath); } catch (e) { /* swallow cleanup error */ }
}
return res.status(400).send({
status: "failed",
message: "Invalid file path detected."
});
}
// Use the validated path from multer
filePath = uploadedPath;
// ---------------------------
// End safe path handling
// ---------------------------
// Validate extension (based on original filename uploaded by user)
if (!req.file.originalname.toLowerCase().endsWith(".csv")) {
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) {}
}
return res.status(400).send({
status: "failed",
message: "Invalid file type. Only CSV allowed."
});
}
const rows = [];
// -------------------------------------------------------
// READ CSV AND NORMALIZE HEADERS
// -------------------------------------------------------
await new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on("data", (rawRow) => {
const row = {};
for (const key in rawRow) {
const normalizedKey = key
.replace(/\*/g, "")
.replace(/\([^)]*\)/g, "")
.trim()
.replace(/[\/\-\s]+/g, "_")
.replace(/[^\w]+/g, "")
.toLowerCase()
.replace(/_{2,}/g, "_")
.replace(/^_+|_+$/g, "");
row[normalizedKey] = rawRow[key]?.trim() || "";
}
const cleaned = Object.values(row).map(v => (v || "").replace(/\s+/g, "").trim());
const empty = cleaned.every(v => v === "");
if (!empty) rows.push(row);
})
.on("end", resolve)
.on("error", reject);
});
if (rows.length === 0) {
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) {}
}
return res.status(400).send({
status: "failed",
message: "CSV file is empty."
});
}
// -------------------------------------------------------
// REQUIRED HEADERS
// -------------------------------------------------------
const required = ["establishment_id", "factory_name", "user_name", "email", "emirate"];
const firstKeys = Object.keys(rows[0]);
const missingHeaders = required.filter(h => !firstKeys.includes(h));
if (missingHeaders.length > 0) {
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) {}
}
return res.status(400).send({
status: "failed",
message: "Missing required columns: " + missingHeaders.join(", ")
});
}
// -------------------------------------------------------
// LOAD REFERENCE TABLES ONCE
// -------------------------------------------------------
const emirates = await Emirate.findAll({ attributes: ["id", "name"] });
const emirateMap = {};
emirates.forEach(e => (emirateMap[e.name.trim().toLowerCase()] = e.id));
const cityTowns = await CityTown.findAll({ attributes: ["id", "name"] });
const cityTownMap = {};
cityTowns.forEach(ct => (cityTownMap[ct.name.trim().toLowerCase()] = ct.id));
function normalizeHS(val) {
if (!val) return "";
return val.toString().normalize("NFKD").replace(/[^\d]/g, "").trim();
}
const products = await Product.findAll({ attributes: ["id", "hs_code"] });
const productMap = {};
products.forEach(p => {
const c = normalizeHS(p.hs_code);
if (c.length >= 1 && c.length <= 10) productMap[c] = p.id;
});
const existing = await Establishment.findAll({
attributes: [
"establishment_code",
"factory_name",
"establishment_contact_email",
"industry_code_production",
"permanent_factory_code",
"industry_code"
]
});
const existingEstSet = new Set(existing.map(e => e.establishment_code));
const existingFactorySet = new Set(existing.map(e => (e.factory_name || "").toLowerCase()));
const existingEmailSet = new Set(existing.map(e => (e.establishment_contact_email || "").toLowerCase()));
const existingIndustryCodeSet = new Set(existing.map(e => e.industry_code_production).filter(Boolean));
const existingPermanentFactoryCodeSet = new Set(existing.map(e => e.permanent_factory_code).filter(Boolean));
const existingIndustryCodeBusinessSet = new Set(existing.map(e => e.industry_code).filter(Boolean));
// FILE duplicate trackers
const fileEstSet = new Set();
const fileFactorySet = new Set();
const fileEmailSet = new Set();
const fileIndustryCodeSet = new Set();
const filePermanentFactoryCodeSet = new Set();
const fileIndustryCodeBusinessSet = new Set();
const errors = [];
const prepared = [];
// -------------------------------------------------------
// PHASE 1: VALIDATE ALL ROWS (NO DB INSERT HERE)
// -------------------------------------------------------
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const rowNum = i + 2;
const est = r.establishment_id;
const factory = r.factory_name;
const email = r.email;
const emirate = r.emirate;
const missing = [];
if (!est) missing.push("Establishment ID");
if (!factory) missing.push("Factory Name");
if (!r.user_name) missing.push("User Name");
if (!email) missing.push("Email");
if (!emirate) missing.push("Emirate");
if (missing.length) {
errors.push({ row: rowNum, error: `Missing required fields: ${missing.join(", ")}` });
continue;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push({ row: rowNum, error: "Invalid Email format" });
continue;
}
// FILE-level duplicates
const estKey = est.trim();
const factoryKey = factory.trim().toLowerCase();
const emailKey = email.trim().toLowerCase();
if (fileEstSet.has(estKey)) {
errors.push({ row: rowNum, error: `Duplicate Establishment ID in file: ${est}` });
continue;
}
if (fileFactorySet.has(factoryKey)) {
errors.push({ row: rowNum, error: `Duplicate Factory Name in file: ${factory}` });
continue;
}
if (fileEmailSet.has(emailKey)) {
errors.push({ row: rowNum, error: `Duplicate Email in file: ${email}` });
continue;
}
// Check industry_code_production duplicates (if provided)
const industryCodeProd = r.industry_code_current_production?.trim();
if (industryCodeProd) {
if (fileIndustryCodeSet.has(industryCodeProd)) {
errors.push({ row: rowNum, error: `Duplicate Industry Code (Current Production) in file: ${industryCodeProd}` });
continue;
}
if (existingIndustryCodeSet.has(industryCodeProd)) {
errors.push({ row: rowNum, error: `Industry Code (Current Production) already exists in database: ${industryCodeProd}` });
continue;
}
fileIndustryCodeSet.add(industryCodeProd);
}
const permanentFactoryCode = r.permanent_factory_code?.trim();
if (permanentFactoryCode) {
if (filePermanentFactoryCodeSet.has(permanentFactoryCode)) {
errors.push({ row: rowNum, error: `Duplicate Permanent Factory Code in file: ${permanentFactoryCode}` });
continue;
}
if (existingPermanentFactoryCodeSet.has(permanentFactoryCode)) {
errors.push({ row: rowNum, error: `Permanent Factory Code already exists in database: ${permanentFactoryCode}` });
continue;
}
filePermanentFactoryCodeSet.add(permanentFactoryCode);
}
// Check industry_code_business_register duplicates (if provided)
const industryCodeBusiness = r.industry_code_business_register?.trim();
if (industryCodeBusiness) {
if (fileIndustryCodeBusinessSet.has(industryCodeBusiness)) {
errors.push({ row: rowNum, error: `Duplicate Industry Code (Business Register) in file: ${industryCodeBusiness}` });
continue;
}
if (existingIndustryCodeBusinessSet.has(industryCodeBusiness)) {
errors.push({ row: rowNum, error: `Industry Code (Business Register) already exists in database: ${industryCodeBusiness}` });
continue;
}
fileIndustryCodeBusinessSet.add(industryCodeBusiness);
}
fileEstSet.add(estKey);
fileFactorySet.add(factoryKey);
fileEmailSet.add(emailKey);
// DB-level duplicates
if (existingEstSet.has(estKey)) {
errors.push({ row: rowNum, error: `Establishment ID already exists: ${est}` });
continue;
}
if (existingFactorySet.has(factoryKey)) {
errors.push({ row: rowNum, error: `Factory Name already exists: ${factory}` });
continue;
}
if (existingEmailSet.has(emailKey)) {
errors.push({ row: rowNum, error: `Email already exists: ${email}` });
continue;
}
// Emirate validation
const emirateId = emirateMap[emirate.toLowerCase()];
if (!emirateId) {
errors.push({ row: rowNum, error: `Invalid Emirate: ${emirate}` });
continue;
}
// HS Code validation
const hsCols = Object.keys(r).filter(k => k.startsWith("hs"));
const hsRaw = hsCols.map(k => r[k]).filter(Boolean);
if (hsRaw.length === 0) {
errors.push({ row: rowNum, error: "At least one HS code is required" });
continue;
}
const cleaned = hsRaw.map(h => normalizeHS(h));
const invalid = cleaned.filter(c => !productMap[c]);
if (invalid.length > 0) {
errors.push({ row: rowNum, error: `Invalid HS Code(s): ${invalid.join(", ")}` });
continue;
}
prepared.push({
r,
rowNum,
est,
factory,
email,
emirateId,
productIds: cleaned.map(c => productMap[c])
});
}
// -------------------------------------------------------
// STOP IF ANY ERRORS FROM PHASE 1
// -------------------------------------------------------
if (errors.length > 0) {
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) {}
}
return res.status(400).send({
status: "failed",
message: "Invalid data. Please check and upload again.",
errors
});
}
// -------------------------------------------------------
// PHASE 2: CITY/TOWN VALIDATION (OPTIONAL BUT MUST BE VALID)
// -------------------------------------------------------
for (const p of prepared) {
const r = p.r;
const name = r.city_town?.trim().toLowerCase() || "";
if (name) {
const id = cityTownMap[name];
if (!id) {
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) {}
}
return res.status(400).send({
status: "failed",
message: "Invalid City/Town found. Upload stopped.",
errors: [{ row: p.rowNum, error: `Invalid City/Town: ${r.city_town}` }]
});
}
r.city_town_id = id;
} else {
r.city_town_id = null;
}
}
// -------------------------------------------------------
// PHASE 3: START TRANSACTION AND INSERT INTO DATABASE
// -------------------------------------------------------
transaction = await sequelize.transaction();
for (const p of prepared) {
const r = p.r;
const est = await Establishment.create({
establishment_code: p.est,
license_number: p.est,
factory_name: p.factory,
establishment_contact_email: p.email,
establishment_emirate_id: p.emirateId,
permanent_factory_code: r.permanent_factory_code || null,
industry_code: r.industry_code_business_register || null,
industry_code_production: r.industry_code_current_production || null,
industry_code_mismatch_remarks: r.industry_code_mismatch_remarks || null,
description: r.description || null,
establishment_address: r.establishment_address || null,
establishment_city_town_id: r.city_town_id || null,
establishment_postal_code: r.postal_code || null,
establishment_po_box: r.po_box || null,
establishment_makani_number: r.makani_number || null,
establishment_contact_person_name: r.contact_person_name || null,
establishment_contact_person_designation: r.contact_person_designation || null,
establishment_mobile_number: r.mobile_number || null,
establishment_website: r.website || null,
emirati_male: Number(r.number_of_emirati_male || 0),
emirati_female: Number(r.number_of_emirati_female || 0),
non_emirati_male: Number(r.number_of_non_emirati_male || 0),
non_emirati_female: Number(r.number_of_non_emirati_female || 0),
total_emirati:
Number(r.number_of_emirati_male || 0) +
Number(r.number_of_emirati_female || 0),
total_employees:
Number(r.number_of_emirati_male || 0) +
Number(r.number_of_emirati_female || 0) +
Number(r.number_of_non_emirati_male || 0) +
Number(r.number_of_non_emirati_female || 0),
created_by: req.user.id
}, { transaction });
const autoPassword = Math.random().toString(36).slice(-10);
const hashed = await bcrypt.hash(autoPassword, 10);
await EstablishmentUser.create({
establishment_id: est.id,
name: r.user_name,
email: p.email,
password: hashed,
created_by: req.user.id
}, { transaction });
const placeHolderData = {
contact_name: r.user_name,
portal_url: process.env.FE_BASE_URL,
username: p.email,
password: autoPassword,
support_email: process.env.SUPPORT_EMAIL,
support_phone: process.env.SUPPORT_PHONE
};
await sendEmailService(p.email, "establishment_user_creation_to_user", placeHolderData);
await EstablishmentProduct.bulkCreate(
p.productIds.map(pid => ({
establishment_id: est.id,
product_id: pid,
created_by: req.user.id
})),
{ transaction }
);
}
// COMMIT TRANSACTION - All inserts successful
await transaction.commit();
transaction = null; // Set to null after commit
// Clean up file after successful commit (safe)
if (filePath && fs.existsSync(filePath)) {
try { fs.unlinkSync(filePath); } catch (e) { logger.error("File cleanup error: " + e.message); }
}
return res.status(200).send({
status: "success",
message: `${prepared.length} establishments inserted successfully.`,
summary: {
total_records: rows.length,
imported: prepared.length,
errors: []
}
});
} catch (err) {
// ROLLBACK TRANSACTION if it exists
if (transaction) {
try {
await transaction.rollback();
logger.error("Transaction rolled back successfully due to error");
} catch (rollbackErr) {
logger.error("Rollback error: " + rollbackErr.message);
}
}
// Clean up file if it exists
if (filePath && fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
} catch (unlinkErr) {
logger.error("File cleanup error: " + unlinkErr.message);
}
}
// Log detailed error information
logger.error("Fatal Error in bulk upload: " + err.message);
if (err.name) {
logger.error("Error Name: " + err.name);
}
// Handle Sequelize validation errors
if (err.name === "SequelizeUniqueConstraintError" || err.name === "SequelizeValidationError") {
logger.error("Validation Errors:");
if (err.errors && Array.isArray(err.errors)) {
err.errors.forEach(validationError => {
logger.error(` - Field: ${validationError.path}, Value: ${validationError.value}, Message: ${validationError.message}`);
});
}
return res.status(400).send({
status: "failed",
message: "Database validation error: " + (err.errors?.[0]?.message || err.message),
errors: err.errors?.map(e => ({
field: e.path,
value: e.value,
message: e.message
}))
});
}
// Handle foreign key constraint errors
if (err.name === "SequelizeForeignKeyConstraintError") {
logger.error("Foreign Key Constraint Error: " + err.message);
return res.status(400).send({
status: "failed",
message: "Foreign key constraint error. Please check your reference data.",
error: err.message
});
}
// Log stack trace in development
if (process.env.NODE_ENV === 'development') {
logger.error("Stack trace: " + err.stack);
}
// Generic error response
return res.status(500).send({
status: "failed",
message: "Unexpected error occurred during bulk upload",
error: err.message
});
}
};
exports.downloadCompanyProfileSample = async (req, res) => {
try {
const safeBasePath = path.resolve(__dirname, "../downloads_csv");
const safeFilePath = path.join(safeBasePath, "company_profile_upload_sample.csv");
// Verify file exists BEFORE sending
if (!fs.existsSync(safeFilePath)) {
return res.status(404).send({
status: "failed",
message: "File not found",
});
}
return res.download(safeFilePath, "company_profile_upload_sample.csv");
} catch (error) {
return res.status(500).send({ status: "failed", message: error.message });
}
};