pwc invoice generation implemented and tables enhanced to support envvoice fields

This commit is contained in:
laxmanhalaki 2026-02-10 20:20:43 +05:30
parent 9060c39f9c
commit 81afd7ec96
29 changed files with 1505 additions and 370 deletions

View File

@ -1,2 +1,2 @@
import{a as s}from"./index-PI_IMErM.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.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};
//# sourceMappingURL=conclusionApi-D5monZ70.js.map
import{a as s}from"./index-7F7W4LDI.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.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};
//# sourceMappingURL=conclusionApi-BJO_6JLT.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-D5monZ70.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
{"version":3,"file":"conclusionApi-BJO_6JLT.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}

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

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 critical 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-PI_IMErM.js"></script>
<script type="module" crossorigin src="/assets/index-7F7W4LDI.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-DNMmNUQL.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-D5NCgjQR.css">
<link rel="stylesheet" crossorigin href="/assets/index-AUbBsmWB.css">
</head>
<body>

View File

@ -0,0 +1,71 @@
# Dealer Claim Financial Settlement Workflow
This document outlines the workflow for financial settlement of dealer claims within the Royal Enfield platform, following the transition from direct DMS integration to an Azure File Storage (AFS) based data exchange with SAP.
## Workflow Overview
The financial settlement process ensures that dealer claims are legally documented and financially settled through Royal Enfield's SAP system.
### 1. Legal Compliance: PWC E-Invoicing
Once the **Dealer Completion Documents** are submitted and approved by the **Initiator (Requestor Evaluation)**, the system triggers the legal compliance step.
- **Service**: `PWCIntegrationService`
- **Action**: Generates a signed E-Invoice via PWC API.
- **Output**: IRN (Invoice Reference Number), Ack No, Ack Date, Signed Invoice (PDF/B64), and QR Code.
- **Purpose**: Ensures the claim is legally recognized under GST regulations.
### 2. Financial Posting: AFS/CSV Integration
The financial settlement is handled by exchanging data files with SAP via **Azure File Storage (AFS)**.
- **Action**: The system generates a **CSV file** containing the following details:
- Invoice Number (from PWC)
- Invoice Amount (with/without GST as per activity type)
- GL Code (Resolved based on Activity Type/IO)
- Internal Order (IO) Number
- Dealer Code
- **Storage**: CSV is uploaded to a designated folder in AFS.
- **SAP Role**: SAP periodically polls AFS, picks up the CSV, and posts the transaction internally.
### 3. Payment Outcome: Credit Note
The result of the financial posting in SAP is a **Credit Note**.
- **Workflow**:
- SAP generates a Credit Note and uploads it back to AFS.
- RE Backend monitors the AFS folder.
- Once a Credit Note is detected, the system retrieves it and attaches it to the workflow request.
- An email notification (using `creditNoteSent.template.ts`) is sent to the dealer.
## Sequence Diagram
```mermaid
sequenceDiagram
participant Dealer
participant Backend
participant PWC
participant AFS as Azure File Storage
participant SAP
Dealer->>Backend: Submit Completion Docs (Actuals)
Backend->>Backend: Initiator Approval
Backend->>PWC: Generate Signed E-Invoice
PWC-->>Backend: Return IRN & QR Code
Backend->>Backend: Generate Settlement CSV
Backend->>AFS: Upload CSV
SAP->>AFS: Pick up CSV
SAP->>SAP: Post Financials
SAP->>AFS: Upload Credit Note
Backend->>AFS: Poll/Retrieve Credit Note
Backend->>Dealer: Send Credit Note Notification
```
## GL Code Resolution
The GL Code is solved dynamically based on:
1. **Activity Type**: Each activity (e.g., Marketing Event, Demo) has a primary GL mapping.
2. **Internal Order (IO)**: If specific IO logic is required, the GL can be overridden.
## Summary of Integration Points
| Component | Integration Type | Responsibility |
| :--- | :--- | :--- |
| **PWC** | REST API | Legal E-Invoice |
| **AFS (Azure)** | File Storage SDK | CSV Exchange |
| **SAP** | Batch Processing | Financial Posting & Credit Note |

653
package-lock.json generated
View File

@ -36,6 +36,7 @@
"pg": "^8.13.1",
"pg-hstore": "^2.3.4",
"prom-client": "^15.1.3",
"puppeteer": "^24.37.2",
"sequelize": "^6.37.5",
"socket.io": "^4.8.1",
"uuid": "^8.3.2",
@ -809,7 +810,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@ -981,7 +981,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -2879,6 +2878,27 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@puppeteer/browsers": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz",
"integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.3",
"tar-fs": "^3.1.1",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -3511,6 +3531,12 @@
"node": ">= 10"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@ -4043,6 +4069,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@ -4446,7 +4482,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-flatten": {
@ -4493,6 +4528,18 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -4544,6 +4591,20 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -4676,6 +4737,97 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz",
"integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -4733,6 +4885,15 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
"integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@ -4916,6 +5077,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@ -5009,7 +5179,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -5111,6 +5280,19 @@
"node": ">= 6"
}
},
"node_modules/chromium-bidi": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz",
"integrity": "sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@ -5434,6 +5616,32 @@
"node": ">= 0.10"
}
},
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"license": "MIT",
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -5559,6 +5767,20 @@
"node": ">=0.10.0"
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5616,6 +5838,12 @@
"node": ">=8"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1566079",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz",
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -5896,11 +6124,19 @@
}
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
@ -5979,6 +6215,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/eslint": {
"version": "9.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
@ -6151,7 +6408,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@ -6191,7 +6447,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@ -6201,7 +6456,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@ -6225,6 +6479,15 @@
"node": ">=6"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -6357,6 +6620,41 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/extract-zip/node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -6364,6 +6662,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@ -6453,6 +6757,15 @@
"bser": "2.1.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@ -6879,6 +7192,29 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/get-uri/node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -7454,7 +7790,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@ -7555,6 +7890,15 @@
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -7568,7 +7912,6 @@
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true,
"license": "MIT"
},
"node_modules/is-binary-path": {
@ -8402,14 +8745,12 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -8451,7 +8792,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@ -8612,7 +8952,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@ -8935,6 +9274,12 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -9104,6 +9449,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
@ -9480,6 +9834,51 @@
"node": ">=6"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -9490,7 +9889,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@ -9503,7 +9901,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
@ -9642,6 +10039,12 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
@ -9747,7 +10150,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@ -9954,6 +10356,15 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prom-client": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
@ -10037,6 +10448,47 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -10050,6 +10502,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -10060,6 +10522,45 @@
"node": ">=6"
}
},
"node_modules/puppeteer": {
"version": "24.37.2",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.2.tgz",
"integrity": "sha512-FV1W/919ve0y0oiS/3Rp5XY4MUNUokpZOH/5M4MMDfrrvh6T9VbdKvAHrAFHBuCxvluDxhjra20W7Iz6HJUcIQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.12.0",
"chromium-bidi": "13.1.1",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1566079",
"puppeteer-core": "24.37.2",
"typed-query-selector": "^2.12.0"
},
"bin": {
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/puppeteer-core": {
"version": "24.37.2",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.2.tgz",
"integrity": "sha512-nN8qwE3TGF2vA/+xemPxbesntTuqD9vCGOiZL2uh8HES3pPzLX20MyQjB42dH2rhQ3W3TljZ4ZaKZ0yX/abQuw==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.12.0",
"chromium-bidi": "13.1.1",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1566079",
"typed-query-selector": "^2.12.0",
"webdriver-bidi-protocol": "0.4.0",
"ws": "^8.19.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@ -10259,7 +10760,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -10756,6 +11256,16 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/snappy": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/snappy/-/snappy-7.3.3.tgz",
@ -10903,11 +11413,39 @@
}
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -11010,6 +11548,17 @@
"node": ">=10.0.0"
}
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -11212,6 +11761,31 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tar-fs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
@ -11314,6 +11888,15 @@
"node": "*"
}
},
"node_modules/text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@ -11706,6 +12289,12 @@
"node": ">= 0.6"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
"license": "MIT"
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -11716,7 +12305,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -11940,6 +12529,12 @@
"node": ">= 8"
}
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz",
"integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -12105,12 +12700,10 @@
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@ -12201,6 +12794,16 @@
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@ -4,7 +4,7 @@
"description": "Royal Enfield Workflow Management System - Backend API (TypeScript)",
"main": "dist/server.js",
"scripts": {
"start": "npm run build && npm run setup && npm run start:prod",
"start": "npm install && npm run build && npm run setup && npm run start:prod",
"dev": "npm run setup && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
"dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
"build": "tsc && tsc-alias",
@ -50,6 +50,7 @@
"pg": "^8.13.1",
"pg-hstore": "^2.3.4",
"prom-client": "^15.1.3",
"puppeteer": "^24.37.2",
"sequelize": "^6.37.5",
"socket.io": "^4.8.1",
"uuid": "^8.3.2",

View File

@ -75,7 +75,7 @@ export class DealerClaimController {
logger.warn('[DealerClaimController] Approver validation error:', { message: error.message });
return ResponseHandler.error(res, error.message, 400);
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[DealerClaimController] Error creating claim request:', error);
return ResponseHandler.error(res, 'Failed to create claim request', 500, errorMessage);
@ -301,7 +301,7 @@ export class DealerClaimController {
try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer,
originalName: file.originalname,
@ -360,7 +360,7 @@ export class DealerClaimController {
try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer,
originalName: file.originalname,
@ -420,7 +420,7 @@ export class DealerClaimController {
try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer,
originalName: file.originalname,
@ -480,7 +480,7 @@ export class DealerClaimController {
try {
const fileBuffer = attendanceSheetFile.buffer || (attendanceSheetFile.path ? fs.readFileSync(attendanceSheetFile.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer,
originalName: attendanceSheetFile.originalname,
@ -561,18 +561,18 @@ export class DealerClaimController {
async validateIO(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const { ioNumber } = req.query;
if (!ioNumber || typeof ioNumber !== 'string') {
return ResponseHandler.error(res, 'IO number is required', 400);
}
// Fetch IO details from SAP (will return mock data until SAP is integrated)
const ioValidation = await sapIntegrationService.validateIONumber(ioNumber.trim());
if (!ioValidation.isValid) {
return ResponseHandler.error(res, ioValidation.error || 'Invalid IO number', 400);
}
return ResponseHandler.success(res, {
ioNumber: ioValidation.ioNumber,
availableBalance: ioValidation.availableBalance,
@ -623,7 +623,7 @@ export class DealerClaimController {
}
const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0;
// Log received data for debugging
logger.info('[DealerClaimController] updateIODetails received:', {
requestId,
@ -633,7 +633,7 @@ export class DealerClaimController {
receivedBlockedAmount: blockedAmount, // Original value from request
userId,
});
// Store in database when blocking amount > 0 OR when ioNumber and ioRemark are provided (for Step 3 approval)
if (blockAmount > 0) {
if (availableBalance === undefined) {
@ -649,9 +649,9 @@ export class DealerClaimController {
blockedAmount: blockAmount,
// remainingBalance will be calculated by the service from SAP's response
};
logger.info('[DealerClaimController] Calling updateIODetails service with:', ioData);
await this.dealerClaimService.updateIODetails(
requestId,
ioData,
@ -660,7 +660,7 @@ export class DealerClaimController {
// Fetch and return the updated IO details from database
const updatedIO = await InternalOrder.findOne({ where: { requestId } });
if (updatedIO) {
return ResponseHandler.success(res, {
message: 'IO blocked successfully in SAP',
@ -755,6 +755,64 @@ export class DealerClaimController {
}
}
/**
* Download E-Invoice PDF
* GET /api/v1/dealer-claims/:requestId/e-invoice/pdf
*/
async downloadInvoicePdf(req: Request, res: Response): Promise<void> {
try {
const identifier = req.params.requestId; // Can be UUID or requestNumber
// Find workflow to get actual UUID
const workflow = await this.findWorkflowByIdentifier(identifier);
if (!workflow) {
return ResponseHandler.error(res, 'Workflow request not found', 404);
}
const requestId = (workflow as any).requestId || (workflow as any).request_id;
if (!requestId) {
return ResponseHandler.error(res, 'Invalid workflow request', 400);
}
const { ClaimInvoice } = await import('../models/ClaimInvoice');
let invoice = await ClaimInvoice.findOne({ where: { requestId } });
if (!invoice) {
return ResponseHandler.error(res, 'Invoice record not found', 404);
}
// Automatically regenerate PDF to ensure latest template/data is used (useful during testing/fixes)
try {
const { pdfService } = await import('../services/pdf.service');
await pdfService.generateInvoicePdf(requestId);
// Re-fetch invoice to get the new filePath
invoice = await ClaimInvoice.findOne({ where: { requestId } });
} catch (pdfError) {
logger.error(`[DealerClaimController] Failed to auto-regenerate PDF:`, pdfError);
// Continue with existing file if regeneration fails
}
if (!invoice || !invoice.filePath) {
return ResponseHandler.error(res, 'Invoice PDF not found', 404);
}
const filePath = path.join(process.cwd(), 'storage', 'invoices', invoice.filePath);
if (!fs.existsSync(filePath)) {
return ResponseHandler.error(res, 'Invoice file not found on server', 404);
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${invoice.filePath}"`);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[DealerClaimController] Error downloading invoice PDF:', error);
return ResponseHandler.error(res, 'Failed to download invoice PDF', 500, errorMessage);
}
}
/**
* Update credit note details (Step 8)
* PUT /api/v1/dealer-claims/:requestId/credit-note
@ -875,7 +933,7 @@ export class DealerClaimController {
// First validate IO number
const ioValidation = await sapIntegrationService.validateIONumber(ioNumber);
if (!ioValidation.isValid) {
return ResponseHandler.error(res, `Invalid IO number: ${ioValidation.error || 'IO number not found in SAP'}`, 400);
}

View File

@ -0,0 +1,17 @@
import { QueryInterface, DataTypes } from 'sequelize';
export async function up(queryInterface: QueryInterface): Promise<void> {
await queryInterface.addColumn('claim_invoices', 'pwc_response', {
type: DataTypes.JSON,
allowNull: true,
});
await queryInterface.addColumn('claim_invoices', 'irp_response', {
type: DataTypes.JSON,
allowNull: true,
});
}
export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.removeColumn('claim_invoices', 'pwc_response');
await queryInterface.removeColumn('claim_invoices', 'irp_response');
}

View File

@ -36,6 +36,8 @@ interface ClaimInvoiceAttributes {
filePath?: string | null;
qrCode?: string | null;
qrImage?: string | null;
pwcResponse?: any;
irpResponse?: any;
errorMessage?: string;
generatedAt?: Date;
description?: string;
@ -43,7 +45,7 @@ interface ClaimInvoiceAttributes {
updatedAt: Date;
}
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'dmsNumber' | 'invoiceDate' | 'irn' | 'ackNo' | 'ackDate' | 'signedInvoice' | 'signedInvoiceUrl' | 'dealerClaimNumber' | 'dealerClaimDate' | 'billingNo' | 'billingDate' | 'taxableValue' | 'cgstTotal' | 'sgstTotal' | 'igstTotal' | 'utgstTotal' | 'cessTotal' | 'tcsAmt' | 'roundOffAmt' | 'placeOfSupply' | 'totalValueInWords' | 'taxValueInWords' | 'creditNature' | 'consignorGsin' | 'gstinDate' | 'filePath' | 'qrCode' | 'qrImage' | 'errorMessage' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> { }
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'dmsNumber' | 'invoiceDate' | 'irn' | 'ackNo' | 'ackDate' | 'signedInvoice' | 'signedInvoiceUrl' | 'dealerClaimNumber' | 'dealerClaimDate' | 'billingNo' | 'billingDate' | 'taxableValue' | 'cgstTotal' | 'sgstTotal' | 'igstTotal' | 'utgstTotal' | 'cessTotal' | 'tcsAmt' | 'roundOffAmt' | 'placeOfSupply' | 'totalValueInWords' | 'taxValueInWords' | 'creditNature' | 'consignorGsin' | 'gstinDate' | 'filePath' | 'qrCode' | 'qrImage' | 'pwcResponse' | 'irpResponse' | 'errorMessage' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> { }
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
public invoiceId!: string;
@ -79,6 +81,8 @@ class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAtt
public filePath?: string | null;
public qrCode?: string | null;
public qrImage?: string | null;
public pwcResponse?: any;
public irpResponse?: any;
public errorMessage?: string;
public generatedAt?: Date;
public description?: string;
@ -261,6 +265,16 @@ ClaimInvoice.init(
allowNull: true,
field: 'qr_image'
},
pwcResponse: {
type: DataTypes.JSON,
allowNull: true,
field: 'pwc_response'
},
irpResponse: {
type: DataTypes.JSON,
allowNull: true,
field: 'irp_response'
},
errorMessage: {
type: DataTypes.TEXT,
allowNull: true,

View File

@ -87,6 +87,7 @@ router.put('/:requestId/io', authenticateToken, asyncHandler(dealerClaimControll
* @access Private
*/
router.put('/:requestId/e-invoice', authenticateToken, asyncHandler(dealerClaimController.updateEInvoice.bind(dealerClaimController)));
router.get('/:requestId/e-invoice/pdf', authenticateToken, asyncHandler(dealerClaimController.downloadInvoicePdf.bind(dealerClaimController)));
/**
* @route PUT /api/v1/dealer-claims/:requestId/credit-note

View File

@ -158,6 +158,7 @@ async function runMigrations(): Promise<void> {
const m43 = require('../migrations/20260113-redesign-dealer-claim-history');
const m44 = require('../migrations/20260123-fix-template-id-schema');
const m45 = require('../migrations/20260209-add-gst-and-pwc-fields');
const m46 = require('../migrations/20260210-add-raw-pwc-responses');
const migrations = [
{ name: '2025103000-create-users', module: m0 },
@ -208,6 +209,7 @@ async function runMigrations(): Promise<void> {
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
{ name: '20260123-fix-template-id-schema', module: m44 },
{ name: '20260209-add-gst-and-pwc-fields', module: m45 },
{ name: '20260210-add-raw-pwc-responses', module: m46 },
];
// Dynamically import sequelize after secrets are loaded

View File

@ -24,27 +24,27 @@ async function generateUniqueCode(
): Promise<string> {
let attempts = 0;
const maxAttempts = 100;
while (attempts < maxAttempts) {
// Generate random 4-digit number (1000-9999)
const randomCode = String(Math.floor(1000 + Math.random() * 9000));
// Check if code already exists in database
const existing = await Dealer.findOne({
where: {
[field]: randomCode
}
});
// Also check if we've already generated this code in this run
if (!existing && !existingCodes.has(randomCode)) {
existingCodes.add(randomCode);
return randomCode;
}
attempts++;
}
// Fallback: use timestamp-based code if random generation fails
const timestampCode = String(Date.now()).slice(-4);
logger.warn(`[Seed Test Dealer] Using timestamp-based code for ${field}: ${timestampCode}`);
@ -104,18 +104,18 @@ async function seedTestDealer(): Promise<void> {
branchDetails: null,
dealerPrincipalName: 'TEST REFLOW',
dealerPrincipalEmailId: 'testreflow@example.com',
dpContactNumber: null,
dpContacts: null,
showroomAddress: null,
showroomPincode: null,
workshopAddress: null,
workshopPincode: null,
locationDistrict: null,
stateWorkshop: null,
dpContactNumber: '9998887776',
dpContacts: 'TEST CONTACT',
showroomAddress: 'No. 335, RE Test Road, Bengaluru - 560098, Karnataka',
showroomPincode: '560098',
workshopAddress: 'Workshop Area B, Test Location',
workshopPincode: '560098',
locationDistrict: 'Bangalore',
stateWorkshop: 'Karnataka',
noOfStudios: 0,
websiteUpdate: 'Yes',
gst: null,
pan: null,
gst: '29AAACE3882D1ZZ', // Test GST
pan: 'AAACE3882D',
firmType: 'Test Firm',
propManagingPartnersDirectors: 'TEST REFLOW',
totalPropPartnersDirectors: 'TEST REFLOW',
@ -128,7 +128,7 @@ async function seedTestDealer(): Promise<void> {
if (existingDealer) {
logger.info('[Seed Test Dealer] Test dealer already exists, updating...');
// Update existing dealer
await existingDealer.update(dealerData);

View File

@ -0,0 +1,19 @@
import { pdfService } from '../services/pdf.service';
import logger from '../utils/logger';
async function main() {
const requestId = '468d45f9-2ea3-4b4d-82fc-6721b60cf8bb';
console.log(`Starting PDF generation for request: ${requestId}...`);
try {
const fileName = await pdfService.generateInvoicePdf(requestId);
console.log(`✅ Success! PDF generated: ${fileName}`);
process.exit(0);
} catch (error) {
console.error('❌ Failed to generate PDF:', error);
process.exit(1);
}
}
main();

View File

@ -30,6 +30,7 @@ export interface DealerInfo {
city?: string | null;
dealerPrincipalName?: string | null;
dealerPrincipalEmailId?: string | null;
gstin?: string | null;
}
/**
@ -106,6 +107,7 @@ export async function getAllDealers(searchTerm?: string, limit: number = 10): Pr
city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
};
});
} catch (error) {
@ -166,6 +168,7 @@ export async function getDealerByCode(dealerCode: string): Promise<DealerInfo |
city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
};
} catch (error) {
logger.error('[DealerService] Error fetching dealer by code:', error);
@ -225,6 +228,7 @@ export async function getDealerByEmail(email: string): Promise<DealerInfo | null
city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
};
} catch (error) {
logger.error('[DealerService] Error fetching dealer by email:', error);

View File

@ -28,14 +28,39 @@ import logger from '../utils/logger';
let workflowServiceInstance: any;
let approvalServiceInstance: any;
let userServiceInstance: any;
/**
* Dealer Claim Service
* Handles business logic specific to dealer claim management workflow
*/
export class DealerClaimService {
private workflowService = new WorkflowService();
private approvalService = new DealerClaimApprovalService();
private userService = new UserService();
private getWorkflowService(): WorkflowService {
if (!workflowServiceInstance) {
const { WorkflowService } = require('./workflow.service');
workflowServiceInstance = new WorkflowService();
}
return workflowServiceInstance;
}
private getApprovalService(): DealerClaimApprovalService {
if (!approvalServiceInstance) {
const { DealerClaimApprovalService } = require('./dealerClaimApproval.service');
approvalServiceInstance = new DealerClaimApprovalService();
}
return approvalServiceInstance;
}
private getUserService(): UserService {
if (!userServiceInstance) {
const { UserService } = require('./user.service');
userServiceInstance = new UserService();
}
return userServiceInstance;
}
/**
* Create a new dealer claim request
@ -101,26 +126,92 @@ export class DealerClaimService {
throw new Error('Approvers array is required. Please assign approvers for all workflow steps.');
}
// Now create workflow request (manager is validated)
// For claim management, requests are submitted immediately (not drafts)
// Step 1 will be active for dealer to submit proposal
const now = new Date();
const workflowRequest = await WorkflowRequest.create({
initiatorId: userId,
requestNumber,
templateType: 'DEALER CLAIM', // Set template type for dealer claim management
// 1. Transform approvers and ensure users exist in database
const userService = this.getUserService();
const transformedLevels = [];
// Define step names mapping
const stepNames: Record<number, string> = {
1: 'Dealer Proposal Submission',
2: 'Requestor Evaluation',
3: 'Department Lead Approval',
4: 'Dealer Completion Documents',
5: 'Requestor Claim Approval'
};
for (const a of claimData.approvers) {
let approverUserId = a.userId;
// If userId missing, ensure user exists by email
if (!approverUserId && a.email) {
try {
const user = await userService.ensureUserExists({ email: a.email });
approverUserId = user.userId;
} catch (e) {
logger.warn(`[DealerClaimService] Could not resolve user for email ${a.email}:`, e);
// If it fails, keep it empty and let the workflow service handle it (or fail early)
}
}
let tatHours = 24; // Default
if (a.tat) {
const val = typeof a.tat === 'number' ? a.tat : parseInt(a.tat as string);
tatHours = a.tatType === 'days' ? val * 24 : val;
}
// Determine level name - use mapped name or fallback to "Step X"
// Also handle "Additional Approver" case if provided
let levelName = stepNames[a.level] || `Step ${a.level}`;
// If it's an additional approver (not one of the standard steps), label it clearly
// Note: The frontend might send extra steps if approvers are added dynamically
// But for initial creation, we usually stick to the standard flow
transformedLevels.push({
levelNumber: a.level,
levelName: levelName,
approverId: approverUserId || '', // Fallback to empty string if still not resolved
approverEmail: a.email,
approverName: a.name || a.email,
tatHours: tatHours,
// New 5-step flow: Level 5 is the final approver (Requestor Claim Approval)
isFinalApprover: a.level === 5
});
}
// 2. Transform participants
const transformedParticipants = [
{
userId: userId,
userName: initiator.displayName || initiator.email,
userEmail: initiator.email,
participantType: 'INITIATOR' as any,
}
];
// Add approvers as participants
for (const level of transformedLevels) {
if (level.approverId) {
transformedParticipants.push({
userId: level.approverId,
userName: level.approverName,
userEmail: level.approverEmail,
participantType: 'APPROVER' as any
});
}
}
const workflowService = this.getWorkflowService();
const workflowRequest = await workflowService.createWorkflow(userId, {
templateType: 'DEALER CLAIM' as any,
workflowType: 'CLAIM_MANAGEMENT',
title: `${claimData.activityName} - Claim Request`,
description: claimData.requestDescription,
priority: Priority.STANDARD,
status: WorkflowStatus.PENDING, // Submitted, not draft
totalLevels: 5, // Fixed 5-step workflow for claim management (Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only)
currentLevel: 1, // Step 1: Dealer Proposal Submission
totalTatHours: 0, // Will be calculated from approval levels
isDraft: false, // Not a draft - submitted and ready for workflow
isDeleted: false,
submissionDate: now, // Set submission date for SLA tracking (required for overall SLA calculation)
});
approvalLevels: transformedLevels,
participants: transformedParticipants,
isDraft: false
} as any);
// Create claim details
await DealerClaimDetails.create({
@ -146,107 +237,9 @@ export class DealerClaimService {
currency: 'INR',
});
// Create 8 approval levels for claim management workflow from approvers array
await this.createClaimApprovalLevelsFromApprovers(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.approvers || []);
// Redundant level creation removed - handled by workflowService.createWorkflow
// Schedule TAT jobs for Step 1 (Dealer Proposal Submission) - first active step
// This ensures SLA tracking starts immediately from request creation
const { tatSchedulerService } = await import('./tatScheduler.service');
const dealerLevel = await ApprovalLevel.findOne({
where: {
requestId: workflowRequest.requestId,
levelNumber: 1 // Step 1: Dealer Proposal Submission
}
});
if (dealerLevel && dealerLevel.approverId && dealerLevel.levelStartTime) {
try {
const workflowPriority = (workflowRequest as any)?.priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs(
workflowRequest.requestId,
(dealerLevel as any).levelId,
dealerLevel.approverId,
Number(dealerLevel.tatHours || 0),
dealerLevel.levelStartTime,
workflowPriority
);
logger.info(`[DealerClaimService] TAT jobs scheduled for Step 1 (Dealer Proposal Submission) - Priority: ${workflowPriority}`);
} catch (tatError) {
logger.error(`[DealerClaimService] Failed to schedule TAT jobs for Step 1:`, tatError);
// Don't fail request creation if TAT scheduling fails
}
}
// Create participants (initiator, dealer, department lead, finance - exclude system)
await this.createClaimParticipants(workflowRequest.requestId, userId, claimData.dealerEmail);
// Get initiator details for activity logging and notifications
const initiatorName = initiator.displayName || initiator.email || 'User';
// Log creation activity
await activityService.log({
requestId: workflowRequest.requestId,
type: 'created',
user: { userId: userId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Claim request created',
details: `Claim request "${workflowRequest.title}" created by ${initiatorName} for dealer ${claimData.dealerName}`
});
// Send notification to INITIATOR confirming submission
await notificationService.sendToUsers([userId], {
title: 'Claim Request Submitted Successfully',
body: `Your claim request "${workflowRequest.title}" has been submitted successfully.`,
requestNumber: requestNumber,
requestId: workflowRequest.requestId,
url: `/request/${requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Get approval levels for notifications
// Step 1: Dealer Proposal Submission (first active step - log assignment at creation)
// Subsequent steps will have assignment logged when they become active (via approval service)
// Notify Step 1 (Dealer) - dealerLevel was already fetched above for TAT scheduling
if (dealerLevel && dealerLevel.approverId) {
// Skip notifications for system processes
const approverEmail = dealerLevel.approverEmail || '';
const isSystemProcess = approverEmail.toLowerCase() === 'system@royalenfield.com'
|| approverEmail.toLowerCase().includes('system')
|| dealerLevel.approverId === 'system'
|| dealerLevel.approverName === 'System Auto-Process';
if (!isSystemProcess) {
// Send notification to Dealer (Step 1) for proposal submission
await notificationService.sendToUsers([dealerLevel.approverId], {
title: 'New Claim Request - Proposal Required',
body: `Claim request "${workflowRequest.title}" requires your proposal submission.`,
requestNumber: requestNumber,
requestId: workflowRequest.requestId,
url: `/request/${requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
// Log assignment activity for dealer (Step 1 - first active step)
await activityService.log({
requestId: workflowRequest.requestId,
type: 'assignment',
user: { userId: userId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Assigned to dealer',
details: `Claim request assigned to dealer ${dealerLevel.approverName || dealerLevel.approverEmail || claimData.dealerName} for proposal submission`
});
} else {
logger.info(`[DealerClaimService] Skipping notification for system process: ${approverEmail} at Step 1`);
}
}
// Note: Step 2, 3, and subsequent steps will have assignment activities logged
// when they become active (when previous step is approved) via the approval service
// Redundant TAT scheduling removed - handled by workflowService.createWorkflow
logger.info(`[DealerClaimService] Created claim request: ${workflowRequest.requestNumber}`);
return workflowRequest;
@ -440,7 +433,8 @@ export class DealerClaimService {
// User doesn't exist - create from Okta
logger.info(`[DealerClaimService] User ${approver.email} not found in DB, syncing from Okta`);
try {
user = await this.userService.ensureUserExists({
const userService = this.getUserService();
user = await userService.ensureUserExists({
email: approver.email.toLowerCase(),
userId: approver.userId, // Pass Okta ID if provided (ensureUserExists will handle it)
}) as any;
@ -608,7 +602,8 @@ export class DealerClaimService {
if (!dealerUser) {
logger.info(`[DealerClaimService] Dealer ${dealerEmail} not found in DB for participants, syncing from Okta`);
try {
dealerUser = await this.userService.ensureUserExists({
const userService = this.getUserService();
dealerUser = await userService.ensureUserExists({
email: dealerEmail.toLowerCase(),
}) as any;
logger.info(`[DealerClaimService] Successfully synced dealer ${dealerEmail} from Okta for participants`);
@ -752,7 +747,8 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] Searching Okta for manager with displayName: "${managerDisplayName}"`);
// Search Okta by displayName
const oktaUsers = await this.userService.searchOktaByDisplayName(managerDisplayName);
const userService = this.getUserService();
const oktaUsers = await userService.searchOktaByDisplayName(managerDisplayName);
if (oktaUsers.length === 0) {
logger.warn(`[DealerClaimService] No reporting manager found in Okta for displayName: "${managerDisplayName}"`);
@ -768,7 +764,7 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] Found single manager match: ${managerEmail} for displayName: "${managerDisplayName}"`);
// Check if user exists in DB, create if doesn't exist
const managerUser = await this.userService.ensureUserExists({
const managerUser = await userService.ensureUserExists({
userId: oktaUser.id,
email: managerEmail,
displayName: oktaUser.profile.displayName || `${oktaUser.profile.firstName || ''} ${oktaUser.profile.lastName || ''}`.trim(),
@ -1324,7 +1320,8 @@ export class DealerClaimService {
: 'Dealer proposal submitted';
// Perform the approval action FIRST - only save snapshot if action succeeds
await this.approvalService.approveLevel(
const approvalService = this.getApprovalService();
await approvalService.approveLevel(
dealerProposalLevel.levelId,
{ action: 'APPROVE', comments: approvalComment },
actualDealerUserId || (request as any).initiatorId || 'system', // Use dealer or initiator ID
@ -1485,7 +1482,8 @@ export class DealerClaimService {
}
// Perform the approval action FIRST - only save snapshot if action succeeds
await this.approvalService.approveLevel(
const approvalService = this.getApprovalService();
await approvalService.approveLevel(
dealerCompletionLevel.levelId,
{ action: 'APPROVE', comments: approvalComment },
actualDealerUserId || (request as any).initiatorId || 'system',
@ -1905,7 +1903,7 @@ export class DealerClaimService {
const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';
// If invoice data not provided, generate via DMS
// If invoice data not provided, generate via PWC E-Invoice service
if (!invoiceData?.eInvoiceNumber) {
const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
const invoiceAmount = invoiceData?.amount
@ -1914,7 +1912,7 @@ export class DealerClaimService {
|| budgetTracking?.initialEstimatedBudget
|| 0;
const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId);
const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId, invoiceAmount);
if (!invoiceResult.success) {
throw new Error(`Failed to generate signed e-invoice via PWC: ${invoiceResult.error}`);
@ -1930,6 +1928,8 @@ export class DealerClaimService {
signedInvoice: invoiceResult.signedInvoice,
qrCode: invoiceResult.qrCode,
qrImage: invoiceResult.qrImage,
pwcResponse: invoiceResult.rawResponse,
irpResponse: invoiceResult.irpResponse,
amount: invoiceAmount,
status: 'GENERATED',
generatedAt: new Date(),
@ -1956,6 +1956,15 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
}
// Generate PDF Invoice
try {
const { pdfService } = require('./pdf.service');
await pdfService.generateInvoicePdf(requestId);
} catch (error) {
logger.error(`[DealerClaimService] Failed to generate PDF for request ${requestId}:`, error);
// Don't throw, we still want to proceed with auto-approval
}
// Check if Requestor Claim Approval is approved - if not, approve it first
// Find dynamically by levelName (handles step shifts due to additional approvers)
const approvalLevels = await ApprovalLevel.findAll({
@ -1975,34 +1984,30 @@ export class DealerClaimService {
requestorClaimLevel = approvalLevels.find((level: any) => level.levelNumber === 5);
}
// Validate that we're at the Requestor Claim Approval step before allowing DMS push
// Validate that we're at the Requestor Claim Approval step before allowing E-Invoice generation
if (requestorClaimLevel && request.currentLevel !== requestorClaimLevel.levelNumber) {
throw new Error(`Cannot push to DMS. Request is currently at step ${request.currentLevel}, but Requestor Claim Approval is at step ${requestorClaimLevel.levelNumber}. Please complete all previous steps first.`);
throw new Error(`Cannot generate E-Invoice. Request is currently at step ${request.currentLevel}, but Requestor Claim Approval is at step ${requestorClaimLevel.levelNumber}. Please complete all previous steps first.`);
}
if (requestorClaimLevel && requestorClaimLevel.status !== ApprovalStatus.APPROVED) {
logger.info(`[DealerClaimService] Requestor Claim Approval not approved yet. Auto-approving for request ${requestId}`);
// Auto-approve Requestor Claim Approval
await this.approvalService.approveLevel(
// E-Invoice Generation is successful - auto-approve the Requestor Claim Approval step
if (requestorClaimLevel && requestorClaimLevel.status !== 'APPROVED') {
const approvalService = this.getApprovalService();
await approvalService.approveLevel(
requestorClaimLevel.levelId,
{ action: 'APPROVE', comments: 'Auto-approved when pushing to DMS. E-Invoice generation will be logged as activity.' },
'system',
{ ipAddress: null, userAgent: 'System Auto-Process' }
{ action: 'APPROVE', comments: 'Auto-approved after successful E-Invoice generation' },
'system'
);
logger.info(`[DealerClaimService] Requestor Claim Approval approved. E-Invoice generation will be logged as activity when DMS webhook is received.`);
} else {
// Requestor Claim Approval already approved
logger.info(`[DealerClaimService] Requestor Claim Approval already approved. E-Invoice generation will be logged as activity when DMS webhook is received.`);
logger.info(`[DealerClaimService] Step "${requestorClaimLevel.levelName}" auto-approved after E-Invoice generation for request ${requestId}`);
}
// Log E-Invoice generation as activity (no approval level needed)
// Log E-Invoice generation as activity
await activityService.log({
requestId,
type: 'status_change',
user: { userId: 'system', name: 'System Auto-Process' },
timestamp: new Date().toISOString(),
action: 'E-Invoice Generation Initiated',
details: `E-Invoice generation initiated via DMS integration for request ${requestNumber}. Waiting for DMS webhook confirmation.`,
action: 'E-Invoice Generated',
details: `E-Invoice generated via PWC integration for request ${requestNumber}. Step "${requestorClaimLevel?.levelName || 'Requestor Claim Approval'}" auto-approved.`,
});
} catch (error) {
logger.error('[DealerClaimService] Error updating e-invoice details:', error);
@ -2012,7 +2017,7 @@ export class DealerClaimService {
/**
* Log E-Invoice Generation as activity (no longer an approval step)
* This method logs the e-invoice generation activity when invoice is generated via DMS webhook
* This method logs the e-invoice generation activity when invoice is generated via PWC service
*/
async logEInvoiceGenerationActivity(requestId: string, invoiceNumber?: string): Promise<void> {
try {
@ -2040,7 +2045,7 @@ export class DealerClaimService {
user: { userId: 'system', name: 'System Auto-Process' },
timestamp: new Date().toISOString(),
action: 'E-Invoice Generated',
details: `E-Invoice generated via DMS. Invoice Number: ${finalInvoiceNumber}. Request: ${requestNumber}`,
details: `E-Invoice generated via PWC. Invoice Number: ${finalInvoiceNumber}. Request: ${requestNumber}`,
});
logger.info(`[DealerClaimService] E-Invoice Generation activity logged for request ${requestId} (Invoice: ${finalInvoiceNumber})`);
@ -2418,7 +2423,7 @@ export class DealerClaimService {
category: 'SUPPORTING',
isDeleted: false
},
order: [['createdAt', 'DESC']]
order: [['uploadedAt', 'DESC']]
});
const snapshotData = {
@ -2503,7 +2508,7 @@ export class DealerClaimService {
category: 'SUPPORTING',
isDeleted: false
},
order: [['createdAt', 'DESC']]
order: [['uploadedAt', 'DESC']]
});
// Store all completion data in JSONB

View File

@ -25,10 +25,17 @@ import { tatSchedulerService } from './tatScheduler.service';
import { DealerClaimService } from './dealerClaim.service';
import { emitToRequestRoom } from '../realtime/socket';
let dealerClaimServiceInstance: any;
export class DealerClaimApprovalService {
// Use lazy initialization to avoid circular dependency
private getDealerClaimService(): DealerClaimService {
return new DealerClaimService();
if (!dealerClaimServiceInstance) {
const { DealerClaimService } = require('./dealerClaim.service');
dealerClaimServiceInstance = new DealerClaimService();
}
return dealerClaimServiceInstance;
}
/**
* Approve a level in a dealer claim workflow
@ -331,7 +338,7 @@ export class DealerClaimApprovalService {
// Activity Creation is now an activity log only - process it automatically
logger.info(`[DealerClaimApproval] Department Lead approved. Processing Activity Creation as activity log.`);
try {
const dealerClaimService = new DealerClaimService();
const dealerClaimService = this.getDealerClaimService();
await dealerClaimService.processActivityCreation(level.requestId);
logger.info(`[DealerClaimApproval] Activity Creation activity logged for request ${level.requestId}`);
} catch (activityError) {

View File

@ -508,7 +508,7 @@ export class DMSWebhookService {
// E-Invoice Generation is now an activity log only, not an approval step
// Log the activity using the dealerClaimService
const { DealerClaimService } = await import('./dealerClaim.service');
const { DealerClaimService } = require('./dealerClaim.service');
const dealerClaimService = new DealerClaimService();
const invoice = await ClaimInvoice.findOne({ where: { requestId } });
const invoiceNumber = invoice?.invoiceNumber || 'N/A';

206
src/services/pdf.service.ts Normal file
View File

@ -0,0 +1,206 @@
import puppeteer from 'puppeteer';
import { WorkflowRequest } from '@models/WorkflowRequest';
import { ClaimInvoice } from '@models/ClaimInvoice';
import { DealerClaimDetails } from '@models/DealerClaimDetails';
import { DealerProposalDetails } from '@models/DealerProposalDetails';
import { findDealerLocally } from './dealer.service';
import path from 'path';
import fs from 'fs';
import logger from '@utils/logger';
import dayjs from 'dayjs';
export class PdfService {
private storagePath = path.join(process.cwd(), 'storage', 'invoices');
constructor() {
if (!fs.existsSync(this.storagePath)) {
fs.mkdirSync(this.storagePath, { recursive: true });
}
}
async generateInvoicePdf(requestId: string): Promise<string> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
const request = await WorkflowRequest.findByPk(requestId);
const invoice = await ClaimInvoice.findOne({ where: { requestId } });
const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
const dealer = await findDealerLocally(claimDetails?.dealerCode, claimDetails?.dealerEmail);
if (!request || !invoice) {
throw new Error('Request or Invoice not found');
}
const htmlContent = this.getInvoiceHtmlTemplate({
request,
invoice,
claimDetails,
proposalDetails,
dealer
});
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
const fileName = `invoice_${requestId}_${Date.now()}.pdf`;
const filePath = path.join(this.storagePath, fileName);
await page.pdf({
path: filePath,
format: 'A4',
printBackground: true,
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' }
});
await invoice.update({ filePath: fileName });
return fileName;
} catch (error) {
logger.error(`[PdfService] Error generating PDF for request ${requestId}:`, error);
throw error;
} finally {
await browser.close();
}
}
private getInvoiceHtmlTemplate(data: any): string {
const { request, invoice, dealer, claimDetails } = data;
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
const logoUrl = 'https://www.royalenfield.com/content/dam/royal-enfield/india/logos/logo.png';
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; color: #333; }
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
.logo { height: 40px; }
.qr-code { width: 150px; height: 150px; }
.irn-details { font-size: 11px; margin-top: 10px; }
.irn-details div { margin-bottom: 4px; }
.title { text-align: center; font-size: 24px; font-weight: bold; margin: 30px 0; border-top: 1px solid #ccc; padding-top: 20px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-bottom: 30px; font-size: 12px; }
.info-section h3 { font-size: 14px; margin-bottom: 10px; }
.info-row { display: flex; margin-bottom: 5px; }
.info-label { width: 120px; font-weight: bold; }
.info-value { flex: 1; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 10px; }
th, td { border: 1px solid #333; padding: 6px; text-align: left; }
th { background-color: #f2f2f2; }
.totals { margin-top: 20px; width: 300px; margin-left: auto; font-size: 12px; }
.totals-row { display: flex; justify-content: space-between; padding: 5px 0; }
.totals-row.grand-total { border-top: 2px solid #333; font-weight: bold; margin-top: 10px; }
.words { margin-top: 30px; font-size: 11px; font-style: italic; }
.footer { margin-top: 60px; text-align: right; font-size: 12px; }
.signature { margin-top: 40px; border-top: 1px solid #333; width: 200px; margin-left: auto; text-align: center; padding-top: 5px; }
</style>
</head>
<body>
<div class="header">
<div>
<img src="${logoUrl}" class="logo" />
<div class="irn-details">
<div><strong>IRN No :</strong> ${invoice.irn || 'N/A'}</div>
<div><strong>Ack No :</strong> ${invoice.ackNo || 'N/A'}</div>
<div><strong>Ack Date & Time :</strong> ${invoice.ackDate ? dayjs(invoice.ackDate).format('YYYY-MM-DD HH:mm:ss') : 'N/A'}</div>
</div>
</div>
<img src="${qrImage}" class="qr-code" />
</div>
<div class="title">WARRANTY CLAIM TAX INVOICE</div>
<div class="info-grid">
<div class="info-section">
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">33AAACE3882D1ZZ</div></div>
<div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div>
<br/>
<div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div>
<div class="info-row"><div class="info-label">Invoice No.</div><div class="info-value">${invoice.invoiceNumber || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Invoice Date</div><div class="info-value">${invoice.invoiceDate ? dayjs(invoice.invoiceDate).format('DD-MM-YYYY') : 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Chassis No.</div><div class="info-value">N/A</div></div>
</div>
<div class="info-section">
<div class="info-row"><div class="info-label">Dealer</div><div class="info-value">${dealer?.dealerName || claimDetails?.dealerName || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Store</div><div class="info-value">${dealer?.dealerName || claimDetails?.dealerName || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Supplier GSTIN</div><div class="info-value">${dealer?.gstin || 'N/A'}</div></div>
<br/>
<div class="info-row"><div class="info-label">POS</div><div class="info-value">${dealer?.state || dealer?.city || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Claim Invoice No.</div><div class="info-value">${request.requestNumber || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Claim Invoice Date</div><div class="info-value">${dayjs().format('DD-MM-YYYY')}</div></div>
<div class="info-row"><div class="info-label">Job Card No.</div><div class="info-value">N/A</div></div>
<div class="info-row"><div class="info-label">KMS.Reading</div><div class="info-value">N/A</div></div>
<div class="info-row"><div class="info-label">Last Approval Date</div><div class="info-value">${invoice.generatedAt ? dayjs(invoice.generatedAt).format('DD-MM-YYYY') : 'N/A'}</div></div>
</div>
</div>
<table>
<thead>
<tr>
<th>Part</th>
<th>Description</th>
<th>HSN/SAC</th>
<th>Qty</th>
<th>Rate</th>
<th>Discount</th>
<th>UOM</th>
<th>Taxable Value</th>
<th>IGST %</th>
<th>IGST</th>
<th>CGST %</th>
<th>CGST</th>
<th>SGST %</th>
<th>SGST</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>CLAIM</td>
<td>${request.title || 'Warranty Claim'}</td>
<td>998881</td>
<td>1.00</td>
<td>${Number(invoice.amount || 0).toFixed(2)}</td>
<td>0.00</td>
<td>EA</td>
<td>${Number(invoice.amount || 0).toFixed(2)}</td>
<td>18.00</td>
<td>${(Number(invoice.amount || 0) * 0.18).toFixed(2)}</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00</td>
<td>${(Number(invoice.amount || 0) * 1.18).toFixed(2)}</td>
</tr>
</tbody>
</table>
<div class="totals">
<div class="totals-row"><span>TOTAL:</span><span>${Number(invoice.amount || 0).toFixed(2)}</span></div>
<div class="totals-row"><span>TOTAL TAX:</span><span>${(Number(invoice.amount || 0) * 0.18).toFixed(2)}</span></div>
<div class="totals-row grand-total"><span>GRAND TOTAL:</span><span>${(Number(invoice.amount || 0) * 1.18).toFixed(2)}</span></div>
</div>
<div class="words">
<div><strong>TOTAL VALUE IN WORDS:</strong> Rupees ${invoice.totalValueInWords || 'N/A'}</div>
<div><strong>TOTAL TAX IN WORDS:</strong> Rupees ${invoice.taxValueInWords || 'N/A'}</div>
</div>
<div class="footer">
<p>For ${dealer?.name || 'Authorised Dealer'}</p>
<div class="signature">Authorised signatory</div>
</div>
</body>
</html>
`;
}
}
export const pdfService = new PdfService();

View File

@ -5,6 +5,8 @@ import { ActivityType } from '../models/ActivityType';
import { WorkflowRequest } from '../models/WorkflowRequest';
import { ClaimInvoice } from '../models/ClaimInvoice';
import { InternalOrder } from '../models/InternalOrder';
import { User } from '../models/User';
import { DealerClaimDetails } from '../models/DealerClaimDetails';
/**
* PWC E-Invoice Integration Service
@ -12,13 +14,13 @@ import { InternalOrder } from '../models/InternalOrder';
*/
export class PWCIntegrationService {
private apiUrl: string;
private appKey: string;
private appSecret: string;
private customerId: string;
private token: string;
constructor() {
this.apiUrl = process.env.PWC_API_URL || 'https://api.qa.einvoice.aw.navigatetax.pwc.co.in';
this.appKey = process.env.PWC_APP_KEY || '';
this.appSecret = process.env.PWC_APP_SECRET || '';
this.apiUrl = process.env.PWC_API_URL || 'https://api.qa.einvoice.aw.navigatetax.pwc.co.in/qa/v1/en/push';
this.customerId = process.env.PWC_CUSTOMER_ID || '';
this.token = process.env.PWC_TOKEN || '';
}
/**
@ -43,7 +45,7 @@ export class PWCIntegrationService {
/**
* Generate Signed Invoice via PWC API
*/
async generateSignedInvoice(requestId: string): Promise<{
async generateSignedInvoice(requestId: string, amount?: number): Promise<{
success: boolean;
irn?: string;
ackNo?: string;
@ -51,75 +53,155 @@ export class PWCIntegrationService {
signedInvoice?: string;
qrCode?: string;
qrImage?: string;
rawResponse?: any;
irpResponse?: any;
error?: string;
}> {
try {
const request = await WorkflowRequest.findByPk(requestId, {
include: ['claimDetails', 'initiator']
include: [{ model: User, as: 'initiator' }, { model: DealerClaimDetails, as: 'claimDetails' }]
});
if (!request) return { success: false, error: 'Request not found' };
const dealer = await Dealer.findOne({ where: { dlrcode: (request as any).claimDetails?.dealerCode } });
const activity = await ActivityType.findOne({ where: { title: (request as any).claimDetails?.activityType } });
const claimDetails = (request as any).claimDetails;
const dealer = await Dealer.findOne({ where: { dlrcode: claimDetails?.dealerCode } });
const activity = await ActivityType.findOne({ where: { title: claimDetails?.activityType } });
if (!dealer || !activity) {
return { success: false, error: 'Dealer or Activity details missing' };
}
// Construct PWC Payload (keeping existing logic for now)
const payload = {
UserGstin: "33AAACE3882D1ZZ",
DocDtls: {
Typ: "INV",
No: `INV-${Date.now()}`,
Dt: new Date().toLocaleDateString('en-GB').replace(/\//g, '-')
},
SellerDtls: {
Gstin: dealer.gst || "33AAACE3882D1ZZ",
LglNm: dealer.dealership || 'Dealer',
Addr1: dealer.showroomAddress || "Address Line 1",
Loc: dealer.location || "Location",
Pin: 600001,
Stcd: "33"
},
BuyerDtls: {
Gstin: "33AAACE3882D1ZZ",
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
Addr1: "No. 2, Thiruvottiyur High Road",
Loc: "Thiruvottiyur",
Pin: 600019,
Stcd: "33",
Pos: "33"
},
ItemList: [
{
SlNo: "1",
PrdDesc: activity.title,
IsServc: "Y",
HsnCd: activity.hsnCode || activity.sacCode || "9983",
Qty: 1,
Unit: "OTH",
UnitPrce: (request as any).amount,
TotAmt: (request as any).amount,
GstRt: activity.gstRate || 18,
AssAmt: (request as any).amount,
IgstAmt: activity.gstRate === 18 ? ((request as any).amount * 0.18) : 0,
TotItemVal: (request as any).amount * 1.18
// Fallback for amount if not provided
const finalAmount = Number(amount || (request as any).amount || 0);
// Helper to format number to 2 decimal places
const formatAmount = (val: number) => Number(val.toFixed(2));
// Extract State Code from Dealer GSTIN
let dealerGst = (dealer as any).gst;
// HOTFIX: For PWC QA Environment, use a known valid GSTIN if dealer has the invalid test one
// The test GSTIN 29AAACE3882D1ZZ is not registered in PWC QA Master, causing Error 701
const isQA = this.apiUrl.includes('qa');
const invalidTestGst = '29AAACE3882D1ZZ';
const validQaGst = '24AAAPI3182M002'; // Registered in PWC QA
if (isQA && (!dealerGst || dealerGst === invalidTestGst)) {
dealerGst = validQaGst;
}
// Final fallback if still empty
dealerGst = dealerGst || validQaGst;
let dealerStateCode = "24"; // Default fallback (Gujarat for 24...)
// Try to extract from GSTIN (first 2 chars)
if (dealerGst && dealerGst.length >= 2 && !isNaN(Number(dealerGst.substring(0, 2)))) {
dealerStateCode = dealerGst.substring(0, 2);
} else if ((dealer as any).stateCode) {
dealerStateCode = (dealer as any).stateCode;
}
// Calculate tax amounts
const gstRate = Number(activity.gstRate || 18);
const isIGST = dealerStateCode !== "33"; // If dealer state != Buyer state (33), it's IGST
const assAmt = finalAmount;
const igstAmt = isIGST ? (finalAmount * (gstRate / 100)) : 0;
const cgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
const sgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
const totalTax = igstAmt + cgstAmt + sgstAmt;
const totalItemVal = finalAmount + totalTax;
// Construct PWC Payload - Aligned with sample format provided by user
const payload = [
{
User_GSTIN: dealerGst, // Use Dealer GSTIN as User_GSTIN to ensure match
Version: "1.01",
IRN: "",
SourceSystem: "RE_WORKFLOW",
is_irn: "Y",
is_ewb: "N",
email: (request as any).initiator?.email || "system@royalenfield.com",
TranDtls: {
TaxSch: "GST",
SubType: "SUPPLY",
SubTypeDescription: "Others",
SupTyp: "B2B",
RegRev: "N",
Typ: "REG",
DiffPercentage: "0",
Taxability: "Taxable",
InterIntra: isIGST ? "Inter" : "Intra",
CancelFlag: "N"
},
DocDtls: {
Typ: "Inv",
No: (request as any).requestNumber || `INV-${Date.now()}`,
Dt: new Date().toLocaleDateString('en-GB') // DD/MM/YYYY
},
SellerDtls: {
Gstin: dealerGst,
LglNm: (dealer as any).dealership || 'Dealer',
TrdNm: (dealer as any).dealership || 'Dealer',
Addr1: (dealer as any).showroomAddress || "Address Line 1",
Loc: (dealer as any).location || "Location",
Pin: (dealerGst === validQaGst && String((dealer as any).showroomPincode || '').substring(0, 1) !== '3')
? 380001
: Number((dealer as any).showroomPincode) || 600001,
Stcd: dealerStateCode,
Ph: (dealer as any).dpContactNumber || "9998887776",
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
},
BuyerDtls: {
Gstin: "33AAACE3882D1ZZ", // Royal Enfield GST
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
TrdNm: "ROYAL ENFIELD",
Addr1: "No. 2, Thiruvottiyur High Road",
Loc: "Thiruvottiyur",
Pin: 600019,
Stcd: "33",
Pos: "33"
},
ItemList: [
{
SlNo: "1",
PrdNm: activity.title,
PrdDesc: activity.title,
HsnCd: activity.hsnCode || activity.sacCode || "9983",
IsServc: "Y",
Qty: 1,
Unit: "OTH",
UnitPrice: formatAmount(finalAmount), // Ensure number
TotAmt: formatAmount(finalAmount), // Ensure number
AssAmt: formatAmount(assAmt), // Ensure number
GstRt: gstRate,
IgstAmt: formatAmount(igstAmt),
CgstAmt: formatAmount(cgstAmt),
SgstAmt: formatAmount(sgstAmt),
TotItemVal: formatAmount(totalItemVal)
}
],
ValDtls: {
AssVal: formatAmount(assAmt),
IgstVal: formatAmount(igstAmt),
CgstVal: formatAmount(cgstAmt),
SgstVal: formatAmount(sgstAmt),
TotInvVal: formatAmount(totalItemVal)
}
],
ValDtls: {
AssVal: (request as any).amount,
IgstVal: activity.gstRate === 18 ? ((request as any).amount * 0.18) : 0,
TotInvVal: (request as any).amount * 1.18
}
};
];
logger.info(`[PWC] Sending e-invoice request for ${request.requestNumber}`);
const response = await axios.post(`${this.apiUrl}/generate`, payload, {
headers: { 'AppKey': this.appKey, 'AppSecret': this.appSecret }
const response = await axios.post(this.apiUrl, payload, {
headers: {
'customerid': this.customerId,
'token': this.token
}
});
console.log('PWC Response:', JSON.stringify(response.data));
// Parse PWC Response based on provided structure
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]
@ -146,9 +228,25 @@ export class PWCIntegrationService {
}
if (!irn) {
const errorMsg = responseData?.irp_response?.message || 'E-Invoice generation failed';
logger.error(`[PWC] E-Invoice failed for ${request.requestNumber}: ${errorMsg}`);
return { success: false, error: errorMsg };
const mainMsg = responseData?.pwc_response?.message || responseData?.irp_response?.message || 'E-Invoice generation failed';
// Extract detailed error messages from irp_response.data.error_details
const errorDetails = responseData?.irp_response?.data?.error_details;
let errorMessage = mainMsg;
if (Array.isArray(errorDetails) && errorDetails.length > 0) {
const detailsStr = errorDetails.map((e: any) => `${e.ErrorCode}: ${e.ErrorMessage}`).join('; ');
errorMessage = `${mainMsg} - [${detailsStr}]`;
}
// Also check validation remarks
const validationRemarks = responseData?.pwc_response?.validation_remarks;
if (validationRemarks && Object.keys(validationRemarks).length > 0) {
errorMessage += ` - Validation: ${JSON.stringify(validationRemarks)}`;
}
logger.error(`[PWC] E-Invoice failed for ${request.requestNumber}: ${errorMessage}`);
return { success: false, error: errorMessage };
}
return {
@ -158,7 +256,9 @@ export class PWCIntegrationService {
ackDate: ackDate ? new Date(ackDate) : undefined,
signedInvoice,
qrCode,
qrImage: qrB64
qrImage: qrB64,
rawResponse: responseData?.pwc_response,
irpResponse: responseData?.irp_response
};
} catch (error) {

View File

@ -34,6 +34,12 @@ export class SAPIntegrationService {
* Check if SAP integration is configured
*/
private isConfigured(): boolean {
// Check if SAP bypass is explicitly enabled
if (process.env.SAP_BYPASS === 'true') {
logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable');
return false;
}
return !!this.sapBaseUrl && !!this.sapUsername && !!this.sapPassword;
}

View File

@ -2459,6 +2459,7 @@ export class WorkflowService {
requestNumber,
initiatorId,
templateType: workflowData.templateType,
workflowType: workflowData.workflowType || 'NON_TEMPLATIZED',
title: workflowData.title ? sanitizeHtml(workflowData.title) : workflowData.title,
description: workflowData.description ? sanitizeHtml(workflowData.description) : workflowData.description,
priority: workflowData.priority,
@ -2471,29 +2472,29 @@ export class WorkflowService {
submissionDate: isDraftRequested ? undefined : now
});
// Create approval levels
for (const levelData of workflowData.approvalLevels) {
await ApprovalLevel.create({
requestId: workflow.requestId,
levelNumber: levelData.levelNumber,
levelName: levelData.levelName,
approverId: levelData.approverId,
approverEmail: levelData.approverEmail,
approverName: levelData.approverName,
tatHours: levelData.tatHours,
// tatDays is auto-calculated by database as a generated column
status: ApprovalStatus.PENDING,
elapsedHours: 0,
remainingHours: levelData.tatHours,
tatPercentageUsed: 0,
isFinalApprover: levelData.isFinalApprover || false
});
// Create approval levels if skipCreation is false
if (!workflowData.skipCreation) {
for (const levelData of workflowData.approvalLevels) {
await ApprovalLevel.create({
requestId: workflow.requestId,
levelNumber: levelData.levelNumber,
levelName: levelData.levelName,
approverId: levelData.approverId,
approverEmail: levelData.approverEmail,
approverName: levelData.approverName,
tatHours: levelData.tatHours,
// tatDays is auto-calculated by database as a generated column
status: ApprovalStatus.PENDING,
elapsedHours: 0,
remainingHours: levelData.tatHours,
tatPercentageUsed: 0,
isFinalApprover: levelData.isFinalApprover || false
});
}
}
// Create participants if provided
// Deduplicate participants by userId (database has unique constraint on request_id + user_id)
// Priority: INITIATOR > APPROVER > SPECTATOR (keep the highest privilege role)
if (workflowData.participants) {
// Create participants if provided and skipCreation is false
if (workflowData.participants && !workflowData.skipCreation) {
const participantMap = new Map<string, typeof workflowData.participants[0]>();
const rolePriority: Record<string, number> = {
'INITIATOR': 3,
@ -2568,13 +2569,15 @@ export class WorkflowService {
}
return workflow;
} catch (error) {
} catch (error: any) {
logWithContext('error', 'Failed to create workflow', {
userId: initiatorId,
priority: workflowData.priority,
error,
errorMessage: error.message,
errorStack: error.stack,
error: error, // Keep for full object in some loggers
});
throw new Error('Failed to create workflow');
throw new Error(`Failed to create workflow: ${error.message}`);
}
}

View File

@ -4,7 +4,8 @@ export interface WorkflowRequest {
requestId: string;
requestNumber: string;
initiatorId: string;
templateType: 'CUSTOM' | 'TEMPLATE';
templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
workflowType?: string;
title: string;
description: string;
priority: Priority;
@ -23,13 +24,15 @@ export interface WorkflowRequest {
}
export interface CreateWorkflowRequest {
templateType: 'CUSTOM' | 'TEMPLATE';
templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
workflowType?: string;
title: string;
description: string;
priority: Priority;
approvalLevels: CreateApprovalLevel[];
participants?: CreateParticipant[];
isDraft?: boolean;
skipCreation?: boolean; // Flag to skip record creation if handled manually by calling service
}
export interface UpdateWorkflowRequest {

View File

@ -20,8 +20,9 @@ const SENSITIVE_PATTERN = new RegExp(
/**
* Mask sensitive values in strings (API keys, passwords, tokens)
* Uses a WeakSet to prevent infinite recursion on circular objects
*/
const maskSensitiveData = (value: any): any => {
const maskSensitiveData = (value: any, visited = new WeakSet()): any => {
if (typeof value === 'string') {
// Mask patterns like "API_KEY = abc123" or "password: secret"
let masked = value.replace(SENSITIVE_PATTERN, (match, key, val) => {
@ -45,10 +46,24 @@ const maskSensitiveData = (value: any): any => {
}
if (Array.isArray(value)) {
return value.map(maskSensitiveData);
if (visited.has(value)) return '[Circular]';
visited.add(value);
return value.map(item => maskSensitiveData(item, visited));
}
if (value && typeof value === 'object') {
// Special handling for common non-recursive objects to improve performance
if (value instanceof Date || value instanceof RegExp) {
return value;
}
if (visited.has(value)) return '[Circular]';
visited.add(value);
// Prevent deep recursion into huge circular objects like Sequelize instances, Request, Response
// If it looks like a Sequelize instance or complex object, we might want to be careful
// but visited.has() should handle it.
const masked: any = {};
for (const [k, v] of Object.entries(value)) {
const keyLower = k.toLowerCase();
@ -56,7 +71,7 @@ const maskSensitiveData = (value: any): any => {
if (SENSITIVE_KEYS.some(sk => keyLower.includes(sk))) {
masked[k] = typeof v === 'string' && v.length > 0 ? '***REDACTED***' : v;
} else {
masked[k] = maskSensitiveData(v);
masked[k] = maskSensitiveData(v, visited);
}
}
return masked;