2118 lines
68 KiB
JavaScript
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,
|
|
});
|
|
}
|
|
|
|
};
|
|
|