first commit

This commit is contained in:
Isuru Bimsara
2026-08-14 22:46:02 +05:30
commit 81d0e65686
132 changed files with 24398 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/basis.util.js
const calculateBasis = (marginPercent, usdRate) => {
if (!usdRate || usdRate <= 0) {
throw new Error("Invalid USD rate");
}
const multiplier = (100 + marginPercent) / 100; // e.g. 25% -> 1.25
const basis = multiplier / usdRate;
return Number(basis).toFixed(10);
};
module.exports = { calculateBasis };
+129
View File
@@ -0,0 +1,129 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/cache.util.js
const redis = require("../config/redisClient");
const { log } = require("./consoleLog.utill");
const db = require("../models");
const User = db.User;
///////////////////////////////
// User Cache //
/////////////////////////////
const CACHE_TTL = 20 * 60; // 20 minutes (in seconds)
const getCacheKey = (email) => `user:${email}`;
async function getCachedUser(email) {
const key = getCacheKey(email);
try {
// 1. Check Redis first
if (process.env.CACHE === "true") {
const cached = await redis.get(key);
if (cached) {
log("⚡ Cache HIT - returning user from Redis");
return JSON.parse(cached);
}
}
log("Cache MISS - fetching user from DB");
// 2. Fetch from DB
const user = await User.findOne({ where: { email } });
if (!user) return null;
const plainUser = user.get({ plain: true });
// 3. Store in Redis
if (process.env.CACHE === "true") {
await redis.set(
key,
JSON.stringify(plainUser),
"EX",
CACHE_TTL
);
}
return plainUser;
} catch (err) {
log("Error fetching user:", err);
throw new Error("Failed to fetch user");
}
}
// Invalidate cache
async function clearUserCache(email) {
const key = getCacheKey(email);
try {
await redis.del(key);
log(`Cache cleared for user: ${email}`);
} catch (err) {
log("Error clearing cache:", err);
}
}
///////////////////////////////
// Permission Cache //
/////////////////////////////
const PERM_TTL = 10 * 60;
const getPermKey = (userId) => `perm:user:${userId}`;
async function getCachedPermissions(userId) {
if (process.env.CACHE !== "true") return null;
try {
const cached = await redis.get(getPermKey(userId));
return cached ? JSON.parse(cached) : null;
} catch (err) {
console.log("Cache read error:", err);
return null;
}
}
async function setCachedPermissions(userId, permissions) {
if (process.env.CACHE !== "true") return;
try {
await redis.set(
getPermKey(userId),
JSON.stringify(permissions),
"EX",
PERM_TTL
);
} catch (err) {
console.log("Cache write error:", err);
}
}
async function clearPermissionCache(userId) {
try {
await redis.del(getPermKey(userId));
} catch (err) {
console.log("Cache delete error:", err);
}
}
module.exports = {
getCachedUser,
clearUserCache,
getCachedPermissions,
setCachedPermissions,
clearPermissionCache
};
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/calendar.util.js
const db = require("../models");
const { log } = require("./consoleLog.utill");
const Calendar = db.calender;
const CalenderHoliday = db.calenderHolidays;
const calculateDeadline = async () => {
let deadlineDate = new Date();
const currentYear = deadlineDate.getFullYear();
// Get calendar for current year
const calendar = await Calendar.findOne({
where: {
year: currentYear
}
});
if (!calendar) {
throw new Error(`Calendar for year ${currentYear} not found`);
}
deadlineDate.setDate(deadlineDate.getDate() + 1);
while (true) {
const month = deadlineDate.getMonth() + 1;
const day = deadlineDate.getDate();
// Get holidays for this month
const holidays = await CalenderHoliday.findAll({
where: {
calender_id: calendar.calender_id,
month: month,
shouldConsiderAsHoliday: true
}
});
const isWeekend =
deadlineDate.getDay() === 0 ||
deadlineDate.getDay() === 6;
const holiday = holidays.find(
h => h.day === day
);
const isHoliday =
isWeekend || !!holiday;
log(
`${deadlineDate.toISOString().split("T")[0]} => Holiday: ${isHoliday}`
);
if (!isHoliday) {
return deadlineDate;
}
// Move one more day
deadlineDate.setDate(
deadlineDate.getDate() + 1
);
}
};
module.exports = {
calculateDeadline
};
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/consoleLog.util.js
const isDevelopment = process.env.NODE_ENV === "development";
const logQueue = require("../queues/log.queue");
const log = async (...args) => {
if (isDevelopment) {
console.log(...args);
return;
}
try {
const message = args
.map((arg) => {
if (arg instanceof Error) {
return `${arg.message}\n${arg.stack}`;
}
if (typeof arg === "object") {
return JSON.stringify(arg);
}
return String(arg);
})
.join(" ");
await logQueue.add("log", {
message,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error("Log queue failed:", err);
}
};
function runningEnvironment() {
return !isDevelopment;
}
module.exports = {
log,
runningEnvironment,
};
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/document.util.js
const { Document } = require("../models");
const { generateId, generateDocumentReferenceNo } = require("./idGen.util");
const {log} = require("./consoleLog.utill");
const DOCUMENT_STATUS = {
DRAFT: "DRAFT",
FINAL: "FINAL",
};
const validateStatus = (status) => {
if (!Object.values(DOCUMENT_STATUS).includes(status)) {
throw new Error(
`Invalid status "${status}". Allowed values: ${Object.values(
DOCUMENT_STATUS,
).join(", ")}`,
);
}
};
const createDraftDocument = async (document, doc_data) => {
log("Creating draft document data...");
log("Incoming document data:", JSON.stringify(doc_data, null, 2));
const result = await Document.create({
doc_id: generateId(),
reference_no: doc_data.reference_no || "N/A",
doc_type: document,
data: doc_data,
status: DOCUMENT_STATUS.DRAFT,
});
log("Draft document created with ID:", result.doc_id);
return result;
};
const finalizeDocument = async (document, doc_data) => {
if (!doc_data.doc_id) {
throw new Error("Document data ID is required for FINAL status.");
}
const [updatedRows] = await Document.update(
{
reference_no: doc_data.reference_no || "N/A",
doc_type: document,
data: doc_data,
status: DOCUMENT_STATUS.FINAL,
},
{
where: {
doc_id: doc_data.doc_id,
},
},
);
if (updatedRows === 0) {
throw new Error("Document data not found or already updated.");
}
return await Document.findOne({
where: {
doc_id: doc_data.doc_id,
},
});
};
const createDocumentData = async (document, doc_data, status) => {
try {
validateStatus(status);
switch (status) {
case DOCUMENT_STATUS.DRAFT:
return await createDraftDocument(document, doc_data);
case DOCUMENT_STATUS.FINAL:
return await finalizeDocument(document, doc_data);
default:
throw new Error(`Unhandled document status: ${status}`);
}
} catch (error) {
console.error("❌ Document data operation failed:", error.message);
throw error;
}
};
module.exports = {
createDocumentData,
DOCUMENT_STATUS,
};
+168
View File
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/documentJob.util.js
const documentQueue = require("../queues/pdf.queue");
/**
* Queue a document generation job
* @param {string} document - Document type (e.g., 'precost')
* @param {string} documentType - Format (e.g., 'pdf', 'excel')
* @param {object} data - Data to pass to document generator
* @returns {Promise<object>} Job object with id
*/
const queueDocumentJob = async (document, documentType, data) => {
try {
const job = await documentQueue .add("generate-document", {
document,
documentType,
data,
});
return {
success: true,
jobId: job.id,
createdAt: new Date(),
};
} catch (error) {
throw new Error(`Failed to queue document job: ${error.message}`);
}
};
/**
* Get job status
* @param {string|number} jobId - Job ID
* @returns {Promise<object>} Job status and details
*/
const getDocumentJobStatus = async (jobId) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
return {
success: false,
message: "Job not found",
};
}
const state = await job.getState();
const result = job.returnvalue;
return {
success: true,
jobId: job.id,
state,
result: state === "completed" ? result : null,
error: state === "failed" ? job.failedReason : null,
createdAt: new Date(job.timestamp),
startedAt: job.processedOn ? new Date(job.processedOn) : null,
completedAt: job.finishedOn ? new Date(job.finishedOn) : null,
};
} catch (error) {
throw new Error(`Failed to get job status: ${error.message}`);
}
};
/**
* Wait for job completion with timeout
* @param {string|number} jobId - Job ID
* @param {number} timeout - Timeout in milliseconds (default: 300000 = 5 minutes)
* @returns {Promise<object>} Job result when completed
*/
const waitForDocumentJob = async (jobId, timeout = 300000) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
throw new Error("Job not found");
}
return await job.waitUntilFinished(
{ timeout },
documentQueue .events
);
} catch (error) {
throw new Error(`Failed to wait for job: ${error.message}`);
}
};
/**
* Remove a job from queue
* @param {string|number} jobId - Job ID
* @returns {Promise<boolean>} Success status
*/
const cancelDocumentJob = async (jobId) => {
try {
const job = await documentQueue .getJob(jobId);
if (!job) {
throw new Error("Job not found");
}
await job.remove();
return true;
} catch (error) {
throw new Error(`Failed to cancel job: ${error.message}`);
}
};
/**
* Get all active jobs
* @returns {Promise<array>} Array of active job details
*/
const getActiveJobs = async () => {
try {
const jobs = await documentQueue .getActiveCount();
const activeJobs = await documentQueue .getJobs(["active"], 0, -1);
return {
count: jobs,
jobs: activeJobs.map((job) => ({
id: job.id,
data: job.data,
state: "active",
progress: job.progress(),
})),
};
} catch (error) {
throw new Error(`Failed to get active jobs: ${error.message}`);
}
};
/**
* Get all pending jobs
* @returns {Promise<array>} Array of pending job details
*/
const getPendingJobs = async () => {
try {
const jobs = await documentQueue .getWaitingCount();
const pendingJobs = await documentQueue .getJobs(["waiting"], 0, -1);
return {
count: jobs,
jobs: pendingJobs.map((job) => ({
id: job.id,
data: job.data,
state: "waiting",
})),
};
} catch (error) {
throw new Error(`Failed to get pending jobs: ${error.message}`);
}
};
module.exports = {
queueDocumentJob,
getDocumentJobStatus,
waitForDocumentJob,
cancelDocumentJob,
getActiveJobs,
getPendingJobs,
};
+91
View File
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/excel/confirmOrder.excel.js
const ExcelJS = require("exceljs");
/**
* Generate Excel template buffer
*/
exports.generateTemplate = async (items) => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet("Confirm Order");
sheet.columns = [
{ header: "Item ID", key: "id", width: 25 },
{ header: "Item Name", key: "item_name", width: 30 },
{ header: "Original Quantity", key: "quantity", width: 20 },
{ header: "Confirmed Quantity", key: "confirmed_quantity", width: 20 },
];
const headerRow = sheet.getRow(1);
headerRow.eachCell((cell) => {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "1F4E78" },
};
cell.font = {
bold: true,
color: { argb: "FFFFFF" },
};
cell.alignment = {
vertical: "middle",
horizontal: "center",
};
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
});
items.forEach((item) => {
sheet.addRow({
id: item.data_id,
item_name: item.item_name,
quantity: item.quantity,
confirmed_quantity: item.quantity,
});
});
return workbook.xlsx.writeBuffer();
};
/**
* Parse uploaded Excel buffer
*/
exports.parseExcel = async (buffer) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const sheet = workbook.getWorksheet("Confirm Order");
if (!sheet) throw new Error("Invalid sheet name");
const confirmedMap = new Map();
sheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
const itemId = row.getCell(1).value;
const confirmedQty = Number(row.getCell(4).value);
if (itemId) {
confirmedMap.set(itemId, confirmedQty);
}
});
return confirmedMap;
};
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/generateExcelTemplate.utill.js
const ExcelJS = require("exceljs");
exports.generateExcelTemplate = async (columns, vendors) => {
const workbook = new ExcelJS.Workbook();
// =========================
// Template Sheet
// =========================
const worksheet = workbook.addWorksheet("Template");
const headers = columns.map((col) =>
col.is_required ? `${col.column_name} *` : col.column_name,
);
const headerRow = worksheet.addRow(headers);
headerRow.eachCell((cell) => {
cell.font = { bold: true };
cell.alignment = { vertical: "middle", horizontal: "center" };
});
headers.forEach((header, i) => {
worksheet.getColumn(i + 1).width = Math.max(header.length + 5, 20);
});
columns.forEach((col, index) => {
const headerCell = worksheet.getCell(1, index + 1);
if (col.is_required) {
headerCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF4CCCC" },
};
headerCell.font = {
bold: true,
color: { argb: "FF000000" },
};
} else {
headerCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFF2CC" },
};
headerCell.font = {
bold: true,
color: { argb: "FF000000" },
};
}
if (col.validation_type === "number") {
for (let i = 2; i <= 100; i++) {
worksheet.getCell(i, index + 1).dataValidation = {
type: "whole",
operator: "greaterThanOrEqual",
formulae: [0],
showErrorMessage: true,
error: "Please enter a valid number",
};
}
}
});
worksheet.views = [{ state: "frozen", ySplit: 1 }];
// =========================
// Vendors Sheet
// =========================
const vendorSheet = workbook.addWorksheet("Vendors");
const vendorHeader = vendorSheet.addRow(["Supplier ID", "Supplier Name"]);
vendorHeader.eachCell((cell) => {
cell.font = {
bold: true,
color: { argb: "FFFFFFFF" },
};
cell.alignment = {
vertical: "middle",
horizontal: "center",
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "4472C4" },
};
});
// Add vendor rows
vendors.forEach((vendor) => {
vendorSheet.addRow([vendor.vendor_id, vendor.name]);
});
// Optional styling
vendorSheet.columns = [{ width: 25 }, { width: 35 }];
vendorSheet.views = [{ state: "frozen", ySplit: 1 }];
return workbook;
};
+101
View File
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utills/validateExcel.utill.js
const ExcelJS = require("exceljs");
exports.validateExcel = async (buffer, columns) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.worksheets[0];
if (!worksheet) {
throw { status: 400, message: "Excel sheet is empty" };
}
const expectedHeaders = columns.map((col) =>
col.is_required ? `${col.column_name} *` : col.column_name
);
const headerRow = worksheet.getRow(1);
for (let i = 1; i <= expectedHeaders.length; i++) {
const cellValue = (headerRow.getCell(i).value || "").toString().trim();
if (cellValue !== expectedHeaders[i - 1]) {
throw {
status: 400,
message: `Invalid header in column ${i}`,
expected: expectedHeaders[i - 1],
received: cellValue,
};
}
}
const errors = [];
const validData = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
let rowData = {};
let rowHasError = false;
columns.forEach((col, index) => {
let value = row.getCell(index + 1).value;
if (value && typeof value === "object") {
value = value.text || value.result || "";
}
value = value?.toString().trim() || "";
if (col.is_required && !value) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Required field is missing",
});
rowHasError = true;
}
if (value) {
if (col.validation_type === "number" && isNaN(value)) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Must be a number",
});
rowHasError = true;
}
if (
col.validation_type === "boolean" &&
!["true", "false"].includes(value.toLowerCase())
) {
errors.push({
row: rowNumber,
column: col.column_name,
message: "Must be true or false",
});
rowHasError = true;
}
}
rowData[col.column_name] = value;
});
if (!rowHasError) {
validData.push(rowData);
}
});
return { errors, validData };
};
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/hashPassword.util.js
const bcrypt = require("bcrypt");
const hashPassword = async (password) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
}
const checkPassword = async (password, hashedPassword) => {
return await bcrypt.compare(password, hashedPassword);
}
module.exports = { hashPassword, checkPassword };
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/confirmOrder.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateOrderNumber = async () => {
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const period = `${yy}${mm}`;
const sequenceKey = `ORD-${period}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `OSC/${period}/${pad(nextNo)}`;
});
};
module.exports = { generateOrderNumber };
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/dispatchNote.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateDeliveryNote = async (orderNo) => {
const sequenceKey = `DN-${orderNo}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `DN/${orderNo}/${String(nextNo).padStart(2, "0")}`;
});
};
module.exports = { generateDeliveryNote };
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/inquiry.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const {log} = require("../consoleLog.utill");
const generateInquiryNumber = async (port) => {
const year = new Date().getFullYear();
const sequenceKey = `INQ-${year}-${port}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
log(`Generated inquiry number: OSC/INQ/${year}/${port}/${pad(nextNo)}`);
return `OSC/INQ/${year}/${port}/${pad(nextNo)}`;
});
};
module.exports = { generateInquiryNumber };
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/po.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generatePONumber = async (orderNo) => {
const sequenceKey = `PO-${orderNo}`;
return await sequelize.transaction(async (transaction) => {
const nextVendor = await nextSequence(sequenceKey, transaction);
return `PO/${orderNo}/V${String(nextVendor).padStart(2, "0")}`;
});
};
module.exports = { generatePONumber };
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/id/quotation.util.js
const { nextSequence, pad } = require("../referenceNumber.util");
const { sequelize } = require("../../models");
const generateRFQNumber = async () => {
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const period = `${yy}${mm}`;
const sequenceKey = `RFQ-${period}`;
return await sequelize.transaction(async (transaction) => {
const nextNo = await nextSequence(sequenceKey, transaction);
return `RFQ/OSC/${period}/${pad(nextNo)}`;
});
};
module.exports = { generateRFQNumber };
+160
View File
@@ -0,0 +1,160 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/idGen.util.js
const { v4: uuidv4 } = require("uuid");
const { nextSequence } = require("./referenceNumber.util");
const {log} = require("./consoleLog.utill");
const {generateInquiryNumber} = require("./id/inquiryID.utill");
const { generateRFQNumber } = require("./id/quotation.utill");
const {generatePONumber} = require("./id/po.utill");
const { generateDeliveryNote } = require("./id/dispatchNote.utill");
const generateUserId = () => {
return "usr_" + Math.random().toString(36).slice(2, 10);
};
const generateClientId = () => {
const prefix = "cli_";
return prefix + uuidv4();
};
const generateInquRefNo = async (port) => {
const reference_number = await generateInquiryNumber(port);
log("Generated Number", reference_number)
return reference_number;
};
const generateInquId = async (port) => {
const prefix = "inq_";
const inquID = prefix + uuidv4();
return inquID;
};
const generateCategoryId = () => {
const prefix = "cte_";
return prefix + uuidv4();
};
const generateRemarkId = () => {
const prefix = "rmk_";
return prefix + uuidv4();
};
const generateTodoId = () => {
const prefix = "tdo_";
return prefix + uuidv4();
};
const generateVendorStatusId = () => {
const prefix = "vst_";
return prefix + uuidv4();
}
const generateVendorId = () => {
const prefix = "vnd_";
return prefix + uuidv4();
}
const generateVendorPICId = () => {
const prefix = "vpic_";
return prefix + uuidv4();
}
const generateVendorDocumentId = () => {
const prefix = "vdoc_";
return prefix + uuidv4();
}
const generateColumnId = () => {
const prefix = "col_";
return prefix + uuidv4();
}
const generatePreCostId = () => {
const prefix = "pc_";
return prefix + uuidv4();
}
const generatePreCostPaymanentId = () => {
const prefix = "pcp_";
return prefix + uuidv4();
}
const generateCalenderId = () => {
const prefix = "cal_";
return prefix + uuidv4();
}
const generateId = () => {
return uuidv4();
};
const generateInvoiceNo = async () => {
const invoiceNumber = await nextSequence("invoice");
return `INV-${invoiceNumber.toString().padStart(6, "0")}`;
}
const generateDocumentReferenceNo = async (type) => {
const normalizedType = String(type).toUpperCase().replace(/[\s_-]+/g, "");
if (normalizedType === "QUOTATION"){
// Implement quotation number generation logic here
return 'N/A'
} else if (normalizedType === "PRECOST"){
// Implement precost number generation logic here
const precostRef = await generateRFQNumber()
return precostRef
} else if (normalizedType === "PO"){
// Implement contract number generation logic here
const Ref = await generatePONumber("OSC/2602")
return Ref
} else if (normalizedType === "BILL"){
return 'N/A'
// Implement bill number generation logic here
} else if (normalizedType === "PAYMENT"){
return 'N/A'
// Implement payment number generation logic here
} else if (normalizedType === "INVOICE"){
// Implement invoice number generation logic here
const invoiceNo = await generateInvoiceNo();
return invoiceNo;
} else if (normalizedType === "CREDITNOTE") {
return 'N/A';
} else if (normalizedType === "DELIVERYNOTE") {
const orderNo = "OSC/2602";
const deliveryNoteNo = await generateDeliveryNote(orderNo);
return deliveryNoteNo;
}else {
throw new Error("Unsupported document type for ID generation");
}
}
module.exports = {
generateUserId,
generateClientId,
generateInquId,
generateInquRefNo,
generateId,
generateCategoryId,
generateRemarkId,
generateTodoId,
generateVendorStatusId,
generateVendorId,
generateVendorPICId,
generateVendorDocumentId,
generateColumnId,
generatePreCostId,
generatePreCostPaymanentId,
generateDocumentReferenceNo,
generateCalenderId,
};
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/imageToBase64.utill.js
const axios = require("axios");
const imageToBase64 = async (url) => {
const response = await axios.get(url, {
responseType: "arraybuffer",
});
const base64 = Buffer.from(response.data, "binary").toString("base64");
const mimeType = response.headers["content-type"];
return `data:${mimeType};base64,${base64}`;
};
module.exports = { imageToBase64 };
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/jwt.util.js
const jwt = require("jsonwebtoken");
require("dotenv").config();
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret_key";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1d"; // token validity
/**
* Generate JWT token
* @param {Object} payload - usually { id, email, role }
* @returns string
*/
const generateToken = (payload) => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
/**
* Verify JWT token
* @param {string} token
* @returns payload or throws error
*/
const verifyToken = (token) => {
return jwt.verify(token, JWT_SECRET);
};
module.exports = { generateToken, verifyToken };
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/mailer.util.js
const nodemailer = require("nodemailer");
const fs = require("fs");
const path = require("path");
const mailConfig = require("../config/mail.config");
const transporter = nodemailer.createTransport({
host: mailConfig.host,
port: mailConfig.port,
secure: mailConfig.secure,
auth: mailConfig.auth,
});
transporter.verify((err) => {
if (err) console.error("Mail server connection failed", err);
else console.log("Mail server ready");
});
// Utility to load template and replace placeholders
const loadTemplate = (templateName, variables = {}) => {
const templatePath = path.join(__dirname, "../templates/emails", `${templateName}.html`);
let template = fs.readFileSync(templatePath, "utf-8");
Object.keys(variables).forEach((key) => {
const regex = new RegExp(`{{${key}}}`, "g");
template = template.replace(regex, variables[key]);
});
return template;
};
const send = async ({ to, subject, templateName, templateVars = {}, text }) => {
const html = templateName ? loadTemplate(templateName, templateVars) : undefined;
const mailOptions = {
from: mailConfig.from,
to,
subject,
text,
html,
};
try {
const info = await transporter.sendMail(mailOptions);
console.log("Email sent:", info.messageId);
return info;
} catch (err) {
console.error("Error sending email:", err);
throw err;
}
};
const sendMail = async (options) => {
const today = new Date();
// Full formatted date: "January 1, 2026"
const formattedDate = today.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
// Only year: "2026"
const currentYear = today.getFullYear();
const templateVars = {
...options.templateVars, // keep existing vars
currentDate: formattedDate,
currentYear: currentYear,
};
return send({ ...options, templateVars });
};
module.exports = { sendMail };
View File
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/otp.util.js
const otpCache = new Map();
function generateOTP(key){
const otp = Math.floor(100000 + Math.random() * 900000).toString();
saveOTP(key, otp);
return otp;
}
function saveOTP(key, otp, ttl = 5 * 60 * 1000) { // default TTL: 5 mins
const expiresAt = Date.now() + ttl;
otpCache.set(key, { otp, expiresAt });
}
// Validate OTP
function validateOTP(key, otp) {
const record = otpCache.get(key);
if (!record) return false;
if (Date.now() > record.expiresAt) {
otpCache.delete(key);
return false;
}
const isValid = record.otp === otp;
if (isValid) otpCache.delete(key); // OTP can be used only once
return isValid;
}
module.exports = { generateOTP, validateOTP };
+113
View File
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/passwordReset.util.js
const crypto = require("crypto");
const createRedisConnection = require("../config/redis.config");
const redis = createRedisConnection();
const db = require("../models");
const User = db.User;
const { log } = require("./consoleLog.utill");
const { hashPassword } = require("./hashPassword.util");
const RESET_TOKEN_TTL = process.env.RESET_TOKEN_TTL || 10 * 60; // 10 minutes
/**
* Generate password reset token
*
* @param {string} userId
* @returns {Promise<string>}
*/
async function generateResetToken(userId) {
try {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto
.createHash("sha256")
.update(token)
.digest("hex");
await redis.set(
`passwordReset:user:${userId}`,
tokenHash,
"EX",
RESET_TOKEN_TTL
);
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token);
return token;
} catch (error) {
log("Password reset token generation failed", error);
throw new Error("Failed to generate password reset token");
}
}
/**
* Reset password using token
*
* @param {string} userId
* @param {string} token
* @param {string} newPassword
*/
async function resetPassword(userId, token, newPassword) {
try {
const storedHash = await redis.get(
`passwordReset:user:${userId}`
);
if (!storedHash) {
throw new Error("Invalid or expired reset token");
}
const tokenHash = crypto
.createHash("sha256")
.update(token)
.digest("hex");
const isValid =
crypto.timingSafeEqual(
Buffer.from(storedHash),
Buffer.from(tokenHash)
);
if (!isValid) {
throw new Error("Invalid or expired reset token");
}
const user = await User.findByPk(userId);
if (!user) {
throw new Error("User not found");
}
user.password = await hashPassword(newPassword);
await user.save();
await redis.del(`passwordReset:user:${userId}`);
log(
`Password reset completed successfully for user ${userId}`
);
return true;
} catch (error) {
log("Password reset failed", error);
throw error;
}
}
module.exports = {
generateResetToken,
resetPassword
};
+197
View File
@@ -0,0 +1,197 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*/
// app/utils/pdfGenerator.js
const path = require("path");
const ejs = require("ejs");
const puppeteer = require("puppeteer");
const fs = require("fs");
const { imageToBase64 } = require("./imageToBase64.utill");
const { log, runningEnvironment } = require("./consoleLog.utill");
let browser;
let executablePath = null;
const isProd = runningEnvironment();
if (isProd) {
executablePath = "/usr/bin/chromium";
} else {
executablePath = puppeteer.executablePath();
}
/**
* Launch or reuse Puppeteer browser safely
*/
async function getBrowser() {
if (!browser || !browser.isConnected?.()) {
console.log("🚀 Launching Chromium from Docker system path...");
browser = await puppeteer.launch({
headless: "new",
executablePath: executablePath,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--no-zygote"
],
});
console.log("✅ Browser launched");
}
return browser;
}
/**
* Close browser safely
*/
async function closeBrowser() {
if (browser) {
try {
await browser.close();
} catch (e) {}
browser = null;
}
}
/**
* Core PDF renderer
*/
async function renderPdf(templateFile, data = {}, opts = {}) {
const templatePath = path.join(__dirname, "../templates", templateFile);
console.log("📝 Rendering:", templateFile);
// Convert image to base64 if needed
if (data.logoUrl && !data.logoBase64) {
try {
data.logoBase64 = await imageToBase64(data.logoUrl);
} catch (err) {
console.error("❌ Logo conversion failed:", err.message);
data.logoBase64 = null;
}
}
const html = await ejs.renderFile(templatePath, data);
const browserInstance = await getBrowser();
const page = await browserInstance.newPage();
try {
// Safety timeouts
await page.setDefaultTimeout(60000);
await page.setDefaultNavigationTimeout(60000);
await page.setViewport({ width: 1200, height: 800 });
console.log("📄 Loading HTML...");
await page.setContent(html, { waitUntil: "networkidle0" });
await page.emulateMediaType("print");
// Wait for fonts
await page.evaluate(() => document.fonts?.ready || Promise.resolve());
const margin = {
top: 20,
bottom: 20,
left: 10,
right: 10,
...opts.margin,
};
// Optional: measure content (kept for logging only)
const contentHeightPx = await page.evaluate(() => {
const body = document.body;
const htmlEl = document.documentElement;
return Math.max(
body.scrollHeight,
body.offsetHeight,
htmlEl.scrollHeight,
htmlEl.offsetHeight
);
});
console.log("📏 Content height:", contentHeightPx, "px");
// ❗ REMOVED scaling logic (causes PDF distortion in production)
const showPageNumbers = opts.showPageNumbers === true;
console.log("⚙️ Generating PDF...");
const pdfBuffer = await page.pdf({
format: "A4",
printBackground: true,
preferCSSPageSize: true,
margin: {
top: `${margin.top}mm`,
bottom: `${margin.bottom}mm`,
left: `${margin.left}mm`,
right: `${margin.right}mm`,
},
displayHeaderFooter: showPageNumbers,
headerTemplate: `<div></div>`,
footerTemplate: showPageNumbers
? `
<div style="font-size:10px;width:100%;text-align:center;">
Page <span class="pageNumber"></span> / <span class="totalPages"></span>
</div>`
: `<div></div>`,
});
console.log("✅ PDF generated");
return pdfBuffer;
} catch (err) {
console.error("❌ PDF generation error:", err.message);
throw err;
} finally {
await page.close().catch(() => {});
}
}
/**
* Helpers
*/
const generatePdfFromPreCost = (data) =>
renderPdf("documents/precost/precost.ejs", data);
const generatePdfFromInvoice = (data) =>
renderPdf("documents/invoice/invoice.ejs", data);
const generatePdfFromDispatchNote = (data) =>
renderPdf("documents/dispatchNote/dispatchNote.ejs", data);
const generatePdfFromDeliveryNote = (data) =>
renderPdf("documents/deliveryNote/deliveryNote.ejs", data);
const generatePdfFromPO = (data) =>
renderPdf("documents/po/po.ejs", data);
const generatePdfFromCustomDocument = (data) =>
renderPdf("documents/customDocument/customDocument.ejs", data);
const generatePdfFromCreditNote = (data) =>
renderPdf("documents/creditNote/creditNote.ejs", data);
module.exports = {
generatePdfFromPreCost,
generatePdfFromInvoice,
generatePdfFromDispatchNote,
generatePdfFromDeliveryNote,
generatePdfFromPO,
generatePdfFromCustomDocument,
generatePdfFromCreditNote,
renderPdf,
closeBrowser,
};
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// utils/referenceNumber.util.js
const { referenceNumber } = require("../models");
const { sequelize } = require("../models");
const crypto = require("crypto");
const pad = (num, size = 4) => {
return String(num).padStart(size, "0");
};
const nextSequence = async (sequenceKey, transaction) => {
const options = {
where: { sequence_key: sequenceKey },
};
// Only add transaction and lock if transaction is provided
if (transaction) {
options.transaction = transaction;
options.lock = transaction.LOCK.UPDATE;
}
let sequence = await referenceNumber.findOne(options);
if (!sequence) {
const createOptions = transaction ? { transaction } : {};
sequence = await referenceNumber.create(
{
id: crypto.randomUUID(),
sequence_key: sequenceKey,
current_number: 1,
},
createOptions
);
return 1;
}
sequence.current_number += 1;
const saveOptions = transaction ? { transaction } : {};
await sequence.save(saveOptions);
return sequence.current_number;
};
module.exports = { nextSequence, pad };
+90
View File
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/s3Upload.util.js
const { PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const s3 = require("../config/s3.config");
const { v4: uuidv4 } = require("uuid");
const path = require("path");
const createRedisConnection = require("../config/redis.config");
const redis = createRedisConnection();
const { log } = require("./consoleLog.utill");
// Upload + return key (BEST PRACTICE)
const uploadToS3 = async (file, usedFor) => {
try {
const fileExtension = path.extname(file.originalname);
const fileName = `${uuidv4()}${fileExtension}`;
let key = `uploads/${fileName}`;
if (usedFor === "asset") {
key = `assets/${fileName}`;
}
log("Uploading file to S3 with key:", key);
const params = {
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
};
await s3.send(new PutObjectCommand(params));
// Return KEY (not URL)
return key;
} catch (error) {
log("S3 Upload Error:", error);
throw new Error("File upload failed");
}
};
// Generate signed URL with Redis caching
const getSignedFileUrl = async (key) => {
try {
const cacheKey = `s3:url:${key}`;
// 1. Check cache
const cachedUrl = await redis.get(cacheKey);
if (cachedUrl) {
log("⚡ Cache HIT - returning signed URL from Redis");
return cachedUrl;
}
log("🐢 Cache MISS - generating new signed URL");
// 2. Generate signed URL
const command = new GetObjectCommand({
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: key,
});
const signedUrl = await getSignedUrl(s3, command, {
expiresIn: 3600, // 1 hour
});
// 3. Store in Redis (TTL: 55 minutes)
await redis.set(cacheKey, signedUrl, "EX", 3300);
return signedUrl;
} catch (error) {
log("Signed URL Error:", error);
throw new Error("Failed to generate file URL");
}
};
module.exports = {
uploadToS3,
getSignedFileUrl,
};
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/validation/validateSupplier.util.js
const db = require("../../models");
const Supplier = db.Vendor;
const extractCellValue = (value) => {
if (value && typeof value === "object") {
return value.text || value.result || "";
}
return value?.toString().trim() || "";
};
const validateSupplierColumn = async (worksheet, columns) => {
const supplierIdCol =
columns.findIndex((col) => col.column_name === "supplier_id") + 1;
if (!supplierIdCol) return;
const supplierIds = new Set();
const errors = [];
// 🔹 Extract values (ONLY collect non-empty)
for (let i = 2; i <= worksheet.rowCount; i++) {
let value = extractCellValue(
worksheet.getRow(i).getCell(supplierIdCol).value
);
if (value) {
supplierIds.add(value);
}
}
if (supplierIds.size === 0) return [];
// 🔹 Batch DB validation
const suppliers = await Supplier.findAll({
where: {
vendor_id: [...supplierIds],
},
});
const validIds = new Set(suppliers.map((s) => s.vendor_id));
// 🔹 Validate ONLY non-empty values
for (let i = 2; i <= worksheet.rowCount; i++) {
let value = extractCellValue(
worksheet.getRow(i).getCell(supplierIdCol).value
);
if (value && !validIds.has(value)) {
errors.push({
row: i,
column: "supplier_id",
message: `Invalid supplier ID: ${value}`,
});
}
}
return errors;
};
module.exports = { validateSupplierColumn };
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/utils/venStat.utill.js
// Function to calculate vendor status based on approval flags
const calVendorStatus = (is_md_approved, is_manager_approved) => {
// 1. Nothing decided yet
if (is_manager_approved === null && is_md_approved === null) {
return "Pending Approval";
}
// 2. Manager decided, MD pending
if (is_md_approved === null) {
if (is_manager_approved === true) return "Manager Approved";
if (is_manager_approved === false) return "Manager Rejected";
}
// 3. MD decision is FINAL (always overrides)
if (is_md_approved === true) {
return "Approved";
}
if (is_md_approved === false) {
return "MD Rejected";
}
return "Pending Approval";
};
module.exports = {
calVendorStatus,
}