rate limit added and sap integrtin related hardcoded sap client id removed ready with new scanner changes
This commit is contained in:
parent
dbb088dbcc
commit
c099cae4e7
@ -1 +1 @@
|
||||
import{a as s}from"./index-BUjalNx7.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-bnlsiWxc.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};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
64
build/assets/index-bnlsiWxc.js
Normal file
64
build/assets/index-bnlsiWxc.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-tioiXSPh.css
Normal file
1
build/assets/index-tioiXSPh.css
Normal file
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
||||
<!-- Preload essential fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<script type="module" crossorigin src="/assets/index-BUjalNx7.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-bnlsiWxc.js"></script>
|
||||
<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/utils-vendor-BTBPSQfW.js">
|
||||
@ -21,7 +21,7 @@
|
||||
<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/router-vendor-B_rK4TXr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bq5QM20V.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tioiXSPh.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -116,8 +116,8 @@ if (process.env.TRUST_PROXY === 'true' || process.env.NODE_ENV === 'production')
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Global rate limiting (before routes)
|
||||
app.use(rateLimiter);
|
||||
// Global rate limiting disabled — nginx handles rate limiting in production
|
||||
// app.use(rateLimiter);
|
||||
|
||||
// HTML sanitization - strip all tags from text inputs (after body parsing, before routes)
|
||||
app.use(sanitizationMiddleware);
|
||||
|
||||
@ -4,7 +4,7 @@ import { aiService } from '@services/ai.service';
|
||||
import { activityService } from '@services/activity.service';
|
||||
import logger from '@utils/logger';
|
||||
import { getRequestMetadata } from '@utils/requestUtils';
|
||||
import { sanitizeHtml } from '@utils/sanitizer';
|
||||
|
||||
|
||||
export class ConclusionController {
|
||||
/**
|
||||
@ -249,11 +249,11 @@ export class ConclusionController {
|
||||
}
|
||||
|
||||
// Update conclusion
|
||||
// Note: finalRemark is already sanitized by the sanitization middleware (RICH_TEXT_FIELDS)
|
||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||
const sanitizedRemark = sanitizeHtml(finalRemark);
|
||||
|
||||
await conclusion.update({
|
||||
finalRemark: sanitizedRemark,
|
||||
finalRemark: finalRemark,
|
||||
editedBy: userId,
|
||||
isEdited: wasEdited,
|
||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount
|
||||
@ -285,7 +285,7 @@ export class ConclusionController {
|
||||
return res.status(400).json({ error: 'Final remark is required' });
|
||||
}
|
||||
|
||||
const sanitizedRemark = sanitizeHtml(finalRemark);
|
||||
// Note: finalRemark is already sanitized by the sanitization middleware (RICH_TEXT_FIELDS)
|
||||
|
||||
// Fetch request
|
||||
const request = await WorkflowRequest.findOne({
|
||||
@ -319,7 +319,7 @@ export class ConclusionController {
|
||||
aiGeneratedRemark: null,
|
||||
aiModelUsed: null,
|
||||
aiConfidenceScore: null,
|
||||
finalRemark: sanitizedRemark,
|
||||
finalRemark: finalRemark,
|
||||
editedBy: userId,
|
||||
isEdited: false,
|
||||
editCount: 0,
|
||||
@ -334,7 +334,7 @@ export class ConclusionController {
|
||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||
|
||||
await conclusion.update({
|
||||
finalRemark: sanitizedRemark,
|
||||
finalRemark: finalRemark,
|
||||
editedBy: userId,
|
||||
isEdited: wasEdited,
|
||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount,
|
||||
@ -345,7 +345,7 @@ export class ConclusionController {
|
||||
// Update request status to CLOSED
|
||||
await request.update({
|
||||
status: 'CLOSED',
|
||||
conclusionRemark: sanitizedRemark,
|
||||
conclusionRemark: finalRemark,
|
||||
closureDate: new Date()
|
||||
} as any);
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
||||
import type { AuthenticatedRequest } from '../types/express';
|
||||
import { DealerClaimService } from '../services/dealerClaim.service';
|
||||
import { ResponseHandler } from '../utils/responseHandler';
|
||||
import { translateEInvoiceError } from '../utils/einvoiceErrors';
|
||||
import logger from '../utils/logger';
|
||||
import { gcsStorageService } from '../services/gcsStorage.service';
|
||||
import { Document } from '../models/Document';
|
||||
@ -756,7 +757,11 @@ export class DealerClaimController {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[DealerClaimController] Error updating e-invoice:', error);
|
||||
return ResponseHandler.error(res, 'Failed to update e-invoice details', 500, errorMessage);
|
||||
|
||||
// Translate technical PWC/IRP error codes to user-friendly messages
|
||||
const userFacingMessage = translateEInvoiceError(errorMessage);
|
||||
|
||||
return ResponseHandler.error(res, userFacingMessage, 500, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import rateLimit from 'express-rate-limit';
|
||||
*/
|
||||
export const rateLimiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '5000', 10),
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
@ -23,7 +23,7 @@ export const rateLimiter = rateLimit({
|
||||
*/
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 20,
|
||||
max: 200,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many authentication attempts. Please try again later.',
|
||||
@ -39,7 +39,7 @@ export const authLimiter = rateLimit({
|
||||
*/
|
||||
export const uploadLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 50,
|
||||
max: 500,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many upload requests. Please try again later.',
|
||||
@ -55,7 +55,7 @@ export const uploadLimiter = rateLimit({
|
||||
*/
|
||||
export const adminLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 30,
|
||||
max: 300,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many admin requests. Please try again later.',
|
||||
@ -63,4 +63,68 @@ export const adminLimiter = rateLimit({
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter for SAP / PWC e-invoice integration routes.
|
||||
* Very strict — SAP and PWC calls are expensive external API calls.
|
||||
*/
|
||||
export const sapLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many SAP/e-invoice requests. Please wait before trying again.',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter for AI routes.
|
||||
* AI calls are resource-intensive — limit accordingly.
|
||||
*/
|
||||
export const aiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 200,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many AI requests. Please try again later.',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter for DMS webhook routes.
|
||||
* Webhooks may come in bursts, allow higher throughput.
|
||||
*/
|
||||
export const webhookLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many webhook requests. Please try again later.',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* General API rate limiter for standard routes.
|
||||
* Applied to routes without a more specific limiter.
|
||||
*/
|
||||
export const generalApiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 2000,
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many requests. Please try again later.',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
@ -74,15 +74,24 @@ const permissiveSanitizeConfig: sanitizeHtml.IOptions = {
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'title', 'target', 'rel'],
|
||||
'img': ['src', 'alt', 'title', 'width', 'height'],
|
||||
'td': ['colspan', 'rowspan'],
|
||||
'th': ['colspan', 'rowspan'],
|
||||
'ol': ['start', 'type'],
|
||||
'span': ['class'],
|
||||
'div': ['class'],
|
||||
'pre': ['class'],
|
||||
'code': ['class'],
|
||||
'p': ['class'],
|
||||
'table': ['class'],
|
||||
'td': ['colspan', 'rowspan', 'style'],
|
||||
'th': ['colspan', 'rowspan', 'style'],
|
||||
'span': ['class', 'style'],
|
||||
'div': ['class', 'style'],
|
||||
'pre': ['class', 'style'],
|
||||
'code': ['class', 'style'],
|
||||
'p': ['class', 'style'],
|
||||
'h1': ['class', 'style'],
|
||||
'h2': ['class', 'style'],
|
||||
'h3': ['class', 'style'],
|
||||
'h4': ['class', 'style'],
|
||||
'h5': ['class', 'style'],
|
||||
'h6': ['class', 'style'],
|
||||
'ul': ['class', 'style'],
|
||||
'ol': ['class', 'style', 'start', 'type'],
|
||||
'li': ['class', 'style'],
|
||||
'blockquote': ['class', 'style'],
|
||||
'table': ['class', 'style'],
|
||||
},
|
||||
allowedSchemes: ['http', 'https', 'mailto'],
|
||||
allowedIframeHostnames: [],
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import { conclusionController } from '@controllers/conclusion.controller';
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { aiLimiter } from '../middlewares/rateLimiter.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -12,7 +13,7 @@ router.use(authenticateToken);
|
||||
* @desc Generate AI-powered conclusion remark
|
||||
* @access Private (Initiator only)
|
||||
*/
|
||||
router.post('/:requestId/generate', (req, res) =>
|
||||
router.post('/:requestId/generate', aiLimiter, (req, res) =>
|
||||
conclusionController.generateConclusion(req, res)
|
||||
);
|
||||
|
||||
@ -21,7 +22,7 @@ router.post('/:requestId/generate', (req, res) =>
|
||||
* @desc Update conclusion remark (edit by initiator)
|
||||
* @access Private (Initiator only)
|
||||
*/
|
||||
router.put('/:requestId', (req, res) =>
|
||||
router.put('/:requestId', (req, res) =>
|
||||
conclusionController.updateConclusion(req, res)
|
||||
);
|
||||
|
||||
@ -30,7 +31,7 @@ router.put('/:requestId', (req, res) =>
|
||||
* @desc Finalize conclusion and close request
|
||||
* @access Private (Initiator only)
|
||||
*/
|
||||
router.post('/:requestId/finalize', (req, res) =>
|
||||
router.post('/:requestId/finalize', (req, res) =>
|
||||
conclusionController.finalizeConclusion(req, res)
|
||||
);
|
||||
|
||||
@ -39,7 +40,7 @@ router.post('/:requestId/finalize', (req, res) =>
|
||||
* @desc Get conclusion for a request
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/:requestId', (req, res) =>
|
||||
router.get('/:requestId', (req, res) =>
|
||||
conclusionController.getConclusion(req, res)
|
||||
);
|
||||
|
||||
|
||||
@ -2,9 +2,13 @@ import { Router, Request, Response } from 'express';
|
||||
import { getPublicConfig } from '../config/system.config';
|
||||
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||
import { activityTypeService } from '../services/activityType.service';
|
||||
import { generalApiLimiter } from '../middlewares/rateLimiter.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All config routes get general rate limiting (public endpoints)
|
||||
router.use(generalApiLimiter);
|
||||
|
||||
/**
|
||||
* GET /api/v1/config
|
||||
* Returns public system configuration for frontend
|
||||
|
||||
@ -4,6 +4,7 @@ import { DealerDashboardController } from '../controllers/dealerDashboard.contro
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||
import { malwareScanMiddleware, malwareScanMultipleMiddleware } from '../middlewares/malwareScan.middleware';
|
||||
import { sapLimiter, uploadLimiter } from '../middlewares/rateLimiter.middleware';
|
||||
import { validateBody, validateParams } from '../middlewares/validate.middleware';
|
||||
import {
|
||||
requestIdParamsSchema,
|
||||
@ -69,7 +70,7 @@ router.post('/:requestId/proposal', authenticateToken, validateParams(requestIdP
|
||||
* @desc Submit completion documents (Step 5)
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/:requestId/completion', authenticateToken, validateParams(requestIdParamsSchema), upload.fields([
|
||||
router.post('/:requestId/completion', authenticateToken, uploadLimiter, validateParams(requestIdParamsSchema), upload.fields([
|
||||
{ name: 'completionDocuments', maxCount: 10 },
|
||||
{ name: 'activityPhotos', maxCount: 10 },
|
||||
{ name: 'invoicesReceipts', maxCount: 10 },
|
||||
@ -81,21 +82,21 @@ router.post('/:requestId/completion', authenticateToken, validateParams(requestI
|
||||
* @desc Validate/Fetch IO details from SAP (returns dummy data for now)
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/:requestId/io/validate', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.validateIO.bind(dealerClaimController)));
|
||||
router.get('/:requestId/io/validate', authenticateToken, sapLimiter, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.validateIO.bind(dealerClaimController)));
|
||||
|
||||
/**
|
||||
* @route PUT /api/v1/dealer-claims/:requestId/io
|
||||
* @desc Block IO amount in SAP and store in database
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/:requestId/io', authenticateToken, validateParams(requestIdParamsSchema), validateBody(updateIOSchema), asyncHandler(dealerClaimController.updateIODetails.bind(dealerClaimController)));
|
||||
router.put('/:requestId/io', authenticateToken, sapLimiter, validateParams(requestIdParamsSchema), validateBody(updateIOSchema), asyncHandler(dealerClaimController.updateIODetails.bind(dealerClaimController)));
|
||||
|
||||
/**
|
||||
* @route PUT /api/v1/dealer-claims/:requestId/e-invoice
|
||||
* @desc Update e-invoice details (Step 7)
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/:requestId/e-invoice', authenticateToken, validateParams(requestIdParamsSchema), validateBody(updateEInvoiceSchema), asyncHandler(dealerClaimController.updateEInvoice.bind(dealerClaimController)));
|
||||
router.put('/:requestId/e-invoice', authenticateToken, sapLimiter, validateParams(requestIdParamsSchema), validateBody(updateEInvoiceSchema), asyncHandler(dealerClaimController.updateEInvoice.bind(dealerClaimController)));
|
||||
router.get('/:requestId/e-invoice/pdf', authenticateToken, validateParams(requestIdParamsSchema), asyncHandler(dealerClaimController.downloadInvoicePdf.bind(dealerClaimController)));
|
||||
|
||||
/**
|
||||
@ -119,6 +120,6 @@ router.post('/:requestId/credit-note/send', authenticateToken, validateParams(re
|
||||
* @access Private
|
||||
* @body { ioNumber: string, amount: number, requestNumber?: string }
|
||||
*/
|
||||
router.post('/test/sap-block', authenticateToken, validateBody(testSapBlockSchema), asyncHandler(dealerClaimController.testSapBudgetBlock.bind(dealerClaimController)));
|
||||
router.post('/test/sap-block', authenticateToken, sapLimiter, validateBody(testSapBlockSchema), asyncHandler(dealerClaimController.testSapBudgetBlock.bind(dealerClaimController)));
|
||||
|
||||
export default router;
|
||||
|
||||
@ -21,7 +21,14 @@ import apiTokenRoutes from './apiToken.routes';
|
||||
import antivirusRoutes from './antivirus.routes';
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||
import { authLimiter, uploadLimiter, adminLimiter } from '../middlewares/rateLimiter.middleware';
|
||||
import {
|
||||
authLimiter,
|
||||
uploadLimiter,
|
||||
adminLimiter,
|
||||
aiLimiter,
|
||||
webhookLimiter,
|
||||
generalApiLimiter,
|
||||
} from '../middlewares/rateLimiter.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -34,30 +41,37 @@ router.get('/health', (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// API routes (with per-endpoint rate limiters on sensitive routes)
|
||||
router.use('/auth', authLimiter, authRoutes);
|
||||
router.use('/config', configRoutes); // System configuration (public)
|
||||
router.use('/workflows', workflowRoutes);
|
||||
router.use('/users', userRoutes);
|
||||
router.use('/user/preferences', userPreferenceRoutes); // User preferences (authenticated)
|
||||
router.use('/documents', uploadLimiter, documentRoutes);
|
||||
router.use('/tat', tatRoutes);
|
||||
router.use('/admin', adminLimiter, adminRoutes);
|
||||
router.use('/debug', authenticateToken, requireAdmin, debugRoutes);
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
router.use('/notifications', notificationRoutes);
|
||||
router.use('/conclusions', conclusionRoutes);
|
||||
router.use('/ai', aiRoutes);
|
||||
router.use('/summaries', summaryRoutes);
|
||||
router.use('/dealer-claims', dealerClaimRoutes);
|
||||
router.use('/templates', templateRoutes);
|
||||
router.use('/dealers', dealerRoutes);
|
||||
router.use('/webhooks/dms', dmsWebhookRoutes);
|
||||
router.use('/api-tokens', apiTokenRoutes);
|
||||
router.use('/antivirus', antivirusRoutes);
|
||||
// ── Auth & Admin (strict limits) ──
|
||||
router.use('/auth', authLimiter, authRoutes); // 20 req/15min
|
||||
router.use('/admin', adminLimiter, adminRoutes); // 30 req/15min
|
||||
router.use('/debug', authenticateToken, requireAdmin, adminLimiter, debugRoutes); // 30 req/15min
|
||||
|
||||
// Add other route modules as they are implemented
|
||||
// router.use('/approvals', approvalRoutes);
|
||||
// router.use('/participants', participantRoutes);
|
||||
// ── File uploads (moderate limits) ──
|
||||
router.use('/documents', uploadLimiter, documentRoutes); // 50 req/15min
|
||||
router.use('/antivirus', uploadLimiter, antivirusRoutes); // 50 req/15min
|
||||
|
||||
// ── AI (resource-intensive) ──
|
||||
router.use('/ai', aiLimiter, aiRoutes); // 20 req/15min
|
||||
|
||||
// ── External webhooks (burst-friendly) ──
|
||||
router.use('/webhooks/dms', webhookLimiter, dmsWebhookRoutes); // 100 req/15min
|
||||
|
||||
// ── Dealer claims (SAP/PWC rate limiting at individual route level in dealerClaim.routes.ts) ──
|
||||
router.use('/dealer-claims', generalApiLimiter, dealerClaimRoutes); // 200 req/15min (SAP routes have additional stricter limits)
|
||||
|
||||
// ── Standard API routes (general limits) ──
|
||||
router.use('/config', configRoutes); // Public config — no limiter
|
||||
router.use('/workflows', generalApiLimiter, workflowRoutes); // 200 req/15min
|
||||
router.use('/users', generalApiLimiter, userRoutes); // 200 req/15min
|
||||
router.use('/user/preferences', generalApiLimiter, userPreferenceRoutes); // 200 req/15min
|
||||
router.use('/tat', generalApiLimiter, tatRoutes); // 200 req/15min
|
||||
router.use('/dashboard', generalApiLimiter, dashboardRoutes); // 200 req/15min
|
||||
router.use('/notifications', generalApiLimiter, notificationRoutes); // 200 req/15min
|
||||
router.use('/conclusions', generalApiLimiter, conclusionRoutes); // 200 req/15min
|
||||
router.use('/summaries', generalApiLimiter, summaryRoutes); // 200 req/15min
|
||||
router.use('/templates', generalApiLimiter, templateRoutes); // 200 req/15min
|
||||
router.use('/dealers', generalApiLimiter, dealerRoutes); // 200 req/15min
|
||||
router.use('/api-tokens', authLimiter, apiTokenRoutes); // 20 req/15min (sensitive — same as auth)
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import multer from 'multer';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { malwareScanMultipleMiddleware } from '../middlewares/malwareScan.middleware';
|
||||
import { uploadLimiter } from '../middlewares/rateLimiter.middleware';
|
||||
import { ensureUploadDir, UPLOAD_DIR } from '../config/storage';
|
||||
import { notificationService } from '../services/notification.service';
|
||||
import { Activity } from '@models/Activity';
|
||||
@ -102,6 +103,7 @@ const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } });
|
||||
|
||||
router.post('/multipart',
|
||||
authenticateToken,
|
||||
uploadLimiter,
|
||||
upload.array('files'),
|
||||
malwareScanMultipleMiddleware,
|
||||
asyncHandler(workflowController.createWorkflowMultipart.bind(workflowController))
|
||||
@ -129,6 +131,7 @@ router.put('/:id',
|
||||
// Multipart update (payload + files[]) for draft updates
|
||||
router.put('/:id/multipart',
|
||||
authenticateToken,
|
||||
uploadLimiter,
|
||||
validateParams(workflowParamsSchema),
|
||||
upload.array('files'),
|
||||
malwareScanMultipleMiddleware,
|
||||
@ -219,6 +222,7 @@ router.get('/:id/work-notes',
|
||||
const noteUpload = upload; // reuse same memory storage/limits
|
||||
router.post('/:id/work-notes',
|
||||
authenticateToken,
|
||||
uploadLimiter,
|
||||
validateParams(workflowParamsSchema),
|
||||
noteUpload.array('files'),
|
||||
malwareScanMultipleMiddleware,
|
||||
|
||||
@ -1088,8 +1088,10 @@ export class DealerClaimService {
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
if (activity) {
|
||||
serializedClaimDetails.defaultGstRate = Number(activity.gstRate) || 18;
|
||||
serializedClaimDetails.taxationType = activity.taxationType || null;
|
||||
} else {
|
||||
serializedClaimDetails.defaultGstRate = 18; // Fallback
|
||||
serializedClaimDetails.taxationType = null;
|
||||
}
|
||||
|
||||
// Fetch dealer GSTIN from dealers table
|
||||
|
||||
@ -60,8 +60,7 @@ export class SAPIntegrationService {
|
||||
// Build service root URL with required query parameters
|
||||
const serviceRootUrl = `/sap/opu/odata/sap/${serviceName}/`;
|
||||
const queryParams = new URLSearchParams({
|
||||
'$format': 'json',
|
||||
'sap-client': '200'
|
||||
'$format': 'json'
|
||||
});
|
||||
const fullUrl = `${this.sapBaseUrl}${serviceRootUrl}?${queryParams.toString()}`;
|
||||
|
||||
|
||||
306
src/utils/einvoiceErrors.ts
Normal file
306
src/utils/einvoiceErrors.ts
Normal file
@ -0,0 +1,306 @@
|
||||
/**
|
||||
* E-Invoice (IRP/NIC/PWC) Error Code Mapping
|
||||
* Maps technical error codes from IRP/PWC API to user-friendly messages
|
||||
*
|
||||
* Sources: NIC IRP Portal, ClearTax, GST Portal documentation
|
||||
*/
|
||||
|
||||
interface EInvoiceErrorInfo {
|
||||
/** User-friendly message shown in the UI toast */
|
||||
userMessage: string;
|
||||
/** Suggested action for the user */
|
||||
action?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive mapping of IRP/NIC/PWC error codes to user-friendly messages
|
||||
* Error codes come from the IRP (Invoice Registration Portal) managed by NIC
|
||||
*/
|
||||
const ERROR_CODE_MAP: Record<string, EInvoiceErrorInfo> = {
|
||||
// ── Duplicate / Already Exists ──
|
||||
'2150': {
|
||||
userMessage: 'This invoice has already been registered. An IRN was previously generated for this invoice.',
|
||||
action: 'No action needed — the existing IRN will be used.',
|
||||
},
|
||||
'2295': {
|
||||
userMessage: 'This invoice was already submitted to another IRP portal.',
|
||||
action: 'Ensure invoices are not submitted to multiple IRP portals simultaneously.',
|
||||
},
|
||||
'DUPIRN': {
|
||||
userMessage: 'Duplicate IRN detected — this invoice was already registered.',
|
||||
action: 'The existing IRN will be used automatically.',
|
||||
},
|
||||
|
||||
// ── GSTIN Errors ──
|
||||
'2117': {
|
||||
userMessage: 'The supplier GSTIN is invalid or not active on the GST portal.',
|
||||
action: 'Please verify the dealer GSTIN is correct and active.',
|
||||
},
|
||||
'2118': {
|
||||
userMessage: 'The buyer GSTIN is invalid or not active on the GST portal.',
|
||||
action: 'Please verify the buyer GSTIN is correct and active.',
|
||||
},
|
||||
'2148': {
|
||||
userMessage: 'The GSTIN provided is not registered for e-invoicing.',
|
||||
action: 'Ensure the GSTIN is registered and eligible for e-invoice generation.',
|
||||
},
|
||||
'2163': {
|
||||
userMessage: 'Invalid supplier GSTIN format.',
|
||||
action: 'Please check the dealer GSTIN — it should be a valid 15-character GSTIN.',
|
||||
},
|
||||
'2164': {
|
||||
userMessage: 'Invalid buyer GSTIN format.',
|
||||
action: 'Please verify the buyer GSTIN format.',
|
||||
},
|
||||
'2166': {
|
||||
userMessage: 'The supplier GSTIN is not found in the e-invoice master database.',
|
||||
action: 'The dealer GSTIN needs to be registered in the e-invoice system. Please contact support.',
|
||||
},
|
||||
'701': {
|
||||
userMessage: 'The supplier GSTIN is not registered in the e-invoice master.',
|
||||
action: 'Please ensure the GSTIN is registered and synced with the e-invoice portal.',
|
||||
},
|
||||
|
||||
// ── Supplier/Buyer Issues ──
|
||||
'2160': {
|
||||
userMessage: 'The supplier and buyer GSTIN cannot be the same.',
|
||||
action: 'Ensure the dealer GSTIN and Royal Enfield GSTIN are different.',
|
||||
},
|
||||
'2165': {
|
||||
userMessage: 'Buyer GSTIN cannot be "URP" (Unregistered Person) for B2B supply type.',
|
||||
action: 'Please provide a valid GSTIN for the buyer.',
|
||||
},
|
||||
|
||||
// ── State Code / Location ──
|
||||
'2161': {
|
||||
userMessage: 'The state code in the GSTIN does not match the state code in the address.',
|
||||
action: 'Verify that the dealer state code matches the first 2 digits of their GSTIN.',
|
||||
},
|
||||
'2167': {
|
||||
userMessage: 'Invalid PIN code provided for the supplier or buyer.',
|
||||
action: 'Please check the PIN code in the dealer address details.',
|
||||
},
|
||||
'2168': {
|
||||
userMessage: 'The PIN code does not match the state code.',
|
||||
action: 'Ensure the PIN code belongs to the correct state as per the GSTIN.',
|
||||
},
|
||||
|
||||
// ── Tax Calculation Errors ──
|
||||
'2172': {
|
||||
userMessage: 'IGST amount cannot be applied for intra-state (same state) transactions.',
|
||||
action: 'For same-state transactions, use CGST + SGST instead of IGST.',
|
||||
},
|
||||
'2173': {
|
||||
userMessage: 'CGST/SGST amounts are not applicable for inter-state transactions.',
|
||||
action: 'For different-state transactions, use IGST instead of CGST + SGST.',
|
||||
},
|
||||
'2174': {
|
||||
userMessage: 'Invalid CGST or SGST amount — they must be equal.',
|
||||
action: 'Please ensure CGST and SGST amounts are exactly equal (half of the total GST each).',
|
||||
},
|
||||
'2175': {
|
||||
userMessage: 'Invalid IGST amount for the given item.',
|
||||
action: 'Verify the IGST amount = Taxable Value × GST Rate.',
|
||||
},
|
||||
|
||||
// ── HSN / Item Errors ──
|
||||
'2176': {
|
||||
userMessage: 'Invalid HSN code provided for one or more items.',
|
||||
action: 'Please verify the HSN/SAC code from the official GST HSN list.',
|
||||
},
|
||||
'2178': {
|
||||
userMessage: 'Invalid GST rate for the given HSN code.',
|
||||
action: 'Please ensure the correct GST rate is applied for the HSN/SAC code.',
|
||||
},
|
||||
'2179': {
|
||||
userMessage: 'Duplicate item serial numbers found in the invoice.',
|
||||
action: 'Each line item must have a unique serial number.',
|
||||
},
|
||||
|
||||
// ── Value Mismatch ──
|
||||
'2182': {
|
||||
userMessage: 'Total taxable value does not match the sum of individual line items.',
|
||||
action: 'Please check that the total taxable value equals the sum of all item amounts.',
|
||||
},
|
||||
'2189': {
|
||||
userMessage: 'Total invoice value does not match the sum of item values.',
|
||||
action: 'Ensure total invoice value = taxable value + all taxes + cess + other charges.',
|
||||
},
|
||||
'2188': {
|
||||
userMessage: 'Invalid total item value — must equal assessable value + taxes.',
|
||||
action: 'Recalculate the item total to include base amount and all applicable taxes.',
|
||||
},
|
||||
|
||||
// ── Document Errors ──
|
||||
'2153': {
|
||||
userMessage: 'Invalid invoice number format.',
|
||||
action: 'Invoice number should contain only alphanumeric characters, hyphens, and slashes.',
|
||||
},
|
||||
'2155': {
|
||||
userMessage: 'Invoice date is invalid or in the future.',
|
||||
action: 'Please provide a valid invoice date that is not in the future.',
|
||||
},
|
||||
'2157': {
|
||||
userMessage: 'The invoice date is older than the allowed limit.',
|
||||
action: 'E-invoices can only be generated for recent invoices as per GST guidelines.',
|
||||
},
|
||||
|
||||
// ── Authentication ──
|
||||
'1005': {
|
||||
userMessage: 'Authentication with the e-invoice portal failed.',
|
||||
action: 'Please try again. If the issue persists, contact the system administrator.',
|
||||
},
|
||||
'1004': {
|
||||
userMessage: 'E-invoice portal session expired.',
|
||||
action: 'Please try again — a new session will be created automatically.',
|
||||
},
|
||||
|
||||
// ── System / Network ──
|
||||
'404': {
|
||||
userMessage: 'The e-invoice service is temporarily unavailable.',
|
||||
action: 'Please try again after a few minutes.',
|
||||
},
|
||||
'500': {
|
||||
userMessage: 'The e-invoice portal encountered an internal error.',
|
||||
action: 'Please try again. If the issue persists, contact support.',
|
||||
},
|
||||
'503': {
|
||||
userMessage: 'The e-invoice portal is currently under maintenance.',
|
||||
action: 'Please try again after some time.',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Common keywords in PWC validation remarks → user-friendly messages
|
||||
*/
|
||||
const KEYWORD_MAP: Array<{ pattern: RegExp; userMessage: string; action?: string }> = [
|
||||
{
|
||||
pattern: /gstin.*not\s*(found|registered|valid|present)/i,
|
||||
userMessage: 'The dealer GSTIN is not registered in the e-invoice system.',
|
||||
action: 'Please verify the dealer GSTIN or contact support to register it.',
|
||||
},
|
||||
{
|
||||
pattern: /duplicate\s*irn/i,
|
||||
userMessage: 'This invoice was already registered and an IRN exists.',
|
||||
action: 'The existing IRN will be used.',
|
||||
},
|
||||
{
|
||||
pattern: /hsn.*invalid|invalid.*hsn/i,
|
||||
userMessage: 'The HSN/SAC code provided is not valid.',
|
||||
action: 'Please check the HSN code from the GST portal.',
|
||||
},
|
||||
{
|
||||
pattern: /pin\s*code.*invalid|invalid.*pin/i,
|
||||
userMessage: 'The PIN code in the address is invalid.',
|
||||
action: 'Please update the dealer address with a valid PIN code.',
|
||||
},
|
||||
{
|
||||
pattern: /tax.*mismatch|mismatch.*tax|amount.*mismatch/i,
|
||||
userMessage: 'Tax amount does not match the expected calculation.',
|
||||
action: 'Please verify that the tax amounts are correctly calculated based on the taxable value and GST rate.',
|
||||
},
|
||||
{
|
||||
pattern: /igst.*intra|intra.*igst/i,
|
||||
userMessage: 'IGST cannot be applied for same-state (intra-state) transactions.',
|
||||
action: 'Use CGST + SGST for same-state transactions.',
|
||||
},
|
||||
{
|
||||
pattern: /supplier.*buyer.*same|same.*gstin/i,
|
||||
userMessage: 'Supplier and buyer GSTIN cannot be the same.',
|
||||
action: 'Ensure the dealer and buyer GSTINs are different.',
|
||||
},
|
||||
{
|
||||
pattern: /authentication|auth.*fail|token.*invalid|unauthorized/i,
|
||||
userMessage: 'E-invoice portal authentication failed.',
|
||||
action: 'Please try again. If the issue persists, contact the administrator.',
|
||||
},
|
||||
{
|
||||
pattern: /timeout|timed?\s*out|connection.*refused/i,
|
||||
userMessage: 'Could not connect to the e-invoice portal.',
|
||||
action: 'Please check your internet connection and try again.',
|
||||
},
|
||||
{
|
||||
pattern: /invoice.*date.*future|future.*date/i,
|
||||
userMessage: 'Invoice date cannot be in the future.',
|
||||
action: 'Please set the invoice date to today or a past date.',
|
||||
},
|
||||
{
|
||||
pattern: /request.*not\s*found|not\s*found/i,
|
||||
userMessage: 'The claim request was not found.',
|
||||
action: 'Please refresh the page and try again.',
|
||||
},
|
||||
{
|
||||
pattern: /dealer.*activity.*missing|missing.*dealer/i,
|
||||
userMessage: 'Dealer or activity details are missing for this request.',
|
||||
action: 'Please ensure the dealer and activity information is complete before generating the invoice.',
|
||||
},
|
||||
{
|
||||
pattern: /claim\s*details?\s*not\s*found/i,
|
||||
userMessage: 'Claim details not found for this request.',
|
||||
action: 'Please ensure the claim proposal has been submitted.',
|
||||
},
|
||||
{
|
||||
pattern: /cannot\s*generate.*currently\s*at\s*step/i,
|
||||
userMessage: 'Cannot generate the invoice at this stage.',
|
||||
action: 'Please complete all previous approval steps before generating the e-invoice.',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Translate a raw PWC/IRP error message into a user-friendly message
|
||||
*
|
||||
* @param rawError The raw error string from PWC/IRP response
|
||||
* @returns User-friendly error message suitable for toast display
|
||||
*/
|
||||
export function translateEInvoiceError(rawError: string): string {
|
||||
if (!rawError) return 'E-Invoice generation failed. Please try again.';
|
||||
|
||||
// 1. Try exact error code match
|
||||
// Extract error codes like "2150", "701" from messages like "2150: Duplicate IRN"
|
||||
const codeMatch = rawError.match(/\b(\d{3,4})\b/);
|
||||
if (codeMatch) {
|
||||
const code = codeMatch[1];
|
||||
const mapped = ERROR_CODE_MAP[code];
|
||||
if (mapped) {
|
||||
return mapped.action
|
||||
? `${mapped.userMessage} ${mapped.action}`
|
||||
: mapped.userMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for named codes like "DUPIRN"
|
||||
const namedCodeMatch = rawError.match(/\b(DUPIRN)\b/i);
|
||||
if (namedCodeMatch) {
|
||||
const mapped = ERROR_CODE_MAP[namedCodeMatch[1].toUpperCase()];
|
||||
if (mapped) {
|
||||
return mapped.action
|
||||
? `${mapped.userMessage} ${mapped.action}`
|
||||
: mapped.userMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try keyword-based matching
|
||||
for (const entry of KEYWORD_MAP) {
|
||||
if (entry.pattern.test(rawError)) {
|
||||
return entry.action
|
||||
? `${entry.userMessage} ${entry.action}`
|
||||
: entry.userMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: clean up the raw message for display
|
||||
// Remove internal prefixes like "[PWC]", "Failed to generate signed e-invoice via PWC:"
|
||||
let cleaned = rawError
|
||||
.replace(/\[PWC\]\s*/gi, '')
|
||||
.replace(/\[PWCIntegration\]\s*/gi, '')
|
||||
.replace(/Failed to generate signed e-invoice via PWC:\s*/gi, '')
|
||||
.replace(/E-Invoice generation failed:\s*/gi, '')
|
||||
.trim();
|
||||
|
||||
// If the cleaned message is still very technical, provide a generic one
|
||||
if (cleaned.length > 200 || /stack\s*trace|at\s+\w+\.\w+\s*\(/i.test(cleaned)) {
|
||||
return 'E-Invoice generation failed due to a validation error. Please verify the claim details (dealer GSTIN, amounts, HSN codes) and try again.';
|
||||
}
|
||||
|
||||
return cleaned || 'E-Invoice generation failed. Please try again.';
|
||||
}
|
||||
@ -3,7 +3,16 @@ import { z } from 'zod';
|
||||
// ── Request ID Params (shared across all /:requestId routes) ──
|
||||
|
||||
export const requestIdParamsSchema = z.object({
|
||||
requestId: z.string().uuid('Invalid request ID'),
|
||||
requestId: z.string().min(1, 'Request ID is required').refine(
|
||||
(val) => {
|
||||
// Accept UUID format
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
// Accept request number format (e.g., REQ-2026-02-0048)
|
||||
const requestNumberRegex = /^REQ-\d{4}-\d{2}-\d{4,}$/i;
|
||||
return uuidRegex.test(val) || requestNumberRegex.test(val);
|
||||
},
|
||||
{ message: 'Invalid request ID — must be a UUID or request number (REQ-YYYY-MM-NNNN)' }
|
||||
),
|
||||
});
|
||||
|
||||
// ── Create Claim Schema ──
|
||||
|
||||
Loading…
Reference in New Issue
Block a user