Compare commits

..

2 Commits

33 changed files with 1661 additions and 643 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}; 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-D5monZ70.js.map //# 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 --> <!-- Preload critical fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-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/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.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/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js"> <link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js"> <link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
<link rel="stylesheet" crossorigin href="/assets/index-D5NCgjQR.css"> <link rel="stylesheet" crossorigin href="/assets/index-AUbBsmWB.css">
</head> </head>
<body> <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": "^8.13.1",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"puppeteer": "^24.37.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@ -809,7 +810,6 @@
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1",
@ -981,7 +981,6 @@
"version": "7.28.5", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -2879,6 +2878,27 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause" "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": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -3511,6 +3531,12 @@
"node": ">= 10" "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": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@ -4043,6 +4069,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2", "version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@ -4446,7 +4482,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/array-flatten": { "node_modules/array-flatten": {
@ -4493,6 +4528,18 @@
"safer-buffer": "^2.1.0" "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": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -4544,6 +4591,20 @@
"proxy-from-env": "^1.1.0" "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": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -4676,6 +4737,97 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "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": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -4733,6 +4885,15 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "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": { "node_modules/bcryptjs": {
"version": "2.4.3", "version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@ -4916,6 +5077,15 @@
"node": ">= 0.4.0" "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": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "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", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -5111,6 +5280,19 @@
"node": ">= 6" "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": { "node_modules/ci-info": {
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@ -5434,6 +5616,32 @@
"node": ">= 0.10" "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": { "node_modules/create-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -5559,6 +5767,20 @@
"node": ">=0.10.0" "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -5616,6 +5838,12 @@
"node": ">=8" "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": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "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": { "node_modules/error-ex": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-arrayish": "^0.2.1" "is-arrayish": "^0.2.1"
@ -5979,6 +6215,27 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/eslint": {
"version": "9.38.0", "version": "9.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
@ -6151,7 +6408,6 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"bin": { "bin": {
"esparse": "bin/esparse.js", "esparse": "bin/esparse.js",
@ -6191,7 +6447,6 @@
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
@ -6201,7 +6456,6 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -6225,6 +6479,15 @@
"node": ">=6" "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": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -6357,6 +6620,41 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT" "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": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -6364,6 +6662,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@ -6453,6 +6757,15 @@
"bser": "2.1.1" "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": { "node_modules/fecha": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@ -6879,6 +7192,29 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "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": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -7454,7 +7790,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"parent-module": "^1.0.0", "parent-module": "^1.0.0",
@ -7555,6 +7890,15 @@
"url": "https://opencollective.com/ioredis" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -7568,7 +7912,6 @@
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
@ -8402,14 +8745,12 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@ -8451,7 +8792,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "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==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
@ -8612,7 +8952,6 @@
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
@ -8935,6 +9274,12 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -9104,6 +9449,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
@ -9480,6 +9834,51 @@
"node": ">=6" "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": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "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", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"callsites": "^3.0.0" "callsites": "^3.0.0"
@ -9503,7 +9901,6 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
@ -9642,6 +10039,12 @@
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" "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": { "node_modules/pg": {
"version": "8.16.3", "version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
@ -9747,7 +10150,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@ -9954,6 +10356,15 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT" "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": { "node_modules/prom-client": {
"version": "15.1.3", "version": "15.1.3",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
@ -10037,6 +10448,47 @@
"node": ">= 0.10" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -10050,6 +10502,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -10060,6 +10522,45 @@
"node": ">=6" "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": { "node_modules/pure-rand": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@ -10259,7 +10760,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -10756,6 +11256,16 @@
"node": ">=8" "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": { "node_modules/snappy": {
"version": "7.3.3", "version": "7.3.3",
"resolved": "https://registry.npmjs.org/snappy/-/snappy-7.3.3.tgz", "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "devOptional": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -11010,6 +11548,17 @@
"node": ">=10.0.0" "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": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -11212,6 +11761,31 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/tdigest": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
@ -11314,6 +11888,15 @@
"node": "*" "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": { "node_modules/text-hex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@ -11706,6 +12289,12 @@
"node": ">= 0.6" "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": { "node_modules/typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -11716,7 +12305,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -11940,6 +12529,12 @@
"node": ">= 8" "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": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -12105,12 +12700,10 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.3", "version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@ -12201,6 +12794,16 @@
"node": ">=12" "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": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "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)", "description": "Royal Enfield Workflow Management System - Backend API (TypeScript)",
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "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": "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", "dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
"build": "tsc && tsc-alias", "build": "tsc && tsc-alias",
@ -50,6 +50,7 @@
"pg": "^8.13.1", "pg": "^8.13.1",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"puppeteer": "^24.37.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View File

@ -75,7 +75,7 @@ export class DealerClaimController {
logger.warn('[DealerClaimController] Approver validation error:', { message: error.message }); logger.warn('[DealerClaimController] Approver validation error:', { message: error.message });
return ResponseHandler.error(res, error.message, 400); return ResponseHandler.error(res, error.message, 400);
} }
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[DealerClaimController] Error creating claim request:', error); logger.error('[DealerClaimController] Error creating claim request:', error);
return ResponseHandler.error(res, 'Failed to create claim request', 500, errorMessage); return ResponseHandler.error(res, 'Failed to create claim request', 500, errorMessage);
@ -301,7 +301,7 @@ export class DealerClaimController {
try { try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from('')); const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({ const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer, buffer: fileBuffer,
originalName: file.originalname, originalName: file.originalname,
@ -360,7 +360,7 @@ export class DealerClaimController {
try { try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from('')); const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({ const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer, buffer: fileBuffer,
originalName: file.originalname, originalName: file.originalname,
@ -420,7 +420,7 @@ export class DealerClaimController {
try { try {
const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from('')); const fileBuffer = file.buffer || (file.path ? fs.readFileSync(file.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({ const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer, buffer: fileBuffer,
originalName: file.originalname, originalName: file.originalname,
@ -480,7 +480,7 @@ export class DealerClaimController {
try { try {
const fileBuffer = attendanceSheetFile.buffer || (attendanceSheetFile.path ? fs.readFileSync(attendanceSheetFile.path) : Buffer.from('')); const fileBuffer = attendanceSheetFile.buffer || (attendanceSheetFile.path ? fs.readFileSync(attendanceSheetFile.path) : Buffer.from(''));
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex'); const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const uploadResult = await gcsStorageService.uploadFileWithFallback({ const uploadResult = await gcsStorageService.uploadFileWithFallback({
buffer: fileBuffer, buffer: fileBuffer,
originalName: attendanceSheetFile.originalname, originalName: attendanceSheetFile.originalname,
@ -561,18 +561,18 @@ export class DealerClaimController {
async validateIO(req: AuthenticatedRequest, res: Response): Promise<void> { async validateIO(req: AuthenticatedRequest, res: Response): Promise<void> {
try { try {
const { ioNumber } = req.query; const { ioNumber } = req.query;
if (!ioNumber || typeof ioNumber !== 'string') { if (!ioNumber || typeof ioNumber !== 'string') {
return ResponseHandler.error(res, 'IO number is required', 400); return ResponseHandler.error(res, 'IO number is required', 400);
} }
// Fetch IO details from SAP (will return mock data until SAP is integrated) // Fetch IO details from SAP (will return mock data until SAP is integrated)
const ioValidation = await sapIntegrationService.validateIONumber(ioNumber.trim()); const ioValidation = await sapIntegrationService.validateIONumber(ioNumber.trim());
if (!ioValidation.isValid) { if (!ioValidation.isValid) {
return ResponseHandler.error(res, ioValidation.error || 'Invalid IO number', 400); return ResponseHandler.error(res, ioValidation.error || 'Invalid IO number', 400);
} }
return ResponseHandler.success(res, { return ResponseHandler.success(res, {
ioNumber: ioValidation.ioNumber, ioNumber: ioValidation.ioNumber,
availableBalance: ioValidation.availableBalance, availableBalance: ioValidation.availableBalance,
@ -623,7 +623,7 @@ export class DealerClaimController {
} }
const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0; const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0;
// Log received data for debugging // Log received data for debugging
logger.info('[DealerClaimController] updateIODetails received:', { logger.info('[DealerClaimController] updateIODetails received:', {
requestId, requestId,
@ -633,7 +633,7 @@ export class DealerClaimController {
receivedBlockedAmount: blockedAmount, // Original value from request receivedBlockedAmount: blockedAmount, // Original value from request
userId, userId,
}); });
// Store in database when blocking amount > 0 OR when ioNumber and ioRemark are provided (for Step 3 approval) // Store in database when blocking amount > 0 OR when ioNumber and ioRemark are provided (for Step 3 approval)
if (blockAmount > 0) { if (blockAmount > 0) {
if (availableBalance === undefined) { if (availableBalance === undefined) {
@ -649,9 +649,9 @@ export class DealerClaimController {
blockedAmount: blockAmount, blockedAmount: blockAmount,
// remainingBalance will be calculated by the service from SAP's response // remainingBalance will be calculated by the service from SAP's response
}; };
logger.info('[DealerClaimController] Calling updateIODetails service with:', ioData); logger.info('[DealerClaimController] Calling updateIODetails service with:', ioData);
await this.dealerClaimService.updateIODetails( await this.dealerClaimService.updateIODetails(
requestId, requestId,
ioData, ioData,
@ -660,7 +660,7 @@ export class DealerClaimController {
// Fetch and return the updated IO details from database // Fetch and return the updated IO details from database
const updatedIO = await InternalOrder.findOne({ where: { requestId } }); const updatedIO = await InternalOrder.findOne({ where: { requestId } });
if (updatedIO) { if (updatedIO) {
return ResponseHandler.success(res, { return ResponseHandler.success(res, {
message: 'IO blocked successfully in SAP', 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) * Update credit note details (Step 8)
* PUT /api/v1/dealer-claims/:requestId/credit-note * PUT /api/v1/dealer-claims/:requestId/credit-note
@ -875,7 +933,7 @@ export class DealerClaimController {
// First validate IO number // First validate IO number
const ioValidation = await sapIntegrationService.validateIONumber(ioNumber); const ioValidation = await sapIntegrationService.validateIONumber(ioNumber);
if (!ioValidation.isValid) { if (!ioValidation.isValid) {
return ResponseHandler.error(res, `Invalid IO number: ${ioValidation.error || 'IO number not found in SAP'}`, 400); 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; filePath?: string | null;
qrCode?: string | null; qrCode?: string | null;
qrImage?: string | null; qrImage?: string | null;
pwcResponse?: any;
irpResponse?: any;
errorMessage?: string; errorMessage?: string;
generatedAt?: Date; generatedAt?: Date;
description?: string; description?: string;
@ -43,7 +45,7 @@ interface ClaimInvoiceAttributes {
updatedAt: Date; 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 { class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
public invoiceId!: string; public invoiceId!: string;
@ -79,6 +81,8 @@ class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAtt
public filePath?: string | null; public filePath?: string | null;
public qrCode?: string | null; public qrCode?: string | null;
public qrImage?: string | null; public qrImage?: string | null;
public pwcResponse?: any;
public irpResponse?: any;
public errorMessage?: string; public errorMessage?: string;
public generatedAt?: Date; public generatedAt?: Date;
public description?: string; public description?: string;
@ -261,6 +265,16 @@ ClaimInvoice.init(
allowNull: true, allowNull: true,
field: 'qr_image' field: 'qr_image'
}, },
pwcResponse: {
type: DataTypes.JSON,
allowNull: true,
field: 'pwc_response'
},
irpResponse: {
type: DataTypes.JSON,
allowNull: true,
field: 'irp_response'
},
errorMessage: { errorMessage: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true, allowNull: true,

View File

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

View File

@ -49,7 +49,7 @@ router.use('/templates', templateRoutes);
router.use('/dealers', dealerRoutes); router.use('/dealers', dealerRoutes);
router.use('/webhooks/dms', dmsWebhookRoutes); router.use('/webhooks/dms', dmsWebhookRoutes);
// TODO: Add other route modules as they are implemented // Add other route modules as they are implemented
// router.use('/approvals', approvalRoutes); // router.use('/approvals', approvalRoutes);
// router.use('/participants', participantRoutes); // router.use('/participants', participantRoutes);

View File

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

View File

@ -58,7 +58,7 @@ interface DealerSeedData {
} }
// Sample data based on the provided table // Sample data based on the provided table
// TODO: Replace with your actual dealer data from Excel/CSV // Replace with your actual dealer data from Excel/CSV
const dealersData: DealerSeedData[] = [ const dealersData: DealerSeedData[] = [
{ {
salesCode: '5124', salesCode: '5124',
@ -116,7 +116,7 @@ async function seedDealersTable(): Promise<void> {
for (const dealerData of dealersData) { for (const dealerData of dealersData) {
// Use dlrcode or domainId as unique identifier if available // Use dlrcode or domainId as unique identifier if available
const uniqueIdentifier = dealerData.dlrcode || dealerData.domainId || dealerData.salesCode; const uniqueIdentifier = dealerData.dlrcode || dealerData.domainId || dealerData.salesCode;
if (!uniqueIdentifier) { if (!uniqueIdentifier) {
logger.warn('[Seed Dealers Table] Skipping dealer record without unique identifier'); logger.warn('[Seed Dealers Table] Skipping dealer record without unique identifier');
continue; continue;
@ -130,15 +130,15 @@ async function seedDealersTable(): Promise<void> {
const existingDealer = whereConditions.length > 0 const existingDealer = whereConditions.length > 0
? await Dealer.findOne({ ? await Dealer.findOne({
where: { where: {
[Op.or]: whereConditions [Op.or]: whereConditions
} }
}) })
: null; : null;
if (existingDealer) { if (existingDealer) {
logger.info(`[Seed Dealers Table] Dealer ${uniqueIdentifier} already exists, updating...`); logger.info(`[Seed Dealers Table] Dealer ${uniqueIdentifier} already exists, updating...`);
// Update existing dealer // Update existing dealer
await existingDealer.update({ await existingDealer.update({
...dealerData, ...dealerData,

View File

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

@ -673,7 +673,7 @@ export class DashboardService {
totalCompleted, totalCompleted,
compliantWorkflows: compliantCount, compliantWorkflows: compliantCount,
changeFromPrevious: { changeFromPrevious: {
compliance: '+5.8%', // TODO: Calculate actual change compliance: '+5.8%', // Calculate actual change
cycleTime: '-0.5h' cycleTime: '-0.5h'
} }
}; };

View File

@ -30,6 +30,7 @@ export interface DealerInfo {
city?: string | null; city?: string | null;
dealerPrincipalName?: string | null; dealerPrincipalName?: string | null;
dealerPrincipalEmailId?: 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, city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null, dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null, dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
}; };
}); });
} catch (error) { } catch (error) {
@ -166,6 +168,7 @@ export async function getDealerByCode(dealerCode: string): Promise<DealerInfo |
city: dealer.city || null, city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null, dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null, dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
}; };
} catch (error) { } catch (error) {
logger.error('[DealerService] Error fetching dealer by code:', 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, city: dealer.city || null,
dealerPrincipalName: dealer.dealerPrincipalName || null, dealerPrincipalName: dealer.dealerPrincipalName || null,
dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null, dealerPrincipalEmailId: dealer.dealerPrincipalEmailId || null,
gstin: dealer.gst || null,
}; };
} catch (error) { } catch (error) {
logger.error('[DealerService] Error fetching dealer by email:', 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 * Dealer Claim Service
* Handles business logic specific to dealer claim management workflow * Handles business logic specific to dealer claim management workflow
*/ */
export class DealerClaimService { export class DealerClaimService {
private workflowService = new WorkflowService(); private getWorkflowService(): WorkflowService {
private approvalService = new DealerClaimApprovalService(); if (!workflowServiceInstance) {
private userService = new UserService(); 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 * 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.'); throw new Error('Approvers array is required. Please assign approvers for all workflow steps.');
} }
// Now create workflow request (manager is validated) // 1. Transform approvers and ensure users exist in database
// For claim management, requests are submitted immediately (not drafts) const userService = this.getUserService();
// Step 1 will be active for dealer to submit proposal const transformedLevels = [];
const now = new Date();
const workflowRequest = await WorkflowRequest.create({ // Define step names mapping
initiatorId: userId, const stepNames: Record<number, string> = {
requestNumber, 1: 'Dealer Proposal Submission',
templateType: 'DEALER CLAIM', // Set template type for dealer claim management 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', workflowType: 'CLAIM_MANAGEMENT',
title: `${claimData.activityName} - Claim Request`, title: `${claimData.activityName} - Claim Request`,
description: claimData.requestDescription, description: claimData.requestDescription,
priority: Priority.STANDARD, priority: Priority.STANDARD,
status: WorkflowStatus.PENDING, // Submitted, not draft approvalLevels: transformedLevels,
totalLevels: 5, // Fixed 5-step workflow for claim management (Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only) participants: transformedParticipants,
currentLevel: 1, // Step 1: Dealer Proposal Submission isDraft: false
totalTatHours: 0, // Will be calculated from approval levels } as any);
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)
});
// Create claim details // Create claim details
await DealerClaimDetails.create({ await DealerClaimDetails.create({
@ -146,107 +237,9 @@ export class DealerClaimService {
currency: 'INR', currency: 'INR',
}); });
// Create 8 approval levels for claim management workflow from approvers array // Redundant level creation removed - handled by workflowService.createWorkflow
await this.createClaimApprovalLevelsFromApprovers(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.approvers || []);
// Schedule TAT jobs for Step 1 (Dealer Proposal Submission) - first active step // Redundant TAT scheduling removed - handled by workflowService.createWorkflow
// 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
logger.info(`[DealerClaimService] Created claim request: ${workflowRequest.requestNumber}`); logger.info(`[DealerClaimService] Created claim request: ${workflowRequest.requestNumber}`);
return workflowRequest; return workflowRequest;
@ -440,7 +433,8 @@ export class DealerClaimService {
// User doesn't exist - create from Okta // User doesn't exist - create from Okta
logger.info(`[DealerClaimService] User ${approver.email} not found in DB, syncing from Okta`); logger.info(`[DealerClaimService] User ${approver.email} not found in DB, syncing from Okta`);
try { try {
user = await this.userService.ensureUserExists({ const userService = this.getUserService();
user = await userService.ensureUserExists({
email: approver.email.toLowerCase(), email: approver.email.toLowerCase(),
userId: approver.userId, // Pass Okta ID if provided (ensureUserExists will handle it) userId: approver.userId, // Pass Okta ID if provided (ensureUserExists will handle it)
}) as any; }) as any;
@ -608,7 +602,8 @@ export class DealerClaimService {
if (!dealerUser) { if (!dealerUser) {
logger.info(`[DealerClaimService] Dealer ${dealerEmail} not found in DB for participants, syncing from Okta`); logger.info(`[DealerClaimService] Dealer ${dealerEmail} not found in DB for participants, syncing from Okta`);
try { try {
dealerUser = await this.userService.ensureUserExists({ const userService = this.getUserService();
dealerUser = await userService.ensureUserExists({
email: dealerEmail.toLowerCase(), email: dealerEmail.toLowerCase(),
}) as any; }) as any;
logger.info(`[DealerClaimService] Successfully synced dealer ${dealerEmail} from Okta for participants`); 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}"`); logger.info(`[DealerClaimService] Searching Okta for manager with displayName: "${managerDisplayName}"`);
// Search Okta by displayName // 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) { if (oktaUsers.length === 0) {
logger.warn(`[DealerClaimService] No reporting manager found in Okta for displayName: "${managerDisplayName}"`); 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}"`); logger.info(`[DealerClaimService] Found single manager match: ${managerEmail} for displayName: "${managerDisplayName}"`);
// Check if user exists in DB, create if doesn't exist // 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, userId: oktaUser.id,
email: managerEmail, email: managerEmail,
displayName: oktaUser.profile.displayName || `${oktaUser.profile.firstName || ''} ${oktaUser.profile.lastName || ''}`.trim(), displayName: oktaUser.profile.displayName || `${oktaUser.profile.firstName || ''} ${oktaUser.profile.lastName || ''}`.trim(),
@ -1324,7 +1320,8 @@ export class DealerClaimService {
: 'Dealer proposal submitted'; : 'Dealer proposal submitted';
// Perform the approval action FIRST - only save snapshot if action succeeds // Perform the approval action FIRST - only save snapshot if action succeeds
await this.approvalService.approveLevel( const approvalService = this.getApprovalService();
await approvalService.approveLevel(
dealerProposalLevel.levelId, dealerProposalLevel.levelId,
{ action: 'APPROVE', comments: approvalComment }, { action: 'APPROVE', comments: approvalComment },
actualDealerUserId || (request as any).initiatorId || 'system', // Use dealer or initiator ID 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 // Perform the approval action FIRST - only save snapshot if action succeeds
await this.approvalService.approveLevel( const approvalService = this.getApprovalService();
await approvalService.approveLevel(
dealerCompletionLevel.levelId, dealerCompletionLevel.levelId,
{ action: 'APPROVE', comments: approvalComment }, { action: 'APPROVE', comments: approvalComment },
actualDealerUserId || (request as any).initiatorId || 'system', 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'; 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) { if (!invoiceData?.eInvoiceNumber) {
const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } }); const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } });
const invoiceAmount = invoiceData?.amount const invoiceAmount = invoiceData?.amount
@ -1914,7 +1912,7 @@ export class DealerClaimService {
|| budgetTracking?.initialEstimatedBudget || budgetTracking?.initialEstimatedBudget
|| 0; || 0;
const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId); const invoiceResult = await pwcIntegrationService.generateSignedInvoice(requestId, invoiceAmount);
if (!invoiceResult.success) { if (!invoiceResult.success) {
throw new Error(`Failed to generate signed e-invoice via PWC: ${invoiceResult.error}`); throw new Error(`Failed to generate signed e-invoice via PWC: ${invoiceResult.error}`);
@ -1930,6 +1928,8 @@ export class DealerClaimService {
signedInvoice: invoiceResult.signedInvoice, signedInvoice: invoiceResult.signedInvoice,
qrCode: invoiceResult.qrCode, qrCode: invoiceResult.qrCode,
qrImage: invoiceResult.qrImage, qrImage: invoiceResult.qrImage,
pwcResponse: invoiceResult.rawResponse,
irpResponse: invoiceResult.irpResponse,
amount: invoiceAmount, amount: invoiceAmount,
status: 'GENERATED', status: 'GENERATED',
generatedAt: new Date(), generatedAt: new Date(),
@ -1956,6 +1956,15 @@ export class DealerClaimService {
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`); 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 // Check if Requestor Claim Approval is approved - if not, approve it first
// Find dynamically by levelName (handles step shifts due to additional approvers) // Find dynamically by levelName (handles step shifts due to additional approvers)
const approvalLevels = await ApprovalLevel.findAll({ const approvalLevels = await ApprovalLevel.findAll({
@ -1975,34 +1984,30 @@ export class DealerClaimService {
requestorClaimLevel = approvalLevels.find((level: any) => level.levelNumber === 5); 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) { 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) { // E-Invoice Generation is successful - auto-approve the Requestor Claim Approval step
logger.info(`[DealerClaimService] Requestor Claim Approval not approved yet. Auto-approving for request ${requestId}`); if (requestorClaimLevel && requestorClaimLevel.status !== 'APPROVED') {
// Auto-approve Requestor Claim Approval const approvalService = this.getApprovalService();
await this.approvalService.approveLevel( await approvalService.approveLevel(
requestorClaimLevel.levelId, requestorClaimLevel.levelId,
{ action: 'APPROVE', comments: 'Auto-approved when pushing to DMS. E-Invoice generation will be logged as activity.' }, { action: 'APPROVE', comments: 'Auto-approved after successful E-Invoice generation' },
'system', 'system'
{ ipAddress: null, userAgent: 'System Auto-Process' }
); );
logger.info(`[DealerClaimService] Requestor Claim Approval 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}`);
} 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.`);
} }
// Log E-Invoice generation as activity (no approval level needed) // Log E-Invoice generation as activity
await activityService.log({ await activityService.log({
requestId, requestId,
type: 'status_change', type: 'status_change',
user: { userId: 'system', name: 'System Auto-Process' }, user: { userId: 'system', name: 'System Auto-Process' },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'E-Invoice Generation Initiated', action: 'E-Invoice Generated',
details: `E-Invoice generation initiated via DMS integration for request ${requestNumber}. Waiting for DMS webhook confirmation.`, details: `E-Invoice generated via PWC integration for request ${requestNumber}. Step "${requestorClaimLevel?.levelName || 'Requestor Claim Approval'}" auto-approved.`,
}); });
} catch (error) { } catch (error) {
logger.error('[DealerClaimService] Error updating e-invoice details:', 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) * 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> { async logEInvoiceGenerationActivity(requestId: string, invoiceNumber?: string): Promise<void> {
try { try {
@ -2040,7 +2045,7 @@ export class DealerClaimService {
user: { userId: 'system', name: 'System Auto-Process' }, user: { userId: 'system', name: 'System Auto-Process' },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'E-Invoice Generated', 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})`); logger.info(`[DealerClaimService] E-Invoice Generation activity logged for request ${requestId} (Invoice: ${finalInvoiceNumber})`);
@ -2215,14 +2220,6 @@ export class DealerClaimService {
dealerName: claimDetails.dealerName, dealerName: claimDetails.dealerName,
}); });
// TODO: Implement email service to send credit note to dealer
// await emailService.sendCreditNoteToDealer({
// dealerEmail: claimDetails.dealerEmail,
// dealerName: claimDetails.dealerName,
// creditNoteNumber: creditNote.creditNoteNumber,
// creditNoteAmount: creditNote.creditNoteAmount,
// requestNumber: requestNumber,
// });
} catch (error) { } catch (error) {
logger.error('[DealerClaimService] Error sending credit note to dealer:', error); logger.error('[DealerClaimService] Error sending credit note to dealer:', error);
@ -2426,7 +2423,7 @@ export class DealerClaimService {
category: 'SUPPORTING', category: 'SUPPORTING',
isDeleted: false isDeleted: false
}, },
order: [['createdAt', 'DESC']] order: [['uploadedAt', 'DESC']]
}); });
const snapshotData = { const snapshotData = {
@ -2511,7 +2508,7 @@ export class DealerClaimService {
category: 'SUPPORTING', category: 'SUPPORTING',
isDeleted: false isDeleted: false
}, },
order: [['createdAt', 'DESC']] order: [['uploadedAt', 'DESC']]
}); });
// Store all completion data in JSONB // Store all completion data in JSONB

View File

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

View File

@ -64,30 +64,6 @@ export class DMSIntegrationService {
}; };
} }
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.post(`${this.dmsBaseUrl}/api/invoices/generate`, {
// request_number: invoiceData.requestNumber,
// dealer_code: invoiceData.dealerCode,
// dealer_name: invoiceData.dealerName,
// amount: invoiceData.amount,
// description: invoiceData.description,
// io_number: invoiceData.ioNumber,
// tax_details: invoiceData.taxDetails
// }, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// eInvoiceNumber: response.data.e_invoice_number,
// dmsNumber: response.data.dms_number,
// invoiceDate: new Date(response.data.invoice_date),
// invoiceUrl: response.data.invoice_url
// };
logger.warn('[DMS] DMS e-invoice generation not implemented, generating mock invoice'); logger.warn('[DMS] DMS e-invoice generation not implemented, generating mock invoice');
const mockInvoiceNumber = `EINV-${Date.now()}`; const mockInvoiceNumber = `EINV-${Date.now()}`;
@ -145,31 +121,6 @@ export class DMSIntegrationService {
}; };
} }
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.post(`${this.dmsBaseUrl}/api/credit-notes/generate`, {
// request_number: creditNoteData.requestNumber,
// e_invoice_number: creditNoteData.eInvoiceNumber,
// dealer_code: creditNoteData.dealerCode,
// dealer_name: creditNoteData.dealerName,
// amount: creditNoteData.amount,
// reason: creditNoteData.reason,
// description: creditNoteData.description
// }, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// creditNoteNumber: response.data.credit_note_number,
// creditNoteDate: new Date(response.data.credit_note_date),
// creditNoteAmount: response.data.credit_note_amount,
// creditNoteUrl: response.data.credit_note_url
// };
logger.warn('[DMS] DMS credit note generation not implemented, generating mock credit note'); logger.warn('[DMS] DMS credit note generation not implemented, generating mock credit note');
const mockCreditNoteNumber = `CN-${Date.now()}`; const mockCreditNoteNumber = `CN-${Date.now()}`;
return { return {
@ -217,23 +168,7 @@ export class DMSIntegrationService {
}; };
} }
// TODO: Implement actual DMS API call ;
// Example:
// const response = await axios.get(`${this.dmsBaseUrl}/api/invoices/${eInvoiceNumber}/status`, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: true,
// status: response.data.status,
// invoiceNumber: response.data.invoice_number,
// dmsNumber: response.data.dms_number,
// invoiceDate: new Date(response.data.invoice_date),
// amount: response.data.amount
// };
logger.warn('[DMS] DMS invoice status check not implemented, returning mock status'); logger.warn('[DMS] DMS invoice status check not implemented, returning mock status');
return { return {
@ -277,20 +212,7 @@ export class DMSIntegrationService {
}; };
} }
// TODO: Implement actual DMS API call
// Example:
// const response = await axios.get(`${this.dmsBaseUrl}/api/invoices/${eInvoiceNumber}/download`, {
// headers: {
// 'Authorization': `Bearer ${this.dmsApiKey}`
// },
// responseType: 'arraybuffer'
// });
//
// return {
// success: true,
// documentBuffer: Buffer.from(response.data),
// mimeType: response.headers['content-type'] || 'application/pdf'
// };
logger.warn('[DMS] DMS invoice download not implemented, returning mock URL'); logger.warn('[DMS] DMS invoice download not implemented, returning mock URL');
return { return {

View File

@ -508,7 +508,7 @@ export class DMSWebhookService {
// E-Invoice Generation is now an activity log only, not an approval step // E-Invoice Generation is now an activity log only, not an approval step
// Log the activity using the dealerClaimService // Log the activity using the dealerClaimService
const { DealerClaimService } = await import('./dealerClaim.service'); const { DealerClaimService } = require('./dealerClaim.service');
const dealerClaimService = new DealerClaimService(); const dealerClaimService = new DealerClaimService();
const invoice = await ClaimInvoice.findOne({ where: { requestId } }); const invoice = await ClaimInvoice.findOne({ where: { requestId } });
const invoiceNumber = invoice?.invoiceNumber || 'N/A'; 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 { WorkflowRequest } from '../models/WorkflowRequest';
import { ClaimInvoice } from '../models/ClaimInvoice'; import { ClaimInvoice } from '../models/ClaimInvoice';
import { InternalOrder } from '../models/InternalOrder'; import { InternalOrder } from '../models/InternalOrder';
import { User } from '../models/User';
import { DealerClaimDetails } from '../models/DealerClaimDetails';
/** /**
* PWC E-Invoice Integration Service * PWC E-Invoice Integration Service
@ -12,13 +14,13 @@ import { InternalOrder } from '../models/InternalOrder';
*/ */
export class PWCIntegrationService { export class PWCIntegrationService {
private apiUrl: string; private apiUrl: string;
private appKey: string; private customerId: string;
private appSecret: string; private token: string;
constructor() { constructor() {
this.apiUrl = process.env.PWC_API_URL || 'https://api.qa.einvoice.aw.navigatetax.pwc.co.in'; this.apiUrl = process.env.PWC_API_URL || 'https://api.qa.einvoice.aw.navigatetax.pwc.co.in/qa/v1/en/push';
this.appKey = process.env.PWC_APP_KEY || ''; this.customerId = process.env.PWC_CUSTOMER_ID || '';
this.appSecret = process.env.PWC_APP_SECRET || ''; this.token = process.env.PWC_TOKEN || '';
} }
/** /**
@ -43,7 +45,7 @@ export class PWCIntegrationService {
/** /**
* Generate Signed Invoice via PWC API * Generate Signed Invoice via PWC API
*/ */
async generateSignedInvoice(requestId: string): Promise<{ async generateSignedInvoice(requestId: string, amount?: number): Promise<{
success: boolean; success: boolean;
irn?: string; irn?: string;
ackNo?: string; ackNo?: string;
@ -51,75 +53,155 @@ export class PWCIntegrationService {
signedInvoice?: string; signedInvoice?: string;
qrCode?: string; qrCode?: string;
qrImage?: string; qrImage?: string;
rawResponse?: any;
irpResponse?: any;
error?: string; error?: string;
}> { }> {
try { try {
const request = await WorkflowRequest.findByPk(requestId, { 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' }; if (!request) return { success: false, error: 'Request not found' };
const dealer = await Dealer.findOne({ where: { dlrcode: (request as any).claimDetails?.dealerCode } }); const claimDetails = (request as any).claimDetails;
const activity = await ActivityType.findOne({ where: { title: (request as any).claimDetails?.activityType } }); const dealer = await Dealer.findOne({ where: { dlrcode: claimDetails?.dealerCode } });
const activity = await ActivityType.findOne({ where: { title: claimDetails?.activityType } });
if (!dealer || !activity) { if (!dealer || !activity) {
return { success: false, error: 'Dealer or Activity details missing' }; return { success: false, error: 'Dealer or Activity details missing' };
} }
// Construct PWC Payload (keeping existing logic for now) // Fallback for amount if not provided
const payload = { const finalAmount = Number(amount || (request as any).amount || 0);
UserGstin: "33AAACE3882D1ZZ",
DocDtls: { // Helper to format number to 2 decimal places
Typ: "INV", const formatAmount = (val: number) => Number(val.toFixed(2));
No: `INV-${Date.now()}`,
Dt: new Date().toLocaleDateString('en-GB').replace(/\//g, '-') // Extract State Code from Dealer GSTIN
}, let dealerGst = (dealer as any).gst;
SellerDtls: {
Gstin: dealer.gst || "33AAACE3882D1ZZ", // HOTFIX: For PWC QA Environment, use a known valid GSTIN if dealer has the invalid test one
LglNm: dealer.dealership || 'Dealer', // The test GSTIN 29AAACE3882D1ZZ is not registered in PWC QA Master, causing Error 701
Addr1: dealer.showroomAddress || "Address Line 1", const isQA = this.apiUrl.includes('qa');
Loc: dealer.location || "Location", const invalidTestGst = '29AAACE3882D1ZZ';
Pin: 600001, const validQaGst = '24AAAPI3182M002'; // Registered in PWC QA
Stcd: "33"
}, if (isQA && (!dealerGst || dealerGst === invalidTestGst)) {
BuyerDtls: { dealerGst = validQaGst;
Gstin: "33AAACE3882D1ZZ", }
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
Addr1: "No. 2, Thiruvottiyur High Road", // Final fallback if still empty
Loc: "Thiruvottiyur", dealerGst = dealerGst || validQaGst;
Pin: 600019,
Stcd: "33", let dealerStateCode = "24"; // Default fallback (Gujarat for 24...)
Pos: "33"
}, // Try to extract from GSTIN (first 2 chars)
ItemList: [ if (dealerGst && dealerGst.length >= 2 && !isNaN(Number(dealerGst.substring(0, 2)))) {
{ dealerStateCode = dealerGst.substring(0, 2);
SlNo: "1", } else if ((dealer as any).stateCode) {
PrdDesc: activity.title, dealerStateCode = (dealer as any).stateCode;
IsServc: "Y", }
HsnCd: activity.hsnCode || activity.sacCode || "9983",
Qty: 1, // Calculate tax amounts
Unit: "OTH", const gstRate = Number(activity.gstRate || 18);
UnitPrce: (request as any).amount, const isIGST = dealerStateCode !== "33"; // If dealer state != Buyer state (33), it's IGST
TotAmt: (request as any).amount,
GstRt: activity.gstRate || 18, const assAmt = finalAmount;
AssAmt: (request as any).amount, const igstAmt = isIGST ? (finalAmount * (gstRate / 100)) : 0;
IgstAmt: activity.gstRate === 18 ? ((request as any).amount * 0.18) : 0, const cgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
TotItemVal: (request as any).amount * 1.18 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}`); logger.info(`[PWC] Sending e-invoice request for ${request.requestNumber}`);
const response = await axios.post(`${this.apiUrl}/generate`, payload, { const response = await axios.post(this.apiUrl, payload, {
headers: { 'AppKey': this.appKey, 'AppSecret': this.appSecret } headers: {
'customerid': this.customerId,
'token': this.token
}
}); });
console.log('PWC Response:', JSON.stringify(response.data));
// Parse PWC Response based on provided structure // Parse PWC Response based on provided structure
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }] // Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]
@ -146,9 +228,25 @@ export class PWCIntegrationService {
} }
if (!irn) { if (!irn) {
const errorMsg = responseData?.irp_response?.message || 'E-Invoice generation failed'; const mainMsg = responseData?.pwc_response?.message || responseData?.irp_response?.message || 'E-Invoice generation failed';
logger.error(`[PWC] E-Invoice failed for ${request.requestNumber}: ${errorMsg}`);
return { success: false, error: errorMsg }; // 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 { return {
@ -158,7 +256,9 @@ export class PWCIntegrationService {
ackDate: ackDate ? new Date(ackDate) : undefined, ackDate: ackDate ? new Date(ackDate) : undefined,
signedInvoice, signedInvoice,
qrCode, qrCode,
qrImage: qrB64 qrImage: qrB64,
rawResponse: responseData?.pwc_response,
irpResponse: responseData?.irp_response
}; };
} catch (error) { } catch (error) {

View File

@ -34,6 +34,12 @@ export class SAPIntegrationService {
* Check if SAP integration is configured * Check if SAP integration is configured
*/ */
private isConfigured(): boolean { 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; return !!this.sapBaseUrl && !!this.sapUsername && !!this.sapPassword;
} }
@ -59,10 +65,10 @@ export class SAPIntegrationService {
'sap-client': '200' 'sap-client': '200'
}); });
const fullUrl = `${this.sapBaseUrl}${serviceRootUrl}?${queryParams.toString()}`; const fullUrl = `${this.sapBaseUrl}${serviceRootUrl}?${queryParams.toString()}`;
logger.debug(`[SAP] Fetching CSRF token from service: ${serviceName}`); logger.debug(`[SAP] Fetching CSRF token from service: ${serviceName}`);
logger.debug(`[SAP] CSRF token request URL: ${fullUrl}`); logger.debug(`[SAP] CSRF token request URL: ${fullUrl}`);
// Use standalone axios request with Basic Auth in header // Use standalone axios request with Basic Auth in header
// We need to capture cookies from this response to use in POST request // We need to capture cookies from this response to use in POST request
const response = await axios.get(fullUrl, { const response = await axios.get(fullUrl, {
@ -85,16 +91,16 @@ export class SAPIntegrationService {
}); });
// SAP returns CSRF token in response headers (check multiple case variations) // SAP returns CSRF token in response headers (check multiple case variations)
const csrfToken = response.headers['x-csrf-token'] || const csrfToken = response.headers['x-csrf-token'] ||
response.headers['X-CSRF-Token'] || response.headers['X-CSRF-Token'] ||
response.headers['X-Csrf-Token'] || response.headers['X-Csrf-Token'] ||
response.headers['x-csrf-token']; response.headers['x-csrf-token'];
// Extract cookies from response headers // Extract cookies from response headers
// SAP sets cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext // SAP sets cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext
const setCookieHeaders = response.headers['set-cookie'] as string | string[] | undefined; const setCookieHeaders = response.headers['set-cookie'] as string | string[] | undefined;
let cookies = ''; let cookies = '';
if (setCookieHeaders) { if (setCookieHeaders) {
if (Array.isArray(setCookieHeaders)) { if (Array.isArray(setCookieHeaders)) {
// Extract cookie values from Set-Cookie headers // Extract cookie values from Set-Cookie headers
@ -107,36 +113,36 @@ export class SAPIntegrationService {
cookies = setCookieHeaders.split(';')[0].trim(); cookies = setCookieHeaders.split(';')[0].trim();
} }
} }
// Log full GET response for debugging // Log full GET response for debugging
logger.info(`[SAP] GET Response Status: ${response.status} ${response.statusText || ''}`); logger.info(`[SAP] GET Response Status: ${response.status} ${response.statusText || ''}`);
logger.info(`[SAP] GET Response Headers:`, JSON.stringify(response.headers, null, 2)); logger.info(`[SAP] GET Response Headers:`, JSON.stringify(response.headers, null, 2));
logger.info(`[SAP] GET Response Data:`, JSON.stringify(response.data, null, 2)); logger.info(`[SAP] GET Response Data:`, JSON.stringify(response.data, null, 2));
if (csrfToken && typeof csrfToken === 'string' && csrfToken !== 'fetch') { if (csrfToken && typeof csrfToken === 'string' && csrfToken !== 'fetch') {
logger.info(`[SAP] CSRF token obtained successfully (length: ${csrfToken.length})`); logger.info(`[SAP] CSRF token obtained successfully (length: ${csrfToken.length})`);
logger.debug(`[SAP] CSRF token preview: ${csrfToken.substring(0, 20)}...`); logger.debug(`[SAP] CSRF token preview: ${csrfToken.substring(0, 20)}...`);
if (cookies) { if (cookies) {
logger.debug(`[SAP] Session cookies captured: ${cookies.substring(0, 50)}...`); logger.debug(`[SAP] Session cookies captured: ${cookies.substring(0, 50)}...`);
} else { } else {
logger.warn('[SAP] No cookies found in CSRF token response - POST may fail'); logger.warn('[SAP] No cookies found in CSRF token response - POST may fail');
} }
return { csrfToken, cookies }; return { csrfToken, cookies };
} }
logger.warn('[SAP] CSRF token not found in response headers or invalid'); logger.warn('[SAP] CSRF token not found in response headers or invalid');
logger.debug('[SAP] Response status:', response.status); logger.debug('[SAP] Response status:', response.status);
logger.debug('[SAP] Available headers:', Object.keys(response.headers).filter(h => h.toLowerCase().includes('csrf'))); logger.debug('[SAP] Available headers:', Object.keys(response.headers).filter(h => h.toLowerCase().includes('csrf')));
return null; return null;
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
if (axiosError.response) { if (axiosError.response) {
logger.error(`[SAP] Failed to get CSRF token: ${axiosError.response.status} ${axiosError.response.statusText}`); logger.error(`[SAP] Failed to get CSRF token: ${axiosError.response.status} ${axiosError.response.statusText}`);
logger.error(`[SAP] Response data:`, axiosError.response.data); logger.error(`[SAP] Response data:`, axiosError.response.data);
if (axiosError.response.status === 401 || axiosError.response.status === 403) { if (axiosError.response.status === 401 || axiosError.response.status === 403) {
logger.error('[SAP] Authentication failed while fetching CSRF token - check SAP credentials'); logger.error('[SAP] Authentication failed while fetching CSRF token - check SAP credentials');
} else if (axiosError.response.status === 404) { } else if (axiosError.response.status === 404) {
@ -152,7 +158,7 @@ export class SAPIntegrationService {
} else { } else {
logger.error('[SAP] Error setting up CSRF token request:', error instanceof Error ? error.message : 'Unknown error'); logger.error('[SAP] Error setting up CSRF token request:', error instanceof Error ? error.message : 'Unknown error');
} }
return null; return null;
} }
} }
@ -163,7 +169,7 @@ export class SAPIntegrationService {
private createSapClient() { private createSapClient() {
// Check if SSL verification should be disabled (for testing with self-signed certs) // Check if SSL verification should be disabled (for testing with self-signed certs)
const disableSSLVerification = process.env.SAP_DISABLE_SSL_VERIFY === 'true'; const disableSSLVerification = process.env.SAP_DISABLE_SSL_VERIFY === 'true';
const client = axios.create({ const client = axios.create({
baseURL: this.sapBaseUrl, baseURL: this.sapBaseUrl,
timeout: this.sapTimeout, timeout: this.sapTimeout,
@ -265,11 +271,11 @@ export class SAPIntegrationService {
} }
const sapClient = this.createSapClient(); const sapClient = this.createSapClient();
// SAP OData endpoint: GetSenderDataSet with filter on IONumber // SAP OData endpoint: GetSenderDataSet with filter on IONumber
// Service name is configurable via SAP_SERVICE_NAME env variable // Service name is configurable via SAP_SERVICE_NAME env variable
const endpoint = this.buildODataEndpoint('GetSenderDataSet'); const endpoint = this.buildODataEndpoint('GetSenderDataSet');
// Build OData query parameters matching the working URL format // Build OData query parameters matching the working URL format
// $filter: Filter by IO number // $filter: Filter by IO number
// $select: Select specific fields (Sender, ResponseDate, GetIODetailsSet01) // $select: Select specific fields (Sender, ResponseDate, GetIODetailsSet01)
@ -281,18 +287,18 @@ export class SAPIntegrationService {
'$expand': 'GetIODetailsSet01', '$expand': 'GetIODetailsSet01',
'$format': 'json' '$format': 'json'
}); });
const fullUrl = `${endpoint}?${queryParams.toString()}`; const fullUrl = `${endpoint}?${queryParams.toString()}`;
logger.info(`[SAP] Validating IO number: ${ioNumber} using service: ${this.sapServiceName}`); logger.info(`[SAP] Validating IO number: ${ioNumber} using service: ${this.sapServiceName}`);
logger.debug(`[SAP] Request URL: ${this.sapBaseUrl}${fullUrl}`); logger.debug(`[SAP] Request URL: ${this.sapBaseUrl}${fullUrl}`);
const response = await sapClient.get(fullUrl); const response = await sapClient.get(fullUrl);
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
// SAP OData response format: { d: { results: [...] } } // SAP OData response format: { d: { results: [...] } }
const results = response.data.d?.results || response.data.results || []; const results = response.data.d?.results || response.data.results || [];
if (results.length === 0) { if (results.length === 0) {
logger.warn(`[SAP] IO number ${ioNumber} not found in SAP`); logger.warn(`[SAP] IO number ${ioNumber} not found in SAP`);
return { return {
@ -308,11 +314,11 @@ export class SAPIntegrationService {
// Get first result (should be only one for a specific IO number) // Get first result (should be only one for a specific IO number)
const senderData = results[0]; const senderData = results[0];
// IO details are in the expanded GetIODetailsSet01 entity set // IO details are in the expanded GetIODetailsSet01 entity set
// Structure: senderData.GetIODetailsSet01.results[0] // Structure: senderData.GetIODetailsSet01.results[0]
const ioDetailsSet = senderData.GetIODetailsSet01; const ioDetailsSet = senderData.GetIODetailsSet01;
if (!ioDetailsSet || !ioDetailsSet.results || !Array.isArray(ioDetailsSet.results) || ioDetailsSet.results.length === 0) { if (!ioDetailsSet || !ioDetailsSet.results || !Array.isArray(ioDetailsSet.results) || ioDetailsSet.results.length === 0) {
logger.warn(`[SAP] No IO details found in expanded GetIODetailsSet01 for IO ${ioNumber}`); logger.warn(`[SAP] No IO details found in expanded GetIODetailsSet01 for IO ${ioNumber}`);
return { return {
@ -325,34 +331,34 @@ export class SAPIntegrationService {
error: 'IO details not found in SAP response' error: 'IO details not found in SAP response'
}; };
} }
// Get the first IO detail from the results array // Get the first IO detail from the results array
const ioDetails = ioDetailsSet.results[0]; const ioDetails = ioDetailsSet.results[0];
// Map SAP response fields to our format based on actual response structure: // Map SAP response fields to our format based on actual response structure:
// - AvailableAmount: string with trailing space (e.g., "14333415.00 ") // - AvailableAmount: string with trailing space (e.g., "14333415.00 ")
// - IODescription: description text // - IODescription: description text
// - IONumber: IO number // - IONumber: IO number
// - BlockedAmount: may not be present in response, default to 0 // - BlockedAmount: may not be present in response, default to 0
// - Currency: may not be present, default to INR // - Currency: may not be present, default to INR
// Parse AvailableAmount - it's a string that may have trailing spaces // Parse AvailableAmount - it's a string that may have trailing spaces
const availableAmountStr = (ioDetails.AvailableAmount || '0').toString().trim(); const availableAmountStr = (ioDetails.AvailableAmount || '0').toString().trim();
const availableBalance = parseFloat(availableAmountStr) || 0; const availableBalance = parseFloat(availableAmountStr) || 0;
// BlockedAmount may not be in the response, default to 0 // BlockedAmount may not be in the response, default to 0
// If it exists, it might also be a string with trailing space // If it exists, it might also be a string with trailing space
const blockedAmountStr = (ioDetails.BlockedAmount || ioDetails.Blocked || '0').toString().trim(); const blockedAmountStr = (ioDetails.BlockedAmount || ioDetails.Blocked || '0').toString().trim();
const blockedAmount = parseFloat(blockedAmountStr) || 0; const blockedAmount = parseFloat(blockedAmountStr) || 0;
const remainingBalance = availableBalance - blockedAmount; const remainingBalance = availableBalance - blockedAmount;
// Currency may not be in response, default to INR // Currency may not be in response, default to INR
const currency = (ioDetails.Currency || ioDetails.CurrencyCode || ioDetails.Curr || 'INR').toString().trim(); const currency = (ioDetails.Currency || ioDetails.CurrencyCode || ioDetails.Curr || 'INR').toString().trim();
// Description from IODescription field // Description from IODescription field
const description = ioDetails.IODescription || ioDetails.Description || ioDetails.Text || ioDetails.ShortText || undefined; const description = ioDetails.IODescription || ioDetails.Description || ioDetails.Text || ioDetails.ShortText || undefined;
// IO Number from the IO details // IO Number from the IO details
const validatedIONumber = ioDetails.IONumber || ioDetails.InternalOrder || ioNumber; const validatedIONumber = ioDetails.IONumber || ioDetails.InternalOrder || ioNumber;
@ -416,7 +422,7 @@ export class SAPIntegrationService {
} }
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
if (axiosError.response) { if (axiosError.response) {
// SAP returned an error response // SAP returned an error response
logger.error(`[SAP] Error validating IO number ${ioNumber}:`, { logger.error(`[SAP] Error validating IO number ${ioNumber}:`, {
@ -424,7 +430,7 @@ export class SAPIntegrationService {
statusText: axiosError.response.statusText, statusText: axiosError.response.statusText,
data: axiosError.response.data data: axiosError.response.data
}); });
return { return {
isValid: false, isValid: false,
ioNumber, ioNumber,
@ -494,16 +500,16 @@ export class SAPIntegrationService {
} }
const sapClient = this.createSapClient(); const sapClient = this.createSapClient();
// SAP OData endpoint for budget blocking // SAP OData endpoint for budget blocking
// Service: ZFI_BUDGET_BLOCK_API_SRV // Service: ZFI_BUDGET_BLOCK_API_SRV
// Entity Set: RequesterInputSet // Entity Set: RequesterInputSet
const endpoint = `/sap/opu/odata/sap/${this.sapBlockServiceName}/RequesterInputSet`; const endpoint = `/sap/opu/odata/sap/${this.sapBlockServiceName}/RequesterInputSet`;
// Format current date/time in ISO format: "2025-08-29T10:51:00" // Format current date/time in ISO format: "2025-08-29T10:51:00"
const now = new Date(); const now = new Date();
const requestDateTime = now.toISOString().replace(/\.\d{3}Z$/, ''); // Remove milliseconds and Z const requestDateTime = now.toISOString().replace(/\.\d{3}Z$/, ''); // Remove milliseconds and Z
// Build request payload matching SAP API structure // Build request payload matching SAP API structure
const requestPayload = { const requestPayload = {
Request_Date_Time: requestDateTime, Request_Date_Time: requestDateTime,
@ -517,26 +523,26 @@ export class SAPIntegrationService {
lt_io_output: [], lt_io_output: [],
ls_response: [] ls_response: []
}; };
logger.info(`[SAP] Blocking budget for IO ${ioNumber}, Amount: ${amount}, Request: ${requestNumber}`); logger.info(`[SAP] Blocking budget for IO ${ioNumber}, Amount: ${amount}, Request: ${requestNumber}`);
logger.debug(`[SAP] Budget block request payload:`, JSON.stringify(requestPayload, null, 2)); logger.debug(`[SAP] Budget block request payload:`, JSON.stringify(requestPayload, null, 2));
// Get CSRF token and cookies for POST request (SAP OData requires both) // Get CSRF token and cookies for POST request (SAP OData requires both)
// SAP sets session cookies during CSRF token fetch that must be included in POST // SAP sets session cookies during CSRF token fetch that must be included in POST
const csrfData = await this.getCsrfToken(this.sapBlockServiceName); const csrfData = await this.getCsrfToken(this.sapBlockServiceName);
if (!csrfData || !csrfData.csrfToken) { if (!csrfData || !csrfData.csrfToken) {
logger.warn('[SAP] CSRF token not available, request may fail with CSRF validation error'); logger.warn('[SAP] CSRF token not available, request may fail with CSRF validation error');
logger.warn('[SAP] This is expected if SAP requires CSRF tokens for POST requests'); logger.warn('[SAP] This is expected if SAP requires CSRF tokens for POST requests');
} }
// Build headers with CSRF token, cookies, and other required headers // Build headers with CSRF token, cookies, and other required headers
// Force JSON format via Accept header (SAP returns XML by default for POST) // Force JSON format via Accept header (SAP returns XML by default for POST)
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Accept': 'application/json, application/atom+xml;q=0.9', // Prefer JSON, fallback to XML 'Accept': 'application/json, application/atom+xml;q=0.9', // Prefer JSON, fallback to XML
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
// Add CSRF token if available (required by SAP for POST/PUT/DELETE) // Add CSRF token if available (required by SAP for POST/PUT/DELETE)
// Use lowercase 'x-csrf-token' as per SAP requirement // Use lowercase 'x-csrf-token' as per SAP requirement
if (csrfData?.csrfToken) { if (csrfData?.csrfToken) {
@ -545,7 +551,7 @@ export class SAPIntegrationService {
} else { } else {
logger.warn('[SAP] CSRF token not available - request may fail with CSRF validation error'); logger.warn('[SAP] CSRF token not available - request may fail with CSRF validation error');
} }
// Add cookies if available (SAP session cookies required for POST) // Add cookies if available (SAP session cookies required for POST)
// Cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext // Cookies like: SAP_SESSIONID_DRE_200 and sap-usercontext
if (csrfData?.cookies) { if (csrfData?.cookies) {
@ -554,24 +560,24 @@ export class SAPIntegrationService {
} else { } else {
logger.warn('[SAP] No session cookies available - request may fail'); logger.warn('[SAP] No session cookies available - request may fail');
} }
// Some SAP systems also require these headers // Some SAP systems also require these headers
headers['X-Requested-With'] = 'XMLHttpRequest'; headers['X-Requested-With'] = 'XMLHttpRequest';
// NOTE: Do NOT add query parameters ($format, sap-client) to POST requests // NOTE: Do NOT add query parameters ($format, sap-client) to POST requests
// SAP OData does not allow SystemQueryOptions in POST requests // SAP OData does not allow SystemQueryOptions in POST requests
// Query parameters are only allowed for GET requests // Query parameters are only allowed for GET requests
// Use the endpoint directly without query parameters // Use the endpoint directly without query parameters
const urlWithParams = endpoint; const urlWithParams = endpoint;
logger.debug(`[SAP] POST request URL: ${urlWithParams}`); logger.debug(`[SAP] POST request URL: ${urlWithParams}`);
logger.debug(`[SAP] Request headers (CSRF token and cookies masked):`, { logger.debug(`[SAP] Request headers (CSRF token and cookies masked):`, {
...headers, ...headers,
'x-csrf-token': csrfData?.csrfToken ? `${csrfData.csrfToken.substring(0, 10)}...` : 'not set', 'x-csrf-token': csrfData?.csrfToken ? `${csrfData.csrfToken.substring(0, 10)}...` : 'not set',
'Cookie': csrfData?.cookies ? `${csrfData.cookies.substring(0, 30)}...` : 'not set' 'Cookie': csrfData?.cookies ? `${csrfData.cookies.substring(0, 30)}...` : 'not set'
}); });
logger.debug(`[SAP] Using username: ${this.sapUsername}`); logger.debug(`[SAP] Using username: ${this.sapUsername}`);
// Ensure auth is explicitly included in POST request config // Ensure auth is explicitly included in POST request config
// The axios instance has auth configured, but we'll include it explicitly to be safe // The axios instance has auth configured, but we'll include it explicitly to be safe
// This ensures auth is sent even if the instance config is overridden // This ensures auth is sent even if the instance config is overridden
@ -587,21 +593,21 @@ export class SAPIntegrationService {
}) : undefined, }) : undefined,
validateStatus: (status: number) => status < 500 // Don't throw on 4xx validateStatus: (status: number) => status < 500 // Don't throw on 4xx
}; };
logger.debug(`[SAP] POST request config prepared (auth included)`); logger.debug(`[SAP] POST request config prepared (auth included)`);
const response = await sapClient.post(urlWithParams, requestPayload, postConfig); const response = await sapClient.post(urlWithParams, requestPayload, postConfig);
// Log full response for debugging // Log full response for debugging
logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`); logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`);
logger.info(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2)); logger.info(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2));
// Check if response is XML (SAP returns XML/Atom by default for POST) // Check if response is XML (SAP returns XML/Atom by default for POST)
const contentType = response.headers['content-type'] || ''; const contentType = response.headers['content-type'] || '';
const isXML = contentType.includes('xml') || contentType.includes('atom') || const isXML = contentType.includes('xml') || contentType.includes('atom') ||
(typeof response.data === 'string' && response.data.trim().startsWith('<')); (typeof response.data === 'string' && response.data.trim().startsWith('<'));
let responseData: any = response.data; let responseData: any = response.data;
// Parse XML if needed // Parse XML if needed
if (isXML && typeof response.data === 'string') { if (isXML && typeof response.data === 'string') {
logger.info(`[SAP] Response is XML, parsing to JSON...`); logger.info(`[SAP] Response is XML, parsing to JSON...`);
@ -624,7 +630,7 @@ export class SAPIntegrationService {
// Continue with original data // Continue with original data
} }
} }
// Log response data summary // Log response data summary
if (responseData) { if (responseData) {
if (responseData.entry) { if (responseData.entry) {
@ -633,11 +639,11 @@ export class SAPIntegrationService {
logger.info(`[SAP] Response has OData 'd' wrapper`); logger.info(`[SAP] Response has OData 'd' wrapper`);
} }
} }
// Also log the request that was sent // Also log the request that was sent
logger.info(`[SAP] POST Request URL: ${urlWithParams}`); logger.info(`[SAP] POST Request URL: ${urlWithParams}`);
logger.info(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2)); logger.info(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2));
if (response.status === 200 || response.status === 201) { if (response.status === 200 || response.status === 201) {
// Parse SAP response // Parse SAP response
// Response structure may vary, but typically contains: // Response structure may vary, but typically contains:
@ -645,7 +651,7 @@ export class SAPIntegrationService {
// - Blocked amount confirmation // - Blocked amount confirmation
// - Remaining balance (in lt_io_output[0].Available_Amount for XML) // - Remaining balance (in lt_io_output[0].Available_Amount for XML)
// - Block ID or reference number // - Block ID or reference number
// Helper function to extract remaining balance from various field names // Helper function to extract remaining balance from various field names
// For XML: Available_Amount in lt_io_output[0] (may be prefixed with 'd:' namespace) // For XML: Available_Amount in lt_io_output[0] (may be prefixed with 'd:' namespace)
// For JSON: RemainingBalance, Remaining, Available_Amount, etc. // For JSON: RemainingBalance, Remaining, Available_Amount, etc.
@ -654,43 +660,43 @@ export class SAPIntegrationService {
logger.debug(`[SAP] extractRemainingBalance: obj is null/undefined`); logger.debug(`[SAP] extractRemainingBalance: obj is null/undefined`);
return 0; return 0;
} }
// Helper to extract value from field (handles both direct values and nested #text nodes) // Helper to extract value from field (handles both direct values and nested #text nodes)
const getFieldValue = (fieldName: string): any => { const getFieldValue = (fieldName: string): any => {
const field = obj[fieldName]; const field = obj[fieldName];
if (field === undefined || field === null) return null; if (field === undefined || field === null) return null;
// If it's an object with #text property (XML parser sometimes does this) // If it's an object with #text property (XML parser sometimes does this)
if (typeof field === 'object' && field['#text'] !== undefined) { if (typeof field === 'object' && field['#text'] !== undefined) {
return field['#text']; return field['#text'];
} }
// Direct value // Direct value
return field; return field;
}; };
// Try various field name variations (both JSON and XML formats) // Try various field name variations (both JSON and XML formats)
// XML namespace prefixes: 'd:Available_Amount', 'd:RemainingBalance', etc. // XML namespace prefixes: 'd:Available_Amount', 'd:RemainingBalance', etc.
// IMPORTANT: Check 'd:Available_Amount' first as that's what SAP returns in XML // IMPORTANT: Check 'd:Available_Amount' first as that's what SAP returns in XML
// Also check without namespace prefix as parser might strip it // Also check without namespace prefix as parser might strip it
const value = getFieldValue('d:Available_Amount') ?? // XML format with namespace prefix (PRIORITY) const value = getFieldValue('d:Available_Amount') ?? // XML format with namespace prefix (PRIORITY)
getFieldValue('Available_Amount') ?? // XML format without prefix (parser might strip 'd:') getFieldValue('Available_Amount') ?? // XML format without prefix (parser might strip 'd:')
getFieldValue('d:AvailableAmount') ?? // CamelCase variation with prefix getFieldValue('d:AvailableAmount') ?? // CamelCase variation with prefix
getFieldValue('AvailableAmount') ?? // CamelCase variation without prefix getFieldValue('AvailableAmount') ?? // CamelCase variation without prefix
getFieldValue('d:RemainingBalance') ?? getFieldValue('d:RemainingBalance') ??
getFieldValue('RemainingBalance') ?? getFieldValue('RemainingBalance') ??
getFieldValue('RemainingAmount') ?? getFieldValue('RemainingAmount') ??
getFieldValue('Remaining') ?? getFieldValue('Remaining') ??
getFieldValue('AvailableBalance') ?? getFieldValue('AvailableBalance') ??
getFieldValue('Balance') ?? getFieldValue('Balance') ??
getFieldValue('Available') ?? getFieldValue('Available') ??
null; null;
if (value === null || value === undefined) { if (value === null || value === undefined) {
logger.debug(`[SAP] extractRemainingBalance: No value found. Object keys:`, Object.keys(obj)); logger.debug(`[SAP] extractRemainingBalance: No value found. Object keys:`, Object.keys(obj));
// Log all keys that might be relevant // Log all keys that might be relevant
const relevantKeys = Object.keys(obj).filter(k => const relevantKeys = Object.keys(obj).filter(k =>
k.toLowerCase().includes('available') || k.toLowerCase().includes('available') ||
k.toLowerCase().includes('amount') || k.toLowerCase().includes('amount') ||
k.toLowerCase().includes('remaining') || k.toLowerCase().includes('remaining') ||
k.toLowerCase().includes('balance') k.toLowerCase().includes('balance')
@ -700,47 +706,47 @@ export class SAPIntegrationService {
} }
return 0; return 0;
} }
// Convert to string first, then parse (handles both string "14291525.00" and number) // Convert to string first, then parse (handles both string "14291525.00" and number)
const valueStr = value?.toString().trim() || '0'; const valueStr = value?.toString().trim() || '0';
const parsed = parseFloat(valueStr); const parsed = parseFloat(valueStr);
if (isNaN(parsed)) { if (isNaN(parsed)) {
logger.warn(`[SAP] extractRemainingBalance: Failed to parse value "${valueStr}" as number`); logger.warn(`[SAP] extractRemainingBalance: Failed to parse value "${valueStr}" as number`);
return 0; return 0;
} }
logger.debug(`[SAP] extractRemainingBalance: Extracted value "${valueStr}" -> ${parsed}`); logger.debug(`[SAP] extractRemainingBalance: Extracted value "${valueStr}" -> ${parsed}`);
return parsed; return parsed;
}; };
// Helper function to extract blocked amount // Helper function to extract blocked amount
const extractBlockedAmount = (obj: any): number => { const extractBlockedAmount = (obj: any): number => {
if (!obj) return amount; if (!obj) return amount;
const value = obj.BlockedAmount || const value = obj.BlockedAmount ||
obj.Amount || obj.Amount ||
obj.Blocked || obj.Blocked ||
amount.toString(); amount.toString();
const parsed = parseFloat(value?.toString() || amount.toString()); const parsed = parseFloat(value?.toString() || amount.toString());
return isNaN(parsed) ? amount : parsed; return isNaN(parsed) ? amount : parsed;
}; };
// Handle different possible response structures // Handle different possible response structures
let success = false; let success = false;
let blockedAmount = amount; let blockedAmount = amount;
let remainingBalance = 0; let remainingBalance = 0;
let blockId: string | undefined; let blockId: string | undefined;
// Parse XML structure: entry -> link[@rel='lt_io_output'] -> inline -> feed -> entry -> content -> properties // Parse XML structure: entry -> link[@rel='lt_io_output'] -> inline -> feed -> entry -> content -> properties
// Or JSON structure: { d: {...} } or { lt_io_output: [...] } // Or JSON structure: { d: {...} } or { lt_io_output: [...] }
// Check for XML structure first (parsed XML from fast-xml-parser) // Check for XML structure first (parsed XML from fast-xml-parser)
let ioOutputData: any = null; let ioOutputData: any = null;
let message = ''; let message = '';
let mainEntryProperties: any = null; let mainEntryProperties: any = null;
// XML structure: entry.link (array) -> find link with @rel='lt_io_output' -> inline.feed.entry // XML structure: entry.link (array) -> find link with @rel='lt_io_output' -> inline.feed.entry
// OR JSON OData format: { d: { lt_io_output: { results: [...] } } } // OR JSON OData format: { d: { lt_io_output: { results: [...] } } }
if (responseData.d) { if (responseData.d) {
@ -773,7 +779,7 @@ export class SAPIntegrationService {
responseData = responseData.d; responseData = responseData.d;
} }
} }
// Sometimes XML parser might create the root element with a different name // Sometimes XML parser might create the root element with a different name
// Check if responseData itself IS the entry (if root element was <entry>) // Check if responseData itself IS the entry (if root element was <entry>)
let actualEntry = responseData.entry; let actualEntry = responseData.entry;
@ -783,25 +789,25 @@ export class SAPIntegrationService {
actualEntry = responseData; actualEntry = responseData;
} }
} }
// Check if responseData might be an array (sometimes XML parser returns arrays) // Check if responseData might be an array (sometimes XML parser returns arrays)
if (Array.isArray(responseData) && responseData.length > 0 && responseData[0]?.entry) { if (Array.isArray(responseData) && responseData.length > 0 && responseData[0]?.entry) {
responseData = responseData[0]; responseData = responseData[0];
} }
// Use actualEntry if we found it, otherwise try responseData.entry // Use actualEntry if we found it, otherwise try responseData.entry
const entry = actualEntry || responseData.entry; const entry = actualEntry || responseData.entry;
if (entry && !ioOutputData) { if (entry && !ioOutputData) {
// Also check main entry properties (sometimes Available_Amount is here) // Also check main entry properties (sometimes Available_Amount is here)
const mainContent = entry.content || {}; const mainContent = entry.content || {};
mainEntryProperties = mainContent['m:properties'] || mainContent.properties || (mainContent['@_type'] === 'application/xml' ? mainContent : null); mainEntryProperties = mainContent['m:properties'] || mainContent.properties || (mainContent['@_type'] === 'application/xml' ? mainContent : null);
if (mainEntryProperties) { if (mainEntryProperties) {
logger.info(`[SAP] Found main entry properties, keys:`, Object.keys(mainEntryProperties)); logger.info(`[SAP] Found main entry properties, keys:`, Object.keys(mainEntryProperties));
} }
// Find lt_io_output link in XML structure // Find lt_io_output link in XML structure
const links = Array.isArray(entry.link) ? entry.link : (entry.link ? [entry.link] : []); const links = Array.isArray(entry.link) ? entry.link : (entry.link ? [entry.link] : []);
const ioOutputLink = links.find((link: any) => { const ioOutputLink = links.find((link: any) => {
@ -809,15 +815,15 @@ export class SAPIntegrationService {
const title = link['@_title'] || link.title || ''; const title = link['@_title'] || link.title || '';
return rel.includes('lt_io_output') || title === 'IOOutputSet' || title === 'lt_io_output'; return rel.includes('lt_io_output') || title === 'IOOutputSet' || title === 'lt_io_output';
}); });
if (ioOutputLink?.inline?.feed?.entry) { if (ioOutputLink?.inline?.feed?.entry) {
const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry) const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry)
? ioOutputLink.inline.feed.entry[0] ? ioOutputLink.inline.feed.entry[0]
: ioOutputLink.inline.feed.entry; : ioOutputLink.inline.feed.entry;
const content = ioEntry.content || {}; const content = ioEntry.content || {};
const properties = content['m:properties'] || content.properties || (content['@_type'] === 'application/xml' ? content : null); const properties = content['m:properties'] || content.properties || (content['@_type'] === 'application/xml' ? content : null);
if (properties) { if (properties) {
ioOutputData = properties; ioOutputData = properties;
message = ioOutputData['d:Message'] || ioOutputData.Message || ioOutputData['#text'] || ''; message = ioOutputData['d:Message'] || ioOutputData.Message || ioOutputData['#text'] || '';
@ -825,7 +831,7 @@ export class SAPIntegrationService {
} }
} }
} }
// Extract data from ioOutputData (already extracted above for both XML and JSON formats) // Extract data from ioOutputData (already extracted above for both XML and JSON formats)
if (ioOutputData) { if (ioOutputData) {
// XML parsed structure - extract from lt_io_output properties // XML parsed structure - extract from lt_io_output properties
@ -833,7 +839,7 @@ export class SAPIntegrationService {
success = message.includes('Successful') || message.includes('Success') || !message.includes('Error'); success = message.includes('Successful') || message.includes('Success') || !message.includes('Error');
blockedAmount = amount; // Use the amount we sent (from lt_io_input) blockedAmount = amount; // Use the amount we sent (from lt_io_input)
remainingBalance = extractRemainingBalance(ioOutputData); // Available_Amount from XML remainingBalance = extractRemainingBalance(ioOutputData); // Available_Amount from XML
// If not found in lt_io_output, try main entry properties // If not found in lt_io_output, try main entry properties
if (remainingBalance === 0 && mainEntryProperties) { if (remainingBalance === 0 && mainEntryProperties) {
logger.info(`[SAP] Available_Amount not found in lt_io_output, trying main entry properties`); logger.info(`[SAP] Available_Amount not found in lt_io_output, trying main entry properties`);
@ -842,51 +848,51 @@ export class SAPIntegrationService {
logger.info(`[SAP] Found Available_Amount in main entry properties: ${remainingBalance}`); logger.info(`[SAP] Found Available_Amount in main entry properties: ${remainingBalance}`);
} }
} }
// Helper function to extract SAP reference number (similar to extractRemainingBalance) // Helper function to extract SAP reference number (similar to extractRemainingBalance)
const extractSapReference = (obj: any): string | undefined => { const extractSapReference = (obj: any): string | undefined => {
if (!obj) return undefined; if (!obj) return undefined;
const getFieldValue = (fieldName: string): any => { const getFieldValue = (fieldName: string): any => {
const field = obj[fieldName]; const field = obj[fieldName];
if (field === undefined || field === null) return null; if (field === undefined || field === null) return null;
// If it's an object with #text property (XML parser sometimes does this) // If it's an object with #text property (XML parser sometimes does this)
if (typeof field === 'object' && field['#text'] !== undefined) { if (typeof field === 'object' && field['#text'] !== undefined) {
return field['#text']; return field['#text'];
} }
// Direct value // Direct value
return field; return field;
}; };
// Try various field name variations for SAP reference number // Try various field name variations for SAP reference number
const value = getFieldValue('d:Sap_Reference_no') ?? // XML format with namespace prefix (PRIORITY) const value = getFieldValue('d:Sap_Reference_no') ?? // XML format with namespace prefix (PRIORITY)
getFieldValue('Sap_Reference_no') ?? // XML format without prefix getFieldValue('Sap_Reference_no') ?? // XML format without prefix
getFieldValue('d:SapReferenceNo') ?? getFieldValue('d:SapReferenceNo') ??
getFieldValue('SapReferenceNo') ?? getFieldValue('SapReferenceNo') ??
getFieldValue('d:Reference') ?? getFieldValue('d:Reference') ??
getFieldValue('Reference') ?? getFieldValue('Reference') ??
getFieldValue('d:BlockId') ?? getFieldValue('d:BlockId') ??
getFieldValue('BlockId') ?? getFieldValue('BlockId') ??
getFieldValue('d:DocumentNumber') ?? getFieldValue('d:DocumentNumber') ??
getFieldValue('DocumentNumber') ?? getFieldValue('DocumentNumber') ??
null; null;
if (value === null || value === undefined) { if (value === null || value === undefined) {
logger.debug(`[SAP] extractSapReference: No value found. Object keys:`, Object.keys(obj)); logger.debug(`[SAP] extractSapReference: No value found. Object keys:`, Object.keys(obj));
return undefined; return undefined;
} }
// Convert to string and trim // Convert to string and trim
const valueStr = String(value).trim(); const valueStr = String(value).trim();
logger.debug(`[SAP] extractSapReference: Extracted value "${valueStr}"`); logger.debug(`[SAP] extractSapReference: Extracted value "${valueStr}"`);
return valueStr || undefined; return valueStr || undefined;
}; };
// Extract SAP reference number using helper function // Extract SAP reference number using helper function
blockId = extractSapReference(ioOutputData) || extractSapReference(mainEntryProperties) || undefined; blockId = extractSapReference(ioOutputData) || extractSapReference(mainEntryProperties) || undefined;
// Log detailed information for debugging // Log detailed information for debugging
logger.info(`[SAP] Extracted from XML lt_io_output:`, { logger.info(`[SAP] Extracted from XML lt_io_output:`, {
message, message,
@ -896,10 +902,10 @@ export class SAPIntegrationService {
sampleKeys: Object.keys(ioOutputData).slice(0, 10), // First 10 keys for debugging sampleKeys: Object.keys(ioOutputData).slice(0, 10), // First 10 keys for debugging
foundInMainEntry: remainingBalance > 0 && mainEntryProperties ? true : false, foundInMainEntry: remainingBalance > 0 && mainEntryProperties ? true : false,
ioOutputDataSample: Object.keys(ioOutputData).reduce((acc: any, key: string) => { ioOutputDataSample: Object.keys(ioOutputData).reduce((acc: any, key: string) => {
if (key.toLowerCase().includes('available') || if (key.toLowerCase().includes('available') ||
key.toLowerCase().includes('amount') || key.toLowerCase().includes('amount') ||
key.toLowerCase().includes('reference') || key.toLowerCase().includes('reference') ||
key.toLowerCase().includes('sap')) { key.toLowerCase().includes('sap')) {
acc[key] = ioOutputData[key]; acc[key] = ioOutputData[key];
} }
return acc; return acc;
@ -933,7 +939,7 @@ export class SAPIntegrationService {
logger.warn('[SAP] Budget block response structure unclear, assuming success'); logger.warn('[SAP] Budget block response structure unclear, assuming success');
logger.warn('[SAP] Response data keys:', Object.keys(responseData || {})); logger.warn('[SAP] Response data keys:', Object.keys(responseData || {}));
} }
// Log what we extracted // Log what we extracted
logger.info(`[SAP] Extracted from response:`, { logger.info(`[SAP] Extracted from response:`, {
success, success,
@ -947,7 +953,7 @@ export class SAPIntegrationService {
hasMainEntryProperties: !!mainEntryProperties, hasMainEntryProperties: !!mainEntryProperties,
mainEntryPropertiesKeys: mainEntryProperties ? Object.keys(mainEntryProperties) : null mainEntryPropertiesKeys: mainEntryProperties ? Object.keys(mainEntryProperties) : null
}); });
// If ioOutputData exists but we didn't extract values, log detailed info // If ioOutputData exists but we didn't extract values, log detailed info
if (ioOutputData && (remainingBalance === 0 || !blockId)) { if (ioOutputData && (remainingBalance === 0 || !blockId)) {
logger.warn(`[SAP] ⚠️ ioOutputData exists but extraction failed. Full ioOutputData:`, JSON.stringify(ioOutputData, null, 2)); logger.warn(`[SAP] ⚠️ ioOutputData exists but extraction failed. Full ioOutputData:`, JSON.stringify(ioOutputData, null, 2));
@ -957,7 +963,7 @@ export class SAPIntegrationService {
return acc; return acc;
}, {})); }, {}));
} }
// If remaining balance is 0, log the full response structure for debugging // If remaining balance is 0, log the full response structure for debugging
if (remainingBalance === 0 && response.status === 200 || response.status === 201) { if (remainingBalance === 0 && response.status === 200 || response.status === 201) {
logger.warn(`[SAP] ⚠️ Remaining balance is 0, but request was successful. Full response structure:`, JSON.stringify(responseData, null, 2)); logger.warn(`[SAP] ⚠️ Remaining balance is 0, but request was successful. Full response structure:`, JSON.stringify(responseData, null, 2));
@ -965,7 +971,7 @@ export class SAPIntegrationService {
if (success) { if (success) {
logger.info(`[SAP] Budget blocked successfully for IO ${ioNumber}. Blocked: ${blockedAmount}, Remaining: ${remainingBalance}`); logger.info(`[SAP] Budget blocked successfully for IO ${ioNumber}. Blocked: ${blockedAmount}, Remaining: ${remainingBalance}`);
// Only return blockId if SAP provided a reference number // Only return blockId if SAP provided a reference number
// Don't generate a fallback - we want the actual SAP document number // Don't generate a fallback - we want the actual SAP document number
if (blockId) { if (blockId) {
@ -973,7 +979,7 @@ export class SAPIntegrationService {
} else { } else {
logger.warn(`[SAP] ⚠️ No SAP Reference Number (Sap_Reference_no) found in response`); logger.warn(`[SAP] ⚠️ No SAP Reference Number (Sap_Reference_no) found in response`);
} }
return { return {
success: true, success: true,
blockId: blockId || undefined, // Only return actual SAP reference number, no fallback blockId: blockId || undefined, // Only return actual SAP reference number, no fallback
@ -994,7 +1000,7 @@ export class SAPIntegrationService {
logger.error(`[SAP] Authentication failed during budget blocking (Status: ${response.status}) - check SAP credentials`); logger.error(`[SAP] Authentication failed during budget blocking (Status: ${response.status}) - check SAP credentials`);
logger.error(`[SAP] Response data:`, response.data); logger.error(`[SAP] Response data:`, response.data);
logger.error(`[SAP] Response headers:`, response.headers); logger.error(`[SAP] Response headers:`, response.headers);
// Check if it's actually a CSRF error disguised as auth error // Check if it's actually a CSRF error disguised as auth error
const responseText = JSON.stringify(response.data || {}); const responseText = JSON.stringify(response.data || {});
if (responseText.includes('CSRF') || responseText.includes('csrf') || responseText.includes('token')) { if (responseText.includes('CSRF') || responseText.includes('csrf') || responseText.includes('token')) {
@ -1006,7 +1012,7 @@ export class SAPIntegrationService {
error: 'SAP CSRF token validation failed - token may have expired or be invalid' error: 'SAP CSRF token validation failed - token may have expired or be invalid'
}; };
} }
return { return {
success: false, success: false,
blockedAmount: 0, blockedAmount: 0,
@ -1016,15 +1022,15 @@ export class SAPIntegrationService {
} else { } else {
// Handle 400 Bad Request - usually means invalid request format // Handle 400 Bad Request - usually means invalid request format
let errorMessage = `SAP API returned status ${response.status}`; let errorMessage = `SAP API returned status ${response.status}`;
if (response.status === 400) { if (response.status === 400) {
errorMessage = 'SAP API returned 400 Bad Request - check request payload format'; errorMessage = 'SAP API returned 400 Bad Request - check request payload format';
// Try to extract error message from response // Try to extract error message from response
if (response.data) { if (response.data) {
try { try {
const errorData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; const errorData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
// Check for common SAP error fields // Check for common SAP error fields
if (errorData.error) { if (errorData.error) {
errorMessage = errorData.error.message?.value || errorData.error.message || errorMessage; errorMessage = errorData.error.message?.value || errorData.error.message || errorMessage;
@ -1035,14 +1041,14 @@ export class SAPIntegrationService {
} else if (errorData.d?.error) { } else if (errorData.d?.error) {
errorMessage = errorData.d.error.message?.value || errorData.d.error.message || errorMessage; errorMessage = errorData.d.error.message?.value || errorData.d.error.message || errorMessage;
} }
logger.error(`[SAP] SAP Error Details:`, JSON.stringify(errorData, null, 2)); logger.error(`[SAP] SAP Error Details:`, JSON.stringify(errorData, null, 2));
} catch (e) { } catch (e) {
logger.error(`[SAP] Error parsing response data:`, response.data); logger.error(`[SAP] Error parsing response data:`, response.data);
} }
} }
} }
logger.error(`[SAP] Unexpected response status during budget blocking: ${response.status}`); logger.error(`[SAP] Unexpected response status during budget blocking: ${response.status}`);
logger.error(`[SAP] Response data:`, response.data); logger.error(`[SAP] Response data:`, response.data);
return { return {
@ -1054,7 +1060,7 @@ export class SAPIntegrationService {
} }
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
if (axiosError.response) { if (axiosError.response) {
// SAP returned an error response // SAP returned an error response
logger.error(`[SAP] Error blocking budget for IO ${ioNumber}:`, { logger.error(`[SAP] Error blocking budget for IO ${ioNumber}:`, {
@ -1062,7 +1068,7 @@ export class SAPIntegrationService {
statusText: axiosError.response.statusText, statusText: axiosError.response.statusText,
data: axiosError.response.data data: axiosError.response.data
}); });
return { return {
success: false, success: false,
blockedAmount: 0, blockedAmount: 0,
@ -1117,22 +1123,7 @@ export class SAPIntegrationService {
}; };
} }
// TODO: Implement actual SAP API call to release budget
// Example:
// const response = await axios.post(`${this.sapBaseUrl}/api/io/${ioNumber}/release`, {
// block_id: blockId,
// reference: requestNumber
// }, {
// headers: {
// 'Authorization': `Bearer ${this.sapApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// success: response.data.success,
// releasedAmount: response.data.released_amount
// };
logger.warn('[SAP] SAP budget release not implemented, simulating release'); logger.warn('[SAP] SAP budget release not implemented, simulating release');
return { return {
@ -1177,23 +1168,7 @@ export class SAPIntegrationService {
}; };
} }
// TODO: Implement actual SAP API call to get dealer info
// Example:
// const response = await axios.get(`${this.sapBaseUrl}/api/dealers/${dealerCode}`, {
// headers: {
// 'Authorization': `Bearer ${this.sapApiKey}`,
// 'Content-Type': 'application/json'
// }
// });
//
// return {
// isValid: response.data.valid,
// dealerCode: response.data.dealer_code,
// dealerName: response.data.dealer_name,
// dealerEmail: response.data.dealer_email,
// dealerPhone: response.data.dealer_phone,
// dealerAddress: response.data.dealer_address
// };
logger.warn('[SAP] SAP dealer lookup not implemented, returning mock data'); logger.warn('[SAP] SAP dealer lookup not implemented, returning mock data');
return { return {

View File

@ -2459,6 +2459,7 @@ export class WorkflowService {
requestNumber, requestNumber,
initiatorId, initiatorId,
templateType: workflowData.templateType, templateType: workflowData.templateType,
workflowType: workflowData.workflowType || 'NON_TEMPLATIZED',
title: workflowData.title ? sanitizeHtml(workflowData.title) : workflowData.title, title: workflowData.title ? sanitizeHtml(workflowData.title) : workflowData.title,
description: workflowData.description ? sanitizeHtml(workflowData.description) : workflowData.description, description: workflowData.description ? sanitizeHtml(workflowData.description) : workflowData.description,
priority: workflowData.priority, priority: workflowData.priority,
@ -2471,29 +2472,29 @@ export class WorkflowService {
submissionDate: isDraftRequested ? undefined : now submissionDate: isDraftRequested ? undefined : now
}); });
// Create approval levels // Create approval levels if skipCreation is false
for (const levelData of workflowData.approvalLevels) { if (!workflowData.skipCreation) {
await ApprovalLevel.create({ for (const levelData of workflowData.approvalLevels) {
requestId: workflow.requestId, await ApprovalLevel.create({
levelNumber: levelData.levelNumber, requestId: workflow.requestId,
levelName: levelData.levelName, levelNumber: levelData.levelNumber,
approverId: levelData.approverId, levelName: levelData.levelName,
approverEmail: levelData.approverEmail, approverId: levelData.approverId,
approverName: levelData.approverName, approverEmail: levelData.approverEmail,
tatHours: levelData.tatHours, approverName: levelData.approverName,
// tatDays is auto-calculated by database as a generated column tatHours: levelData.tatHours,
status: ApprovalStatus.PENDING, // tatDays is auto-calculated by database as a generated column
elapsedHours: 0, status: ApprovalStatus.PENDING,
remainingHours: levelData.tatHours, elapsedHours: 0,
tatPercentageUsed: 0, remainingHours: levelData.tatHours,
isFinalApprover: levelData.isFinalApprover || false tatPercentageUsed: 0,
}); isFinalApprover: levelData.isFinalApprover || false
});
}
} }
// Create participants if provided // Create participants if provided and skipCreation is false
// Deduplicate participants by userId (database has unique constraint on request_id + user_id) if (workflowData.participants && !workflowData.skipCreation) {
// Priority: INITIATOR > APPROVER > SPECTATOR (keep the highest privilege role)
if (workflowData.participants) {
const participantMap = new Map<string, typeof workflowData.participants[0]>(); const participantMap = new Map<string, typeof workflowData.participants[0]>();
const rolePriority: Record<string, number> = { const rolePriority: Record<string, number> = {
'INITIATOR': 3, 'INITIATOR': 3,
@ -2568,13 +2569,15 @@ export class WorkflowService {
} }
return workflow; return workflow;
} catch (error) { } catch (error: any) {
logWithContext('error', 'Failed to create workflow', { logWithContext('error', 'Failed to create workflow', {
userId: initiatorId, userId: initiatorId,
priority: workflowData.priority, 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; requestId: string;
requestNumber: string; requestNumber: string;
initiatorId: string; initiatorId: string;
templateType: 'CUSTOM' | 'TEMPLATE'; templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
workflowType?: string;
title: string; title: string;
description: string; description: string;
priority: Priority; priority: Priority;
@ -23,13 +24,15 @@ export interface WorkflowRequest {
} }
export interface CreateWorkflowRequest { export interface CreateWorkflowRequest {
templateType: 'CUSTOM' | 'TEMPLATE'; templateType: 'CUSTOM' | 'TEMPLATE' | 'DEALER CLAIM';
workflowType?: string;
title: string; title: string;
description: string; description: string;
priority: Priority; priority: Priority;
approvalLevels: CreateApprovalLevel[]; approvalLevels: CreateApprovalLevel[];
participants?: CreateParticipant[]; participants?: CreateParticipant[];
isDraft?: boolean; isDraft?: boolean;
skipCreation?: boolean; // Flag to skip record creation if handled manually by calling service
} }
export interface UpdateWorkflowRequest { export interface UpdateWorkflowRequest {

View File

@ -20,8 +20,9 @@ const SENSITIVE_PATTERN = new RegExp(
/** /**
* Mask sensitive values in strings (API keys, passwords, tokens) * 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') { if (typeof value === 'string') {
// Mask patterns like "API_KEY = abc123" or "password: secret" // Mask patterns like "API_KEY = abc123" or "password: secret"
let masked = value.replace(SENSITIVE_PATTERN, (match, key, val) => { let masked = value.replace(SENSITIVE_PATTERN, (match, key, val) => {
@ -45,10 +46,24 @@ const maskSensitiveData = (value: any): any => {
} }
if (Array.isArray(value)) { 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') { 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 = {}; const masked: any = {};
for (const [k, v] of Object.entries(value)) { for (const [k, v] of Object.entries(value)) {
const keyLower = k.toLowerCase(); const keyLower = k.toLowerCase();
@ -56,7 +71,7 @@ const maskSensitiveData = (value: any): any => {
if (SENSITIVE_KEYS.some(sk => keyLower.includes(sk))) { if (SENSITIVE_KEYS.some(sk => keyLower.includes(sk))) {
masked[k] = typeof v === 'string' && v.length > 0 ? '***REDACTED***' : v; masked[k] = typeof v === 'string' && v.length > 0 ? '***REDACTED***' : v;
} else { } else {
masked[k] = maskSensitiveData(v); masked[k] = maskSensitiveData(v, visited);
} }
} }
return masked; return masked;