rate limit added and sap integrtin related hardcoded sap client id removed ready with new scanner changes

This commit is contained in:
laxmanhalaki 2026-02-25 16:49:00 +05:30
parent dbb088dbcc
commit c099cae4e7
20 changed files with 547 additions and 129 deletions

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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,
});

View File

@ -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: [],

View File

@ -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)
);

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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
View 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.';
}

View File

@ -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 ──