Compare commits
No commits in common. "f679317d4a4a1cfa18fc965cf2bb41e753c18f48" and "5be1e319b0815e583350a4b81a86e25ecb61c868" have entirely different histories.
f679317d4a
...
5be1e319b0
@ -1 +1 @@
|
|||||||
import{a as s}from"./index-yOqi1S1C.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CX5oLBI_.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
import{a as s}from"./index-BCZm9H2Q.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CX5oLBI_.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||||
64
build/assets/index-BCZm9H2Q.js
Normal file
64
build/assets/index-BCZm9H2Q.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-DjE6S9VF.css
Normal file
1
build/assets/index-DjE6S9VF.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
|||||||
<!-- Preload essential fonts and icons -->
|
<!-- Preload essential fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<script type="module" crossorigin src="/assets/index-yOqi1S1C.js"></script>
|
<script type="module" crossorigin src="/assets/index-BCZm9H2Q.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-XBJXaMj2.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DjE6S9VF.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -66,7 +66,7 @@ function deleteTempFile(tempPath: string): void {
|
|||||||
* Malware scan middleware for single file uploads (multer.single)
|
* Malware scan middleware for single file uploads (multer.single)
|
||||||
* Works with memory storage — writes buffer to temp → scans → deletes temp
|
* Works with memory storage — writes buffer to temp → scans → deletes temp
|
||||||
*/
|
*/
|
||||||
export async function malwareScanMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
|
export function malwareScanMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||||
// Skip if no file uploaded
|
// Skip if no file uploaded
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@ -79,14 +79,14 @@ export async function malwareScanMiddleware(req: Request, res: Response, next: N
|
|||||||
req.scanEventId = scanEventId;
|
req.scanEventId = scanEventId;
|
||||||
|
|
||||||
// Handle the async scan
|
// Handle the async scan
|
||||||
await performScan(file, scanEventId, req, res, next);
|
performScan(file, scanEventId, req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Malware scan middleware for multiple file uploads (multer.array / multer.fields)
|
* Malware scan middleware for multiple file uploads (multer.array / multer.fields)
|
||||||
* Scans all files and blocks if ANY file is infected
|
* Scans all files and blocks if ANY file is infected
|
||||||
*/
|
*/
|
||||||
export async function malwareScanMultipleMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
|
export function malwareScanMultipleMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||||
// Handle multer.array()
|
// Handle multer.array()
|
||||||
const files = req.files;
|
const files = req.files;
|
||||||
if (!files || (Array.isArray(files) && files.length === 0)) {
|
if (!files || (Array.isArray(files) && files.length === 0)) {
|
||||||
@ -100,7 +100,7 @@ export async function malwareScanMultipleMiddleware(req: Request, res: Response,
|
|||||||
|
|
||||||
// Handle array of files
|
// Handle array of files
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
await performMultiScan(files, scanEventId, req, res, next);
|
performMultiScan(files, scanEventId, req, res, next);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ export async function malwareScanMultipleMiddleware(req: Request, res: Response,
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
await performMultiScan(allFiles, scanEventId, req, res, next);
|
performMultiScan(allFiles, scanEventId, req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Core scan logic ──
|
// ── Core scan logic ──
|
||||||
@ -132,7 +132,7 @@ async function performScan(
|
|||||||
try {
|
try {
|
||||||
// Step 0: Pre-scan file validation (extension, MIME, magic bytes, blocked patterns)
|
// Step 0: Pre-scan file validation (extension, MIME, magic bytes, blocked patterns)
|
||||||
const maxSizeMB = parseInt(process.env.MAX_FILE_SIZE_MB || '50', 10);
|
const maxSizeMB = parseInt(process.env.MAX_FILE_SIZE_MB || '50', 10);
|
||||||
const validation = await validateFile(
|
const validation = validateFile(
|
||||||
file.originalname,
|
file.originalname,
|
||||||
file.mimetype,
|
file.mimetype,
|
||||||
file.buffer || null,
|
file.buffer || null,
|
||||||
@ -304,7 +304,7 @@ async function performMultiScan(
|
|||||||
|
|
||||||
// Step 0: Pre-scan file validation
|
// Step 0: Pre-scan file validation
|
||||||
const maxSizeMB = parseInt(process.env.MAX_FILE_SIZE_MB || '50', 10);
|
const maxSizeMB = parseInt(process.env.MAX_FILE_SIZE_MB || '50', 10);
|
||||||
const validation = await validateFile(
|
const validation = validateFile(
|
||||||
file.originalname,
|
file.originalname,
|
||||||
file.mimetype,
|
file.mimetype,
|
||||||
file.buffer || null,
|
file.buffer || null,
|
||||||
|
|||||||
@ -63,7 +63,7 @@ router.get('/:requestId', authenticateToken, validateParams(requestIdParamsSchem
|
|||||||
* @desc Submit dealer proposal (Step 1)
|
* @desc Submit dealer proposal (Step 1)
|
||||||
* @access Private
|
* @access Private
|
||||||
*/
|
*/
|
||||||
router.post('/:requestId/proposal', authenticateToken, validateParams(requestIdParamsSchema), upload.single('proposalDocument'), asyncHandler(malwareScanMiddleware), asyncHandler(dealerClaimController.submitProposal.bind(dealerClaimController)));
|
router.post('/:requestId/proposal', authenticateToken, validateParams(requestIdParamsSchema), upload.single('proposalDocument'), malwareScanMiddleware, asyncHandler(dealerClaimController.submitProposal.bind(dealerClaimController)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/v1/dealer-claims/:requestId/completion
|
* @route POST /api/v1/dealer-claims/:requestId/completion
|
||||||
@ -75,7 +75,7 @@ router.post('/:requestId/completion', authenticateToken, uploadLimiter, validate
|
|||||||
{ name: 'activityPhotos', maxCount: 10 },
|
{ name: 'activityPhotos', maxCount: 10 },
|
||||||
{ name: 'invoicesReceipts', maxCount: 10 },
|
{ name: 'invoicesReceipts', maxCount: 10 },
|
||||||
{ name: 'attendanceSheet', maxCount: 1 },
|
{ name: 'attendanceSheet', maxCount: 1 },
|
||||||
]), asyncHandler(malwareScanMultipleMiddleware), asyncHandler(dealerClaimController.submitCompletion.bind(dealerClaimController)));
|
]), malwareScanMultipleMiddleware, asyncHandler(dealerClaimController.submitCompletion.bind(dealerClaimController)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/v1/dealer-claims/:requestId/io/validate
|
* @route GET /api/v1/dealer-claims/:requestId/io/validate
|
||||||
@ -105,7 +105,7 @@ router.get('/:requestId/e-invoice/pdf', authenticateToken, validateParams(reques
|
|||||||
* @access Private
|
* @access Private
|
||||||
*/
|
*/
|
||||||
router.get('/:requestId/e-invoice/csv', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoiceCsv.bind(dealerClaimController)));
|
router.get('/:requestId/e-invoice/csv', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoiceCsv.bind(dealerClaimController)));
|
||||||
router.post('/:requestId/credit-note', authenticateToken, validateParams(requestIdParamsSchema), upload.single('creditNoteFile'), asyncHandler(malwareScanMiddleware), asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
router.post('/:requestId/credit-note', authenticateToken, validateParams(requestIdParamsSchema), upload.single('creditNoteFile'), malwareScanMiddleware, asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
||||||
|
|||||||
@ -22,6 +22,6 @@ const controller = new DocumentController();
|
|||||||
|
|
||||||
// multipart/form-data: file, requestId, optional category
|
// multipart/form-data: file, requestId, optional category
|
||||||
// Middleware chain: auth → multer → malware scan → controller
|
// Middleware chain: auth → multer → malware scan → controller
|
||||||
router.post('/', authenticateToken, upload.single('file'), asyncHandler(malwareScanMiddleware), asyncHandler(controller.upload.bind(controller)));
|
router.post('/', authenticateToken, upload.single('file'), malwareScanMiddleware, asyncHandler(controller.upload.bind(controller)));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -1183,18 +1183,10 @@ export class DealerClaimService {
|
|||||||
hsnCode: item.hsnCode || '',
|
hsnCode: item.hsnCode || '',
|
||||||
gstRate: Number(item.gstRate) || 0,
|
gstRate: Number(item.gstRate) || 0,
|
||||||
gstAmt: Number(item.gstAmt) || 0,
|
gstAmt: Number(item.gstAmt) || 0,
|
||||||
cgstRate: Number(item.cgstRate) || 0,
|
|
||||||
cgstAmt: Number(item.cgstAmt) || 0,
|
cgstAmt: Number(item.cgstAmt) || 0,
|
||||||
sgstRate: Number(item.sgstRate) || 0,
|
|
||||||
sgstAmt: Number(item.sgstAmt) || 0,
|
sgstAmt: Number(item.sgstAmt) || 0,
|
||||||
igstRate: Number(item.igstRate) || 0,
|
|
||||||
igstAmt: Number(item.igstAmt) || 0,
|
igstAmt: Number(item.igstAmt) || 0,
|
||||||
utgstRate: Number(item.utgstRate) || 0,
|
totalAmt: Number(item.totalAmt) || 0
|
||||||
utgstAmt: Number(item.utgstAmt) || 0,
|
|
||||||
cessRate: Number(item.cessRate) || 0,
|
|
||||||
cessAmt: Number(item.cessAmt) || 0,
|
|
||||||
totalAmt: Number(item.totalAmt) || 0,
|
|
||||||
isService: !!item.isService
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Note: costBreakup JSONB field has been removed - only using separate table now
|
// Note: costBreakup JSONB field has been removed - only using separate table now
|
||||||
@ -1276,7 +1268,6 @@ export class DealerClaimService {
|
|||||||
proposalDetails: transformedProposalDetails,
|
proposalDetails: transformedProposalDetails,
|
||||||
completionDetails: serializedCompletionDetails,
|
completionDetails: serializedCompletionDetails,
|
||||||
internalOrder: serializedInternalOrder,
|
internalOrder: serializedInternalOrder,
|
||||||
internalOrders: serializedInternalOrders, // Return full list for UI
|
|
||||||
// New normalized tables
|
// New normalized tables
|
||||||
budgetTracking: serializedBudgetTracking,
|
budgetTracking: serializedBudgetTracking,
|
||||||
invoice: serializedInvoice,
|
invoice: serializedInvoice,
|
||||||
@ -1775,35 +1766,11 @@ export class DealerClaimService {
|
|||||||
// If blocking amount is 0 but ioNumber is provided, just save the IO details without blocking
|
// If blocking amount is 0 but ioNumber is provided, just save the IO details without blocking
|
||||||
if (blockedAmount <= 0) {
|
if (blockedAmount <= 0) {
|
||||||
// Allow saving IO details (ioNumber only) even without blocking amount
|
// Allow saving IO details (ioNumber only) even without blocking amount
|
||||||
// This is useful when Step 3/Requestor Evaluation is in progress but amount hasn't been blocked yet or for linking IO
|
// This is useful when Requestor Evaluation is in progress but amount hasn't been blocked yet
|
||||||
if (ioData.ioNumber) {
|
if (ioData.ioNumber) {
|
||||||
const organizedBy = organizedByUserId || null;
|
const organizedBy = organizedByUserId || null;
|
||||||
|
|
||||||
// Check if an IO record already exists for this request and IO number
|
// Always create a new Internal Order record for each block/provision (supporting multiple IOs)
|
||||||
// This prevents duplicate 0-amount "provisioned" records when re-saving IO details
|
|
||||||
const existingIO = await InternalOrder.findOne({
|
|
||||||
where: {
|
|
||||||
requestId,
|
|
||||||
ioNumber: ioData.ioNumber
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingIO) {
|
|
||||||
// Update existing record with latest remark and organizer info if provided
|
|
||||||
await existingIO.update({
|
|
||||||
ioRemark: ioData.ioRemark || existingIO.ioRemark || '',
|
|
||||||
organizedBy: organizedBy || existingIO.organizedBy || undefined,
|
|
||||||
organizedAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Existing IO record updated for request: ${requestId}`, {
|
|
||||||
ioNumber: ioData.ioNumber,
|
|
||||||
status: existingIO.status
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Internal Order record if none exists for this IO and request
|
|
||||||
await InternalOrder.create({
|
await InternalOrder.create({
|
||||||
requestId,
|
requestId,
|
||||||
ioNumber: ioData.ioNumber,
|
ioNumber: ioData.ioNumber,
|
||||||
@ -1948,6 +1915,15 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update budget tracking with blocked amount FIRST
|
||||||
|
await ClaimBudgetTracking.upsert({
|
||||||
|
requestId,
|
||||||
|
ioBlockedAmount: finalBlockedAmount,
|
||||||
|
ioBlockedAt: new Date(),
|
||||||
|
budgetStatus: BudgetStatus.BLOCKED,
|
||||||
|
currency: 'INR',
|
||||||
|
});
|
||||||
|
|
||||||
// Save IO history AFTER budget tracking update succeeds (only if ioLevel exists)
|
// Save IO history AFTER budget tracking update succeeds (only if ioLevel exists)
|
||||||
if (ioLevel && ioHistoryUserId) {
|
if (ioLevel && ioHistoryUserId) {
|
||||||
try {
|
try {
|
||||||
@ -2600,22 +2576,6 @@ export class DealerClaimService {
|
|||||||
costItems: costItems.map(i => ({
|
costItems: costItems.map(i => ({
|
||||||
description: i.itemDescription,
|
description: i.itemDescription,
|
||||||
amount: Number(i.amount || 0),
|
amount: Number(i.amount || 0),
|
||||||
quantity: Number(i.quantity || 1),
|
|
||||||
hsnCode: i.hsnCode || '',
|
|
||||||
gstRate: Number(i.gstRate || 0),
|
|
||||||
gstAmt: Number(i.gstAmt || 0),
|
|
||||||
cgstRate: Number(i.cgstRate || 0),
|
|
||||||
cgstAmt: Number(i.cgstAmt || 0),
|
|
||||||
sgstRate: Number(i.sgstRate || 0),
|
|
||||||
sgstAmt: Number(i.sgstAmt || 0),
|
|
||||||
igstRate: Number(i.igstRate || 0),
|
|
||||||
igstAmt: Number(i.igstAmt || 0),
|
|
||||||
utgstRate: Number(i.utgstRate || 0),
|
|
||||||
utgstAmt: Number(i.utgstAmt || 0),
|
|
||||||
cessRate: Number(i.cessRate || 0),
|
|
||||||
cessAmt: Number(i.cessAmt || 0),
|
|
||||||
totalAmt: Number(i.totalAmt || 0),
|
|
||||||
isService: !!i.isService,
|
|
||||||
order: i.itemOrder
|
order: i.itemOrder
|
||||||
})),
|
})),
|
||||||
otherDocuments: supportingDocs.map(doc => ({
|
otherDocuments: supportingDocs.map(doc => ({
|
||||||
|
|||||||
@ -284,84 +284,84 @@ export class DealerClaimApprovalService {
|
|||||||
// Fallback: proceed to Step 4 normally if history check fails
|
// Fallback: proceed to Step 4 normally if history check fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`[DealerClaimApproval] No next level found after level ${currentLevelNumber} - this may be the final approval`);
|
||||||
|
}
|
||||||
|
|
||||||
// Important: Update nextLevelNumber in case nextLevel was shifted (e.g. Step 4 skip)
|
if (nextLevel) {
|
||||||
// This ensures WorkflowRequest.currentLevel is updated to the correct active level
|
// Check if next level is paused - if so, don't activate it
|
||||||
const finalNextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
|
if ((nextLevel as any).isPaused || (nextLevel as any).status === 'PAUSED') {
|
||||||
|
logger.warn(`[DealerClaimApproval] Cannot activate next level ${nextLevelNumber} - level is paused`);
|
||||||
|
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
|
||||||
|
}
|
||||||
|
|
||||||
if (nextLevel) {
|
// Activate next level
|
||||||
// Check if next level is paused - if so, don't activate it
|
await nextLevel.update({
|
||||||
if ((nextLevel as any).isPaused || (nextLevel as any).status === 'PAUSED') {
|
status: ApprovalStatus.IN_PROGRESS,
|
||||||
logger.warn(`[DealerClaimApproval] Cannot activate next level ${finalNextLevelNumber} - level is paused`);
|
levelStartTime: now,
|
||||||
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
|
tatStartTime: now
|
||||||
}
|
});
|
||||||
|
|
||||||
// Activate next level
|
// Schedule TAT jobs for the next level
|
||||||
await nextLevel.update({
|
try {
|
||||||
status: ApprovalStatus.IN_PROGRESS,
|
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
||||||
levelStartTime: now,
|
|
||||||
tatStartTime: now
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schedule TAT jobs for the next level
|
await tatSchedulerService.scheduleTatJobs(
|
||||||
|
level.requestId,
|
||||||
|
(nextLevel as any).levelId,
|
||||||
|
(nextLevel as any).approverId,
|
||||||
|
Number((nextLevel as any).tatHours),
|
||||||
|
now,
|
||||||
|
workflowPriority
|
||||||
|
);
|
||||||
|
logger.info(`[DealerClaimApproval] TAT jobs scheduled for next level ${nextLevelNumber} (Priority: ${workflowPriority})`);
|
||||||
|
} catch (tatError) {
|
||||||
|
logger.error(`[DealerClaimApproval] Failed to schedule TAT jobs for next level:`, tatError);
|
||||||
|
// Don't fail the approval if TAT scheduling fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update workflow current level
|
||||||
|
if (nextLevelNumber !== null) {
|
||||||
|
await WorkflowRequest.update(
|
||||||
|
{ currentLevel: nextLevelNumber },
|
||||||
|
{ where: { requestId: level.requestId } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the APPROVE snapshot's changeReason to include movement information
|
||||||
|
// This ensures the approval snapshot shows both the approval and the movement
|
||||||
|
// We don't create a separate WORKFLOW snapshot for approvals - only APPROVE snapshot
|
||||||
try {
|
try {
|
||||||
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
const { DealerClaimHistory } = await import('@models/DealerClaimHistory');
|
||||||
|
const { SnapshotType } = await import('@models/DealerClaimHistory');
|
||||||
|
|
||||||
await tatSchedulerService.scheduleTatJobs(
|
const approvalHistory = await DealerClaimHistory.findOne({
|
||||||
level.requestId,
|
where: {
|
||||||
(nextLevel as any).levelId,
|
requestId: level.requestId,
|
||||||
(nextLevel as any).approverId,
|
approvalLevelId: level.levelId,
|
||||||
Number((nextLevel as any).tatHours),
|
snapshotType: SnapshotType.APPROVE
|
||||||
now,
|
},
|
||||||
workflowPriority
|
order: [['createdAt', 'DESC']]
|
||||||
);
|
});
|
||||||
logger.info(`[DealerClaimApproval] TAT jobs scheduled for next level ${finalNextLevelNumber} (Priority: ${workflowPriority})`);
|
|
||||||
} catch (tatError) {
|
|
||||||
logger.error(`[DealerClaimApproval] Failed to schedule TAT jobs for next level:`, tatError);
|
|
||||||
// Don't fail the approval if TAT scheduling fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update workflow current level
|
if (approvalHistory) {
|
||||||
if (finalNextLevelNumber !== null) {
|
// Use the robust approvalComment from outer scope
|
||||||
await WorkflowRequest.update(
|
const updatedChangeReason = approvalComment
|
||||||
{ currentLevel: finalNextLevelNumber },
|
? `Approved by ${level.approverName || level.approverEmail}, moved to next level (${nextLevelNumber}). Comment: ${approvalComment}`
|
||||||
{ where: { requestId: level.requestId } }
|
: `Approved by ${level.approverName || level.approverEmail}, moved to next level (${nextLevelNumber})`;
|
||||||
);
|
|
||||||
|
|
||||||
// Update the APPROVE snapshot's changeReason to include movement information
|
await approvalHistory.update({
|
||||||
// This ensures the approval snapshot shows both the approval and the movement
|
changeReason: updatedChangeReason
|
||||||
// We don't create a separate WORKFLOW snapshot for approvals - only APPROVE snapshot
|
|
||||||
try {
|
|
||||||
const { DealerClaimHistory } = await import('@models/DealerClaimHistory');
|
|
||||||
const { SnapshotType } = await import('@models/DealerClaimHistory');
|
|
||||||
|
|
||||||
const approvalHistory = await DealerClaimHistory.findOne({
|
|
||||||
where: {
|
|
||||||
requestId: level.requestId,
|
|
||||||
approvalLevelId: level.levelId,
|
|
||||||
snapshotType: SnapshotType.APPROVE
|
|
||||||
},
|
|
||||||
order: [['createdAt', 'DESC']]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (approvalHistory) {
|
|
||||||
// Use the robust approvalComment from outer scope
|
|
||||||
const updatedChangeReason = approvalComment
|
|
||||||
? `Approved by ${level.approverName || level.approverEmail}, moved to next level (${finalNextLevelNumber}). Comment: ${approvalComment}`
|
|
||||||
: `Approved by ${level.approverName || level.approverEmail}, moved to next level (${finalNextLevelNumber})`;
|
|
||||||
|
|
||||||
await approvalHistory.update({
|
|
||||||
changeReason: updatedChangeReason
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (updateError) {
|
|
||||||
// Log error but don't fail - this is just updating the changeReason for better display
|
|
||||||
logger.warn(`[DealerClaimApproval] Failed to update approval history changeReason (non-critical):`, updateError);
|
|
||||||
}
|
}
|
||||||
|
} catch (updateError) {
|
||||||
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${finalNextLevelNumber} for workflow ${level.requestId}`);
|
// Log error but don't fail - this is just updating the changeReason for better display
|
||||||
|
logger.warn(`[DealerClaimApproval] Failed to update approval history changeReason (non-critical):`, updateError);
|
||||||
}
|
}
|
||||||
} // Handle dealer claim-specific step processing
|
|
||||||
|
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle dealer claim-specific step processing
|
||||||
const currentLevelName = (level.levelName || '').toLowerCase();
|
const currentLevelName = (level.levelName || '').toLowerCase();
|
||||||
// Check by levelName first, use levelNumber only as fallback if levelName is missing
|
// Check by levelName first, use levelNumber only as fallback if levelName is missing
|
||||||
// This handles cases where additional approvers shift step numbers
|
// This handles cases where additional approvers shift step numbers
|
||||||
|
|||||||
@ -7,11 +7,8 @@
|
|||||||
* - Path traversal blocking (e.g., ../../etc/passwd)
|
* - Path traversal blocking (e.g., ../../etc/passwd)
|
||||||
* - Magic bytes / file signature validation
|
* - Magic bytes / file signature validation
|
||||||
* - Filename sanitization
|
* - Filename sanitization
|
||||||
* - Configuration-driven allowed file types
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfigValue } from '../configReader.service';
|
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
export interface FileValidationResult {
|
export interface FileValidationResult {
|
||||||
@ -126,29 +123,19 @@ const BLOCKED_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
|||||||
* Validate an uploaded file for security concerns.
|
* Validate an uploaded file for security concerns.
|
||||||
* This runs BEFORE ClamAV and catches things ClamAV won't flag.
|
* This runs BEFORE ClamAV and catches things ClamAV won't flag.
|
||||||
*/
|
*/
|
||||||
export async function validateFile(
|
export function validateFile(
|
||||||
originalName: string,
|
originalName: string,
|
||||||
mimeType: string,
|
mimeType: string,
|
||||||
fileBuffer: Buffer | null,
|
fileBuffer: Buffer | null,
|
||||||
fileSizeBytes: number,
|
fileSizeBytes: number,
|
||||||
maxSizeMB: number = 50,
|
maxSizeMB: number = 50,
|
||||||
): Promise<FileValidationResult> {
|
): FileValidationResult {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
// 1. Extract and validate extension
|
// 1. Extract and validate extension
|
||||||
const ext = originalName.split('.').pop()?.toLowerCase() || '';
|
const ext = originalName.split('.').pop()?.toLowerCase() || '';
|
||||||
|
const allowedExtensions = Object.keys(EXTENSION_MIME_MAP);
|
||||||
// Get allowed extensions from config, fallback to default EXTENSION_MIME_MAP keys
|
|
||||||
const allowedTypesConfig = await getConfigValue('ALLOWED_FILE_TYPES', '');
|
|
||||||
let allowedExtensions: string[];
|
|
||||||
|
|
||||||
if (allowedTypesConfig) {
|
|
||||||
// e.g., "pdf, docx, jpg" -> ["pdf", "docx", "jpg"]
|
|
||||||
allowedExtensions = allowedTypesConfig.split(',').map(e => e.trim().toLowerCase()).filter(e => e);
|
|
||||||
} else {
|
|
||||||
allowedExtensions = Object.keys(EXTENSION_MIME_MAP);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
errors.push('File has no extension');
|
errors.push('File has no extension');
|
||||||
@ -173,14 +160,13 @@ export async function validateFile(
|
|||||||
errors.push('File is empty (0 bytes)');
|
errors.push('File is empty (0 bytes)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. MIME type ↔ extension mismatch detection
|
// 4. MIME type ↔ extension mismatch detection (warning only — browsers/multer can report wrong MIME)
|
||||||
let mimeMatchesExtension = true;
|
let mimeMatchesExtension = true;
|
||||||
if (ext && EXTENSION_MIME_MAP[ext]) {
|
if (ext && EXTENSION_MIME_MAP[ext]) {
|
||||||
const allowedMimes = EXTENSION_MIME_MAP[ext];
|
const allowedMimes = EXTENSION_MIME_MAP[ext];
|
||||||
if (!allowedMimes.includes(mimeType) && mimeType !== 'application/octet-stream') {
|
if (!allowedMimes.includes(mimeType) && mimeType !== 'application/octet-stream') {
|
||||||
mimeMatchesExtension = false;
|
mimeMatchesExtension = false;
|
||||||
// Block if the uploaded file's claimed mimetype doesn't match its extension
|
warnings.push(
|
||||||
errors.push(
|
|
||||||
`MIME type mismatch: file claims ".${ext}" but has MIME "${mimeType}". ` +
|
`MIME type mismatch: file claims ".${ext}" but has MIME "${mimeType}". ` +
|
||||||
`Expected: ${allowedMimes.join(' or ')}`
|
`Expected: ${allowedMimes.join(' or ')}`
|
||||||
);
|
);
|
||||||
@ -200,26 +186,15 @@ export async function validateFile(
|
|||||||
// Check if magic bytes match claimed extension
|
// Check if magic bytes match claimed extension
|
||||||
if (ext) {
|
if (ext) {
|
||||||
const expectedSignatures = MAGIC_BYTES.filter(m => m.ext === ext);
|
const expectedSignatures = MAGIC_BYTES.filter(m => m.ext === ext);
|
||||||
// If we know the expected signatures for this extension, enforce them
|
|
||||||
if (expectedSignatures.length > 0) {
|
if (expectedSignatures.length > 0) {
|
||||||
const matchesAny = expectedSignatures.some(sig => matchesBytes(fileBuffer, sig.bytes, sig.offset));
|
const matchesAny = expectedSignatures.some(sig => matchesBytes(fileBuffer, sig.bytes, sig.offset));
|
||||||
if (!matchesAny) {
|
if (!matchesAny) {
|
||||||
// Block uploads where the file's actual magic bytes do not match the expected ones
|
// Warning only — some legitimate files have variant headers
|
||||||
errors.push(
|
// ClamAV will do the real malware check
|
||||||
`File header does not match ".${ext}" signature — file may be a different type disguised as ".${ext}"`
|
warnings.push(
|
||||||
|
`File header does not match ".${ext}" signature — file may be corrupted or mislabeled`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If we DON'T strictly know the signatures for this extension,
|
|
||||||
// we should check if its bytes match a KNOWN DIFFERENT file type
|
|
||||||
for (const { ext: knownExt, bytes: knownBytes, offset: knownOffset } of MAGIC_BYTES) {
|
|
||||||
if (knownExt !== ext && matchesBytes(fileBuffer, knownBytes, knownOffset)) {
|
|
||||||
errors.push(
|
|
||||||
`File signature mismatch: claims to be ".${ext}" but header matches ".${knownExt}"`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -114,10 +114,12 @@ export class PWCIntegrationService {
|
|||||||
// Extract State Code from Dealer GSTIN
|
// Extract State Code from Dealer GSTIN
|
||||||
let dealerGst = dealer?.gstin;
|
let dealerGst = dealer?.gstin;
|
||||||
|
|
||||||
|
const uatGst = '24AAAPI3182M002';
|
||||||
const isDevOrUat = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'uat';
|
const isDevOrUat = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'uat';
|
||||||
|
|
||||||
if (isDevOrUat) {
|
if (isDevOrUat) {
|
||||||
logger.info(`[PWC] Running in ${process.env.NODE_ENV} mode. Original Dealer GST: ${dealerGst || 'empty'}`);
|
logger.info(`[PWC] Using Dev/UAT authorized GSTIN replacement: ${uatGst} (Original: ${dealerGst || 'empty'})`);
|
||||||
|
dealerGst = uatGst;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[PWC] Final GSTIN being used for authentication and seller: ${dealerGst}`);
|
logger.info(`[PWC] Final GSTIN being used for authentication and seller: ${dealerGst}`);
|
||||||
@ -432,7 +434,7 @@ export class PWCIntegrationService {
|
|||||||
TrdNm: dealer?.dealerName || 'Dealer',
|
TrdNm: dealer?.dealerName || 'Dealer',
|
||||||
Addr1: dealer?.city || "Address Line 1",
|
Addr1: dealer?.city || "Address Line 1",
|
||||||
Loc: dealer?.city || "Location",
|
Loc: dealer?.city || "Location",
|
||||||
Pin: Number(dealer?.pincode || 600001),
|
Pin: Number(dealer?.pincode || (dealerGst === uatGst ? 380001 : 600001)),
|
||||||
Stcd: dealerStateCode,
|
Stcd: dealerStateCode,
|
||||||
Ph: dealer?.phone || "9998887776",
|
Ph: dealer?.phone || "9998887776",
|
||||||
Em: dealer?.email || "Supplier@inv.com"
|
Em: dealer?.email || "Supplier@inv.com"
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user