fcsc_ipi_backend/app/controllers/establishment.controller.js

2118 lines
68 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 User = db.user;
const Product = db.Product;
const IsicMaster = db.IsicMaster;
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");
const sanitize = require("sanitize-html");
const crypto = require('crypto');
const sanitizeStringValue = (value) =>
typeof value === "string"
? sanitize(value, { allowedTags: [], allowedAttributes: {} })
: value;
function formatName(name) {
return name
.replace(/([A-Z])/g, ' $1')
.replace(/\b\w/g, char => char.toUpperCase())
.trim();
}
exports.testEmail = async (req, res) => {
placeHolderData = {
contact_name : '<contactName>',
portal_url : process.env.FE_BASE_URL,
username: '<userName>',
password: '<pwd>',
support_email : process.env.SUPPORT_EMAIL,
support_phone : process.env.SUPPORT_PHONE,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
}
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",
"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_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,
ERN
} = req.body;
// Basic Validation
if (!establishment_code) {
return res.status(400).send({
status: "failed",
message: "Missing required field: Unique License Number",
});
}
if (!establishment_user.name) {
return res.status(400).send({
status: "failed",
message: "User Profile: Name is required",
});
}
if (!establishment_user.email) {
return res.status(400).send({
status: "failed",
message: "User Profile: Email is required",
});
}
if (!factory_name) {
return res.status(400).send({
status: "failed",
message: "Establishment Contact Details: Name is required",
});
}
if (!establishment_emirate_id) {
return res.status(400).send({
status: "failed",
message: "Missing required field: Emirate",
});
}
// if (!establishment_city_town_id) {
// return res.status(400).send({
// status: "failed",
// message: "Missing required field: city/town",
// });
// }
if (!isic_code) {
return res.status(400).send({
status: "failed",
message: "Missing required field: Industry Code (Current Production)",
});
}
if (!corporate_email) {
return res.status(400).send({
status: "failed",
message: "Establishment Contact Details: Email is required",
});
}
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."
});
}
}
const existingEstablishment = await Establishment.findOne({where: { establishment_code }, });
if (existingEstablishment) {
return res.status(400).send({status: "failed",message: "Unique License Number already exists", });
}
const existingUser = await EstablishmentUser.findOne({
where: { email: establishment_user.email }
});
const checkEmailInAdminUser = await User.findOne({ where: { email: establishment_user.email, is_active: true } });
if (existingUser) {
return res.status(409).send({
status: "failed",
message: "User email already exists"
});
}
if (checkEmailInAdminUser) {
logger.info(`Email already exists in Admin Users: ${establishment_user.email}`);
return res.status(409).send({
status: "failed",
message: "This email address is already registered in the admin users."
});
}
// Create establishment record
const establishment = await Establishment.create({
establishment_code: sanitizeStringValue(establishment_code),
factory_name: sanitizeStringValue(factory_name),
permanent_factory_code: sanitizeStringValue(permanent_factory_code),
industry_code: sanitizeStringValue(industry_code),
industry_code_mismatch_remarks: sanitizeStringValue(industry_code_mismatch_remarks),
license_number: sanitizeStringValue(license_number),
isic_code: sanitizeStringValue(isic_code),
description: sanitizeStringValue(description),
establishment_address: sanitizeStringValue(establishment_address),
establishment_city_town_id,
establishment_emirate_id,
establishment_postal_code: sanitizeStringValue(establishment_postal_code),
establishment_po_box: sanitizeStringValue(establishment_po_box),
establishment_makani_number: sanitizeStringValue(establishment_makani_number),
establishment_contact_person_name: sanitizeStringValue(establishment_contact_person_name),
establishment_contact_person_designation: sanitizeStringValue(establishment_contact_person_designation),
establishment_mobile_number: sanitizeStringValue(establishment_mobile_number),
establishment_contact_email: sanitizeStringValue(establishment_contact_email),
establishment_website: sanitizeStringValue(establishment_website),
corporate_same_as_establishment,
corporate_name: sanitizeStringValue(corporate_name),
corporate_address: sanitizeStringValue(corporate_address),
corporate_city_town_id,
corporate_emirate_id,
corporate_postal_code: sanitizeStringValue(corporate_postal_code),
corporate_po_box: sanitizeStringValue(corporate_po_box),
corporate_makani_number: sanitizeStringValue(corporate_makani_number),
corporate_contact_person_name: sanitizeStringValue(corporate_contact_person_name),
corporate_contact_person_designation: sanitizeStringValue(corporate_contact_person_designation),
corporate_mobile_number: sanitizeStringValue(corporate_mobile_number),
corporate_email: sanitizeStringValue(corporate_email),
corporate_website: sanitizeStringValue(corporate_website),
emirati_male,
emirati_female,
non_emirati_male,
non_emirati_female,
total_emirati,
total_employees,
ERN: sanitizeStringValue(ERN),
created_by: req.body.created_by || req.user.id,
created_at: new Date(),
});
// Store password before hashing for email purpose only
const plainPasswordForEmail = establishment_user.password;
// 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,
});
// Prepare sanitized data for email notification
const emailContactName = sanitizeStringValue(establishment_user.name);
const emailUsername = sanitizeStringValue(establishment_user.email);
const emailPortalUrl = process.env.FE_BASE_URL;
const emailSupportEmail = process.env.SUPPORT_EMAIL;
const emailSupportPhone = process.env.SUPPORT_PHONE;
const placeHolderData = {
contact_name: formatName(emailContactName),
portal_url: emailPortalUrl,
username: emailUsername,
password: plainPasswordForEmail,
support_email: emailSupportEmail,
support_phone: emailSupportPhone,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
};
// Send email notification to user
await sendEmailService(emailUsername, 'establishment_user_creation_to_user', placeHolderData);
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;
}, []);
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);
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 res.status(201).send({
status: "success",
message: "Establishment and linked user created successfully."
});
} catch (err) {
if (err.name === "SequelizeUniqueConstraintError") {
const field = err.errors[0].path;
return res.status(400).json({
status: "failed",
message: `${field} already exists`
});
}
logger.error(`Error creating establishment: ${err.message }`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).send({status: "failed",message: "Internal server error" });
}
};
exports.getAllEstablishments = async (req, res) => {
try {
// Set security headers to prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
let {
page = 1,
limit = 10,
search = "",
emirate_id,
isic_code,
status,
sort_by = "created_at",
sort_order = "DESC",
export: exportType,
} = 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: "Products", key: "products", 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 },
];
// 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,
products: item.dataValues.product_count,
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",
});
});
// 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" },
};
});
// Set security headers for Excel download
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
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,
},
});
} catch (err) {
logger.error(`Error on get all establishment: ${err.message }`);
logger.error(`Stack trace: ${err.stack}`);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
return res.status(500).json({
status: "failed",
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) {
logger.error(`Error on get establishment by Id: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).send({'status':"failed",'message': "Internal server error" });
}
};
// Update establishment
exports.updateEstablishment = async (req, res) => {
try {
// convert empty "" → null BEFORE destructure
[
"establishment_code",
"factory_name",
"permanent_factory_code",
"industry_code",
"license_number",
"isic_code"
].forEach(key => {
if (req.body[key] === "") req.body[key] = null;
});
const { id } = req.params;
const {
establishment_products,
establishment_user,
establishment_code,
factory_name,
establishment_emirate_id,
establishment_city_town_id,
isic_code,
corporate_email,
...estData
} = req.body;
const user = await EstablishmentUser.findOne({where:{establishment_id:id}});
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" });
}
if (establishment_code !== undefined && !establishment_code) {
return res.status(400).json({
status: "failed",
message: "Missing required field: Unique License Number",
});
}
// establishment_user validation
if (establishment_user) {
if (!establishment_user.name) {
return res.status(400).json({
status: "failed",
message: "User Profile: Name is required",
});
}
if (!establishment_user.email) {
return res.status(400).json({
status: "failed",
message: "User Profile: Email is required",
});
}
}
// factory name
if (factory_name !== undefined && !factory_name) {
return res.status(400).json({
status: "failed",
message: "Establishment Contact Details: Name is required",
});
}
// emirate
if (establishment_emirate_id !== undefined && !establishment_emirate_id) {
return res.status(400).json({
status: "failed",
message: "Missing required field: Emirate",
});
}
// city / town
// if (establishment_city_town_id !== undefined && !establishment_city_town_id) {
// return res.status(400).json({
// status: "failed",
// message: "Missing required field: City/Town",
// });
// }
// industry code production
if ( isic_code!== undefined && !isic_code) {
return res.status(400).json({
status: "failed",
message: "Missing required field: Industry Code (Current Production)",
});
}
// corporate email
if (corporate_email !== undefined && !corporate_email) {
return res.status(400).json({
status: "failed",
message: "Establishment Contact Details: Email is required",
});
}
// 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" });
}
}
if (establishment_code !== undefined)
estData.establishment_code = establishment_code;
if (factory_name !== undefined)
estData.factory_name = factory_name;
if (establishment_emirate_id !== undefined)
estData.establishment_emirate_id = establishment_emirate_id;
if (establishment_city_town_id !== undefined)
estData.establishment_city_town_id = establishment_city_town_id;
if (corporate_email !== undefined)
estData.corporate_email = corporate_email;
// sync industry code & ISIC
if (isic_code !== undefined) {
estData.isic_code = isic_code;
}
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);
}
}
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" });
} catch(err) {
if (err.name === "SequelizeUniqueConstraintError") {
const field = err.errors[0].path;
return res.status(400).json({ status:"failed", message:`${field} already exists` });
}
logger.error(`Error updating establishment: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).json({ status:"failed", message: "Internal server error" });
}
};
// 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) {
logger.error(`Error deleting establishment: ${ err.message }`);
return res.status(500).send({'status':"failed",'message': "Internal server error" });
}
};
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) {
logger.error(`Error on get all emirates: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).send({status: "failed",message: "Internal server error", });
}
};
exports.getAllCityTowns = async (req, res) => {
try {
let { emirate_id } = req.query;
if (emirate_id) {
emirate_id = Number(emirate_id);
if (isNaN(emirate_id)) {
return res.status(400).send({
status: "failed",
message: "Invalid emirate_id"
});
}
const emirateExists = await Emirate.findOne({
where: { id: emirate_id }
});
if (!emirateExists) {
return res.status(404).send({
status: "failed",
message: "Emirate not found"
});
}
}
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"]],
});
const sanitizedCities = cities.map(city => ({
id: city.id,
name: sanitizeStringValue(city.name),
emirate_id: city.emirate_id,
}));
return res.status(200).send({status: "success", message: "City/Town fetched successfully", data: sanitizedCities, });
} catch (err) {
logger.error(`Error getting all city towns: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).send({ status: "failed", 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) {
logger.error(`Error on getall request: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
res.status(500).json({ status: "failed", message: "Internal server error" });
}
};
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);
logger.error(`Error on create request: ${ err.message }`);
logger.error(`Stack trace: ${err.stack}`);
res.status(500).json({ status: "failed", message: "Internal server error"});
}
};
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
});
const formattedSupportPhone = formatUAEPhoneNumber(process.env.SUPPORT_PHONE);
const placeHolderData = {
username: formatName(user.name),
verfication_code: otp,
otp_expiry: 10,
support_email: process.env.SUPPORT_EMAIL,
support_phone: formattedSupportPhone,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
};
await sendEmailService(registered_email, "iip_reset_password", placeHolderData);
logger.info(`Reset Password email trigger: ${sanitizeForLog(registered_email)}`);
}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
});
const formattedSupportPhone = formatUAEPhoneNumber(process.env.SUPPORT_PHONE);
const placeHolderData = {
username: formatName(user.name),
verfication_code: otp,
otp_expiry: 10,
support_email: process.env.SUPPORT_EMAIL,
support_phone: formattedSupportPhone,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
};
await sendEmailService(registered_email, "iip_reset_password", placeHolderData);
logger.info(`Reset Password email trigger: ${sanitizeForLog(registered_email)}`);
}
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(err.message);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).json({ status: "failed", message: "Internal server error" });
}
};
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.scope("withSensitive").findOne({ where: { email: registered_email } });
console.log(user);
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.scope("withSensitive").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(err.message);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).json({ status: "failed", message: "Internal server error" });
}
};
function formatUAEPhoneNumber(phone) {
if (!phone) return '';
// Convert number → string safely
let digits = String(phone).replace(/\D/g, '');
// Case 1: 9-digit UAE mobile (e.g. 506491429)
if (digits.length === 9 && digits.startsWith('5')) {
digits = '971' + digits;
}
// Case 2: 10-digit starting with 0 (e.g. 0506491429)
if (digits.length === 10 && digits.startsWith('0')) {
digits = '971' + digits.substring(1);
}
// Case 3: Already has country code
if (digits.startsWith('971') && digits.length === 12) {
// ok
} else if (!digits.startsWith('971')) {
return phone; // fail-safe
}
return `+${digits.substring(0, 3)} ${digits.substring(3, 5)} ${digits.substring(5, 8)} ${digits.substring(8)}`;
}
exports.requestOTPForLogin = async (req, res) => {
try {
const { registered_email } = req.body;
if (!registered_email) {
return res.status(400).json({
status: "failed",
message: "Email is required",
});
}
// Check which model contains the user
let loggingUser = await EstablishmentUser.findOne({
where: { email: registered_email }
});
let userModel = EstablishmentUser;
if (!loggingUser) {
loggingUser = await User.findOne({
where: { email: registered_email, is_active: true }
});
userModel = User;
}
if (!loggingUser) {
logger.warn(`OTP requested for non-existing email: ${sanitizeForLog(registered_email)}`);
return res.status(404).json({
status: "failed",
message: "Invalid login request. Please check your email or register to continue.",
});
}
// Secure OTP generation
const otp = crypto.randomInt(100000, 999999).toString();
// Hash OTP before storing
const hashedOtp = await bcrypt.hash(otp, 10);
// Store OTP in the correct model with WHERE clause
const otp_expiry = new Date(Date.now() + 10 * 60 * 1000);
await userModel.update(
{
login_otp: hashedOtp,
login_otp_expires_at: otp_expiry,
},
{
where: { email: registered_email }
}
);
const formattedSupportPhone = formatUAEPhoneNumber(process.env.SUPPORT_PHONE);
const placeHolderData = {
username: formatName(loggingUser.name),
verfication_code: otp,
otp_expiry: 10,
support_email: process.env.SUPPORT_EMAIL,
support_phone: formattedSupportPhone,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
};
await sendEmailService(registered_email, "sign_in_verification_code", placeHolderData);
logger.info(`Login OTP email triggered for: ${sanitizeForLog(registered_email)}`);
return res.status(200).json({
status: "success",
message: "OTP sent successfully to your registered email",
});
} catch (err) {
logger.error(`OTP request failed: ${err.message}`);
logger.error(err.stack);
return res.status(500).json({
status: "failed",
message: "Internal Server Error",
});
}
};
exports.verifyOTPForLogin = async (req, res) => {
try {
let { registered_email, otp } = req.body;
const envDb = process.env.DB_NAME?.toLowerCase();
const isBypassEnv = envDb === "ipidev" || envDb === "ipiuat";
//DEV / UAT OTP BYPASS
if (isBypassEnv && otp === "123456") {
return res.status(200).json({
status: "success",
title: "Verified",
message: "You're signed in. Redirecting to your dashboard..."
});
}
if (!registered_email || !otp) {
return res.status(400).json({
status: "failed",
message: "Email and OTP are required"
});
}
// Find user from either model
let user = await EstablishmentUser.scope("withSensitive").findOne({
where: { email: registered_email }
});
if (!user) {
user = await User.scope("withSensitive").findOne({
where: { email: registered_email, is_active: true }
});
}
// User validation
if (!user) {
return res.status(404).json({
status: "failed",
message: "User not found"
});
}
// Check OTP expiry
if (new Date() > new Date(user.login_otp_expires_at)) {
return res.status(400).json({
status: "failed",
message: "Verification code has expired. Please request a new one"
});
}
if (!user.login_otp) {
return res.status(404).json({
status: "failed",
message: "Verification code not found or invalid user"
});
}
// Verify OTP
const isOtpValid = await bcrypt.compare(otp, user.login_otp);
if (!isOtpValid) {
return res.status(400).json({
status: "failed",
message: "Invalid verification code"
});
}
// Clear OTP after successful verification
await user.update({
login_otp: null,
login_otp_expires_at: null,
});
const safeEmail = sanitizeForLog(registered_email);
logger.info(`OTP verification successful for user: ${safeEmail}`);
return res.status(200).json({
status: "success",
title: "Verified",
message: "You're signed in. Redirecting to your dashboard...",
});
} catch (err) {
logger.error(`OTP verification error: ${err.message}`);
logger.error(`Stack trace: ${err.stack}`);
return res.status(500).json({
status: "failed",
message: "Internal server error"
});
}
};
const GENERIC_ERROR_MSG =
"Invalid data. Please check the instructions given and upload again.";
function sanitizeFilePath(userInput, allowedDirectory) {
if (!userInput || typeof userInput !== 'string') {
throw new Error('Invalid file path input');
}
let sanitized = userInput.replace(/\.\./g, '');
sanitized = sanitized.replace(/[\/\\]+/g, path.sep);
const filename = path.basename(sanitized);
const fullPath = path.join(allowedDirectory, filename);
const resolvedPath = path.resolve(fullPath);
const resolvedBase = path.resolve(allowedDirectory);
if (!resolvedPath.startsWith(resolvedBase)) {
throw new Error('Path traversal attempt detected');
}
return resolvedPath;
}
function validateFileExists(filePath) {
if (!filePath) {
return false;
}
try {
return fs.existsSync(filePath);
} catch (err) {
return false;
}
}
/**
* Safe file deletion with error handling
*/
function deleteFileSecure(filePath) {
if (!filePath) {
return;
}
try {
if (validateFileExists(filePath)) {
fs.unlinkSync(filePath);
}
} catch (err) {
if (logger && logger.error) {
logger.error('File deletion error: ' + err.message);
logger.error(`Stack trace: ${err.stack}`);
}
}
}
exports.establishmentBulkUpload = async (req, res) => {
let transaction = null;
let sanitizedPath = null;
try {
// Check if file was uploaded
if (!req.file) {
return res.status(400).send({
status: "failed",
message: "No file uploaded."
});
}
const { UPLOAD_DIR: ALLOWED_UPLOAD_DIR } = require('../config/upload.config');
try {
sanitizedPath = sanitizeFilePath(req.file.path, ALLOWED_UPLOAD_DIR);
} catch (sanitizeError) {
try {
const unsafePath = req.file.path;
if (unsafePath && fs.existsSync(unsafePath)) {
fs.unlinkSync(unsafePath);
}
} catch (cleanupErr) {
// Silent fail on cleanup
}
return res.status(400).send({
status: "failed",
message: "Invalid file path detected"
});
}
if (!validateFileExists(sanitizedPath)) {
return res.status(400).send({
status: "failed",
message: "File not found after validation"
});
}
const originalName = path.basename(req.file.originalname);
const fileExt = path.extname(originalName).toLowerCase();
if (fileExt !== '.csv') {
deleteFileSecure(sanitizedPath);
return res.status(400).send({
status: "failed",
message: "Invalid file type. Only CSV files are allowed."
});
}
const allowedMimeTypes = ['text/csv', 'application/csv', 'text/plain'];
if (req.file.mimetype && !allowedMimeTypes.some(mime => mime === req.file.mimetype)) {
deleteFileSecure(sanitizedPath);
return res.status(400).send({
status: "failed",
message: "Invalid MIME type. Only CSV files are allowed."
});
}
const rows = [];
await new Promise((resolve, reject) => {
fs.createReadStream(sanitizedPath)
.pipe(csv())
.on("data", (rawRow) => {
const row = {};
for (const key in rawRow) {
if (!rawRow.hasOwnProperty(key)) continue;
const normalizedKey = key
.replace(/\*/g, "")
.replace(/\([^)]*\)/g, "")
.trim()
.replace(/[\/\-\s]+/g, "_")
.replace(/[^\w]+/g, "")
.toLowerCase()
.replace(/_{2,}/g, "_")
.replace(/^_+|_+$/g, "");
row[normalizedKey] = rawRow[key] ? 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) {
deleteFileSecure(sanitizedPath);
return res.status(400).send({
status: "failed",
message: "CSV file is empty."
});
}
// -------------------------------------------------------
// REQUIRED HEADERS
// -------------------------------------------------------
const required = ["establishment_id", "factory_name", "user_name", "email", "emirate", "principal_activity_code", "city_town"];
const firstKeys = Object.keys(rows[0]);
const missingHeaders = required.filter(h => !firstKeys.includes(h));
if (missingHeaders.length > 0) {
deleteFileSecure(sanitizedPath);
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 = {};
for (let i = 0; i < emirates.length; i++) {
const e = emirates[i];
emirateMap[e.name.trim().toLowerCase()] = e.id;
}
const cityTowns = await CityTown.findAll({ attributes: ["id", "name"] });
const cityTownMap = {};
for (let i = 0; i < cityTowns.length; i++) {
const ct = cityTowns[i];
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 = {};
for (let i = 0; i < products.length; i++) {
const p = products[i];
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",
"isic_code",
"permanent_factory_code",
"industry_code"
]
});
const existingEstSet = new Set();
const existingFactorySet = new Set();
const existingEmailSet = new Set();
const existingIndustryCodeSet = new Set();
const existingPermanentFactoryCodeSet = new Set();
const existingIndustryCodeBusinessSet = new Set();
for (let i = 0; i < existing.length; i++) {
const e = existing[i];
existingEstSet.add(e.establishment_code);
existingFactorySet.add((e.factory_name || "").toLowerCase());
existingEmailSet.add((e.establishment_contact_email || "").toLowerCase());
if (e.isic_code) existingIndustryCodeSet.add(e.isic_code);
if (e.permanent_factory_code) existingPermanentFactoryCodeSet.add(e.permanent_factory_code);
if (e.industry_code) existingIndustryCodeBusinessSet.add(e.industry_code);
}
// 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 = [];
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 industry_code_current_production = r.principal_activity_code;
const city_town = r.city_town;
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 (!industry_code_current_production) missing.push("Principal Activity Code");
if (!city_town) missing.push("City/Town");
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 ? r.industry_code_current_production.trim() : null;
// 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 ? r.permanent_factory_code.trim() : null;
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 ? r.industry_code_business_register.trim() : null;
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);
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: r,
rowNum: rowNum,
est: est,
factory: factory,
email: email,
emirateId: emirateId,
productIds: cleaned.map(c => productMap[c])
});
}
if (errors.length > 0) {
deleteFileSecure(sanitizedPath);
return res.status(400).send({
status: "failed",
message: "Invalid data. Please check and upload again.",
errors: errors
});
}
//CITY/TOWN VALIDATION (OPTIONAL BUT MUST BE VALID)
for (let j = 0; j < prepared.length; j++) {
const p = prepared[j];
const r = p.r;
const cityTownValue = r.city_town ? r.city_town.trim().toLowerCase() : "";
const uploadedEmails = prepared.map(p =>
p.email.trim().toLowerCase()
);
// fetch once
const [adminUsers, establishmentUsers] = await Promise.all([
User.findAll({
where: { email: uploadedEmails, is_active: true },
attributes: ["email"]
}),
EstablishmentUser.findAll({
where: { email: uploadedEmails, is_active: true },
attributes: ["email"]
})
]);
const adminEmailSet = new Set(
adminUsers.map(u => u.email.toLowerCase())
);
const establishmentEmailSet = new Set(
establishmentUsers.map(u => u.email.toLowerCase())
);
// collect errors
const emailErrors = prepared
.map(p => {
const email = p.email.trim().toLowerCase();
return establishmentEmailSet.has(email)
? { row: p.rowNum, error: "Email address already exists in company profile" }
: adminEmailSet.has(email)
? { row: p.rowNum, error: "Email address is already registered in the admin users" }
: null;
})
.filter(Boolean);
// stop upload if any error found
if (emailErrors.length > 0) {
deleteFileSecure(sanitizedPath);
return res.status(409).send({
status: "failed",
message: "Email validation failed",
errors: emailErrors
});
}
if (cityTownValue) {
const id = cityTownMap[cityTownValue];
if (!id) {
deleteFileSecure(sanitizedPath);
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;
}
}
transaction = await sequelize.transaction();
for (let k = 0; k < prepared.length; k++) {
const p = prepared[k];
const r = p.r;
// const existEstablishmentUser = await EstablishmentUser.findOne({ where: { email:p.email, is_active: true } });
// const existingAdminUser = await User.findOne({ where: { email:p.email, is_active: true } });
// if (existEstablishmentUser) {
// logger.info(`Email already exists in company profile: ${p.email}`);
// return res.status(409).send({
// status: "failed",
// message: "This email address is already exists in company profile."
// });
// }
// if (existingAdminUser) {
// logger.info(`Email already exists in Admin Users: ${p.email}`);
// return res.status(409).send({
// status: "failed",
// message: "This email address is already registered in the admin users."
// });
// }
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,
isic_code: 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: transaction });
const generateSecurePassword = (length = 12) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%';
let password = '';
const randomBytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
password += chars[randomBytes[i] % chars.length];
}
return password;
};
const autoPassword = generateSecurePassword(12);
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: transaction });
const placeHolderData = {
contact_name:formatName(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,
logo_url: `${process.env.APP_BASE_URL}/assets/FCSCLogo.png`
};
await sendEmailService(p.email, "establishment_user_creation_to_user", placeHolderData);
const productInserts = [];
for (let m = 0; m < p.productIds.length; m++) {
productInserts.push({
establishment_id: est.id,
product_id: p.productIds[m],
created_by: req.user.id
});
}
await EstablishmentProduct.bulkCreate(productInserts, { transaction: transaction });
}
await transaction.commit();
transaction = null;
deleteFileSecure(sanitizedPath);
return res.status(200).send({
status: "success",
message: `${prepared.length} establishments inserted successfully.`,
summary: {
total_records: rows.length,
imported: prepared.length,
errors: []
}
});
} catch (err) {
if (transaction) {
try {
await transaction.rollback();
if (logger && logger.error) {
logger.error("Transaction rolled back successfully due to error");
}
} catch (rollbackErr) {
if (logger && logger.error) {
logger.error("Rollback error: " + rollbackErr.message);
}
}
}
deleteFileSecure(sanitizedPath);
if (logger && logger.error) {
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") {
if (logger && logger.error) {
logger.error("Validation Errors:");
if (err.errors && Array.isArray(err.errors)) {
for (let i = 0; i < err.errors.length; i++) {
const validationError = err.errors[i];
logger.error(` - Field: ${validationError.path}, Value: ${validationError.value}, Message: ${validationError.message}`);
}
}
}
const errorList = [];
if (err.errors && Array.isArray(err.errors)) {
for (let i = 0; i < err.errors.length; i++) {
const e = err.errors[i];
errorList.push({
field: e.path,
value: e.value,
message: e.message
});
}
}
return res.status(400).send({
status: "failed",
message: "Database validation error: " + (err.errors && err.errors[0] ? err.errors[0].message : err.message),
errors: errorList
});
}
// Handle foreign key constraint errors
if (err.name === "SequelizeForeignKeyConstraintError") {
if (logger && logger.error) {
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
});
}
if (process.env.NODE_ENV === 'development') {
if (logger && logger.error) {
logger.error("Stack trace: " + err.stack);
}
}
return res.status(500).send({
status: "failed",
message: "Unexpected error occurred during bulk upload"
});
}
};
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) {
logger.error('Error on downloading company profile sample file' + error.message );
logger.error(`Stack trace: ${error.stack}`);
return res.status(500).send({ status: "failed", message: "Internal server error" });
}
};
exports.searchIsicMaster = async (req, res) => {
const { code, description } = req.query;
try {
const whereClause = { is_active: true };
if (code || description) {
const orConditions = [];
if (code) {
orConditions.push({ code: { [Op.like]: `%${code}%` } });
}
if (description) {
orConditions.push({ description: { [Op.like]: `%${description}%` } });
}
whereClause[Op.or] = orConditions;
}
const rows = await IsicMaster.findAll({ where: whereClause });
return res.status(200).json({
success: "success",
count: rows.length,
data: rows,
});
} catch (error) {
return res.status(500).json({
success: "failed",
message: "Internal server error",
error: error.message,
});
}
};