Compare commits
10 Commits
b85d3e9833
...
a68ca45abc
| Author | SHA1 | Date | |
|---|---|---|---|
| a68ca45abc | |||
| 36aff2dc4c | |||
| 80566a63ef | |||
| 69b90eabd3 | |||
| 010e59811b | |||
| 679a074da8 | |||
| 20344bc589 | |||
| ca936f8f7e | |||
| 35fb4dacf1 | |||
| c8514641c4 |
@ -2,7 +2,8 @@ const db = require("../models");
|
||||
const logger = require("../services/logger");
|
||||
const IPICalculationService = require("../services/ipi_calculation_service");
|
||||
const SubmissionAutoFillService = require("../services/auto_fill_missing_quarterly_submissions_service");
|
||||
const CalculationLog = db.CalculationLog;
|
||||
const CalculationLog = db.CalculationLog;
|
||||
const QuarterlyWindowsConfiguration = db.QuarterlyWindowsConfiguration;
|
||||
|
||||
// Database configuration
|
||||
const dbConfig = {
|
||||
@ -128,6 +129,20 @@ exports.calculate_month = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// check if the year and quarter window created or not and start the calculation
|
||||
const window = await QuarterlyWindowsConfiguration.findOne({
|
||||
where: {
|
||||
year: year,
|
||||
quarter: quarter
|
||||
}
|
||||
});
|
||||
if (!window) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Year and quarter window not created'
|
||||
});
|
||||
}
|
||||
|
||||
const service = new IPICalculationService(dbConfig);
|
||||
const result = await service.runCompleteCalculation(year, quarter);
|
||||
await service.close();
|
||||
@ -233,34 +248,103 @@ exports.calculation_log = async (req, res) => {
|
||||
};
|
||||
|
||||
|
||||
const validateYearQuarter = (year, quarter) => {
|
||||
if (!year || !quarter) {
|
||||
return { ok: false, message: "Year and quarter are required" };
|
||||
}
|
||||
if (!["Q1", "Q2", "Q3", "Q4"].includes(quarter)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "Invalid quarter. Accepted values: Q1, Q2, Q3, Q4",
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
async function runSurveyAutoSubmit(year, quarter) {
|
||||
const autoSubmissionService = new SubmissionAutoFillService(dbConfig);
|
||||
const autoFillMissingSubmissions =
|
||||
await autoSubmissionService.autoFillMissingSubmissions(year, quarter);
|
||||
const verifyCompleteness =
|
||||
await autoSubmissionService.verifyCompleteness(year, quarter);
|
||||
|
||||
return { autoFillMissingSubmissions, verifyCompleteness };
|
||||
}
|
||||
|
||||
async function runQuarterCalculation(year, quarter) {
|
||||
const service = new IPICalculationService(dbConfig);
|
||||
try {
|
||||
const data = await service.runCompleteCalculation(year, quarter);
|
||||
return { message: `IPI calculated for ${year} ${quarter}`, data };
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Survey Auto Submit
|
||||
exports.survey_auto_submit = async (req, res) => {
|
||||
try {
|
||||
const { year, quarter } = req.body;
|
||||
const validation = validateYearQuarter(year, quarter);
|
||||
if (!validation.ok) {
|
||||
return res.status(400).json({ success: false, message: validation.message });
|
||||
}
|
||||
|
||||
const autoSubmissionService = new SubmissionAutoFillService(dbConfig);
|
||||
|
||||
// Auto-fill missing submissions for specific year and quarter
|
||||
const result = await autoSubmissionService.autoFillMissingSubmissions(year, quarter);
|
||||
|
||||
// Get report for specific quarter
|
||||
// const result2 = await autoSubmissionService.getAutoFillReport(year, quarter);
|
||||
|
||||
// Verify completeness for specific quarter
|
||||
const result3 = await autoSubmissionService.verifyCompleteness(year, quarter);
|
||||
|
||||
|
||||
|
||||
res.json({ success: true, autoFillMissingSubmissions: result , verifyCompleteness: result3 });
|
||||
|
||||
|
||||
const result = await runSurveyAutoSubmit(year, quarter);
|
||||
|
||||
res.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error("Error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '',
|
||||
error: error.message
|
||||
message: "Error during survey auto submit",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Auto submit missing surveys, then run quarterly IPI calculation
|
||||
exports.survey_auto_submit_and_calculate = async (req, res) => {
|
||||
try {
|
||||
const { year, quarter } = req.body;
|
||||
const validation = validateYearQuarter(year, quarter);
|
||||
if (!validation.ok) {
|
||||
return res.status(400).json({ success: false, message: validation.message });
|
||||
}
|
||||
|
||||
// check if the year and quarter window created or not and start the calculation
|
||||
const window = await QuarterlyWindowsConfiguration.findOne({
|
||||
where: {
|
||||
year: year,
|
||||
quarter: quarter
|
||||
}
|
||||
});
|
||||
if (!window) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Year and quarter window not created'
|
||||
});
|
||||
}
|
||||
|
||||
const autoSubmitResult = await runSurveyAutoSubmit(year, quarter);
|
||||
const calculationResult = await runQuarterCalculation(year, quarter);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Auto submit and IPI calculation completed for ${year} ${quarter}`,
|
||||
year,
|
||||
quarter,
|
||||
steps: {
|
||||
survey_auto_submit: autoSubmitResult,
|
||||
calculate_quarter: calculationResult,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Error during auto submit and quarterly calculation",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
exports.deployment = async (req, res) => {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
@ -62,7 +63,6 @@ exports.deployment = async (req, res) => {
|
||||
return res.status(401).json({ ok: false, message: 'Invalid API key' });
|
||||
}
|
||||
|
||||
|
||||
// ##############################################################
|
||||
|
||||
const payload = req.body;
|
||||
|
||||
@ -1057,16 +1057,17 @@ exports.forgotPasswordRequestOTP = async (req, res) => {
|
||||
{
|
||||
|
||||
// Validate inputs
|
||||
if (!establishment_name || !establishment_code || !registered_email)
|
||||
return res.status(400).json({ status: "failed", message: "All fields are required" });
|
||||
// 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" });
|
||||
// // 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 },
|
||||
// where: { email: registered_email, establishment_id: establishment.id },
|
||||
where: { email: registered_email },
|
||||
});
|
||||
|
||||
if (!user)
|
||||
|
||||
@ -227,7 +227,7 @@ exports.changeUserPassword = async (req, res) => {
|
||||
}
|
||||
|
||||
// Find user
|
||||
const user = await EstablishmentUser.findByPk(userId);
|
||||
const user = await EstablishmentUser.scope("withSensitive").findByPk(userId);
|
||||
if (!user) {
|
||||
return res.status(404).send({
|
||||
status: "failed",
|
||||
|
||||
@ -43,6 +43,117 @@ function parseWeightInIb(value) {
|
||||
return { ok: true, value: rounded };
|
||||
}
|
||||
|
||||
/** Normalize unit text for case-insensitive matching (e.g. "numbers" → "numbers", " Numbers " → "numbers"). */
|
||||
function normalizeUnitKey(value) {
|
||||
if (value === null || value === undefined) return "";
|
||||
const normalized = String(value).trim().replace(/\s+/g, " ").toLowerCase();
|
||||
if (!normalized || normalized === "not defined") return "";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Map unit_master by uom (full unit name), case-insensitive. */
|
||||
function buildUnitLookupMap(unitMasters) {
|
||||
const unitMap = new Map();
|
||||
for (const unit of unitMasters) {
|
||||
const key = normalizeUnitKey(unit.uom);
|
||||
if (key) {
|
||||
unitMap.set(key, unit.id);
|
||||
}
|
||||
}
|
||||
return unitMap;
|
||||
}
|
||||
|
||||
function resolveUnitId(unitInput, unitMap) {
|
||||
const key = normalizeUnitKey(unitInput);
|
||||
if (!key) return null;
|
||||
return unitMap.get(key) ?? null;
|
||||
}
|
||||
|
||||
function normalizeCsvHeaderKey(key) {
|
||||
if (!key) return "";
|
||||
const withoutBom = String(key).replace(/^\uFEFF/, "");
|
||||
return withoutBom
|
||||
.replace(/\*/g, "")
|
||||
.replace(/\(.*?\)/g, "")
|
||||
.trim()
|
||||
.replace(/[\s\W]+/g, "_")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function mapCsvRowKey(normalizedKey) {
|
||||
if (/^hs.*code$/.test(normalizedKey) || normalizedKey === "hscode") {
|
||||
return "hs_code";
|
||||
}
|
||||
if (/^product.*name$/.test(normalizedKey)) {
|
||||
return "product_name";
|
||||
}
|
||||
if (
|
||||
normalizedKey === "unit" ||
|
||||
normalizedKey === "uom" ||
|
||||
normalizedKey === "measurement_unit" ||
|
||||
normalizedKey === "measure" ||
|
||||
normalizedKey.endsWith("_unit")
|
||||
) {
|
||||
return "unit";
|
||||
}
|
||||
if (/^desc(ription)?$/.test(normalizedKey)) {
|
||||
return "description";
|
||||
}
|
||||
if (
|
||||
normalizedKey === "weight_in_ib" ||
|
||||
normalizedKey === "weight_in_lb" ||
|
||||
/^weight.*(ib|lb)/i.test(normalizedKey) ||
|
||||
normalizedKey === "weight"
|
||||
) {
|
||||
return "weight_in_ib";
|
||||
}
|
||||
return normalizedKey;
|
||||
}
|
||||
|
||||
function transformCsvRow(row) {
|
||||
const cleanRow = {};
|
||||
for (const key in row) {
|
||||
if (!Object.prototype.hasOwnProperty.call(row, key)) continue;
|
||||
const mappedKey = mapCsvRowKey(normalizeCsvHeaderKey(key));
|
||||
const value =
|
||||
row[key] !== undefined && row[key] !== null && String(row[key]).trim() !== ""
|
||||
? String(row[key]).trim()
|
||||
: null;
|
||||
if (cleanRow[mappedKey] && !value) continue;
|
||||
cleanRow[mappedKey] = value;
|
||||
}
|
||||
return cleanRow;
|
||||
}
|
||||
|
||||
function isBlankCsvRow(row) {
|
||||
return (
|
||||
!row.hs_code &&
|
||||
!row.product_name &&
|
||||
!row.unit &&
|
||||
!row.description &&
|
||||
(row.weight_in_ib === undefined ||
|
||||
row.weight_in_ib === null ||
|
||||
String(row.weight_in_ib).trim() === "")
|
||||
);
|
||||
}
|
||||
|
||||
function parseCsvFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
fs.createReadStream(filePath)
|
||||
.pipe(csv())
|
||||
.on("data", (row) => results.push(transformCsvRow(row)))
|
||||
.on("end", () => resolve(results))
|
||||
.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendUploadResponse(res, statusCode, body) {
|
||||
if (res.headersSent) return;
|
||||
return res.status(statusCode).json(body);
|
||||
}
|
||||
|
||||
/** Returns true if a response was sent (4xx). */
|
||||
function tryRespondProductPersistenceError(error, res) {
|
||||
if (!error || res.headersSent) {
|
||||
@ -334,420 +445,380 @@ exports.uploadProductsFromCSV = async (req, res) => {
|
||||
let sanitizedPath = null;
|
||||
|
||||
try {
|
||||
// Validate file upload
|
||||
if (!req.file) {
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: "No file uploaded"
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message:
|
||||
'No CSV file attached. Use multipart form field "file" and select your .csv before submitting.',
|
||||
hint:
|
||||
'In Swagger: open Try it out → pick hs-codes-sample.csv under field "file" → Execute.',
|
||||
expected_field: "file",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
sanitizedPath = sanitizeFilePath(req.file.path, UPLOAD_DIR);
|
||||
} catch (sanitizeError) {
|
||||
// Attempt cleanup with original path if sanitization fails
|
||||
try {
|
||||
const unsafePath = req.file.path;
|
||||
if (unsafePath && fs.existsSync(unsafePath)) {
|
||||
fs.unlinkSync(unsafePath);
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
// Silent fail on cleanup
|
||||
// ignore cleanup errors
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "Invalid file path detected"
|
||||
message: "Invalid file path detected",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate file exists after sanitization
|
||||
if (!validateFileExists(sanitizedPath)) {
|
||||
return res.status(400).send({
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "File not found after validation"
|
||||
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 allowed."
|
||||
});
|
||||
}
|
||||
|
||||
// Validate MIME type
|
||||
const allowedMimes = ['text/csv', 'application/csv', 'text/plain'];
|
||||
if (req.file.mimetype && !allowedMimes.some(mime => mime === req.file.mimetype)) {
|
||||
if (fileExt !== ".csv") {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "Invalid MIME type. Only CSV allowed."
|
||||
message: "Invalid file type. Only CSV allowed.",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate user ID
|
||||
if (!req.user || !req.user.id || isNaN(req.user.id)) {
|
||||
const allowedMimes = ["text/csv", "application/csv", "text/plain", "application/vnd.ms-excel"];
|
||||
if (req.file.mimetype && !allowedMimes.includes(req.file.mimetype)) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: "Invalid or missing User Id."
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "Invalid MIME type. Only CSV allowed.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.user || !req.user.id || Number.isNaN(Number(req.user.id))) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "Invalid or missing User Id.",
|
||||
});
|
||||
}
|
||||
|
||||
// Check file size
|
||||
const fileStats = fs.statSync(sanitizedPath);
|
||||
if (fileStats.size === 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "Uploaded file is empty.",
|
||||
});
|
||||
}
|
||||
|
||||
const csvResults = [];
|
||||
const userId = parseInt(req.user.id, 10);
|
||||
const csvResults = await parseCsvFile(sanitizedPath);
|
||||
|
||||
// Process CSV using sanitized path
|
||||
fs.createReadStream(sanitizedPath)
|
||||
.pipe(csv())
|
||||
.on("data", (row) => {
|
||||
const cleanRow = {};
|
||||
|
||||
for (const key in row) {
|
||||
if (!row.hasOwnProperty(key)) continue;
|
||||
|
||||
let cleanedHeader = key
|
||||
.replace(/\*/g, "")
|
||||
.replace(/\(.*?\)/g, "")
|
||||
.trim();
|
||||
|
||||
const normalizedKey = cleanedHeader
|
||||
.replace(/[\s\W]+/g, "_")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
let mappedKey = normalizedKey;
|
||||
if (/^hs.*code$/.test(normalizedKey) || normalizedKey === "hscode") {
|
||||
mappedKey = "hs_code";
|
||||
} else if (/^product.*name$/.test(normalizedKey)) {
|
||||
mappedKey = "product_name";
|
||||
} else if (/unit|measurement|uom|measure/i.test(normalizedKey)) {
|
||||
mappedKey = "unit";
|
||||
} else if (/desc(ription)?/i.test(normalizedKey)) {
|
||||
mappedKey = "description";
|
||||
} else if (
|
||||
normalizedKey === "weight_in_ib" ||
|
||||
/^weight.*(ib|lb)/i.test(normalizedKey) ||
|
||||
/^weight$/i.test(normalizedKey)
|
||||
) {
|
||||
mappedKey = "weight_in_ib";
|
||||
}
|
||||
|
||||
cleanRow[mappedKey] = row[key] ? row[key].trim() : null;
|
||||
}
|
||||
|
||||
csvResults.push(cleanRow);
|
||||
})
|
||||
.on("end", async () => {
|
||||
try {
|
||||
// Validate CSV has data
|
||||
if (csvResults.length === 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: "CSV file is empty or invalid."
|
||||
});
|
||||
}
|
||||
|
||||
// Validate required columns
|
||||
const requiredCols = ["hs_code", "product_name", "unit", "description"];
|
||||
const optionalCols = ["weight_in_ib"];
|
||||
const allowedCols = requiredCols.concat(optionalCols);
|
||||
const headers = Object.keys(csvResults[0]);
|
||||
|
||||
const missingCols = requiredCols.filter(col => !headers.includes(col));
|
||||
const extraCols = headers.filter(col => !allowedCols.includes(col));
|
||||
|
||||
if (missingCols.length > 0 || extraCols.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message:
|
||||
(missingCols.length ? `Missing columns: ${missingCols.join(", ")}. ` : "") +
|
||||
(extraCols.length ? `Unexpected columns: ${extraCols.join(", ")}.` : "")
|
||||
});
|
||||
}
|
||||
|
||||
// Process and validate rows
|
||||
const normalizedRows = [];
|
||||
const seenHsCodes = new Set();
|
||||
const fileDuplicates = new Set();
|
||||
const validationErrors = [];
|
||||
|
||||
for (let index = 0; index < csvResults.length; index++) {
|
||||
const row = csvResults[index];
|
||||
const rowNumber = index + 1;
|
||||
|
||||
let hsCode = row.hs_code ? row.hs_code.replace(/[-\s/]/g, "").trim() : "";
|
||||
const productName = row.product_name ? row.product_name.replace(/\s+/g, " ").trim() : "";
|
||||
const unit = row.unit ? row.unit.trim().toLowerCase() : "";
|
||||
const description = row.description ? row.description.trim().toLowerCase() : "";
|
||||
|
||||
let weightInIb = null;
|
||||
if (
|
||||
row.weight_in_ib !== undefined &&
|
||||
row.weight_in_ib !== null &&
|
||||
String(row.weight_in_ib).trim() !== ""
|
||||
) {
|
||||
const wp = parseWeightInIb(row.weight_in_ib);
|
||||
if (!wp.ok) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: wp.message,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
weightInIb = wp.value;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!hsCode || !productName) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: "Missing required HS Code or Product Name"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate HS code format
|
||||
if (!/^\d+$/.test(hsCode)) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: "HS Code must be numeric"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate HS code length
|
||||
if (hsCode.length > 10) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: "HS Code must be max 10 digits"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate product name length
|
||||
if (productName.length > 1000) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: "Product Name is too long. Maximum 1000 characters."
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate description length
|
||||
if (description.length > 1000) {
|
||||
validationErrors.push({
|
||||
row: rowNumber,
|
||||
error: "HS Description is too long. Maximum 1000 characters."
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicates in file
|
||||
const normalizedHs = hsCode.replace(/^0+/, "");
|
||||
if (seenHsCodes.has(normalizedHs)) {
|
||||
fileDuplicates.add(hsCode);
|
||||
continue;
|
||||
}
|
||||
seenHsCodes.add(normalizedHs);
|
||||
|
||||
normalizedRows.push({
|
||||
hsCode: hsCode,
|
||||
productName: productName,
|
||||
unit: unit,
|
||||
description: description,
|
||||
weightInIb,
|
||||
});
|
||||
}
|
||||
|
||||
// Check for file duplicates
|
||||
if (fileDuplicates.size > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: "Duplicate HS Codes found within file. Resolve and re-upload.",
|
||||
duplicate_hs_codes_in_file: Array.from(fileDuplicates)
|
||||
});
|
||||
}
|
||||
|
||||
const unitMasters = await UnitMaster.findAll({
|
||||
attributes: ["id", "uom"]
|
||||
});
|
||||
|
||||
const unitMap = new Map();
|
||||
for (let i = 0; i < unitMasters.length; i++) {
|
||||
const unit = unitMasters[i];
|
||||
const key = unit.uom.trim().toLowerCase();
|
||||
unitMap.set(key, unit.id);
|
||||
}
|
||||
|
||||
const existingProducts = await Product.findAll({
|
||||
attributes: ["hs_code", "product_name"]
|
||||
});
|
||||
|
||||
const existingHsCodeSet = new Set();
|
||||
const existingProductNameSet = new Set();
|
||||
for (let i = 0; i < existingProducts.length; i++) {
|
||||
const normalized = existingProducts[i].hs_code.replace(/^0+/, "");
|
||||
existingHsCodeSet.add(normalized);
|
||||
|
||||
const productNameLower = existingProducts[i].product_name.trim().toLowerCase();
|
||||
existingProductNameSet.add(productNameLower);
|
||||
}
|
||||
|
||||
const duplicateHsCodes = [];
|
||||
const duplicateProductNames = [];
|
||||
|
||||
for (let i = 0; i < normalizedRows.length; i++) {
|
||||
const row = normalizedRows[i];
|
||||
const normalizedHs = row.hsCode.replace(/^0+/, "");
|
||||
const productNameLower = row.productName.trim().toLowerCase();
|
||||
|
||||
if (existingHsCodeSet.has(normalizedHs)) {
|
||||
duplicateHsCodes.push({
|
||||
row: i + 1,
|
||||
hs_code: row.hsCode,
|
||||
product_name: row.productName
|
||||
});
|
||||
}
|
||||
|
||||
if (existingProductNameSet.has(productNameLower)) {
|
||||
duplicateProductNames.push({
|
||||
row: i + 1,
|
||||
hs_code: row.hsCode,
|
||||
product_name: row.productName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateHsCodes.length > 0 || duplicateProductNames.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
|
||||
const errorMessages = [];
|
||||
|
||||
if (duplicateHsCodes.length > 0) {
|
||||
errorMessages.push(`${duplicateHsCodes.length} duplicate HS Code(s) found in database`);
|
||||
}
|
||||
|
||||
if (duplicateProductNames.length > 0) {
|
||||
errorMessages.push(`${duplicateProductNames.length} duplicate Product Name(s) found in database`);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: "Upload rejected: " + errorMessages.join(", ") + ". Please remove duplicates and try again.",
|
||||
duplicate_hs_codes: duplicateHsCodes,
|
||||
duplicate_product_names: duplicateProductNames,
|
||||
total_duplicates: duplicateHsCodes.length + duplicateProductNames.length
|
||||
});
|
||||
}
|
||||
|
||||
const toInsert = [];
|
||||
|
||||
for (let i = 0; i < normalizedRows.length; i++) {
|
||||
const row = normalizedRows[i];
|
||||
|
||||
// Validate unit
|
||||
const unitId = unitMap.get(row.unit);
|
||||
if (!unitId) {
|
||||
validationErrors.push({
|
||||
row: i + 1,
|
||||
error: "Invalid unit: " + row.unit
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
toInsert.push({
|
||||
hs_code: row.hsCode,
|
||||
product_name: row.productName,
|
||||
unit_id: unitId,
|
||||
hs_description: row.description,
|
||||
weight_in_ib: row.weightInIb,
|
||||
created_by: userId,
|
||||
created_at: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return res.status(400).send({
|
||||
status: "failed",
|
||||
message: `Upload rejected: ${validationErrors.length} validation error(s) found. Please fix and try again.`,
|
||||
errors: validationErrors
|
||||
});
|
||||
}
|
||||
|
||||
let insertedRecords = [];
|
||||
if (toInsert.length > 0) {
|
||||
insertedRecords = await Product.bulkCreate(toInsert, {
|
||||
validate: true
|
||||
});
|
||||
}
|
||||
|
||||
deleteFileSecure(sanitizedPath);
|
||||
|
||||
return res.status(200).send({
|
||||
status: "success",
|
||||
message: `${insertedRecords.length} products inserted successfully.`,
|
||||
summary: {
|
||||
total_records: csvResults.length,
|
||||
imported: insertedRecords.length,
|
||||
skipped: 0,
|
||||
errors: []
|
||||
}
|
||||
});
|
||||
|
||||
} catch (processingError) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
if (logger && logger.error) {
|
||||
logger.error("CSV processing error: " + processingError.message);
|
||||
logger.error(`Stack trace: ${processingError.stack}`);
|
||||
}
|
||||
if (tryRespondProductPersistenceError(processingError, res)) {
|
||||
return;
|
||||
}
|
||||
return res.status(500).send({
|
||||
status: "failed",
|
||||
message: "Internal server error",
|
||||
});
|
||||
}
|
||||
})
|
||||
.on("error", (streamError) => {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
if (logger && logger.error) {
|
||||
logger.error("Stream error: " + streamError.message);
|
||||
}
|
||||
return res.status(500).send({
|
||||
status: "failed",
|
||||
message: "Error reading CSV file"
|
||||
});
|
||||
if (csvResults.length === 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "CSV file is empty or invalid.",
|
||||
});
|
||||
}
|
||||
|
||||
const requiredCols = ["hs_code", "product_name", "unit"];
|
||||
const optionalCols = ["description", "weight_in_ib"];
|
||||
const allowedCols = requiredCols.concat(optionalCols);
|
||||
const headers = Object.keys(csvResults[0]);
|
||||
|
||||
const missingCols = requiredCols.filter((col) => !headers.includes(col));
|
||||
const extraCols = headers.filter((col) => !allowedCols.includes(col));
|
||||
|
||||
if (missingCols.length > 0 || extraCols.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message:
|
||||
(missingCols.length ? `Missing columns: ${missingCols.join(", ")}. ` : "") +
|
||||
(extraCols.length ? `Unexpected columns: ${extraCols.join(", ")}.` : ""),
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedRows = [];
|
||||
const seenHsCodes = new Set();
|
||||
const fileDuplicates = new Set();
|
||||
const validationErrors = [];
|
||||
|
||||
for (let index = 0; index < csvResults.length; index++) {
|
||||
const row = csvResults[index];
|
||||
const csvLineNumber = index + 2;
|
||||
|
||||
if (isBlankCsvRow(row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hsCode = row.hs_code ? row.hs_code.replace(/[-\s/]/g, "").trim() : "";
|
||||
const productName = row.product_name
|
||||
? row.product_name.replace(/\s+/g, " ").trim()
|
||||
: "";
|
||||
const unit = normalizeUnitKey(row.unit);
|
||||
const description = row.description ? row.description.trim() : "";
|
||||
|
||||
let weightInIb = null;
|
||||
if (
|
||||
row.weight_in_ib !== undefined &&
|
||||
row.weight_in_ib !== null &&
|
||||
String(row.weight_in_ib).trim() !== ""
|
||||
) {
|
||||
const wp = parseWeightInIb(row.weight_in_ib);
|
||||
if (!wp.ok) {
|
||||
validationErrors.push({ row: csvLineNumber, error: wp.message });
|
||||
continue;
|
||||
}
|
||||
weightInIb = wp.value;
|
||||
}
|
||||
|
||||
if (!hsCode || !productName || !unit) {
|
||||
const missing = [];
|
||||
if (!hsCode) missing.push("HS Code");
|
||||
if (!productName) missing.push("Product Name");
|
||||
if (!unit) missing.push("Unit");
|
||||
validationErrors.push({
|
||||
row: csvLineNumber,
|
||||
error: `Missing required field(s): ${missing.join(", ")}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(hsCode)) {
|
||||
validationErrors.push({ row: csvLineNumber, error: "HS Code must be numeric" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hsCode.length > 10) {
|
||||
validationErrors.push({
|
||||
row: csvLineNumber,
|
||||
error: "HS Code must be max 10 digits",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hsCode === "0" || /^0+$/.test(hsCode)) {
|
||||
validationErrors.push({
|
||||
row: csvLineNumber,
|
||||
error: "HS Code cannot be 0",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (productName.length > 1000) {
|
||||
validationErrors.push({
|
||||
row: csvLineNumber,
|
||||
error: "Product Name is too long. Maximum 1000 characters.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (description.length > 1000) {
|
||||
validationErrors.push({
|
||||
row: csvLineNumber,
|
||||
error: "HS Description is too long. Maximum 1000 characters.",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedHs = hsCode.replace(/^0+/, "") || hsCode;
|
||||
if (seenHsCodes.has(normalizedHs)) {
|
||||
fileDuplicates.add(hsCode);
|
||||
continue;
|
||||
}
|
||||
seenHsCodes.add(normalizedHs);
|
||||
|
||||
normalizedRows.push({
|
||||
hsCode,
|
||||
productName,
|
||||
unit,
|
||||
description,
|
||||
weightInIb,
|
||||
csvLineNumber,
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizedRows.length === 0 && validationErrors.length === 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message:
|
||||
"No product data rows found. Add data below the header row (remove blank rows from the template).",
|
||||
});
|
||||
}
|
||||
|
||||
if (fileDuplicates.size > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message:
|
||||
"Duplicate HS Codes found within file. Resolve and re-upload.",
|
||||
duplicate_hs_codes_in_file: Array.from(fileDuplicates),
|
||||
});
|
||||
}
|
||||
|
||||
const unitMasters = await UnitMaster.findAll({
|
||||
attributes: ["id", "uom"],
|
||||
where: { is_active: true },
|
||||
});
|
||||
const unitMap = buildUnitLookupMap(unitMasters);
|
||||
|
||||
const existingProducts = await Product.findAll({
|
||||
attributes: ["hs_code", "product_name"],
|
||||
where: { is_active: true },
|
||||
});
|
||||
|
||||
const existingHsCodeSet = new Set();
|
||||
const existingProductNameSet = new Set();
|
||||
for (const product of existingProducts) {
|
||||
if (product.hs_code != null && String(product.hs_code).trim() !== "") {
|
||||
existingHsCodeSet.add(String(product.hs_code).replace(/^0+/, "") || "0");
|
||||
}
|
||||
if (product.product_name) {
|
||||
existingProductNameSet.add(product.product_name.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateHsCodes = [];
|
||||
const duplicateProductNames = [];
|
||||
|
||||
for (let i = 0; i < normalizedRows.length; i++) {
|
||||
const row = normalizedRows[i];
|
||||
const normalizedHs = row.hsCode.replace(/^0+/, "") || row.hsCode;
|
||||
const productNameLower = row.productName.trim().toLowerCase();
|
||||
|
||||
if (existingHsCodeSet.has(normalizedHs)) {
|
||||
duplicateHsCodes.push({
|
||||
row: row.csvLineNumber,
|
||||
hs_code: row.hsCode,
|
||||
product_name: row.productName,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingProductNameSet.has(productNameLower)) {
|
||||
duplicateProductNames.push({
|
||||
row: row.csvLineNumber,
|
||||
hs_code: row.hsCode,
|
||||
product_name: row.productName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateHsCodes.length > 0 || duplicateProductNames.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
const errorMessages = [];
|
||||
if (duplicateHsCodes.length > 0) {
|
||||
errorMessages.push(
|
||||
`${duplicateHsCodes.length} duplicate HS Code(s) found in database`
|
||||
);
|
||||
}
|
||||
if (duplicateProductNames.length > 0) {
|
||||
errorMessages.push(
|
||||
`${duplicateProductNames.length} duplicate Product Name(s) found in database`
|
||||
);
|
||||
}
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message:
|
||||
"Upload rejected: " +
|
||||
errorMessages.join(", ") +
|
||||
". Please remove duplicates and try again.",
|
||||
duplicate_hs_codes: duplicateHsCodes,
|
||||
duplicate_product_names: duplicateProductNames,
|
||||
total_duplicates:
|
||||
duplicateHsCodes.length + duplicateProductNames.length,
|
||||
});
|
||||
}
|
||||
|
||||
const toInsert = [];
|
||||
|
||||
for (let i = 0; i < normalizedRows.length; i++) {
|
||||
const row = normalizedRows[i];
|
||||
const unitId = resolveUnitId(row.unit, unitMap);
|
||||
|
||||
if (!unitId) {
|
||||
validationErrors.push({
|
||||
row: row.csvLineNumber,
|
||||
error: `Invalid unit "${row.unit}". Use the unit name (uom) from Unit Master (e.g. Numbers, Kilogram). Matching is case-insensitive.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
toInsert.push({
|
||||
hs_code: parseInt(row.hsCode, 10),
|
||||
product_name: row.productName,
|
||||
unit_id: unitId,
|
||||
hs_description: row.description || null,
|
||||
weight_in_ib: row.weightInIb,
|
||||
created_by: userId,
|
||||
created_at: new Date(),
|
||||
is_active: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: `Upload rejected: ${validationErrors.length} validation error(s) found. Please fix and try again.`,
|
||||
errors: validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
if (toInsert.length === 0) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
return sendUploadResponse(res, 400, {
|
||||
status: "failed",
|
||||
message: "No valid product rows to import.",
|
||||
});
|
||||
}
|
||||
|
||||
let insertedRecords = [];
|
||||
try {
|
||||
insertedRecords = await Product.bulkCreate(toInsert, {
|
||||
validate: true,
|
||||
individualHooks: false,
|
||||
});
|
||||
} catch (bulkError) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
logger.error("Product bulkCreate error: " + bulkError.message);
|
||||
logger.error(`Stack trace: ${bulkError.stack}`);
|
||||
if (tryRespondProductPersistenceError(bulkError, res)) {
|
||||
return;
|
||||
}
|
||||
throw bulkError;
|
||||
}
|
||||
|
||||
deleteFileSecure(sanitizedPath);
|
||||
|
||||
return sendUploadResponse(res, 201, {
|
||||
status: "success",
|
||||
message: `${insertedRecords.length} product(s) inserted successfully.`,
|
||||
summary: {
|
||||
total_records: csvResults.length,
|
||||
imported: insertedRecords.length,
|
||||
skipped: csvResults.length - insertedRecords.length,
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
deleteFileSecure(sanitizedPath);
|
||||
if (logger && logger.error) {
|
||||
logger.error("Upload error: " + error.message);
|
||||
logger.error(`Stack trace: ${error.stack}`);
|
||||
logger.error("Upload error: " + error.message);
|
||||
logger.error(`Stack trace: ${error.stack}`);
|
||||
if (tryRespondProductPersistenceError(error, res)) {
|
||||
return;
|
||||
}
|
||||
return res.status(500).send({
|
||||
status: "failed",
|
||||
message: "Internal server error"
|
||||
return sendUploadResponse(res, 500, {
|
||||
status: "failed",
|
||||
message: "Internal server error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -137,7 +137,7 @@ exports.changeAdminUserPassword = async (req, res) => {
|
||||
}
|
||||
|
||||
// Find user
|
||||
const user = await User.findByPk(userId);
|
||||
const user = await User.scope("withSensitive").findByPk(userId);
|
||||
if (!user) {
|
||||
return res.status(404).send({ status: "failed", message: "User not found" });
|
||||
}
|
||||
|
||||
@ -2,10 +2,23 @@ require("dotenv").config();
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
|
||||
const appSignature = req.headers["app_signature"];
|
||||
const appSignature = (
|
||||
req.headers["app_signature"] || req.headers["x-app-signature"] || ""
|
||||
).trim();
|
||||
const expectedSignature = (process.env.APP_SIGNATURE || "").trim();
|
||||
|
||||
if (!appSignature || appSignature !== process.env.APP_SIGNATURE) {
|
||||
return res.status(403).json({ message: "Access denied, invalid or missing APP_SIGNATURE" });
|
||||
if (!appSignature) {
|
||||
return res.status(403).json({
|
||||
status: "failed",
|
||||
message: "Missing APP_SIGNATURE header.",
|
||||
});
|
||||
}
|
||||
|
||||
if (appSignature !== expectedSignature) {
|
||||
return res.status(403).json({
|
||||
status: "failed",
|
||||
message: "Invalid APP_SIGNATURE. Use the exact value from server .env.",
|
||||
});
|
||||
}
|
||||
|
||||
next(); // allow request to continue
|
||||
|
||||
116
app/middleware/csvUpload.middleware.js
Normal file
116
app/middleware/csvUpload.middleware.js
Normal file
@ -0,0 +1,116 @@
|
||||
const multer = require("multer");
|
||||
const { UPLOAD_DIR } = require("../config/upload.config");
|
||||
|
||||
const CSV_UPLOAD_FIELD = "file";
|
||||
|
||||
const upload = multer({
|
||||
dest: UPLOAD_DIR,
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
function respondCsvUploadError(res, statusCode, message, extra = {}) {
|
||||
return res.status(statusCode).json({
|
||||
status: "failed",
|
||||
message,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses multipart upload and sets req.file from form field "file".
|
||||
* Returns helpful errors when Swagger/Postman sends the wrong shape.
|
||||
*/
|
||||
function handleCsvUpload(req, res, next) {
|
||||
upload.any()(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
"CSV file is too large. Maximum allowed size is 10 MB."
|
||||
);
|
||||
}
|
||||
if (err.code === "LIMIT_UNEXPECTED_FILE") {
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
`Unexpected file field "${err.field}". Use form field name "${CSV_UPLOAD_FIELD}" for the CSV.`,
|
||||
{ expected_field: CSV_UPLOAD_FIELD, received_field: err.field }
|
||||
);
|
||||
}
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
"Could not read uploaded file. Please upload a valid .csv file.",
|
||||
{ detail: err.message }
|
||||
);
|
||||
}
|
||||
|
||||
const files = Array.isArray(req.files) ? req.files : [];
|
||||
|
||||
if (files.length === 0) {
|
||||
const contentType = req.headers["content-type"] || "";
|
||||
|
||||
if (!contentType.includes("multipart/form-data")) {
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
"CSV upload must use multipart/form-data, not JSON.",
|
||||
{
|
||||
hint: `Send your .csv in form field "${CSV_UPLOAD_FIELD}". In Swagger: choose the file under "${CSV_UPLOAD_FIELD}" before Execute.`,
|
||||
expected_field: CSV_UPLOAD_FIELD,
|
||||
received_content_type: contentType || "(missing)",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
`No CSV file received. Attach a .csv file to form field "${CSV_UPLOAD_FIELD}".`,
|
||||
{
|
||||
hint:
|
||||
'In Swagger UI: click "Try it out" → use the file picker for "' +
|
||||
CSV_UPLOAD_FIELD +
|
||||
'" (e.g. hs-codes-sample.csv) → then Execute. Do not leave the file field empty.',
|
||||
expected_field: CSV_UPLOAD_FIELD,
|
||||
example_curl:
|
||||
'curl -F "file=@hs-codes-sample.csv;type=text/csv" ...',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const csvFile = files.find((f) => f.fieldname === CSV_UPLOAD_FIELD);
|
||||
|
||||
if (!csvFile) {
|
||||
const receivedFields = [...new Set(files.map((f) => f.fieldname))];
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
`CSV must use form field "${CSV_UPLOAD_FIELD}", but received: ${receivedFields.join(", ")}.`,
|
||||
{
|
||||
hint: `Change the form field name to "${CSV_UPLOAD_FIELD}" and upload again.`,
|
||||
expected_field: CSV_UPLOAD_FIELD,
|
||||
received_fields: receivedFields,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (files.length > 1) {
|
||||
return respondCsvUploadError(
|
||||
res,
|
||||
400,
|
||||
"Only one CSV file can be uploaded per request.",
|
||||
{
|
||||
hint: `Send a single file in field "${CSV_UPLOAD_FIELD}".`,
|
||||
received_file_count: files.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
req.file = csvFile;
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { handleCsvUpload, CSV_UPLOAD_FIELD };
|
||||
@ -27,6 +27,7 @@ const multer = require("multer");
|
||||
const { UPLOAD_DIR } = require("../config/upload.config");
|
||||
|
||||
const upload = multer({ dest: UPLOAD_DIR });
|
||||
const { handleCsvUpload } = require("../middleware/csvUpload.middleware");
|
||||
|
||||
|
||||
|
||||
@ -133,6 +134,49 @@ router.post("/survey_auto_submit",[] , calculationController.survey_auto_submit)
|
||||
|
||||
router.post("/calculate_quarter",[] , calculationController.calculate_quarter);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/survey_auto_submit_and_calculate:
|
||||
* post:
|
||||
* summary: Auto submit missing surveys, then calculate IPI for the quarter
|
||||
* description: |
|
||||
* Runs in sequence:
|
||||
* 1. survey_auto_submit — auto-fill missing submissions and verify completeness
|
||||
* 2. calculate_quarter — run IPI calculation for the same year and quarter
|
||||
* tags: [IIP Calculation]
|
||||
* security:
|
||||
* - appSignature: []
|
||||
* - CSRF: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - year
|
||||
* - quarter
|
||||
* properties:
|
||||
* year:
|
||||
* type: integer
|
||||
* example: 2025
|
||||
* quarter:
|
||||
* type: string
|
||||
* enum: [Q1, Q2, Q3, Q4]
|
||||
* example: Q2
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Auto submit and quarterly calculation completed successfully
|
||||
* 400:
|
||||
* description: Invalid year or quarter
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post(
|
||||
"/survey_auto_submit_and_calculate",
|
||||
[],
|
||||
calculationController.survey_auto_submit_and_calculate
|
||||
);
|
||||
|
||||
router.get("/calculate_month",[] , calculationController.calculate_month);
|
||||
|
||||
@ -1415,29 +1459,36 @@ router.delete("/products/:id",[verifySignature, verifyToken], productController.
|
||||
* description: This API accepts CSV file and inserts multiple products in bulk. CSV header columns must match Product table columns.
|
||||
* tags: [Products]
|
||||
* security:
|
||||
* - appSignature: []
|
||||
* - CSRF: []
|
||||
* cookieAuth: [] # or bearerAuth: [] if you use Authorization header
|
||||
* - bearerAuth: []
|
||||
* appSignature: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - file
|
||||
* properties:
|
||||
* file:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: CSV file to upload
|
||||
* description: CSV file — form field name must be "file"
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Products uploaded successfully
|
||||
* 400:
|
||||
* description: No file uploaded
|
||||
* description: Missing file, wrong form field, or validation error
|
||||
* 403:
|
||||
* description: Missing or invalid APP_SIGNATURE or auth token
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post("/products/uploadCSV",[ verifySignature, verifyToken, upload.single("file")], productController.uploadProductsFromCSV);
|
||||
router.post(
|
||||
"/products/uploadCSV",
|
||||
[verifySignature, verifyToken, handleCsvUpload],
|
||||
productController.uploadProductsFromCSV
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
12
server.js
12
server.js
@ -251,7 +251,10 @@ app.use((req, res, next) => {
|
||||
"/api/forgot-password/verify-otp",
|
||||
"/api/csrf-token",
|
||||
"/api/auth/request-otp",
|
||||
"/api/auth/verify-otp"
|
||||
"/api/auth/verify-otp",
|
||||
"/api/products/uploadCSV",
|
||||
"/api/establishments/uploadCSV",
|
||||
"/api/unit_master/uploadCSV",
|
||||
];
|
||||
|
||||
if (csrfExcludedPaths.includes(req.path)) {
|
||||
@ -281,6 +284,13 @@ app.use((err, req, res, next) => {
|
||||
message: "Invalid or missing CSRF token",
|
||||
});
|
||||
}
|
||||
if (err.name === "MulterError") {
|
||||
return res.status(400).json({
|
||||
status: "failed",
|
||||
message: err.message,
|
||||
hint: 'Upload the CSV using form field "file".',
|
||||
});
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user