Compare commits
No commits in common. "651576f51d96ed36d1fa7dcf72dffd6e23b2e2ab" and "51fcedca1b697fcfc8cb1847c3049ab05a816f9a" have entirely different histories.
651576f51d
...
51fcedca1b
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-BuDjHQd8.js";import"./radix-vendor-DA0cB_hD.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BMozKGOM.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
import{a as t}from"./index-fG9vuU_E.js";import"./radix-vendor-DA0cB_hD.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BPwaxA-i.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-XzPsIGYn.js.map
|
//# sourceMappingURL=conclusionApi-CFqAjzFU.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-XzPsIGYn.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
{"version":3,"file":"conclusionApi-CFqAjzFU.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/assets/index-DAM_E-zB.css
Normal file
1
build/assets/index-DAM_E-zB.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
70
build/assets/index-fG9vuU_E.js
Normal file
70
build/assets/index-fG9vuU_E.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-fG9vuU_E.js.map
Normal file
1
build/assets/index-fG9vuU_E.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
|||||||
import{g as s}from"./index-BuDjHQd8.js";import"./radix-vendor-DA0cB_hD.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BMozKGOM.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";function R(o){const{requestId:e,status:t,request:a,navigate:r}=o;if((t==null?void 0:t.toLowerCase())==="draft"||t==="DRAFT"){r(`/edit-request/${e}`);return}const i=s(e);r(i)}export{R as navigateToRequest};
|
import{g as s}from"./index-fG9vuU_E.js";import"./radix-vendor-DA0cB_hD.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BPwaxA-i.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";function R(o){const{requestId:e,status:t,request:a,navigate:r}=o;if((t==null?void 0:t.toLowerCase())==="draft"||t==="DRAFT"){r(`/edit-request/${e}`);return}const i=s(e);r(i)}export{R as navigateToRequest};
|
||||||
//# sourceMappingURL=requestNavigation-Dsrv0hfs.js.map
|
//# sourceMappingURL=requestNavigation-KN4bh371.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"requestNavigation-Dsrv0hfs.js","sources":["../../src/utils/requestNavigation.ts"],"sourcesContent":["/**\r\n * Global Request Navigation Utility\r\n * \r\n * Centralized navigation logic for request-related routes.\r\n * This utility decides where to navigate when clicking on request cards\r\n * from anywhere in the application.\r\n * \r\n * Features:\r\n * - Single point of navigation logic\r\n * - Handles draft vs active requests\r\n * - Supports different flow types (CUSTOM, DEALER_CLAIM)\r\n * - Type-safe navigation\r\n */\r\n\r\nimport { NavigateFunction } from 'react-router-dom';\r\nimport { getRequestDetailRoute, RequestFlowType } from './requestTypeUtils';\r\n\r\nexport interface RequestNavigationOptions {\r\n requestId: string;\r\n requestTitle?: string;\r\n status?: string;\r\n request?: any; // Full request object if available\r\n navigate: NavigateFunction;\r\n}\r\n\r\n/**\r\n * Navigate to the appropriate request detail page based on request type\r\n * \r\n * This is the single point of navigation for all request cards.\r\n * It handles:\r\n * - Draft requests (navigate to edit)\r\n * - Different flow types (CUSTOM, DEALER_CLAIM)\r\n * - Status-based routing\r\n */\r\nexport function navigateToRequest(options: RequestNavigationOptions): void {\r\n const { requestId, status, request, navigate } = options;\r\n\r\n // Check if request is a draft - if so, route to edit form instead of detail view\r\n const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';\r\n if (isDraft) {\r\n navigate(`/edit-request/${requestId}`);\r\n return;\r\n }\r\n\r\n // Determine the appropriate route based on request type\r\n const route = getRequestDetailRoute(requestId, request);\r\n navigate(route);\r\n}\r\n\r\n/**\r\n * Navigate to create a new request based on flow type\r\n */\r\nexport function navigateToCreateRequest(\r\n navigate: NavigateFunction,\r\n flowType: RequestFlowType = 'CUSTOM'\r\n): void {\r\n const route = flowType === 'DEALER_CLAIM' \r\n ? '/claim-management' \r\n : '/new-request';\r\n navigate(route);\r\n}\r\n\r\n/**\r\n * Create a navigation handler function for request cards\r\n * This can be used directly in onClick handlers\r\n */\r\nexport function createRequestNavigationHandler(\r\n navigate: NavigateFunction\r\n) {\r\n return (requestId: string, requestTitle?: string, status?: string, request?: any) => {\r\n navigateToRequest({\r\n requestId,\r\n requestTitle,\r\n status,\r\n request,\r\n navigate,\r\n });\r\n };\r\n}\r\n"],"names":["navigateToRequest","options","requestId","status","request","navigate","route","getRequestDetailRoute"],"mappings":"6RAkCO,SAASA,EAAkBC,EAAyC,CACzE,KAAM,CAAE,UAAAC,EAAW,OAAAC,EAAQ,QAAAC,EAAS,SAAAC,GAAaJ,EAIjD,IADgBE,GAAA,YAAAA,EAAQ,iBAAkB,SAAWA,IAAW,QACnD,CACXE,EAAS,iBAAiBH,CAAS,EAAE,EACrC,MACF,CAGA,MAAMI,EAAQC,EAAsBL,CAAkB,EACtDG,EAASC,CAAK,CAChB"}
|
{"version":3,"file":"requestNavigation-KN4bh371.js","sources":["../../src/utils/requestNavigation.ts"],"sourcesContent":["/**\r\n * Global Request Navigation Utility\r\n * \r\n * Centralized navigation logic for request-related routes.\r\n * This utility decides where to navigate when clicking on request cards\r\n * from anywhere in the application.\r\n * \r\n * Features:\r\n * - Single point of navigation logic\r\n * - Handles draft vs active requests\r\n * - Supports different flow types (CUSTOM, DEALER_CLAIM)\r\n * - Type-safe navigation\r\n */\r\n\r\nimport { NavigateFunction } from 'react-router-dom';\r\nimport { getRequestDetailRoute, RequestFlowType } from './requestTypeUtils';\r\n\r\nexport interface RequestNavigationOptions {\r\n requestId: string;\r\n requestTitle?: string;\r\n status?: string;\r\n request?: any; // Full request object if available\r\n navigate: NavigateFunction;\r\n}\r\n\r\n/**\r\n * Navigate to the appropriate request detail page based on request type\r\n * \r\n * This is the single point of navigation for all request cards.\r\n * It handles:\r\n * - Draft requests (navigate to edit)\r\n * - Different flow types (CUSTOM, DEALER_CLAIM)\r\n * - Status-based routing\r\n */\r\nexport function navigateToRequest(options: RequestNavigationOptions): void {\r\n const { requestId, status, request, navigate } = options;\r\n\r\n // Check if request is a draft - if so, route to edit form instead of detail view\r\n const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';\r\n if (isDraft) {\r\n navigate(`/edit-request/${requestId}`);\r\n return;\r\n }\r\n\r\n // Determine the appropriate route based on request type\r\n const route = getRequestDetailRoute(requestId, request);\r\n navigate(route);\r\n}\r\n\r\n/**\r\n * Navigate to create a new request based on flow type\r\n */\r\nexport function navigateToCreateRequest(\r\n navigate: NavigateFunction,\r\n flowType: RequestFlowType = 'CUSTOM'\r\n): void {\r\n const route = flowType === 'DEALER_CLAIM' \r\n ? '/claim-management' \r\n : '/new-request';\r\n navigate(route);\r\n}\r\n\r\n/**\r\n * Create a navigation handler function for request cards\r\n * This can be used directly in onClick handlers\r\n */\r\nexport function createRequestNavigationHandler(\r\n navigate: NavigateFunction\r\n) {\r\n return (requestId: string, requestTitle?: string, status?: string, request?: any) => {\r\n navigateToRequest({\r\n requestId,\r\n requestTitle,\r\n status,\r\n request,\r\n navigate,\r\n });\r\n };\r\n}\r\n"],"names":["navigateToRequest","options","requestId","status","request","navigate","route","getRequestDetailRoute"],"mappings":"6RAkCO,SAASA,EAAkBC,EAAyC,CACzE,KAAM,CAAE,UAAAC,EAAW,OAAAC,EAAQ,QAAAC,EAAS,SAAAC,GAAaJ,EAIjD,IADgBE,GAAA,YAAAA,EAAQ,iBAAkB,SAAWA,IAAW,QACnD,CACXE,EAAS,iBAAiBH,CAAS,EAAE,EACrC,MACF,CAGA,MAAMI,EAAQC,EAAsBL,CAAkB,EACtDG,EAASC,CAAK,CAChB"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,15 +52,15 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-BuDjHQd8.js"></script>
|
<script type="module" crossorigin src="/assets/index-fG9vuU_E.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DA0cB_hD.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DA0cB_hD.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BMozKGOM.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BPwaxA-i.js">
|
||||||
<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-CRr9x_Jp.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DImBgs5K.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DAM_E-zB.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
# Tanflow SSO User Data Mapping
|
|
||||||
|
|
||||||
This document outlines all user information available from Tanflow IAM Suite and how it maps to our User model for user creation.
|
|
||||||
|
|
||||||
## Tanflow Userinfo Endpoint Response
|
|
||||||
|
|
||||||
Tanflow uses **OpenID Connect (OIDC) standard claims** via the `/protocol/openid-connect/userinfo` endpoint. The following fields are available:
|
|
||||||
|
|
||||||
### Standard OIDC Claims (Available from Tanflow)
|
|
||||||
|
|
||||||
| Tanflow Field | OIDC Standard Claim | Type | Description | Currently Extracted |
|
|
||||||
|--------------|---------------------|------|--------------|-------------------|
|
|
||||||
| `sub` | `sub` | string | **REQUIRED** - Subject identifier (unique user ID) | ✅ Yes (as `oktaSub`) |
|
|
||||||
| `email` | `email` | string | Email address | ✅ Yes |
|
|
||||||
| `email_verified` | `email_verified` | boolean | Email verification status | ❌ No |
|
|
||||||
| `preferred_username` | `preferred_username` | string | Preferred username (fallback for email) | ✅ Yes (fallback) |
|
|
||||||
| `name` | `name` | string | Full display name | ✅ Yes (as `displayName`) |
|
|
||||||
| `given_name` | `given_name` | string | First name | ✅ Yes (as `firstName`) |
|
|
||||||
| `family_name` | `family_name` | string | Last name | ✅ Yes (as `lastName`) |
|
|
||||||
| `phone_number` | `phone_number` | string | Phone number | ✅ Yes (as `phone`) |
|
|
||||||
| `phone_number_verified` | `phone_number_verified` | boolean | Phone verification status | ❌ No |
|
|
||||||
| `address` | `address` | object | Address object (structured) | ❌ No |
|
|
||||||
| `locale` | `locale` | string | User locale (e.g., "en-US") | ❌ No |
|
|
||||||
| `picture` | `picture` | string | Profile picture URL | ❌ No |
|
|
||||||
| `website` | `website` | string | Website URL | ❌ No |
|
|
||||||
| `profile` | `profile` | string | Profile page URL | ❌ No |
|
|
||||||
| `birthdate` | `birthdate` | string | Date of birth | ❌ No |
|
|
||||||
| `gender` | `gender` | string | Gender | ❌ No |
|
|
||||||
| `zoneinfo` | `zoneinfo` | string | Timezone (e.g., "America/New_York") | ❌ No |
|
|
||||||
| `updated_at` | `updated_at` | number | Last update timestamp | ❌ No |
|
|
||||||
|
|
||||||
### Custom Tanflow Claims (May be available)
|
|
||||||
|
|
||||||
These are **custom claims** that Tanflow may include based on their configuration:
|
|
||||||
|
|
||||||
| Tanflow Field | Type | Description | Currently Extracted |
|
|
||||||
|--------------|------|-------------|-------------------|
|
|
||||||
| `employeeId` | string | Employee ID from HR system | ✅ Yes |
|
|
||||||
| `employee_id` | string | Alternative employee ID field | ✅ Yes (fallback) |
|
|
||||||
| `department` | string | Department/Division | ✅ Yes |
|
|
||||||
| `designation` | string | Job designation/position | ✅ Yes |
|
|
||||||
| `title` | string | Job title | ✅ Yes (as `designation` fallback) |
|
|
||||||
| `organization` | string | Organization name | ❌ No |
|
|
||||||
| `division` | string | Division name | ❌ No |
|
|
||||||
| `location` | string | Office location | ❌ No |
|
|
||||||
| `manager` | string | Manager name/email | ❌ No |
|
|
||||||
| `manager_id` | string | Manager employee ID | ❌ No |
|
|
||||||
| `cost_center` | string | Cost center code | ❌ No |
|
|
||||||
| `employee_type` | string | Employee type (Full-time, Contract, etc.) | ❌ No |
|
|
||||||
| `hire_date` | string | Date of hire | ❌ No |
|
|
||||||
| `office_location` | string | Office location | ❌ No |
|
|
||||||
| `country` | string | Country code | ❌ No |
|
|
||||||
| `city` | string | City name | ❌ No |
|
|
||||||
| `state` | string | State/Province | ❌ No |
|
|
||||||
| `postal_code` | string | Postal/ZIP code | ❌ No |
|
|
||||||
| `groups` | array | Group memberships | ❌ No |
|
|
||||||
| `roles` | array | User roles | ❌ No |
|
|
||||||
|
|
||||||
## Current Extraction Logic
|
|
||||||
|
|
||||||
**Location:** `Re_Backend/src/services/auth.service.ts` → `exchangeTanflowCodeForTokens()`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const userData: SSOUserData = {
|
|
||||||
oktaSub: tanflowSub, // Reuse oktaSub field for Tanflow sub
|
|
||||||
email: tanflowUserInfo.email || tanflowUserInfo.preferred_username || '',
|
|
||||||
employeeId: tanflowUserInfo.employeeId || tanflowUserInfo.employee_id || undefined,
|
|
||||||
firstName: tanflowUserInfo.given_name || tanflowUserInfo.firstName || undefined,
|
|
||||||
lastName: tanflowUserInfo.family_name || tanflowUserInfo.lastName || undefined,
|
|
||||||
displayName: tanflowUserInfo.name || tanflowUserInfo.displayName || undefined,
|
|
||||||
department: tanflowUserInfo.department || undefined,
|
|
||||||
designation: tanflowUserInfo.title || tanflowUserInfo.designation || undefined,
|
|
||||||
phone: tanflowUserInfo.phone_number || tanflowUserInfo.phone || undefined,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Model Fields Mapping
|
|
||||||
|
|
||||||
**Location:** `Re_Backend/src/models/User.ts`
|
|
||||||
|
|
||||||
| User Model Field | Tanflow Source | Required | Notes |
|
|
||||||
|-----------------|----------------|----------|-------|
|
|
||||||
| `userId` | Auto-generated UUID | ✅ | Primary key |
|
|
||||||
| `oktaSub` | `sub` | ✅ | Unique identifier from Tanflow |
|
|
||||||
| `email` | `email` or `preferred_username` | ✅ | Primary identifier |
|
|
||||||
| `employeeId` | `employeeId` or `employee_id` | ❌ | Optional HR system ID |
|
|
||||||
| `firstName` | `given_name` or `firstName` | ❌ | Optional |
|
|
||||||
| `lastName` | `family_name` or `lastName` | ❌ | Optional |
|
|
||||||
| `displayName` | `name` or `displayName` | ❌ | Auto-generated if missing |
|
|
||||||
| `department` | `department` | ❌ | Optional |
|
|
||||||
| `designation` | `title` or `designation` | ❌ | Optional |
|
|
||||||
| `phone` | `phone_number` or `phone` | ❌ | Optional |
|
|
||||||
| `manager` | `manager` | ❌ | **NOT currently extracted** |
|
|
||||||
| `secondEmail` | N/A | ❌ | Not available from Tanflow |
|
|
||||||
| `jobTitle` | `title` | ❌ | **NOT currently extracted** |
|
|
||||||
| `employeeNumber` | N/A | ❌ | Not available from Tanflow |
|
|
||||||
| `postalAddress` | `address` (structured) | ❌ | **NOT currently extracted** |
|
|
||||||
| `mobilePhone` | N/A | ❌ | Not available from Tanflow |
|
|
||||||
| `adGroups` | `groups` | ❌ | **NOT currently extracted** |
|
|
||||||
| `location` | `address`, `city`, `state`, `country` | ❌ | **NOT currently extracted** |
|
|
||||||
| `role` | Default: 'USER' | ✅ | Default role assigned |
|
|
||||||
| `isActive` | Default: true | ✅ | Auto-set to true |
|
|
||||||
| `lastLogin` | Current timestamp | ✅ | Auto-set on login |
|
|
||||||
|
|
||||||
## Recommended Enhancements
|
|
||||||
|
|
||||||
### 1. Extract Additional Fields
|
|
||||||
|
|
||||||
Consider extracting these fields if available from Tanflow:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Enhanced extraction (to be implemented)
|
|
||||||
const userData: SSOUserData = {
|
|
||||||
// ... existing fields ...
|
|
||||||
|
|
||||||
// Additional fields
|
|
||||||
manager: tanflowUserInfo.manager || undefined,
|
|
||||||
jobTitle: tanflowUserInfo.title || tanflowUserInfo.designation || undefined,
|
|
||||||
postalAddress: tanflowUserInfo.address ? JSON.stringify(tanflowUserInfo.address) : undefined,
|
|
||||||
adGroups: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups : undefined,
|
|
||||||
|
|
||||||
// Location object
|
|
||||||
location: {
|
|
||||||
city: tanflowUserInfo.city || undefined,
|
|
||||||
state: tanflowUserInfo.state || undefined,
|
|
||||||
country: tanflowUserInfo.country || undefined,
|
|
||||||
office: tanflowUserInfo.office_location || undefined,
|
|
||||||
timezone: tanflowUserInfo.zoneinfo || undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Log Available Fields
|
|
||||||
|
|
||||||
Add logging to see what Tanflow actually returns:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
logger.info('Tanflow userinfo response', {
|
|
||||||
availableFields: Object.keys(tanflowUserInfo),
|
|
||||||
hasEmail: !!tanflowUserInfo.email,
|
|
||||||
hasEmployeeId: !!(tanflowUserInfo.employeeId || tanflowUserInfo.employee_id),
|
|
||||||
hasDepartment: !!tanflowUserInfo.department,
|
|
||||||
hasManager: !!tanflowUserInfo.manager,
|
|
||||||
hasGroups: Array.isArray(tanflowUserInfo.groups),
|
|
||||||
groupsCount: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups.length : 0,
|
|
||||||
sampleData: {
|
|
||||||
sub: tanflowUserInfo.sub?.substring(0, 10) + '...',
|
|
||||||
email: tanflowUserInfo.email?.substring(0, 10) + '...',
|
|
||||||
name: tanflowUserInfo.name,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Creation Flow
|
|
||||||
|
|
||||||
1. **Token Exchange** → Get `access_token` from Tanflow
|
|
||||||
2. **Userinfo Call** → Call `/protocol/openid-connect/userinfo` with `access_token`
|
|
||||||
3. **Extract Data** → Map Tanflow fields to `SSOUserData` interface
|
|
||||||
4. **User Lookup** → Check if user exists by `email`
|
|
||||||
5. **Create/Update** → Create new user or update existing user
|
|
||||||
6. **Generate Tokens** → Generate JWT access/refresh tokens
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
1. **Test with Real Tanflow Account**
|
|
||||||
- Log actual userinfo response
|
|
||||||
- Document all available fields
|
|
||||||
- Verify field mappings
|
|
||||||
|
|
||||||
2. **Handle Missing Fields**
|
|
||||||
- Ensure graceful fallbacks
|
|
||||||
- Don't fail if optional fields are missing
|
|
||||||
- Log warnings for missing expected fields
|
|
||||||
|
|
||||||
3. **Validate Required Fields**
|
|
||||||
- `sub` (oktaSub) - REQUIRED
|
|
||||||
- `email` or `preferred_username` - REQUIRED
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. ✅ **Current Implementation** - Basic OIDC claims extraction
|
|
||||||
2. 🔄 **Enhancement** - Extract additional custom claims (manager, groups, location)
|
|
||||||
3. 🔄 **Logging** - Add detailed logging of Tanflow response
|
|
||||||
4. 🔄 **Testing** - Test with real Tanflow account to see actual fields
|
|
||||||
5. 🔄 **Documentation** - Update this doc with actual Tanflow response structure
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Tanflow uses **Keycloak** under the hood (based on URL structure)
|
|
||||||
- Keycloak supports custom user attributes that may be available
|
|
||||||
- Some fields may require specific realm/client configuration in Tanflow
|
|
||||||
- Contact Tanflow support to confirm available custom claims
|
|
||||||
|
|
||||||
@ -12,10 +12,6 @@ const ssoConfig: SSOConfig = {
|
|||||||
oktaClientId: process.env.OKTA_CLIENT_ID || '',
|
oktaClientId: process.env.OKTA_CLIENT_ID || '',
|
||||||
oktaClientSecret: process.env.OKTA_CLIENT_SECRET || '',
|
oktaClientSecret: process.env.OKTA_CLIENT_SECRET || '',
|
||||||
oktaApiToken: process.env.OKTA_API_TOKEN || '', // SSWS token for Users API
|
oktaApiToken: process.env.OKTA_API_TOKEN || '', // SSWS token for Users API
|
||||||
// Tanflow configuration for token exchange
|
|
||||||
tanflowBaseUrl: process.env.TANFLOW_BASE_URL || 'https://ssodev.rebridge.co.in/realms/RE',
|
|
||||||
tanflowClientId: process.env.TANFLOW_CLIENT_ID || 'REFLOW',
|
|
||||||
tanflowClientSecret: process.env.TANFLOW_CLIENT_SECRET || 'cfIzMlwAMF1m4QWAP5StzZbV47HIrCox',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ssoConfig };
|
export { ssoConfig };
|
||||||
|
|||||||
@ -159,128 +159,6 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Tanflow authorization code for tokens
|
|
||||||
* POST /api/v1/auth/tanflow/token-exchange
|
|
||||||
*/
|
|
||||||
async exchangeTanflowToken(req: Request, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
logger.info('Tanflow token exchange request received', {
|
|
||||||
body: {
|
|
||||||
code: req.body?.code ? `${req.body.code.substring(0, 10)}...` : 'MISSING',
|
|
||||||
redirectUri: req.body?.redirectUri,
|
|
||||||
state: req.body?.state ? 'PRESENT' : 'MISSING',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { code, redirectUri } = validateTokenExchange(req.body);
|
|
||||||
logger.info('Tanflow token exchange validation passed', { redirectUri });
|
|
||||||
|
|
||||||
const result = await this.authService.exchangeTanflowCodeForTokens(code, redirectUri);
|
|
||||||
|
|
||||||
// Log login activity
|
|
||||||
const requestMeta = getRequestMetadata(req);
|
|
||||||
await activityService.log({
|
|
||||||
requestId: SYSTEM_EVENT_REQUEST_ID,
|
|
||||||
type: 'login',
|
|
||||||
user: {
|
|
||||||
userId: result.user.userId,
|
|
||||||
name: result.user.displayName || result.user.email,
|
|
||||||
email: result.user.email
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'User Login',
|
|
||||||
details: `User logged in via Tanflow SSO from ${requestMeta.ipAddress || 'unknown IP'}`,
|
|
||||||
metadata: {
|
|
||||||
loginMethod: 'TANFLOW_SSO',
|
|
||||||
employeeId: result.user.employeeId,
|
|
||||||
department: result.user.department,
|
|
||||||
role: result.user.role
|
|
||||||
},
|
|
||||||
ipAddress: requestMeta.ipAddress,
|
|
||||||
userAgent: requestMeta.userAgent,
|
|
||||||
category: 'AUTHENTICATION',
|
|
||||||
severity: 'INFO'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set tokens in httpOnly cookies (production) or return in body (development)
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const cookieOptions = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: isProduction,
|
|
||||||
sameSite: isProduction ? ('none' as const) : ('lax' as const),
|
|
||||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
||||||
path: '/',
|
|
||||||
};
|
|
||||||
|
|
||||||
res.cookie('accessToken', result.accessToken, cookieOptions);
|
|
||||||
res.cookie('refreshToken', result.refreshToken, cookieOptions);
|
|
||||||
|
|
||||||
// In production, don't return tokens in response body (security)
|
|
||||||
// In development, include tokens for cross-port setup
|
|
||||||
if (isProduction) {
|
|
||||||
ResponseHandler.success(res, {
|
|
||||||
user: result.user,
|
|
||||||
idToken: result.oktaIdToken, // Include id_token for Tanflow logout
|
|
||||||
}, 'Authentication successful');
|
|
||||||
} else {
|
|
||||||
ResponseHandler.success(res, {
|
|
||||||
user: result.user,
|
|
||||||
accessToken: result.accessToken,
|
|
||||||
refreshToken: result.refreshToken,
|
|
||||||
idToken: result.oktaIdToken,
|
|
||||||
}, 'Authentication successful');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Tanflow token exchange failed:', error);
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
ResponseHandler.error(res, 'Tanflow authentication failed', 400, errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh Tanflow access token
|
|
||||||
* POST /api/v1/auth/tanflow/refresh
|
|
||||||
*/
|
|
||||||
async refreshTanflowToken(req: Request, res: Response): Promise<void> {
|
|
||||||
try {
|
|
||||||
const refreshToken = req.body?.refreshToken;
|
|
||||||
|
|
||||||
if (!refreshToken) {
|
|
||||||
ResponseHandler.error(res, 'Refresh token is required', 400, 'Refresh token is required in request body');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccessToken = await this.authService.refreshTanflowToken(refreshToken);
|
|
||||||
|
|
||||||
// Set new access token in cookie
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
const cookieOptions = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: isProduction,
|
|
||||||
sameSite: isProduction ? ('none' as const) : ('lax' as const),
|
|
||||||
maxAge: 24 * 60 * 60 * 1000,
|
|
||||||
path: '/',
|
|
||||||
};
|
|
||||||
|
|
||||||
res.cookie('accessToken', newAccessToken, cookieOptions);
|
|
||||||
|
|
||||||
if (isProduction) {
|
|
||||||
ResponseHandler.success(res, {
|
|
||||||
message: 'Token refreshed successfully'
|
|
||||||
}, 'Token refreshed successfully');
|
|
||||||
} else {
|
|
||||||
ResponseHandler.success(res, {
|
|
||||||
accessToken: newAccessToken
|
|
||||||
}, 'Token refreshed successfully');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Tanflow token refresh failed:', error);
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
ResponseHandler.error(res, 'Token refresh failed', 401, errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user
|
* Logout user
|
||||||
* POST /api/v1/auth/logout
|
* POST /api/v1/auth/logout
|
||||||
|
|||||||
@ -20,18 +20,6 @@ router.post('/token-exchange',
|
|||||||
asyncHandler(authController.exchangeToken.bind(authController))
|
asyncHandler(authController.exchangeToken.bind(authController))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tanflow token exchange endpoint (no authentication required)
|
|
||||||
router.post('/tanflow/token-exchange',
|
|
||||||
validateBody(tokenExchangeSchema),
|
|
||||||
asyncHandler(authController.exchangeTanflowToken.bind(authController))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tanflow token refresh endpoint (no authentication required)
|
|
||||||
router.post('/tanflow/refresh',
|
|
||||||
validateBody(refreshTokenSchema),
|
|
||||||
asyncHandler(authController.refreshTanflowToken.bind(authController))
|
|
||||||
);
|
|
||||||
|
|
||||||
// SSO callback endpoint (no authentication required)
|
// SSO callback endpoint (no authentication required)
|
||||||
router.post('/sso-callback',
|
router.post('/sso-callback',
|
||||||
validateBody(ssoCallbackSchema),
|
validateBody(ssoCallbackSchema),
|
||||||
|
|||||||
@ -427,8 +427,7 @@ export class ApprovalService {
|
|||||||
|
|
||||||
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||||
|
|
||||||
// Check if this is Department Lead approval in a claim management workflow
|
// Check if this is Department Lead approval in a claim management workflow, and next level is Activity Creation (auto-step)
|
||||||
// Activity Creation is now an activity log only, not an approval step
|
|
||||||
const workflowType = (wf as any)?.workflowType;
|
const workflowType = (wf as any)?.workflowType;
|
||||||
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
||||||
|
|
||||||
@ -436,34 +435,50 @@ export class ApprovalService {
|
|||||||
const currentLevelName = (level.levelName || '').toLowerCase();
|
const currentLevelName = (level.levelName || '').toLowerCase();
|
||||||
const isDeptLeadApproval = currentLevelName.includes('department lead') || level.levelNumber === 3;
|
const isDeptLeadApproval = currentLevelName.includes('department lead') || level.levelNumber === 3;
|
||||||
|
|
||||||
// Check if current level is Requestor Claim Approval (Step 5, was Step 6)
|
// Check if next level is Activity Creation (by levelName or approverEmail, not hardcoded step number)
|
||||||
const currentLevelNameForStep5 = (level.levelName || '').toLowerCase();
|
const nextLevelName = (nextLevel?.levelName || '').toLowerCase();
|
||||||
const isRequestorClaimApproval = currentLevelNameForStep5.includes('requestor') &&
|
const nextLevelEmail = (nextLevel?.approverEmail || '').toLowerCase();
|
||||||
(currentLevelNameForStep5.includes('claim') || currentLevelNameForStep5.includes('approval')) ||
|
const isActivityCreationNext = nextLevelName.includes('activity creation') ||
|
||||||
level.levelNumber === 5;
|
(nextLevelEmail === 'system@royalenfield.com' && nextLevelNumber > level.levelNumber);
|
||||||
|
|
||||||
if (isClaimManagement && isDeptLeadApproval) {
|
// Check if current level is Requestor Claim Approval (Step 6) and next is E-Invoice Generation (Step 7)
|
||||||
// Activity Creation is now an activity log only - process it automatically
|
const currentLevelNameForStep6 = (level.levelName || '').toLowerCase();
|
||||||
logger.info(`[Approval] Department Lead approved for claim management workflow. Processing Activity Creation as activity log.`);
|
const isRequestorClaimApproval = currentLevelNameForStep6.includes('requestor') &&
|
||||||
|
(currentLevelNameForStep6.includes('claim') || currentLevelNameForStep6.includes('approval')) ||
|
||||||
|
level.levelNumber === 6;
|
||||||
|
|
||||||
|
const nextLevelNameForStep7 = (nextLevel?.levelName || '').toLowerCase();
|
||||||
|
const nextLevelEmailForStep7 = (nextLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isEInvoiceGenerationNext = nextLevelNameForStep7.includes('e-invoice') ||
|
||||||
|
nextLevelNameForStep7.includes('invoice generation') ||
|
||||||
|
(nextLevelEmailForStep7 === 'system@royalenfield.com' && nextLevelNumber > level.levelNumber);
|
||||||
|
|
||||||
|
if (isClaimManagement && isDeptLeadApproval && isActivityCreationNext && nextLevel) {
|
||||||
|
// Activity Creation is an auto-step - process it automatically
|
||||||
|
logger.info(`[Approval] Department Lead approved for claim management workflow. Auto-processing Activity Creation (Level ${nextLevelNumber})`);
|
||||||
try {
|
try {
|
||||||
const dealerClaimService = new DealerClaimService();
|
const dealerClaimService = new DealerClaimService();
|
||||||
await dealerClaimService.processActivityCreation(level.requestId);
|
await dealerClaimService.processActivityCreation(level.requestId);
|
||||||
logger.info(`[Approval] Activity Creation activity logged for request ${level.requestId}`);
|
logger.info(`[Approval] Activity Creation auto-processing completed for request ${level.requestId}`);
|
||||||
} catch (activityError) {
|
} catch (step4Error) {
|
||||||
logger.error(`[Approval] Error processing Activity Creation activity for request ${level.requestId}:`, activityError);
|
logger.error(`[Approval] Error auto-processing Activity Creation for request ${level.requestId}:`, step4Error);
|
||||||
// Don't fail the Department Lead approval if Activity Creation logging fails - log and continue
|
// Don't fail the Department Lead approval if Activity Creation processing fails - log and continue
|
||||||
}
|
}
|
||||||
} else if (isClaimManagement && isRequestorClaimApproval) {
|
} else if (isClaimManagement && isRequestorClaimApproval && isEInvoiceGenerationNext && nextLevel && nextLevel.approverEmail === 'system@royalenfield.com') {
|
||||||
// E-Invoice Generation is now an activity log only - will be logged when invoice is generated via DMS webhook
|
// E-Invoice Generation is an auto-step - activate it but don't process invoice generation
|
||||||
logger.info(`[Approval] Requestor Claim Approval approved for claim management workflow. E-Invoice generation will be logged as activity when DMS webhook is received.`);
|
// Invoice generation will be handled by DMS webhook when invoice is created
|
||||||
|
logger.info(`[Approval] Requestor Claim Approval approved for claim management workflow. E-Invoice Generation (Level ${nextLevelNumber}) activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
|
// E-Invoice Generation will remain in IN_PROGRESS until webhook creates invoice and auto-approves it
|
||||||
|
// Continue with normal flow to activate E-Invoice Generation
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wf && nextLevel) {
|
if (wf && nextLevel) {
|
||||||
// Normal flow - notify next approver (skip for auto-steps)
|
// Normal flow - notify next approver (skip for auto-steps)
|
||||||
// Check if it's an auto-step by checking approverEmail or levelName
|
// Check if it's an auto-step by checking approverEmail or levelName
|
||||||
// Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are no longer approval steps
|
|
||||||
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com'
|
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com'
|
||||||
|| (nextLevel as any).approverName === 'System Auto-Process';
|
|| (nextLevel as any).approverName === 'System Auto-Process'
|
||||||
|
|| (nextLevel as any).levelName === 'Activity Creation'
|
||||||
|
|| (nextLevel as any).levelName === 'E-Invoice Generation';
|
||||||
|
|
||||||
// Log approval activity
|
// Log approval activity
|
||||||
activityService.log({
|
activityService.log({
|
||||||
@ -479,8 +494,7 @@ export class ApprovalService {
|
|||||||
|
|
||||||
// Log assignment activity for next level (when it becomes active)
|
// Log assignment activity for next level (when it becomes active)
|
||||||
// IMPORTANT: Skip notifications and assignment logging for system/auto-steps
|
// IMPORTANT: Skip notifications and assignment logging for system/auto-steps
|
||||||
// System steps are any step with system@royalenfield.com
|
// System steps are: Activity Creation (Step 4), E-Invoice Generation (Step 7), and any step with system@royalenfield.com
|
||||||
// Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps
|
|
||||||
// These steps are processed automatically and should NOT trigger notifications
|
// These steps are processed automatically and should NOT trigger notifications
|
||||||
if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') {
|
if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') {
|
||||||
// Additional checks: ensure approverEmail and approverName are not system-related
|
// Additional checks: ensure approverEmail and approverName are not system-related
|
||||||
|
|||||||
@ -283,13 +283,7 @@ export class AuthService {
|
|||||||
if (userData.department) userUpdateData.department = userData.department;
|
if (userData.department) userUpdateData.department = userData.department;
|
||||||
if (userData.designation) userUpdateData.designation = userData.designation;
|
if (userData.designation) userUpdateData.designation = userData.designation;
|
||||||
if (userData.phone) userUpdateData.phone = userData.phone;
|
if (userData.phone) userUpdateData.phone = userData.phone;
|
||||||
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from SSO
|
if (userData.manager) userUpdateData.manager = userData.manager; // Manager name from Okta
|
||||||
if (userData.jobTitle) userUpdateData.jobTitle = userData.jobTitle; // Job title from SSO
|
|
||||||
if (userData.postalAddress) userUpdateData.postalAddress = userData.postalAddress; // Address from SSO
|
|
||||||
if (userData.mobilePhone) userUpdateData.mobilePhone = userData.mobilePhone; // Mobile phone from SSO
|
|
||||||
if (userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0) {
|
|
||||||
userUpdateData.adGroups = userData.adGroups; // Group memberships from SSO
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user exists by email (primary identifier)
|
// Check if user exists by email (primary identifier)
|
||||||
let user = await User.findOne({
|
let user = await User.findOne({
|
||||||
@ -319,11 +313,7 @@ export class AuthService {
|
|||||||
department: userData.department || null,
|
department: userData.department || null,
|
||||||
designation: userData.designation || null,
|
designation: userData.designation || null,
|
||||||
phone: userData.phone || null,
|
phone: userData.phone || null,
|
||||||
manager: userData.manager || null, // Manager name from SSO
|
manager: userData.manager || null, // Manager name from Okta
|
||||||
jobTitle: userData.jobTitle || null, // Job title from SSO
|
|
||||||
postalAddress: userData.postalAddress || null, // Address from SSO
|
|
||||||
mobilePhone: userData.mobilePhone || null, // Mobile phone from SSO
|
|
||||||
adGroups: userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0 ? userData.adGroups : null, // Groups from SSO
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
lastLogin: new Date()
|
lastLogin: new Date()
|
||||||
@ -796,247 +786,4 @@ export class AuthService {
|
|||||||
throw new Error(`Okta authentication failed: ${error.message || 'Unknown error'}`);
|
throw new Error(`Okta authentication failed: ${error.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Tanflow authorization code for tokens
|
|
||||||
* Similar to Okta flow but uses Tanflow IAM endpoints
|
|
||||||
*/
|
|
||||||
async exchangeTanflowCodeForTokens(code: string, redirectUri: string): Promise<LoginResponse> {
|
|
||||||
try {
|
|
||||||
// Validate configuration
|
|
||||||
if (!ssoConfig.tanflowClientId || ssoConfig.tanflowClientId.trim() === '') {
|
|
||||||
throw new Error('TANFLOW_CLIENT_ID is not configured. Please set it in your .env file.');
|
|
||||||
}
|
|
||||||
if (!ssoConfig.tanflowClientSecret || ssoConfig.tanflowClientSecret.trim() === '') {
|
|
||||||
throw new Error('TANFLOW_CLIENT_SECRET is not configured. Please set it in your .env file.');
|
|
||||||
}
|
|
||||||
if (!code || code.trim() === '') {
|
|
||||||
throw new Error('Authorization code is required');
|
|
||||||
}
|
|
||||||
if (!redirectUri || redirectUri.trim() === '') {
|
|
||||||
throw new Error('Redirect URI is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Exchanging code with Tanflow', {
|
|
||||||
redirectUri,
|
|
||||||
codePrefix: code.substring(0, 10) + '...',
|
|
||||||
tanflowBaseUrl: ssoConfig.tanflowBaseUrl,
|
|
||||||
clientId: ssoConfig.tanflowClientId,
|
|
||||||
hasClientSecret: !!ssoConfig.tanflowClientSecret,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/token`;
|
|
||||||
|
|
||||||
// Exchange authorization code for tokens
|
|
||||||
const tokenResponse = await axios.post(
|
|
||||||
tokenEndpoint,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
client_id: ssoConfig.tanflowClientId!,
|
|
||||||
client_secret: ssoConfig.tanflowClientSecret!,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
responseType: 'json',
|
|
||||||
validateStatus: (status) => status < 500,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for error response from Tanflow
|
|
||||||
if (tokenResponse.status !== 200) {
|
|
||||||
logger.error('Tanflow token exchange failed', {
|
|
||||||
status: tokenResponse.status,
|
|
||||||
statusText: tokenResponse.statusText,
|
|
||||||
data: tokenResponse.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorData = tokenResponse.data || {};
|
|
||||||
const errorMessage = errorData.error_description || errorData.error || 'Unknown error from Tanflow';
|
|
||||||
throw new Error(`Tanflow token exchange failed (${tokenResponse.status}): ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenResponse.data || typeof tokenResponse.data !== 'object') {
|
|
||||||
logger.error('Invalid response from Tanflow', {
|
|
||||||
dataType: typeof tokenResponse.data,
|
|
||||||
isArray: Array.isArray(tokenResponse.data),
|
|
||||||
data: tokenResponse.data,
|
|
||||||
});
|
|
||||||
throw new Error('Invalid response format from Tanflow');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { access_token, refresh_token, id_token } = tokenResponse.data;
|
|
||||||
|
|
||||||
if (!access_token) {
|
|
||||||
logger.error('Missing access_token in Tanflow response', {
|
|
||||||
responseKeys: Object.keys(tokenResponse.data || {}),
|
|
||||||
hasRefreshToken: !!refresh_token,
|
|
||||||
hasIdToken: !!id_token,
|
|
||||||
});
|
|
||||||
throw new Error('Failed to obtain access token from Tanflow - access_token missing in response');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Successfully obtained tokens from Tanflow', {
|
|
||||||
hasAccessToken: !!access_token,
|
|
||||||
hasRefreshToken: !!refresh_token,
|
|
||||||
hasIdToken: !!id_token,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get user info from Tanflow userinfo endpoint
|
|
||||||
const userInfoEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/userinfo`;
|
|
||||||
const userInfoResponse = await axios.get(userInfoEndpoint, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tanflowUserInfo = userInfoResponse.data;
|
|
||||||
const tanflowSub = tanflowUserInfo.sub || '';
|
|
||||||
|
|
||||||
if (!tanflowSub) {
|
|
||||||
throw new Error('Tanflow sub (subject identifier) is required but not found in response');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log available fields from Tanflow for debugging and planning
|
|
||||||
logger.info('Tanflow userinfo response received', {
|
|
||||||
availableFields: Object.keys(tanflowUserInfo),
|
|
||||||
hasEmail: !!tanflowUserInfo.email,
|
|
||||||
hasPreferredUsername: !!tanflowUserInfo.preferred_username,
|
|
||||||
hasEmployeeId: !!(tanflowUserInfo.employeeId || tanflowUserInfo.employee_id),
|
|
||||||
hasDepartment: !!tanflowUserInfo.department,
|
|
||||||
hasDesignation: !!(tanflowUserInfo.title || tanflowUserInfo.designation),
|
|
||||||
hasManager: !!tanflowUserInfo.manager,
|
|
||||||
hasGroups: Array.isArray(tanflowUserInfo.groups),
|
|
||||||
groupsCount: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups.length : 0,
|
|
||||||
hasLocation: !!(tanflowUserInfo.city || tanflowUserInfo.state || tanflowUserInfo.country),
|
|
||||||
hasAddress: !!tanflowUserInfo.address,
|
|
||||||
sampleData: {
|
|
||||||
sub: tanflowUserInfo.sub?.substring(0, 10) + '...',
|
|
||||||
email: tanflowUserInfo.email?.substring(0, 10) + '...',
|
|
||||||
name: tanflowUserInfo.name,
|
|
||||||
given_name: tanflowUserInfo.given_name,
|
|
||||||
family_name: tanflowUserInfo.family_name,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract user data from Tanflow userinfo
|
|
||||||
// Tanflow uses standard OIDC claims, similar to Okta
|
|
||||||
// Also supports custom claims based on Tanflow configuration
|
|
||||||
const userData: SSOUserData = {
|
|
||||||
oktaSub: tanflowSub, // Reuse oktaSub field for Tanflow sub
|
|
||||||
email: tanflowUserInfo.email || tanflowUserInfo.preferred_username || '',
|
|
||||||
employeeId: tanflowUserInfo.employeeId || tanflowUserInfo.employee_id || undefined,
|
|
||||||
firstName: tanflowUserInfo.given_name || tanflowUserInfo.firstName || undefined,
|
|
||||||
lastName: tanflowUserInfo.family_name || tanflowUserInfo.lastName || undefined,
|
|
||||||
displayName: tanflowUserInfo.name || tanflowUserInfo.displayName || undefined,
|
|
||||||
department: tanflowUserInfo.department || undefined,
|
|
||||||
designation: tanflowUserInfo.title || tanflowUserInfo.designation || undefined,
|
|
||||||
phone: tanflowUserInfo.phone_number || tanflowUserInfo.phone || undefined,
|
|
||||||
// Additional fields that may be available from Tanflow (custom claims)
|
|
||||||
manager: tanflowUserInfo.manager || undefined,
|
|
||||||
jobTitle: tanflowUserInfo.title || tanflowUserInfo.designation || undefined,
|
|
||||||
postalAddress: tanflowUserInfo.address ? (typeof tanflowUserInfo.address === 'string' ? tanflowUserInfo.address : JSON.stringify(tanflowUserInfo.address)) : undefined,
|
|
||||||
mobilePhone: tanflowUserInfo.mobile_phone || tanflowUserInfo.mobilePhone || undefined,
|
|
||||||
adGroups: Array.isArray(tanflowUserInfo.groups) ? tanflowUserInfo.groups : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!userData.oktaSub || !userData.email) {
|
|
||||||
throw new Error('Email and Tanflow sub are required');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Extracted user data from Tanflow', {
|
|
||||||
tanflowSub: userData.oktaSub,
|
|
||||||
email: userData.email,
|
|
||||||
employeeId: userData.employeeId || 'not provided',
|
|
||||||
hasDepartment: !!userData.department,
|
|
||||||
hasDesignation: !!userData.designation,
|
|
||||||
hasManager: !!userData.manager,
|
|
||||||
hasJobTitle: !!userData.jobTitle,
|
|
||||||
hasPostalAddress: !!userData.postalAddress,
|
|
||||||
hasMobilePhone: !!userData.mobilePhone,
|
|
||||||
hasAdGroups: !!userData.adGroups && Array.isArray(userData.adGroups) && userData.adGroups.length > 0,
|
|
||||||
adGroupsCount: userData.adGroups && Array.isArray(userData.adGroups) ? userData.adGroups.length : 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle SSO callback to create/update user and generate our tokens
|
|
||||||
const result = await this.handleSSOCallback(userData);
|
|
||||||
|
|
||||||
// Return our JWT tokens along with Tanflow tokens
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
// Store Tanflow tokens separately if needed (especially id_token for logout)
|
|
||||||
oktaRefreshToken: refresh_token, // Reuse oktaRefreshToken field
|
|
||||||
oktaAccessToken: access_token, // Reuse oktaAccessToken field
|
|
||||||
oktaIdToken: id_token, // Reuse oktaIdToken field for Tanflow logout
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
logAuthEvent('auth_failure', undefined, {
|
|
||||||
action: 'tanflow_token_exchange_failed',
|
|
||||||
errorMessage: error.message,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
tanflowError: error.response?.data?.error,
|
|
||||||
tanflowErrorDescription: error.response?.data?.error_description,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error.response?.data) {
|
|
||||||
const errorData = error.response.data;
|
|
||||||
if (typeof errorData === 'object' && !Array.isArray(errorData)) {
|
|
||||||
const errorMsg = errorData.error_description || errorData.error || error.message;
|
|
||||||
throw new Error(`Tanflow authentication failed: ${errorMsg}`);
|
|
||||||
} else {
|
|
||||||
logger.error('Unexpected error response format from Tanflow', {
|
|
||||||
dataType: typeof errorData,
|
|
||||||
isArray: Array.isArray(errorData),
|
|
||||||
});
|
|
||||||
throw new Error(`Tanflow authentication failed: Unexpected response format. Status: ${error.response.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Tanflow authentication failed: ${error.message || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh Tanflow access token using refresh token
|
|
||||||
*/
|
|
||||||
async refreshTanflowToken(refreshToken: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
if (!ssoConfig.tanflowClientId || !ssoConfig.tanflowClientSecret) {
|
|
||||||
throw new Error('Tanflow client credentials not configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenEndpoint = `${ssoConfig.tanflowBaseUrl}/protocol/openid-connect/token`;
|
|
||||||
|
|
||||||
const response = await axios.post(
|
|
||||||
tokenEndpoint,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
client_id: ssoConfig.tanflowClientId!,
|
|
||||||
client_secret: ssoConfig.tanflowClientSecret!,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== 200 || !response.data.access_token) {
|
|
||||||
throw new Error('Failed to refresh Tanflow token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data.access_token;
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error('Tanflow token refresh failed:', error);
|
|
||||||
throw new Error(`Tanflow token refresh failed: ${error.message || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,18 +64,7 @@ export class DealerClaimService {
|
|||||||
// Generate request number
|
// Generate request number
|
||||||
const requestNumber = await generateRequestNumber();
|
const requestNumber = await generateRequestNumber();
|
||||||
|
|
||||||
// Validate initiator - check if userId is a valid UUID first
|
// Validate initiator
|
||||||
const isValidUUID = (str: string): boolean => {
|
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
||||||
return uuidRegex.test(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isValidUUID(userId)) {
|
|
||||||
// If userId is not a UUID (might be Okta ID), try to find by email or other means
|
|
||||||
// This shouldn't happen in normal flow, but handle gracefully
|
|
||||||
throw new Error(`Invalid initiator ID format. Expected UUID, got: ${userId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initiator = await User.findByPk(userId);
|
const initiator = await User.findByPk(userId);
|
||||||
if (!initiator) {
|
if (!initiator) {
|
||||||
throw new Error('Initiator not found');
|
throw new Error('Initiator not found');
|
||||||
@ -99,7 +88,7 @@ export class DealerClaimService {
|
|||||||
description: claimData.requestDescription,
|
description: claimData.requestDescription,
|
||||||
priority: Priority.STANDARD,
|
priority: Priority.STANDARD,
|
||||||
status: WorkflowStatus.PENDING, // Submitted, not draft
|
status: WorkflowStatus.PENDING, // Submitted, not draft
|
||||||
totalLevels: 5, // Fixed 5-step workflow for claim management (Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only)
|
totalLevels: 8, // Fixed 8-step workflow for claim management
|
||||||
currentLevel: 1, // Step 1: Dealer Proposal Submission
|
currentLevel: 1, // Step 1: Dealer Proposal Submission
|
||||||
totalTatHours: 0, // Will be calculated from approval levels
|
totalTatHours: 0, // Will be calculated from approval levels
|
||||||
isDraft: false, // Not a draft - submitted and ready for workflow
|
isDraft: false, // Not a draft - submitted and ready for workflow
|
||||||
@ -235,40 +224,16 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
logger.info(`[DealerClaimService] Created claim request: ${workflowRequest.requestNumber}`);
|
logger.info(`[DealerClaimService] Created claim request: ${workflowRequest.requestNumber}`);
|
||||||
return workflowRequest;
|
return workflowRequest;
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
// Log detailed error information for debugging
|
logger.error('[DealerClaimService] Error creating claim request:', error);
|
||||||
const errorDetails: any = {
|
|
||||||
message: error.message,
|
|
||||||
name: error.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sequelize validation errors
|
|
||||||
if (error.errors && Array.isArray(error.errors)) {
|
|
||||||
errorDetails.validationErrors = error.errors.map((e: any) => ({
|
|
||||||
field: e.path,
|
|
||||||
message: e.message,
|
|
||||||
value: e.value,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sequelize database errors
|
|
||||||
if (error.parent) {
|
|
||||||
errorDetails.databaseError = {
|
|
||||||
message: error.parent.message,
|
|
||||||
code: error.parent.code,
|
|
||||||
detail: error.parent.detail,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error('[DealerClaimService] Error creating claim request:', errorDetails);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create 5-step approval levels for claim management from approvers array
|
* Create 8-step approval levels for claim management from approvers array
|
||||||
* Validates and creates approval levels based on user-provided approvers
|
* Validates and creates approval levels based on user-provided approvers
|
||||||
* Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are handled as activity logs only, not approval steps
|
* Similar to custom request flow - all approvers are manually assigned
|
||||||
*/
|
*/
|
||||||
private async createClaimApprovalLevelsFromApprovers(
|
private async createClaimApprovalLevelsFromApprovers(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
@ -281,9 +246,6 @@ export class DealerClaimService {
|
|||||||
level: number;
|
level: number;
|
||||||
tat?: number | string;
|
tat?: number | string;
|
||||||
tatType?: 'hours' | 'days';
|
tatType?: 'hours' | 'days';
|
||||||
stepName?: string; // For additional approvers
|
|
||||||
isAdditional?: boolean; // Flag for additional approvers
|
|
||||||
originalStepLevel?: number; // Original step level for fixed steps
|
|
||||||
}> = []
|
}> = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const initiator = await User.findByPk(initiatorId);
|
const initiator = await User.findByPk(initiatorId);
|
||||||
@ -291,134 +253,65 @@ export class DealerClaimService {
|
|||||||
throw new Error('Initiator not found');
|
throw new Error('Initiator not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step definitions with default TAT (only manual approval steps)
|
// Step definitions with default TAT
|
||||||
// Note: Activity Creation (was level 4), E-Invoice Generation (was level 7), and Credit Note Confirmation (was level 8)
|
|
||||||
// are now handled as activity logs only, not approval steps
|
|
||||||
const stepDefinitions = [
|
const stepDefinitions = [
|
||||||
{ level: 1, name: 'Dealer Proposal Submission', defaultTat: 72, isAuto: false },
|
{ level: 1, name: 'Dealer Proposal Submission', defaultTat: 72, isAuto: false },
|
||||||
{ level: 2, name: 'Requestor Evaluation', defaultTat: 48, isAuto: false },
|
{ level: 2, name: 'Requestor Evaluation', defaultTat: 48, isAuto: false },
|
||||||
{ level: 3, name: 'Department Lead Approval', defaultTat: 72, isAuto: false },
|
{ level: 3, name: 'Department Lead Approval', defaultTat: 72, isAuto: false },
|
||||||
{ level: 4, name: 'Dealer Completion Documents', defaultTat: 120, isAuto: false },
|
{ level: 4, name: 'Activity Creation', defaultTat: 1, isAuto: true },
|
||||||
{ level: 5, name: 'Requestor Claim Approval', defaultTat: 48, isAuto: false },
|
{ level: 5, name: 'Dealer Completion Documents', defaultTat: 120, isAuto: false },
|
||||||
|
{ level: 6, name: 'Requestor Claim Approval', defaultTat: 48, isAuto: false },
|
||||||
|
{ level: 7, name: 'E-Invoice Generation', defaultTat: 1, isAuto: true },
|
||||||
|
{ level: 8, name: 'Credit Note Confirmation', defaultTat: 48, isAuto: true }, // System/Finance step handled by webhook
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sort approvers by level to process in order
|
// Process each step
|
||||||
const sortedApprovers = [...approvers].sort((a, b) => a.level - b.level);
|
for (const stepDef of stepDefinitions) {
|
||||||
|
const approver = approvers.find((a) => a.level === stepDef.level);
|
||||||
|
|
||||||
// Track which original steps have been processed
|
|
||||||
const processedOriginalSteps = new Set<number>();
|
|
||||||
|
|
||||||
// Process approvers in order by their level
|
|
||||||
for (const approver of sortedApprovers) {
|
|
||||||
let approverId: string | null = null;
|
let approverId: string | null = null;
|
||||||
let approverEmail = '';
|
let approverEmail = '';
|
||||||
let approverName = 'System';
|
let approverName = 'System';
|
||||||
let tatHours = 48; // Default TAT
|
let tatHours = stepDef.defaultTat;
|
||||||
let levelName = '';
|
|
||||||
let isSystemStep = false;
|
|
||||||
let isFinalApprover = false;
|
|
||||||
|
|
||||||
// Find the step definition this approver belongs to
|
if (stepDef.isAuto) {
|
||||||
let stepDef = null;
|
// System steps - handled by webhooks, no user validation needed
|
||||||
|
// Step 8 is System/Finance, others are System
|
||||||
// Check if this is a system step by email (for backwards compatibility)
|
if (stepDef.level === 8) {
|
||||||
const isSystemEmail = approver.email === 'system@royalenfield.com' || approver.email === 'finance@royalenfield.com';
|
approverId = initiatorId; // Use initiator ID as placeholder (no actual user needed)
|
||||||
|
approverName = 'System/Finance';
|
||||||
if (approver.isAdditional) {
|
approverEmail = 'finance@royalenfield.com';
|
||||||
// Additional approver - use stepName from frontend
|
|
||||||
levelName = approver.stepName || 'Additional Approver';
|
|
||||||
isSystemStep = false;
|
|
||||||
isFinalApprover = false;
|
|
||||||
} else {
|
} else {
|
||||||
// Fixed step - find by originalStepLevel first, then by matching level
|
approverId = initiatorId; // System steps use initiator ID as placeholder
|
||||||
const originalLevel = approver.originalStepLevel || approver.level;
|
approverName = 'System Auto-Process';
|
||||||
stepDef = stepDefinitions.find(s => s.level === originalLevel);
|
approverEmail = 'system@royalenfield.com';
|
||||||
|
|
||||||
if (!stepDef) {
|
|
||||||
// Try to find by current level if originalStepLevel not provided
|
|
||||||
stepDef = stepDefinitions.find(s => s.level === approver.level);
|
|
||||||
}
|
}
|
||||||
|
tatHours = stepDef.defaultTat;
|
||||||
// System steps (Activity Creation, E-Invoice Generation, Credit Note Confirmation) are no longer approval steps
|
} else if (approver) {
|
||||||
// They are handled as activity logs only
|
// User-provided approver
|
||||||
// If approver has system email but no step definition found, skip creating approval level
|
|
||||||
if (!stepDef && isSystemEmail) {
|
|
||||||
logger.info(`[DealerClaimService] Skipping system step approver at level ${approver.level} - system steps are now activity logs only`);
|
|
||||||
continue; // Skip creating approval level for system steps
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stepDef) {
|
|
||||||
levelName = stepDef.name;
|
|
||||||
isSystemStep = false; // No system steps in approval levels anymore
|
|
||||||
isFinalApprover = stepDef.level === 5; // Last step is now Requestor Claim Approval (level 5)
|
|
||||||
processedOriginalSteps.add(stepDef.level);
|
|
||||||
} else {
|
|
||||||
// Fallback - shouldn't happen but handle gracefully
|
|
||||||
levelName = `Step ${approver.level}`;
|
|
||||||
isSystemStep = false;
|
|
||||||
logger.warn(`[DealerClaimService] Could not find step definition for approver at level ${approver.level}, using fallback name`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure levelName is never empty and truncate if too long (max 100 chars)
|
|
||||||
if (!levelName || levelName.trim() === '') {
|
|
||||||
levelName = approver.isAdditional
|
|
||||||
? `Additional Approver - Level ${approver.level}`
|
|
||||||
: `Step ${approver.level}`;
|
|
||||||
logger.warn(`[DealerClaimService] levelName was empty for approver at level ${approver.level}, using fallback: ${levelName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate levelName to max 100 characters (database constraint)
|
|
||||||
if (levelName.length > 100) {
|
|
||||||
logger.warn(`[DealerClaimService] levelName too long (${levelName.length} chars) for level ${approver.level}, truncating to 100 chars`);
|
|
||||||
levelName = levelName.substring(0, 97) + '...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// System steps are no longer created as approval levels - they are activity logs only
|
|
||||||
// This code path should not be reached anymore, but kept for safety
|
|
||||||
if (isSystemStep) {
|
|
||||||
logger.warn(`[DealerClaimService] System step detected but should not create approval level. Skipping.`);
|
|
||||||
continue; // Skip creating approval level for system steps
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// User-provided approver (fixed or additional)
|
|
||||||
if (!approver.email) {
|
if (!approver.email) {
|
||||||
throw new Error(`Approver email is required for level ${approver.level}: ${levelName}`);
|
throw new Error(`Approver email is required for Step ${stepDef.level}: ${stepDef.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate TAT in hours
|
// Calculate TAT in hours
|
||||||
if (approver.tat) {
|
if (approver.tat) {
|
||||||
const tat = Number(approver.tat);
|
const tat = Number(approver.tat);
|
||||||
if (isNaN(tat) || tat <= 0) {
|
if (isNaN(tat) || tat <= 0) {
|
||||||
throw new Error(`Invalid TAT for level ${approver.level}. TAT must be a positive number.`);
|
throw new Error(`Invalid TAT for Step ${stepDef.level}. TAT must be a positive number.`);
|
||||||
}
|
}
|
||||||
tatHours = approver.tatType === 'days' ? tat * 24 : tat;
|
tatHours = approver.tatType === 'days' ? tat * 24 : tat;
|
||||||
} else if (stepDef) {
|
|
||||||
tatHours = stepDef.defaultTat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user exists in database (create from Okta if needed)
|
// Ensure user exists in database (create from Okta if needed)
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
// Helper function to check if a string is a valid UUID
|
if (approver.userId) {
|
||||||
const isValidUUID = (str: string): boolean => {
|
// User ID provided - use it
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
||||||
return uuidRegex.test(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to find user by userId if it's a valid UUID
|
|
||||||
if (approver.userId && isValidUUID(approver.userId)) {
|
|
||||||
try {
|
|
||||||
user = await User.findByPk(approver.userId);
|
user = await User.findByPk(approver.userId);
|
||||||
} catch (error: any) {
|
|
||||||
// If findByPk fails (e.g., invalid UUID format), log and continue to email lookup
|
|
||||||
logger.debug(`[DealerClaimService] Could not find user by userId ${approver.userId}, will try email lookup`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user not found by ID (or userId was not a valid UUID), try email
|
|
||||||
if (!user && approver.email) {
|
if (!user && approver.email) {
|
||||||
|
// User not found by ID, try to find or create by email
|
||||||
user = await User.findOne({ where: { email: approver.email.toLowerCase() } });
|
user = await User.findOne({ where: { email: approver.email.toLowerCase() } });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -427,7 +320,6 @@ export class DealerClaimService {
|
|||||||
try {
|
try {
|
||||||
user = await this.userService.ensureUserExists({
|
user = await this.userService.ensureUserExists({
|
||||||
email: approver.email.toLowerCase(),
|
email: approver.email.toLowerCase(),
|
||||||
userId: approver.userId, // Pass Okta ID if provided (ensureUserExists will handle it)
|
|
||||||
}) as any;
|
}) as any;
|
||||||
logger.info(`[DealerClaimService] Successfully synced user ${approver.email} from Okta`);
|
logger.info(`[DealerClaimService] Successfully synced user ${approver.email} from Okta`);
|
||||||
} catch (oktaError: any) {
|
} catch (oktaError: any) {
|
||||||
@ -438,112 +330,45 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not resolve user for level ${approver.level}: ${approver.email}`);
|
throw new Error(`Could not resolve user for Step ${stepDef.level}: ${approver.email}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
approverId = user.userId;
|
approverId = user.userId;
|
||||||
approverEmail = user.email;
|
approverEmail = user.email;
|
||||||
approverName = approver.name || user.displayName || user.email || 'Approver';
|
approverName = approver.name || user.displayName || user.email || 'Approver';
|
||||||
|
} else {
|
||||||
|
// No approver provided for required step
|
||||||
|
throw new Error(`Approver is required for Step ${stepDef.level}: ${stepDef.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a valid approverId
|
// Ensure we have a valid approverId
|
||||||
if (!approverId) {
|
if (!approverId) {
|
||||||
logger.error(`[DealerClaimService] No approverId resolved for level ${approver.level}, using initiator as fallback`);
|
logger.error(`[DealerClaimService] No approverId resolved for step ${stepDef.level}, using initiator as fallback`);
|
||||||
approverId = initiatorId;
|
approverId = initiatorId;
|
||||||
approverEmail = approverEmail || initiator.email;
|
approverEmail = approverEmail || initiator.email;
|
||||||
approverName = approverName || 'Unknown Approver';
|
approverName = approverName || 'Unknown Approver';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure approverId is a valid UUID before creating
|
// Create approval level
|
||||||
const isValidUUID = (str: string): boolean => {
|
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
||||||
return uuidRegex.test(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!approverId || !isValidUUID(approverId)) {
|
|
||||||
logger.error(`[DealerClaimService] Invalid approverId for level ${approver.level}: ${approverId}`);
|
|
||||||
throw new Error(`Invalid approver ID format for level ${approver.level}. Expected UUID.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create approval level using the approver's level (which may be shifted)
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isStep1 = approver.level === 1;
|
const isStep1 = stepDef.level === 1;
|
||||||
|
|
||||||
try {
|
|
||||||
// Check for duplicate level_number for this request_id (unique constraint)
|
|
||||||
const existingLevel = await ApprovalLevel.findOne({
|
|
||||||
where: {
|
|
||||||
requestId,
|
|
||||||
levelNumber: approver.level
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingLevel) {
|
|
||||||
logger.error(`[DealerClaimService] Duplicate level number ${approver.level} already exists for request ${requestId}`);
|
|
||||||
throw new Error(`Level ${approver.level} already exists for this request. This may indicate a duplicate approver.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ApprovalLevel.create({
|
await ApprovalLevel.create({
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: approver.level, // Use the approver's level (may be shifted)
|
levelNumber: stepDef.level,
|
||||||
levelName: levelName, // Already validated and truncated above
|
levelName: stepDef.name,
|
||||||
approverId: approverId,
|
approverId: approverId,
|
||||||
approverEmail: approverEmail || '',
|
approverEmail,
|
||||||
approverName: approverName || 'Unknown',
|
approverName,
|
||||||
tatHours: tatHours || 0,
|
tatHours: tatHours,
|
||||||
status: isStep1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING,
|
status: stepDef.level === 1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING,
|
||||||
isFinalApprover: isFinalApprover || false,
|
isFinalApprover: stepDef.level === 8,
|
||||||
elapsedHours: 0,
|
elapsedHours: 0,
|
||||||
remainingHours: tatHours || 0,
|
remainingHours: tatHours,
|
||||||
tatPercentageUsed: 0,
|
tatPercentageUsed: 0,
|
||||||
levelStartTime: isStep1 ? now : undefined,
|
levelStartTime: isStep1 ? now : undefined,
|
||||||
tatStartTime: isStep1 ? now : undefined,
|
tatStartTime: isStep1 ? now : undefined,
|
||||||
// Note: tatDays is NOT included - it's auto-calculated by the database
|
|
||||||
} as any);
|
} as any);
|
||||||
} catch (createError: any) {
|
|
||||||
// Log detailed validation errors
|
|
||||||
const errorDetails: any = {
|
|
||||||
message: createError.message,
|
|
||||||
name: createError.name,
|
|
||||||
level: approver.level,
|
|
||||||
levelName: levelName?.substring(0, 50), // Truncate for logging
|
|
||||||
approverId,
|
|
||||||
approverEmail,
|
|
||||||
approverName: approverName?.substring(0, 50),
|
|
||||||
tatHours,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sequelize validation errors
|
|
||||||
if (createError.errors && Array.isArray(createError.errors)) {
|
|
||||||
errorDetails.validationErrors = createError.errors.map((e: any) => ({
|
|
||||||
field: e.path,
|
|
||||||
message: e.message,
|
|
||||||
value: e.value,
|
|
||||||
type: e.type,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database constraint errors
|
|
||||||
if (createError.parent) {
|
|
||||||
errorDetails.databaseError = {
|
|
||||||
message: createError.parent.message,
|
|
||||||
code: createError.parent.code,
|
|
||||||
detail: createError.parent.detail,
|
|
||||||
constraint: createError.parent.constraint,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(`[DealerClaimService] Failed to create approval level for level ${approver.level}:`, errorDetails);
|
|
||||||
throw new Error(`Failed to create approval level ${approver.level} (${levelName}): ${createError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that required fixed steps were processed
|
|
||||||
const requiredSteps = stepDefinitions.filter(s => !s.isAuto);
|
|
||||||
for (const requiredStep of requiredSteps) {
|
|
||||||
if (!processedOriginalSteps.has(requiredStep.level)) {
|
|
||||||
logger.warn(`[DealerClaimService] Required step ${requiredStep.level} (${requiredStep.name}) was not found in approvers array`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,18 +461,6 @@ export class DealerClaimService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if a string is a valid UUID
|
|
||||||
const isValidUUID = (str: string): boolean => {
|
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
||||||
return uuidRegex.test(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only try to find user if approverId is a valid UUID
|
|
||||||
if (!isValidUUID(approverId)) {
|
|
||||||
logger.warn(`[DealerClaimService] Invalid UUID format for approverId: ${approverId}, skipping participant creation`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const approverUser = await User.findByPk(approverId);
|
const approverUser = await User.findByPk(approverId);
|
||||||
if (approverUser) {
|
if (approverUser) {
|
||||||
participantsToAdd.push({
|
participantsToAdd.push({
|
||||||
@ -1314,24 +1127,8 @@ export class DealerClaimService {
|
|||||||
throw new Error('Invalid claim request');
|
throw new Error('Invalid claim request');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the "Dealer Completion Documents" step by levelName (handles step shifts due to additional approvers)
|
if (request.currentLevel !== 5) {
|
||||||
const approvalLevels = await ApprovalLevel.findAll({
|
throw new Error('Completion documents can only be submitted at step 5');
|
||||||
where: { requestId },
|
|
||||||
order: [['levelNumber', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
const dealerCompletionStep = approvalLevels.find((level: any) => {
|
|
||||||
const levelName = (level.levelName || '').toLowerCase();
|
|
||||||
return levelName.includes('dealer completion') || levelName.includes('completion documents');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dealerCompletionStep) {
|
|
||||||
throw new Error('Dealer Completion Documents step not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if current level matches the Dealer Completion Documents step (handles step shifts)
|
|
||||||
if (request.currentLevel !== dealerCompletionStep.levelNumber) {
|
|
||||||
throw new Error(`Completion documents can only be submitted at the Dealer Completion Documents step (currently at step ${request.currentLevel})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save completion details
|
// Save completion details
|
||||||
@ -1374,10 +1171,10 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback: try to find by levelNumber 4 (new position after removing system steps)
|
// Fallback: try to find by levelNumber 5 (for backwards compatibility)
|
||||||
if (!dealerCompletionLevel) {
|
if (!dealerCompletionLevel) {
|
||||||
dealerCompletionLevel = await ApprovalLevel.findOne({
|
dealerCompletionLevel = await ApprovalLevel.findOne({
|
||||||
where: { requestId, levelNumber: 4 }
|
where: { requestId, levelNumber: 5 }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1760,54 +1557,37 @@ 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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Requestor Claim Approval is approved - if not, approve it first
|
// Check if Requestor Claim Approval (Step 6) is approved - if not, approve it first
|
||||||
// Find dynamically by levelName (handles step shifts due to additional approvers)
|
// Find dynamically by levelName, not hardcoded step number
|
||||||
const approvalLevels = await ApprovalLevel.findAll({
|
let requestorClaimLevel = await ApprovalLevel.findOne({
|
||||||
where: { requestId },
|
where: {
|
||||||
order: [['levelNumber', 'ASC']]
|
requestId,
|
||||||
});
|
levelName: 'Requestor Claim Approval'
|
||||||
|
|
||||||
let requestorClaimLevel = approvalLevels.find((level: any) => {
|
|
||||||
const levelName = (level.levelName || '').toLowerCase();
|
|
||||||
return levelName.includes('requestor') &&
|
|
||||||
(levelName.includes('claim') || levelName.includes('approval'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback: try to find by levelNumber 5 (new position after removing system steps)
|
|
||||||
// But only if no match found by name (handles edge cases)
|
|
||||||
if (!requestorClaimLevel) {
|
|
||||||
requestorClaimLevel = approvalLevels.find((level: any) => level.levelNumber === 5);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Validate that we're at the Requestor Claim Approval step before allowing DMS push
|
// Fallback: try to find by levelNumber 6 (for backwards compatibility)
|
||||||
if (requestorClaimLevel && request.currentLevel !== requestorClaimLevel.levelNumber) {
|
if (!requestorClaimLevel) {
|
||||||
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.`);
|
requestorClaimLevel = await ApprovalLevel.findOne({
|
||||||
|
where: { requestId, levelNumber: 6 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestorClaimLevel && requestorClaimLevel.status !== ApprovalStatus.APPROVED) {
|
if (requestorClaimLevel && requestorClaimLevel.status !== ApprovalStatus.APPROVED) {
|
||||||
logger.info(`[DealerClaimService] Requestor Claim Approval not approved yet. Auto-approving for request ${requestId}`);
|
logger.info(`[DealerClaimService] Requestor Claim Approval not approved yet. Auto-approving for request ${requestId}`);
|
||||||
// Auto-approve Requestor Claim Approval
|
// Auto-approve Requestor Claim Approval - E-Invoice Generation will be activated but invoice generation will happen via webhook
|
||||||
await this.approvalService.approveLevel(
|
await this.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 when pushing to DMS. Waiting for DMS webhook to generate invoice.' },
|
||||||
'system',
|
'system',
|
||||||
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
{ 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.`);
|
// Note: E-Invoice Generation will be handled by DMS webhook, not here
|
||||||
|
logger.info(`[DealerClaimService] Requestor Claim Approval approved. E-Invoice Generation activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
} else {
|
} else {
|
||||||
// Requestor Claim Approval already approved
|
// Requestor Claim Approval already approved - E-Invoice Generation should be active, waiting for webhook
|
||||||
logger.info(`[DealerClaimService] Requestor Claim Approval already approved. E-Invoice generation will be logged as activity when DMS webhook is received.`);
|
logger.info(`[DealerClaimService] Requestor Claim Approval already approved. E-Invoice Generation should be active. Invoice generation will happen via DMS webhook.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log E-Invoice generation as activity (no approval level needed)
|
|
||||||
await activityService.log({
|
|
||||||
requestId,
|
|
||||||
type: 'status_change',
|
|
||||||
user: { userId: 'system', name: 'System Auto-Process' },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'E-Invoice Generation Initiated',
|
|
||||||
details: `E-Invoice generation initiated via DMS integration for request ${requestNumber}. Waiting for DMS webhook confirmation.`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[DealerClaimService] Error updating e-invoice details:', error);
|
logger.error('[DealerClaimService] Error updating e-invoice details:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -1815,12 +1595,13 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log E-Invoice Generation as activity (no longer an approval step)
|
* Process Step 7: E-Invoice Generation (Deprecated - now handled by DMS webhook)
|
||||||
* This method logs the e-invoice generation activity when invoice is generated via DMS webhook
|
* This method is kept for backward compatibility but invoice generation is now triggered only via DMS webhook
|
||||||
|
* @deprecated Use DMS webhook to trigger invoice generation instead
|
||||||
*/
|
*/
|
||||||
async logEInvoiceGenerationActivity(requestId: string, invoiceNumber?: string): Promise<void> {
|
async processEInvoiceGeneration(requestId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`[DealerClaimService] Logging E-Invoice Generation activity for request ${requestId}`);
|
logger.info(`[DealerClaimService] Processing Step 7: E-Invoice Generation for request ${requestId}`);
|
||||||
|
|
||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
if (!request) {
|
if (!request) {
|
||||||
@ -1829,28 +1610,59 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
const workflowType = (request as any).workflowType;
|
const workflowType = (request as any).workflowType;
|
||||||
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
||||||
logger.warn(`[DealerClaimService] Skipping E-Invoice activity logging - not a claim management workflow (type: ${workflowType})`);
|
logger.warn(`[DealerClaimService] Skipping Step 7 auto-processing - not a claim management workflow (type: ${workflowType})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
|
const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
|
||||||
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
if (!claimDetails) {
|
||||||
const finalInvoiceNumber = invoiceNumber || claimInvoice?.invoiceNumber || 'N/A';
|
throw new Error(`Claim details not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Log E-Invoice Generation as activity
|
// Get Step 7 approval level
|
||||||
await activityService.log({
|
// Get E-Invoice Generation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
|
let eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
type: 'status_change',
|
levelName: 'E-Invoice Generation'
|
||||||
user: { userId: 'system', name: 'System Auto-Process' },
|
}
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'E-Invoice Generated',
|
|
||||||
details: `E-Invoice generated via DMS. Invoice Number: ${finalInvoiceNumber}. Request: ${requestNumber}`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] E-Invoice Generation activity logged for request ${requestId} (Invoice: ${finalInvoiceNumber})`);
|
// Fallback: try to find by levelNumber 7 (for backwards compatibility)
|
||||||
|
if (!eInvoiceLevel) {
|
||||||
|
eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 7
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eInvoiceLevel) {
|
||||||
|
throw new Error(`E-Invoice Generation approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If e-invoice already generated, just auto-approve E-Invoice Generation
|
||||||
|
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
||||||
|
|
||||||
|
if (claimInvoice?.invoiceNumber) {
|
||||||
|
logger.info(`[DealerClaimService] E-Invoice already generated (${claimInvoice.invoiceNumber}). Auto-approving E-Invoice Generation for request ${requestId}`);
|
||||||
|
|
||||||
|
// Auto-approve E-Invoice Generation
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
eInvoiceLevel.levelId,
|
||||||
|
{ action: 'APPROVE', comments: 'E-Invoice generated via DMS. E-Invoice Generation auto-approved.' },
|
||||||
|
'system',
|
||||||
|
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[DealerClaimService] Successfully auto-processed and approved E-Invoice Generation (Level ${eInvoiceLevel.levelNumber}) for request ${requestId}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[DealerClaimService] E-Invoice not generated yet for request ${requestId}. E-Invoice Generation will be processed when e-invoice is generated.`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[DealerClaimService] Error logging E-Invoice Generation activity for request ${requestId}:`, error);
|
logger.error(`[DealerClaimService] Error processing Step 7 (E-Invoice Generation) for request ${requestId}:`, error);
|
||||||
// Don't throw - activity logging is not critical
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1878,6 +1690,10 @@ export class DealerClaimService {
|
|||||||
const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
|
const completionDetails = await DealerCompletionDetails.findOne({ where: { requestId } });
|
||||||
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
||||||
|
|
||||||
|
if (!claimInvoice?.invoiceNumber) {
|
||||||
|
throw new Error('E-Invoice must be generated before creating credit note');
|
||||||
|
}
|
||||||
|
|
||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
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';
|
||||||
|
|
||||||
@ -1888,8 +1704,6 @@ export class DealerClaimService {
|
|||||||
|| completionDetails?.totalClosedExpenses
|
|| completionDetails?.totalClosedExpenses
|
||||||
|| 0;
|
|| 0;
|
||||||
|
|
||||||
// Only generate via DMS if invoice exists, otherwise allow manual entry
|
|
||||||
if (claimInvoice?.invoiceNumber) {
|
|
||||||
const creditNoteResult = await dmsIntegrationService.generateCreditNote({
|
const creditNoteResult = await dmsIntegrationService.generateCreditNote({
|
||||||
requestNumber,
|
requestNumber,
|
||||||
eInvoiceNumber: claimInvoice.invoiceNumber,
|
eInvoiceNumber: claimInvoice.invoiceNumber,
|
||||||
@ -1920,26 +1734,11 @@ export class DealerClaimService {
|
|||||||
creditNoteNumber: creditNoteResult.creditNoteNumber,
|
creditNoteNumber: creditNoteResult.creditNoteNumber,
|
||||||
creditNoteAmount: creditNoteResult.creditNoteAmount
|
creditNoteAmount: creditNoteResult.creditNoteAmount
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// No invoice exists - create credit note manually without invoice link
|
|
||||||
await ClaimCreditNote.upsert({
|
|
||||||
requestId,
|
|
||||||
invoiceId: undefined, // No invoice linked
|
|
||||||
creditNoteNumber: undefined, // Will be set manually later
|
|
||||||
creditNoteDate: creditNoteData?.creditNoteDate || new Date(),
|
|
||||||
creditNoteAmount: creditNoteAmount,
|
|
||||||
status: 'PENDING',
|
|
||||||
reason: creditNoteData?.reason || 'Claim settlement',
|
|
||||||
description: creditNoteData?.description || `Credit note for claim request ${requestNumber} (no invoice)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Credit note created without invoice for request: ${requestId}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Manual entry - just update the fields
|
// Manual entry - just update the fields
|
||||||
await ClaimCreditNote.upsert({
|
await ClaimCreditNote.upsert({
|
||||||
requestId,
|
requestId,
|
||||||
invoiceId: claimInvoice?.invoiceId || undefined, // Allow undefined if no invoice
|
invoiceId: claimInvoice.invoiceId,
|
||||||
creditNoteNumber: creditNoteData.creditNoteNumber,
|
creditNoteNumber: creditNoteData.creditNoteNumber,
|
||||||
creditNoteDate: creditNoteData.creditNoteDate || new Date(),
|
creditNoteDate: creditNoteData.creditNoteDate || new Date(),
|
||||||
creditNoteAmount: creditNoteData.creditNoteAmount,
|
creditNoteAmount: creditNoteData.creditNoteAmount,
|
||||||
@ -1991,27 +1790,62 @@ export class DealerClaimService {
|
|||||||
throw new Error('This operation is only available for claim management workflows');
|
throw new Error('This operation is only available for claim management workflows');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credit Note Confirmation is now an activity log only, not an approval step
|
// Get Step 8 approval level
|
||||||
const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
|
// Get Credit Note Confirmation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
|
let creditNoteLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelName: 'Credit Note Confirmation'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update credit note status to CONFIRMED
|
// Fallback: try to find by levelNumber 8 (for backwards compatibility)
|
||||||
|
if (!creditNoteLevel) {
|
||||||
|
creditNoteLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!creditNoteLevel) {
|
||||||
|
throw new Error(`Credit Note Confirmation approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Credit Note Confirmation is already approved
|
||||||
|
if (creditNoteLevel.status === 'APPROVED') {
|
||||||
|
logger.info(`[DealerClaimService] Credit Note Confirmation already approved for request ${requestId}`);
|
||||||
|
// Still send notification to dealer if credit note wasn't sent before
|
||||||
|
// You can add a flag to track if credit note was already sent
|
||||||
|
} else {
|
||||||
|
// Auto-approve Credit Note Confirmation
|
||||||
|
logger.info(`[DealerClaimService] Auto-approving Credit Note Confirmation (Level ${creditNoteLevel.levelNumber}) for request ${requestId}`);
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
creditNoteLevel.levelId,
|
||||||
|
{
|
||||||
|
action: 'APPROVE',
|
||||||
|
comments: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Credit Note Confirmation auto-approved.`,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
ipAddress: null,
|
||||||
|
userAgent: 'System Auto-Process',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[DealerClaimService] Credit Note Confirmation (Level ${creditNoteLevel.levelNumber}) auto-approved successfully for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update credit note status to SENT
|
||||||
await creditNote.update({
|
await creditNote.update({
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED', // Or 'SENT' if you have that status
|
||||||
confirmedAt: new Date(),
|
confirmedAt: new Date(),
|
||||||
confirmedBy: userId,
|
confirmedBy: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log Credit Note Confirmation as activity (no approval step needed)
|
|
||||||
await activityService.log({
|
|
||||||
requestId,
|
|
||||||
type: 'status_change',
|
|
||||||
user: { userId: userId, name: 'Finance Team' },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'Credit Note Confirmed and Sent',
|
|
||||||
details: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Credit Note Amount: ₹${creditNote.creditNoteAmount || 0}. Request: ${requestNumber}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send notification to dealer (you can implement email service here)
|
// Send notification to dealer (you can implement email service here)
|
||||||
|
// For now, we'll just log it
|
||||||
logger.info(`[DealerClaimService] Credit note sent to dealer`, {
|
logger.info(`[DealerClaimService] Credit note sent to dealer`, {
|
||||||
requestId,
|
requestId,
|
||||||
creditNoteNumber: creditNote.creditNoteNumber,
|
creditNoteNumber: creditNote.creditNoteNumber,
|
||||||
@ -2025,7 +1859,7 @@ export class DealerClaimService {
|
|||||||
// dealerName: claimDetails.dealerName,
|
// dealerName: claimDetails.dealerName,
|
||||||
// creditNoteNumber: creditNote.creditNoteNumber,
|
// creditNoteNumber: creditNote.creditNoteNumber,
|
||||||
// creditNoteAmount: creditNote.creditNoteAmount,
|
// creditNoteAmount: creditNote.creditNoteAmount,
|
||||||
// requestNumber: requestNumber,
|
// requestNumber: (request as any).requestNumber,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -2035,13 +1869,12 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Activity Creation (now activity log only, not an approval step)
|
* Process Step 4: Activity Creation (Auto-processed after Step 3 approval)
|
||||||
* Creates activity confirmation and sends emails to dealer, requestor, and department lead
|
* Creates activity confirmation and sends emails to dealer, requestor, and department lead
|
||||||
* Logs activity instead of creating/approving approval level
|
|
||||||
*/
|
*/
|
||||||
async processActivityCreation(requestId: string): Promise<void> {
|
async processActivityCreation(requestId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`[DealerClaimService] Processing Activity Creation for request ${requestId}`);
|
logger.info(`[DealerClaimService] Processing Step 4: Activity Creation for request ${requestId}`);
|
||||||
|
|
||||||
// Get workflow request
|
// Get workflow request
|
||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
@ -2052,7 +1885,7 @@ export class DealerClaimService {
|
|||||||
// Verify this is a claim management workflow
|
// Verify this is a claim management workflow
|
||||||
const workflowType = (request as any).workflowType;
|
const workflowType = (request as any).workflowType;
|
||||||
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
||||||
logger.warn(`[DealerClaimService] Skipping Activity Creation - not a claim management workflow (type: ${workflowType})`);
|
logger.warn(`[DealerClaimService] Skipping Step 4 auto-processing - not a claim management workflow (type: ${workflowType})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2062,6 +1895,29 @@ export class DealerClaimService {
|
|||||||
throw new Error(`Claim details not found for request ${requestId}`);
|
throw new Error(`Claim details not found for request ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Activity Creation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
|
// This handles cases where approvers are added between steps, causing Activity Creation to shift
|
||||||
|
let activityCreationLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelName: 'Activity Creation'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activityCreationLevel) {
|
||||||
|
// Fallback: try to find by levelNumber 4 (for backwards compatibility)
|
||||||
|
activityCreationLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activityCreationLevel) {
|
||||||
|
throw new Error(`Activity Creation approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get participants for email notifications
|
// Get participants for email notifications
|
||||||
const initiator = await User.findByPk((request as any).initiatorId);
|
const initiator = await User.findByPk((request as any).initiatorId);
|
||||||
const dealerUser = claimDetails.dealerEmail
|
const dealerUser = claimDetails.dealerEmail
|
||||||
@ -2135,7 +1991,7 @@ export class DealerClaimService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log Activity Creation as activity (no approval level needed)
|
// Log activity creation
|
||||||
await activityService.log({
|
await activityService.log({
|
||||||
requestId,
|
requestId,
|
||||||
type: 'status_change',
|
type: 'status_change',
|
||||||
@ -2145,7 +2001,20 @@ export class DealerClaimService {
|
|||||||
details: `Activity "${activityName}" created. Activity confirmation email auto-triggered to dealer, requestor, and department lead. IO confirmation to be made.`,
|
details: `Activity "${activityName}" created. Activity confirmation email auto-triggered to dealer, requestor, and department lead. IO confirmation to be made.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Activity Creation logged as activity for request ${requestId}. Activity creation completed.`);
|
// Activity Creation level is already activated (IN_PROGRESS) by the approval service
|
||||||
|
// Now auto-approve Activity Creation immediately (since it's an auto-step)
|
||||||
|
const activityCreationLevelId = activityCreationLevel.levelId;
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
activityCreationLevelId,
|
||||||
|
{ action: 'APPROVE', comments: 'Activity created automatically. Activity confirmation email sent to dealer, requestor, and department lead.' },
|
||||||
|
'system',
|
||||||
|
{
|
||||||
|
ipAddress: null,
|
||||||
|
userAgent: 'System Auto-Process'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[DealerClaimService] Activity Creation (Level ${activityCreationLevel.levelNumber}) auto-approved for request ${requestId}. Activity creation completed.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[DealerClaimService] Error processing Step 4 activity creation for request ${requestId}:`, error);
|
logger.error(`[DealerClaimService] Error processing Step 4 activity creation for request ${requestId}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -147,7 +147,7 @@ export class DMSWebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-approve Step 7 and move to Step 8
|
// Auto-approve Step 7 and move to Step 8
|
||||||
await this.logEInvoiceGenerationActivity(request.requestId, payload.request_number);
|
await this.autoApproveStep7(request.requestId, payload.request_number);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -197,11 +197,18 @@ export class DMSWebhookService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find invoice to link credit note (optional - credit note can exist without invoice)
|
// Find invoice to link credit note
|
||||||
const invoice = await ClaimInvoice.findOne({
|
const invoice = await ClaimInvoice.findOne({
|
||||||
where: { requestId: request.requestId },
|
where: { requestId: request.requestId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!invoice) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Invoice not found for request: ${payload.request_number}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Find or create credit note record
|
// Find or create credit note record
|
||||||
let creditNote = await ClaimCreditNote.findOne({
|
let creditNote = await ClaimCreditNote.findOne({
|
||||||
where: { requestId: request.requestId },
|
where: { requestId: request.requestId },
|
||||||
@ -211,12 +218,11 @@ export class DMSWebhookService {
|
|||||||
if (!creditNote) {
|
if (!creditNote) {
|
||||||
logger.info('[DMSWebhook] Credit note record not found, creating new credit note from webhook', {
|
logger.info('[DMSWebhook] Credit note record not found, creating new credit note from webhook', {
|
||||||
requestNumber: payload.request_number,
|
requestNumber: payload.request_number,
|
||||||
hasInvoice: !!invoice,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
creditNote = await ClaimCreditNote.create({
|
creditNote = await ClaimCreditNote.create({
|
||||||
requestId: request.requestId,
|
requestId: request.requestId,
|
||||||
invoiceId: invoice?.invoiceId || undefined, // Allow undefined if no invoice exists
|
invoiceId: invoice.invoiceId,
|
||||||
creditNoteNumber: payload.document_no,
|
creditNoteNumber: payload.document_no,
|
||||||
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
||||||
@ -231,12 +237,11 @@ export class DMSWebhookService {
|
|||||||
logger.info('[DMSWebhook] Credit note created successfully from webhook', {
|
logger.info('[DMSWebhook] Credit note created successfully from webhook', {
|
||||||
requestNumber: payload.request_number,
|
requestNumber: payload.request_number,
|
||||||
creditNoteNumber: payload.document_no,
|
creditNoteNumber: payload.document_no,
|
||||||
hasInvoice: !!invoice,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update existing credit note with DMS response data
|
// Update existing credit note with DMS response data
|
||||||
await creditNote.update({
|
await creditNote.update({
|
||||||
invoiceId: invoice?.invoiceId || creditNote.invoiceId, // Preserve existing invoiceId if no invoice found
|
invoiceId: invoice.invoiceId,
|
||||||
creditNoteNumber: payload.document_no,
|
creditNoteNumber: payload.document_no,
|
||||||
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
||||||
@ -253,7 +258,6 @@ export class DMSWebhookService {
|
|||||||
creditNoteNumber: payload.document_no,
|
creditNoteNumber: payload.document_no,
|
||||||
sapCreditNoteNo: payload.sap_credit_note_no,
|
sapCreditNoteNo: payload.sap_credit_note_no,
|
||||||
irnNo: payload.irn_no,
|
irnNo: payload.irn_no,
|
||||||
hasInvoice: !!invoice,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,10 +326,10 @@ export class DMSWebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log E-Invoice Generation as activity (no longer an approval step)
|
* Auto-approve Step 7 (E-Invoice Generation) and move to Step 8
|
||||||
* This is called after invoice is created/updated from DMS webhook
|
* This is called after invoice is created/updated from DMS webhook
|
||||||
*/
|
*/
|
||||||
private async logEInvoiceGenerationActivity(requestId: string, requestNumber: string): Promise<void> {
|
private async autoApproveStep7(requestId: string, requestNumber: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check if this is a claim management workflow
|
// Check if this is a claim management workflow
|
||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
@ -343,28 +347,72 @@ export class DMSWebhookService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// E-Invoice Generation is now an activity log only, not an approval step
|
// Get E-Invoice Generation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
// Log the activity using the dealerClaimService
|
let eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
const { DealerClaimService } = await import('./dealerClaim.service');
|
where: {
|
||||||
const dealerClaimService = new DealerClaimService();
|
requestId,
|
||||||
const invoice = await ClaimInvoice.findOne({ where: { requestId } });
|
levelName: 'E-Invoice Generation',
|
||||||
const invoiceNumber = invoice?.invoiceNumber || 'N/A';
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await dealerClaimService.logEInvoiceGenerationActivity(requestId, invoiceNumber);
|
// Fallback: try to find by levelNumber 7 (for backwards compatibility)
|
||||||
|
if (!eInvoiceLevel) {
|
||||||
|
eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('[DMSWebhook] E-Invoice Generation activity logged successfully', {
|
if (!eInvoiceLevel) {
|
||||||
|
logger.warn('[DMSWebhook] E-Invoice Generation approval level not found', { requestId, requestNumber });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if E-Invoice Generation is already approved
|
||||||
|
if (eInvoiceLevel.status === 'APPROVED') {
|
||||||
|
logger.info('[DMSWebhook] E-Invoice Generation already approved, skipping auto-approval', {
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
invoiceNumber,
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-approve E-Invoice Generation
|
||||||
|
logger.info(`[DMSWebhook] Auto-approving E-Invoice Generation (Level ${eInvoiceLevel.levelNumber})`, {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
levelId: eInvoiceLevel.levelId,
|
||||||
|
levelNumber: eInvoiceLevel.levelNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
eInvoiceLevel.levelId,
|
||||||
|
{
|
||||||
|
action: 'APPROVE',
|
||||||
|
comments: `E-Invoice generated via DMS webhook. Invoice Number: ${(await ClaimInvoice.findOne({ where: { requestId } }))?.invoiceNumber || 'N/A'}. E-Invoice Generation auto-approved.`,
|
||||||
|
},
|
||||||
|
'system', // System user for auto-approval
|
||||||
|
{
|
||||||
|
ipAddress: null,
|
||||||
|
userAgent: 'DMS-Webhook-System',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] E-Invoice Generation auto-approved successfully. Workflow moved to next step', {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
levelNumber: eInvoiceLevel.levelNumber,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
logger.error('[DMSWebhook] Error logging E-Invoice Generation activity:', {
|
logger.error('[DMSWebhook] Error auto-approving Step 7:', {
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
});
|
||||||
// Don't throw error - webhook processing should continue even if activity logging fails
|
// Don't throw error - webhook processing should continue even if Step 7 approval fails
|
||||||
// The invoice is already created/updated, which is the primary goal
|
// The invoice is already created/updated, which is the primary goal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,9 +29,6 @@ export interface SSOConfig {
|
|||||||
oktaClientId: string;
|
oktaClientId: string;
|
||||||
oktaClientSecret: string;
|
oktaClientSecret: string;
|
||||||
oktaApiToken?: string; // Optional - SSWS token for Okta Users API
|
oktaApiToken?: string; // Optional - SSWS token for Okta Users API
|
||||||
tanflowBaseUrl?: string; // Tanflow IAM base URL
|
|
||||||
tanflowClientId?: string; // Tanflow client ID
|
|
||||||
tanflowClientSecret?: string; // Tanflow client secret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthTokens {
|
export interface AuthTokens {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user