sanitized code removed url and mails
This commit is contained in:
parent
81afd7ec96
commit
b32a3505ac
@ -1326,9 +1326,9 @@ GCP_KEY_FILE=./config/gcp-key.json
|
|||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=notifications@royalenfield.com
|
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||||
SMTP_PASSWORD=your_smtp_password
|
SMTP_PASSWORD=your_smtp_password
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
|
||||||
|
|
||||||
# AI Service (for conclusion generation)
|
# AI Service (for conclusion generation)
|
||||||
AI_API_KEY=your_ai_api_key
|
AI_API_KEY=your_ai_api_key
|
||||||
|
|||||||
@ -155,13 +155,13 @@ export async function calculateBusinessDays(
|
|||||||
2. ✅ Imported `calculateElapsedWorkingHours`, `addWorkingHours`, `addWorkingHoursExpress` from `@utils/tatTimeUtils`
|
2. ✅ Imported `calculateElapsedWorkingHours`, `addWorkingHours`, `addWorkingHoursExpress` from `@utils/tatTimeUtils`
|
||||||
3. ✅ Replaced lines 64-65 with proper working hours calculation (now lines 66-77)
|
3. ✅ Replaced lines 64-65 with proper working hours calculation (now lines 66-77)
|
||||||
4. ✅ Gets priority from workflow
|
4. ✅ Gets priority from workflow
|
||||||
5. ⏳ **TODO:** Test TAT breach alerts
|
5. Done: Test TAT breach alerts
|
||||||
|
|
||||||
### Step 2: Add Business Days Function ✅ **DONE**
|
### Step 2: Add Business Days Function ✅ **DONE**
|
||||||
1. ✅ Opened `Re_Backend/src/utils/tatTimeUtils.ts`
|
1. ✅ Opened `Re_Backend/src/utils/tatTimeUtils.ts`
|
||||||
2. ✅ Added `calculateBusinessDays()` function (lines 697-758)
|
2. ✅ Added `calculateBusinessDays()` function (lines 697-758)
|
||||||
3. ✅ Exported the function
|
3. ✅ Exported the function
|
||||||
4. ⏳ **TODO:** Test with various date ranges
|
4. Done: Test with various date ranges
|
||||||
|
|
||||||
### Step 3: Update Workflow Aging Report ✅ **DONE**
|
### Step 3: Update Workflow Aging Report ✅ **DONE**
|
||||||
1. ✅ Built report endpoint using `calculateBusinessDays()`
|
1. ✅ Built report endpoint using `calculateBusinessDays()`
|
||||||
|
|||||||
@ -19,10 +19,10 @@ This command will output something like:
|
|||||||
```
|
```
|
||||||
=======================================
|
=======================================
|
||||||
Public Key:
|
Public Key:
|
||||||
BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
{{VAPID_PUBLIC_KEY}}
|
||||||
|
|
||||||
Private Key:
|
Private Key:
|
||||||
aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz
|
{{VAPID_PRIVATE_KEY}}
|
||||||
|
|
||||||
=======================================
|
=======================================
|
||||||
```
|
```
|
||||||
@ -59,9 +59,9 @@ Add the generated keys to your backend `.env` file:
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
# Notification Service Worker credentials (Web Push / VAPID)
|
# Notification Service Worker credentials (Web Push / VAPID)
|
||||||
VAPID_PUBLIC_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
VAPID_PUBLIC_KEY={{VAPID_PUBLIC_KEY}}
|
||||||
VAPID_PRIVATE_KEY=aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz
|
VAPID_PRIVATE_KEY={{VAPID_PRIVATE_KEY}}
|
||||||
VAPID_CONTACT=mailto:admin@royalenfield.com
|
VAPID_CONTACT=mailto:{{ADMIN_EMAIL}}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important Notes:**
|
**Important Notes:**
|
||||||
@ -75,7 +75,7 @@ Add the **SAME** `VAPID_PUBLIC_KEY` to your frontend `.env` file:
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
# Push Notifications (Web Push / VAPID)
|
# Push Notifications (Web Push / VAPID)
|
||||||
VITE_PUBLIC_VAPID_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
VITE_PUBLIC_VAPID_KEY={{VAPID_PUBLIC_KEY}}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:**
|
**Important:**
|
||||||
|
|||||||
@ -98,7 +98,7 @@ npm run dev
|
|||||||
1. Server will start automatically
|
1. Server will start automatically
|
||||||
2. Log in via SSO
|
2. Log in via SSO
|
||||||
3. Run this SQL to make yourself admin:
|
3. Run this SQL to make yourself admin:
|
||||||
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';
|
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
|
|
||||||
[Config Seed] ✅ Default configurations seeded successfully (30 settings)
|
[Config Seed] ✅ Default configurations seeded successfully (30 settings)
|
||||||
info: ✅ Server started successfully on port 5000
|
info: ✅ Server started successfully on port 5000
|
||||||
@ -112,7 +112,7 @@ psql -d royal_enfield_workflow
|
|||||||
|
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN'
|
SET role = 'ADMIN'
|
||||||
WHERE email = 'your-email@royalenfield.com';
|
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
|
|
||||||
\q
|
\q
|
||||||
```
|
```
|
||||||
|
|||||||
@ -471,7 +471,7 @@ The backend supports web push notifications via VAPID (Voluntary Application Ser
|
|||||||
```
|
```
|
||||||
VAPID_PUBLIC_KEY=<your-public-key>
|
VAPID_PUBLIC_KEY=<your-public-key>
|
||||||
VAPID_PRIVATE_KEY=<your-private-key>
|
VAPID_PRIVATE_KEY=<your-private-key>
|
||||||
VAPID_CONTACT=mailto:admin@royalenfield.com
|
VAPID_CONTACT=mailto:admin@{{API_DOMAIN}}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Add to Frontend `.env`:**
|
3. **Add to Frontend `.env`:**
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,2 @@
|
|||||||
import{a as s}from"./index-7F7W4LDI.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
import{a as s}from"./index-y_ojbF9T.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-BJO_6JLT.js.map
|
//# sourceMappingURL=conclusionApi-DoX_H3Tk.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-BJO_6JLT.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
{"version":3,"file":"conclusionApi-DoX_H3Tk.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
|||||||
<!-- Preload critical fonts and icons -->
|
<!-- Preload critical fonts and icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<script type="module" crossorigin src="/assets/index-7F7W4LDI.js"></script>
|
<script type="module" crossorigin src="/assets/index-y_ojbF9T.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
||||||
|
|||||||
@ -34,7 +34,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
|
|||||||
- **Approver Type**: System (Auto-processed)
|
- **Approver Type**: System (Auto-processed)
|
||||||
- **Action Type**: **AUTO** (System automatically creates activity)
|
- **Action Type**: **AUTO** (System automatically creates activity)
|
||||||
- **TAT**: 1 hour
|
- **TAT**: 1 hour
|
||||||
- **Mapping**: System user (`system@royalenfield.com`)
|
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
|
||||||
- **Status**: Auto-approved when triggered
|
- **Status**: Auto-approved when triggered
|
||||||
|
|
||||||
### Step 5: Dealer Completion Documents
|
### Step 5: Dealer Completion Documents
|
||||||
@ -55,7 +55,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
|
|||||||
- **Approver Type**: System (Auto-processed via DMS)
|
- **Approver Type**: System (Auto-processed via DMS)
|
||||||
- **Action Type**: **AUTO** (System generates e-invoice via DMS integration)
|
- **Action Type**: **AUTO** (System generates e-invoice via DMS integration)
|
||||||
- **TAT**: 1 hour
|
- **TAT**: 1 hour
|
||||||
- **Mapping**: System user (`system@royalenfield.com`)
|
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
|
||||||
- **Status**: Auto-approved when triggered
|
- **Status**: Auto-approved when triggered
|
||||||
|
|
||||||
### Step 8: Credit Note Confirmation
|
### Step 8: Credit Note Confirmation
|
||||||
@ -121,7 +121,7 @@ const dealerUser = await User.findOne({ where: { email: dealerEmail } });
|
|||||||
1. Find user with department containing "Finance" and role = 'MANAGEMENT'
|
1. Find user with department containing "Finance" and role = 'MANAGEMENT'
|
||||||
2. Find user with designation containing "Finance" or "Accountant"
|
2. Find user with designation containing "Finance" or "Accountant"
|
||||||
3. Use configured finance team email from admin_configurations table
|
3. Use configured finance team email from admin_configurations table
|
||||||
4. Fallback: Use default finance email (e.g., finance@royalenfield.com)
|
4. Fallback: Use default finance email (e.g., finance@{{API_DOMAIN}})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|||||||
@ -112,7 +112,7 @@ Your CSV file must have these **44 columns** in the following order:
|
|||||||
| `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) |
|
| `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) |
|
||||||
| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) |
|
| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) |
|
||||||
| `single_format_month_year` | String(50) | No | Format: Sep-2014 |
|
| `single_format_month_year` | String(50) | No | Format: Sep-2014 |
|
||||||
| `domain_id` | String(255) | No | Email domain (e.g., dealer@royalenfield.com) |
|
| `domain_id` | String(255) | No | Email domain (e.g., dealer@{{API_DOMAIN}}) |
|
||||||
| `replacement` | String(50) | No | Replacement status |
|
| `replacement` | String(50) | No | Replacement status |
|
||||||
| `termination_resignation_status` | String(255) | No | Termination/Resignation status |
|
| `termination_resignation_status` | String(255) | No | Termination/Resignation status |
|
||||||
| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD |
|
| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD |
|
||||||
@ -183,7 +183,7 @@ Ensure dates are in `YYYY-MM-DD` format:
|
|||||||
|
|
||||||
```csv
|
```csv
|
||||||
sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode
|
sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode
|
||||||
5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.royalenfield.com,,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386
|
5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.{{API_DOMAIN}},,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist – Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386
|
||||||
```
|
```
|
||||||
|
|
||||||
**What gets auto-generated:**
|
**What gets auto-generated:**
|
||||||
|
|||||||
@ -56,7 +56,7 @@ users {
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userId": "uuid-1",
|
"userId": "uuid-1",
|
||||||
"email": "john.doe@royalenfield.com",
|
"email": "john.doe@{{API_DOMAIN}}",
|
||||||
"employeeId": "E12345", // Regular employee ID
|
"employeeId": "E12345", // Regular employee ID
|
||||||
"designation": "Software Engineer",
|
"designation": "Software Engineer",
|
||||||
"department": "IT",
|
"department": "IT",
|
||||||
@ -68,7 +68,7 @@ users {
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userId": "uuid-2",
|
"userId": "uuid-2",
|
||||||
"email": "test.2@royalenfield.com",
|
"email": "test.2@{{API_DOMAIN}}",
|
||||||
"employeeId": "RE-MH-001", // Dealer code stored here
|
"employeeId": "RE-MH-001", // Dealer code stored here
|
||||||
"designation": "Dealer",
|
"designation": "Dealer",
|
||||||
"department": "Dealer Operations",
|
"department": "Dealer Operations",
|
||||||
|
|||||||
@ -98,8 +98,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
|||||||
|
|
||||||
**Base URL Examples:**
|
**Base URL Examples:**
|
||||||
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
|
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
|
||||||
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice`
|
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
|
||||||
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice`
|
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
|
||||||
|
|
||||||
### 3.2 Request Headers
|
### 3.2 Request Headers
|
||||||
|
|
||||||
@ -205,8 +205,8 @@ User-Agent: DMS-Webhook-Client/1.0
|
|||||||
|
|
||||||
**Base URL Examples:**
|
**Base URL Examples:**
|
||||||
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
|
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
|
||||||
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
|
||||||
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
|
||||||
|
|
||||||
### 4.2 Request Headers
|
### 4.2 Request Headers
|
||||||
|
|
||||||
@ -563,8 +563,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
|||||||
| Environment | Invoice Webhook URL | Credit Note Webhook URL |
|
| Environment | Invoice Webhook URL | Credit Note Webhook URL |
|
||||||
|-------------|---------------------|-------------------------|
|
|-------------|---------------------|-------------------------|
|
||||||
| Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` |
|
| Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` |
|
||||||
| UAT | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note` |
|
| UAT | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
|
||||||
| Production | `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note` |
|
| Production | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -157,7 +157,7 @@ npm run seed:config
|
|||||||
```bash
|
```bash
|
||||||
# Edit the script
|
# Edit the script
|
||||||
nano scripts/assign-admin-user.sql
|
nano scripts/assign-admin-user.sql
|
||||||
# Change: YOUR_EMAIL@royalenfield.com
|
# Change: YOUR_EMAIL@{{API_DOMAIN}}
|
||||||
|
|
||||||
# Run it
|
# Run it
|
||||||
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
|
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
|
||||||
@ -170,7 +170,7 @@ psql -d royal_enfield_workflow
|
|||||||
|
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN'
|
SET role = 'ADMIN'
|
||||||
WHERE email = 'your-email@royalenfield.com';
|
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Verify
|
-- Verify
|
||||||
SELECT email, role FROM users WHERE role = 'ADMIN';
|
SELECT email, role FROM users WHERE role = 'ADMIN';
|
||||||
@ -188,7 +188,7 @@ psql -d royal_enfield_workflow -c "\dt"
|
|||||||
psql -d royal_enfield_workflow -c "\dT+ user_role_enum"
|
psql -d royal_enfield_workflow -c "\dT+ user_role_enum"
|
||||||
|
|
||||||
# Check your user
|
# Check your user
|
||||||
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@royalenfield.com';"
|
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@{{API_DOMAIN}}';"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -241,13 +241,13 @@ Expected output:
|
|||||||
```sql
|
```sql
|
||||||
-- Single user
|
-- Single user
|
||||||
UPDATE users SET role = 'MANAGEMENT'
|
UPDATE users SET role = 'MANAGEMENT'
|
||||||
WHERE email = 'manager@royalenfield.com';
|
WHERE email = 'manager@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Multiple users
|
-- Multiple users
|
||||||
UPDATE users SET role = 'MANAGEMENT'
|
UPDATE users SET role = 'MANAGEMENT'
|
||||||
WHERE email IN (
|
WHERE email IN (
|
||||||
'manager1@royalenfield.com',
|
'manager1@{{API_DOMAIN}}',
|
||||||
'manager2@royalenfield.com'
|
'manager2@{{API_DOMAIN}}'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- By department
|
-- By department
|
||||||
@ -260,13 +260,13 @@ WHERE department = 'Management' AND is_active = true;
|
|||||||
```sql
|
```sql
|
||||||
-- Single user
|
-- Single user
|
||||||
UPDATE users SET role = 'ADMIN'
|
UPDATE users SET role = 'ADMIN'
|
||||||
WHERE email = 'admin@royalenfield.com';
|
WHERE email = 'admin@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Multiple admins
|
-- Multiple admins
|
||||||
UPDATE users SET role = 'ADMIN'
|
UPDATE users SET role = 'ADMIN'
|
||||||
WHERE email IN (
|
WHERE email IN (
|
||||||
'admin1@royalenfield.com',
|
'admin1@{{API_DOMAIN}}',
|
||||||
'admin2@royalenfield.com'
|
'admin2@{{API_DOMAIN}}'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- By department
|
-- By department
|
||||||
@ -331,7 +331,7 @@ SELECT
|
|||||||
mobile_phone,
|
mobile_phone,
|
||||||
array_length(ad_groups, 1) as ad_group_count
|
array_length(ad_groups, 1) as ad_group_count
|
||||||
FROM users
|
FROM users
|
||||||
WHERE email = 'your-email@royalenfield.com';
|
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -344,7 +344,7 @@ WHERE email = 'your-email@royalenfield.com';
|
|||||||
curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
|
curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"email": "test@royalenfield.com",
|
"email": "test@{{API_DOMAIN}}",
|
||||||
"displayName": "Test User",
|
"displayName": "Test User",
|
||||||
"oktaSub": "test-sub-123"
|
"oktaSub": "test-sub-123"
|
||||||
}'
|
}'
|
||||||
@ -353,14 +353,14 @@ curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
|
|||||||
### 2. Check User Created with Default Role
|
### 2. Check User Created with Default Role
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT email, role FROM users WHERE email = 'test@royalenfield.com';
|
SELECT email, role FROM users WHERE email = 'test@{{API_DOMAIN}}';
|
||||||
-- Expected: role = 'USER'
|
-- Expected: role = 'USER'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Update to ADMIN
|
### 3. Update to ADMIN
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com';
|
UPDATE users SET role = 'ADMIN' WHERE email = 'test@{{API_DOMAIN}}';
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Verify API Access
|
### 4. Verify API Access
|
||||||
@ -369,7 +369,7 @@ UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com';
|
|||||||
# Login and get token
|
# Login and get token
|
||||||
curl -X POST http://localhost:5000/api/v1/auth/login \
|
curl -X POST http://localhost:5000/api/v1/auth/login \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"email": "test@royalenfield.com", ...}'
|
-d '{"email": "test@{{API_DOMAIN}}", ...}'
|
||||||
|
|
||||||
# Try admin endpoint (should work if ADMIN role)
|
# Try admin endpoint (should work if ADMIN role)
|
||||||
curl http://localhost:5000/api/v1/admin/configurations \
|
curl http://localhost:5000/api/v1/admin/configurations \
|
||||||
@ -449,7 +449,7 @@ npm run migrate
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Check if user exists
|
-- Check if user exists
|
||||||
SELECT * FROM users WHERE email = 'your-email@royalenfield.com';
|
SELECT * FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Check Okta sub
|
-- Check Okta sub
|
||||||
SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
|
SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
|
||||||
@ -459,7 +459,7 @@ SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Verify role
|
-- Verify role
|
||||||
SELECT email, role, is_active FROM users WHERE email = 'your-email@royalenfield.com';
|
SELECT email, role, is_active FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Check role enum
|
-- Check role enum
|
||||||
\dT+ user_role_enum
|
\dT+ user_role_enum
|
||||||
|
|||||||
@ -29,7 +29,7 @@ This guide provides step-by-step instructions for setting up Google Cloud Storag
|
|||||||
|------|------------------|
|
|------|------------------|
|
||||||
| **Application** | Royal Enfield Workflow System |
|
| **Application** | Royal Enfield Workflow System |
|
||||||
| **Environment** | Production |
|
| **Environment** | Production |
|
||||||
| **Domain** | `https://reflow.royalenfield.com` |
|
| **Domain** | `https://reflow.{{API_DOMAIN}}` |
|
||||||
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
|
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
|
||||||
| **Storage Type** | Google Cloud Storage (GCS) |
|
| **Storage Type** | Google Cloud Storage (GCS) |
|
||||||
| **Region** | `asia-south1` (Mumbai) |
|
| **Region** | `asia-south1` (Mumbai) |
|
||||||
@ -325,8 +325,8 @@ Create `cors-config-prod.json`:
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"origin": [
|
"origin": [
|
||||||
"https://reflow.royalenfield.com",
|
"https://reflow.{{API_DOMAIN}}",
|
||||||
"https://www.royalenfield.com"
|
"https://www.{{API_DOMAIN}}"
|
||||||
],
|
],
|
||||||
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
||||||
"responseHeader": [
|
"responseHeader": [
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|------|-------|
|
|------|-------|
|
||||||
| **Application** | RE Workflow System |
|
| **Application** | RE Workflow System |
|
||||||
| **Environment** | UAT |
|
| **Environment** | UAT |
|
||||||
| **Domain** | https://reflow-uat.royalenfield.com |
|
| **Domain** | https://reflow-uat.{{API_DOMAIN}} |
|
||||||
| **Purpose** | Store workflow documents and attachments |
|
| **Purpose** | Store workflow documents and attachments |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -131,8 +131,8 @@ Apply this CORS policy to allow browser uploads:
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"origin": [
|
"origin": [
|
||||||
"https://reflow-uat.royalenfield.com",
|
"https://reflow-uat.{{API_DOMAIN}}",
|
||||||
"https://reflow.royalenfield.com"
|
"https://reflow.{{API_DOMAIN}}"
|
||||||
],
|
],
|
||||||
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
||||||
"responseHeader": [
|
"responseHeader": [
|
||||||
|
|||||||
@ -72,8 +72,8 @@ The Users API returns a complete user object:
|
|||||||
"employeeID": "E09994",
|
"employeeID": "E09994",
|
||||||
"title": "Supports Business Applications (SAP) portfolio",
|
"title": "Supports Business Applications (SAP) portfolio",
|
||||||
"department": "Deputy Manager - Digital & IT",
|
"department": "Deputy Manager - Digital & IT",
|
||||||
"login": "sanjaysahu@Royalenfield.com",
|
"login": "sanjaysahu@{{API_DOMAIN}}",
|
||||||
"email": "sanjaysahu@royalenfield.com"
|
"email": "sanjaysahu@{{API_DOMAIN}}"
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ Example log:
|
|||||||
### Test with curl
|
### Test with curl
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --location 'https://dev-830839.oktapreview.com/api/v1/users/testuser10@eichergroup.com' \
|
curl --location 'https://{{IDP_DOMAIN}}/api/v1/users/testuser10@eichergroup.com' \
|
||||||
--header 'Authorization: SSWS YOUR_OKTA_API_TOKEN' \
|
--header 'Authorization: SSWS YOUR_OKTA_API_TOKEN' \
|
||||||
--header 'Accept: application/json'
|
--header 'Accept: application/json'
|
||||||
```
|
```
|
||||||
|
|||||||
@ -450,16 +450,16 @@ Before Migration:
|
|||||||
+-------------------------+-----------+
|
+-------------------------+-----------+
|
||||||
| email | is_admin |
|
| email | is_admin |
|
||||||
+-------------------------+-----------+
|
+-------------------------+-----------+
|
||||||
| admin@royalenfield.com | true |
|
| admin@{{API_DOMAIN}} | true |
|
||||||
| user1@royalenfield.com | false |
|
| user1@{{API_DOMAIN}} | false |
|
||||||
+-------------------------+-----------+
|
+-------------------------+-----------+
|
||||||
|
|
||||||
After Migration:
|
After Migration:
|
||||||
+-------------------------+-----------+-----------+
|
+-------------------------+-----------+-----------+
|
||||||
| email | role | is_admin |
|
| email | role | is_admin |
|
||||||
+-------------------------+-----------+-----------+
|
+-------------------------+-----------+-----------+
|
||||||
| admin@royalenfield.com | ADMIN | true |
|
| admin@{{API_DOMAIN}} | ADMIN | true |
|
||||||
| user1@royalenfield.com | USER | false |
|
| user1@{{API_DOMAIN}} | USER | false |
|
||||||
+-------------------------+-----------+-----------+
|
+-------------------------+-----------+-----------+
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -473,17 +473,17 @@ After Migration:
|
|||||||
-- Make user a MANAGEMENT role
|
-- Make user a MANAGEMENT role
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'MANAGEMENT', is_admin = false
|
SET role = 'MANAGEMENT', is_admin = false
|
||||||
WHERE email = 'manager@royalenfield.com';
|
WHERE email = 'manager@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Make user an ADMIN role
|
-- Make user an ADMIN role
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN', is_admin = true
|
SET role = 'ADMIN', is_admin = true
|
||||||
WHERE email = 'admin@royalenfield.com';
|
WHERE email = 'admin@{{API_DOMAIN}}';
|
||||||
|
|
||||||
-- Revert to USER role
|
-- Revert to USER role
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'USER', is_admin = false
|
SET role = 'USER', is_admin = false
|
||||||
WHERE email = 'user@royalenfield.com';
|
WHERE email = 'user@{{API_DOMAIN}}';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Via API (Admin Endpoint)
|
### Via API (Admin Endpoint)
|
||||||
|
|||||||
@ -47,12 +47,12 @@ psql -d royal_enfield_db -f scripts/assign-user-roles.sql
|
|||||||
-- Make specific users ADMIN
|
-- Make specific users ADMIN
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN', is_admin = true
|
SET role = 'ADMIN', is_admin = true
|
||||||
WHERE email IN ('admin@royalenfield.com', 'it.admin@royalenfield.com');
|
WHERE email IN ('admin@{{API_DOMAIN}}', 'it.admin@{{API_DOMAIN}}');
|
||||||
|
|
||||||
-- Make specific users MANAGEMENT
|
-- Make specific users MANAGEMENT
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'MANAGEMENT', is_admin = false
|
SET role = 'MANAGEMENT', is_admin = false
|
||||||
WHERE email IN ('manager@royalenfield.com', 'auditor@royalenfield.com');
|
WHERE email IN ('manager@{{API_DOMAIN}}', 'auditor@{{API_DOMAIN}}');
|
||||||
|
|
||||||
-- Verify roles
|
-- Verify roles
|
||||||
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
|
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
|
||||||
@ -219,7 +219,7 @@ GROUP BY role;
|
|||||||
-- Check specific user
|
-- Check specific user
|
||||||
SELECT email, role, is_admin
|
SELECT email, role, is_admin
|
||||||
FROM users
|
FROM users
|
||||||
WHERE email = 'your-email@royalenfield.com';
|
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test 2: Test API Access
|
### Test 2: Test API Access
|
||||||
@ -356,7 +356,7 @@ WHERE designation ILIKE '%manager%' OR designation ILIKE '%head%';
|
|||||||
```sql
|
```sql
|
||||||
SELECT email, role, is_admin
|
SELECT email, role, is_admin
|
||||||
FROM users
|
FROM users
|
||||||
WHERE email = 'your-email@royalenfield.com';
|
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -314,7 +314,7 @@ JWT_EXPIRY=24h
|
|||||||
REFRESH_TOKEN_EXPIRY=7d
|
REFRESH_TOKEN_EXPIRY=7d
|
||||||
|
|
||||||
# Okta Configuration
|
# Okta Configuration
|
||||||
OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
OKTA_DOMAIN=https://{{IDP_DOMAIN}}
|
||||||
OKTA_CLIENT_ID=your-client-id
|
OKTA_CLIENT_ID=your-client-id
|
||||||
OKTA_CLIENT_SECRET=your-client-secret
|
OKTA_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
@ -334,7 +334,7 @@ GCP_BUCKET_PUBLIC=true
|
|||||||
|
|
||||||
**Identity Provider**: Okta
|
**Identity Provider**: Okta
|
||||||
- **Domain**: Configurable via `OKTA_DOMAIN` environment variable
|
- **Domain**: Configurable via `OKTA_DOMAIN` environment variable
|
||||||
- **Default**: `https://dev-830839.oktapreview.com`
|
- **Default**: `https://{{IDP_DOMAIN}}`
|
||||||
- **Protocol**: OAuth 2.0 / OpenID Connect (OIDC)
|
- **Protocol**: OAuth 2.0 / OpenID Connect (OIDC)
|
||||||
- **Grant Types**: Authorization Code, Resource Owner Password Credentials
|
- **Grant Types**: Authorization Code, Resource Owner Password Credentials
|
||||||
|
|
||||||
@ -650,7 +650,7 @@ graph LR
|
|||||||
{
|
{
|
||||||
"userId": "uuid",
|
"userId": "uuid",
|
||||||
"employeeId": "EMP001",
|
"employeeId": "EMP001",
|
||||||
"email": "user@royalenfield.com",
|
"email": "user@{{API_DOMAIN}}",
|
||||||
"role": "USER" | "MANAGEMENT" | "ADMIN",
|
"role": "USER" | "MANAGEMENT" | "ADMIN",
|
||||||
"iat": 1234567890,
|
"iat": 1234567890,
|
||||||
"exp": 1234654290
|
"exp": 1234654290
|
||||||
@ -1048,7 +1048,7 @@ JWT_EXPIRY=24h
|
|||||||
REFRESH_TOKEN_EXPIRY=7d
|
REFRESH_TOKEN_EXPIRY=7d
|
||||||
|
|
||||||
# Okta
|
# Okta
|
||||||
OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
OKTA_DOMAIN=https://{{IDP_DOMAIN}}
|
||||||
OKTA_CLIENT_ID=your-client-id
|
OKTA_CLIENT_ID=your-client-id
|
||||||
OKTA_CLIENT_SECRET=your-client-secret
|
OKTA_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
@ -1063,7 +1063,7 @@ GCP_BUCKET_PUBLIC=true
|
|||||||
**Frontend (.env):**
|
**Frontend (.env):**
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE_URL=https://api.rebridge.co.in/api/v1
|
VITE_API_BASE_URL=https://api.rebridge.co.in/api/v1
|
||||||
VITE_OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
VITE_OKTA_DOMAIN=https://{{IDP_DOMAIN}}
|
||||||
VITE_OKTA_CLIENT_ID=your-client-id
|
VITE_OKTA_CLIENT_ID=your-client-id
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ await this.createClaimApprovalLevels(
|
|||||||
isAuto: false,
|
isAuto: false,
|
||||||
approverType: 'department_lead' as const,
|
approverType: 'department_lead' as const,
|
||||||
approverId: departmentLead?.userId || null,
|
approverId: departmentLead?.userId || null,
|
||||||
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@royalenfield.com',
|
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@{{API_DOMAIN}}',
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -181,7 +181,7 @@ POST http://localhost:5000/api/v1/auth/login
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"username": "john.doe@royalenfield.com",
|
"username": "john.doe@{{API_DOMAIN}}",
|
||||||
"password": "SecurePassword123!"
|
"password": "SecurePassword123!"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
20
env.example
20
env.example
@ -26,8 +26,8 @@ REFRESH_TOKEN_EXPIRY=7d
|
|||||||
SESSION_SECRET=your_session_secret_here_min_32_chars
|
SESSION_SECRET=your_session_secret_here_min_32_chars
|
||||||
|
|
||||||
# Cloud Storage (GCP)
|
# Cloud Storage (GCP)
|
||||||
GCP_PROJECT_ID=re-workflow-project
|
GCP_PROJECT_ID={{GCP_PROJECT_ID}}
|
||||||
GCP_BUCKET_NAME=re-workflow-documents
|
GCP_BUCKET_NAME={{GCP_BUCKET_NAME}}
|
||||||
GCP_KEY_FILE=./config/gcp-key.json
|
GCP_KEY_FILE=./config/gcp-key.json
|
||||||
|
|
||||||
# Google Secret Manager (Optional - for production)
|
# Google Secret Manager (Optional - for production)
|
||||||
@ -41,9 +41,9 @@ USE_GOOGLE_SECRET_MANAGER=false
|
|||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=notifications@royalenfield.com
|
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||||
SMTP_PASSWORD=your_smtp_password
|
SMTP_PASSWORD=your_smtp_password
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
|
||||||
|
|
||||||
# AI Service (for conclusion generation) - Vertex AI Gemini
|
# AI Service (for conclusion generation) - Vertex AI Gemini
|
||||||
# Uses service account credentials from GCP_KEY_FILE
|
# Uses service account credentials from GCP_KEY_FILE
|
||||||
@ -55,7 +55,7 @@ VERTEX_AI_LOCATION=asia-south1
|
|||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_FILE_PATH=./logs
|
LOG_FILE_PATH=./logs
|
||||||
APP_VERSION=1.2.0
|
APP_VERSION={{APP_VERSION}}
|
||||||
|
|
||||||
# ============ Loki Configuration (Grafana Log Aggregation) ============
|
# ============ Loki Configuration (Grafana Log Aggregation) ============
|
||||||
LOKI_HOST= # e.g., http://loki:3100 or http://monitoring.cloudtopiaa.com:3100
|
LOKI_HOST= # e.g., http://loki:3100 or http://monitoring.cloudtopiaa.com:3100
|
||||||
@ -66,7 +66,7 @@ LOKI_PASSWORD= # Optional: Basic auth password
|
|||||||
CORS_ORIGIN="*"
|
CORS_ORIGIN="*"
|
||||||
|
|
||||||
# Rate Limiting
|
# Rate Limiting
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
|
||||||
RATE_LIMIT_MAX_REQUESTS=100
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
|
||||||
# File Upload
|
# File Upload
|
||||||
@ -83,16 +83,16 @@ OKTA_CLIENT_ID={{okta_client_id}}
|
|||||||
OKTA_CLIENT_SECRET={{okta_client_secret}}
|
OKTA_CLIENT_SECRET={{okta_client_secret}}
|
||||||
|
|
||||||
# Notificaton Service Worker credentials
|
# Notificaton Service Worker credentials
|
||||||
VAPID_PUBLIC_KEY={{vapid_public_key}} note: same key need to add on front end for web push
|
VAPID_PUBLIC_KEY={{VAPID_PUBLIC_KEY}}
|
||||||
VAPID_PRIVATE_KEY={{vapid_private_key}}
|
VAPID_PRIVATE_KEY={{vapid_private_key}}
|
||||||
VAPID_CONTACT=mailto:you@example.com
|
VAPID_CONTACT=mailto:you@example.com
|
||||||
|
|
||||||
#Redis
|
#Redis
|
||||||
REDIS_URL={{REDIS_URL_FOR DELAY JoBS create redis setup and add url here}}
|
REDIS_URL={{REDIS_URL}}
|
||||||
TAT_TEST_MODE=false (on true it will consider 1 hour==1min)
|
TAT_TEST_MODE=false # Set to true to accelerate TAT for testing
|
||||||
|
|
||||||
# SAP Integration (OData Service via Zscaler)
|
# SAP Integration (OData Service via Zscaler)
|
||||||
SAP_BASE_URL=https://RENOIHND01.Eichergroup.com:1443
|
SAP_BASE_URL=https://{{SAP_DOMAIN_HERE}}:{{PORT}}
|
||||||
SAP_USERNAME={{SAP_USERNAME}}
|
SAP_USERNAME={{SAP_USERNAME}}
|
||||||
SAP_PASSWORD={{SAP_PASSWORD}}
|
SAP_PASSWORD={{SAP_PASSWORD}}
|
||||||
SAP_TIMEOUT_MS=30000
|
SAP_TIMEOUT_MS=30000
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN'
|
SET role = 'ADMIN'
|
||||||
WHERE email = 'YOUR_EMAIL@royalenfield.com' -- ← CHANGE THIS
|
WHERE email = 'YOUR_EMAIL@{{API_DOMAIN}}' -- ← CHANGE THIS
|
||||||
RETURNING
|
RETURNING
|
||||||
user_id,
|
user_id,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@ -21,9 +21,9 @@
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'ADMIN'
|
SET role = 'ADMIN'
|
||||||
WHERE email IN (
|
WHERE email IN (
|
||||||
'admin@royalenfield.com',
|
'admin@{{API_DOMAIN}}',
|
||||||
'it.admin@royalenfield.com',
|
'it.admin@{{API_DOMAIN}}',
|
||||||
'system.admin@royalenfield.com'
|
'system.admin@{{API_DOMAIN}}'
|
||||||
-- Add more admin emails here
|
-- Add more admin emails here
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -45,9 +45,9 @@ ORDER BY email;
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
SET role = 'MANAGEMENT'
|
SET role = 'MANAGEMENT'
|
||||||
WHERE email IN (
|
WHERE email IN (
|
||||||
'manager1@royalenfield.com',
|
'manager1@{{API_DOMAIN}}',
|
||||||
'dept.head@royalenfield.com',
|
'dept.head@{{API_DOMAIN}}',
|
||||||
'auditor@royalenfield.com'
|
'auditor@{{API_DOMAIN}}'
|
||||||
-- Add more management emails here
|
-- Add more management emails here
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -162,7 +162,7 @@ SMTP_PORT=587
|
|||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=${SMTP_USER}
|
SMTP_USER=${SMTP_USER}
|
||||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
|
||||||
|
|
||||||
# Vertex AI Gemini Configuration (for conclusion generation)
|
# Vertex AI Gemini Configuration (for conclusion generation)
|
||||||
# Service account credentials should be placed in ./credentials/ folder
|
# Service account credentials should be placed in ./credentials/ folder
|
||||||
@ -232,7 +232,7 @@ show_vapid_instructions() {
|
|||||||
echo " VITE_PUBLIC_VAPID_KEY=<your-public-key>"
|
echo " VITE_PUBLIC_VAPID_KEY=<your-public-key>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "5. The VAPID_CONTACT should be a valid mailto: URL"
|
echo "5. The VAPID_CONTACT should be a valid mailto: URL"
|
||||||
echo " Example: mailto:admin@royalenfield.com"
|
echo " Example: mailto:admin@{{API_DOMAIN}}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!"
|
echo "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
12
src/app.ts
12
src/app.ts
@ -7,6 +7,8 @@ import { UserService } from './services/user.service';
|
|||||||
import { SSOUserData } from './types/auth.types';
|
import { SSOUserData } from './types/auth.types';
|
||||||
import { sequelize } from './config/database';
|
import { sequelize } from './config/database';
|
||||||
import { corsMiddleware } from './middlewares/cors.middleware';
|
import { corsMiddleware } from './middlewares/cors.middleware';
|
||||||
|
import { authenticateToken } from './middlewares/auth.middleware';
|
||||||
|
import { requireAdmin } from './middlewares/authorization.middleware';
|
||||||
import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware';
|
import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware';
|
||||||
import routes from './routes/index';
|
import routes from './routes/index';
|
||||||
import { ensureUploadDir, UPLOAD_DIR } from './config/storage';
|
import { ensureUploadDir, UPLOAD_DIR } from './config/storage';
|
||||||
@ -51,7 +53,7 @@ app.use((req: express.Request, res: express.Response, next: express.NextFunction
|
|||||||
"script-src 'self'",
|
"script-src 'self'",
|
||||||
"script-src-elem 'self'",
|
"script-src-elem 'self'",
|
||||||
"script-src-attr 'none'",
|
"script-src-attr 'none'",
|
||||||
"img-src 'self' data: blob: https://*.royalenfield.com https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com",
|
"img-src 'self' data: blob: https://*.{{API_DOMAIN}} https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com",
|
||||||
"frame-src 'self' blob: data:",
|
"frame-src 'self' blob: data:",
|
||||||
"font-src 'self' https://fonts.gstatic.com data:",
|
"font-src 'self' https://fonts.gstatic.com data:",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
@ -117,7 +119,7 @@ app.use(morgan('combined'));
|
|||||||
app.use(metricsMiddleware);
|
app.use(metricsMiddleware);
|
||||||
|
|
||||||
// Prometheus metrics endpoint - expose metrics for scraping
|
// Prometheus metrics endpoint - expose metrics for scraping
|
||||||
app.use(createMetricsRouter());
|
app.use('/metrics', authenticateToken, requireAdmin, createMetricsRouter());
|
||||||
|
|
||||||
// Health check endpoint (before API routes)
|
// Health check endpoint (before API routes)
|
||||||
app.get('/health', (_req: express.Request, res: express.Response) => {
|
app.get('/health', (_req: express.Request, res: express.Response) => {
|
||||||
@ -134,7 +136,7 @@ app.use('/api/v1', routes);
|
|||||||
|
|
||||||
// Serve uploaded files statically
|
// Serve uploaded files statically
|
||||||
ensureUploadDir();
|
ensureUploadDir();
|
||||||
app.use('/uploads', express.static(UPLOAD_DIR));
|
app.use('/uploads', authenticateToken, express.static(UPLOAD_DIR));
|
||||||
|
|
||||||
// Legacy SSO Callback endpoint for user creation/update (kept for backward compatibility)
|
// Legacy SSO Callback endpoint for user creation/update (kept for backward compatibility)
|
||||||
app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => {
|
app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||||
@ -188,7 +190,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get all users endpoint
|
// Get all users endpoint
|
||||||
app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise<void> => {
|
app.get('/api/v1/users', authenticateToken, requireAdmin, async (_req: express.Request, res: express.Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const users = await userService.getAllUsers();
|
const users = await userService.getAllUsers();
|
||||||
|
|
||||||
@ -293,4 +295,4 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@ -8,9 +8,9 @@ export const emailConfig = {
|
|||||||
pass: process.env.SMTP_PASSWORD || '',
|
pass: process.env.SMTP_PASSWORD || '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@royalenfield.com>',
|
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@{{API_DOMAIN}}>',
|
||||||
|
|
||||||
// Email templates
|
// Email templates
|
||||||
templates: {
|
templates: {
|
||||||
workflowCreated: 'workflow-created',
|
workflowCreated: 'workflow-created',
|
||||||
@ -20,7 +20,7 @@ export const emailConfig = {
|
|||||||
tatReminder: 'tat-reminder',
|
tatReminder: 'tat-reminder',
|
||||||
tatBreached: 'tat-breached',
|
tatBreached: 'tat-breached',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Email settings
|
// Email settings
|
||||||
settings: {
|
settings: {
|
||||||
retryAttempts: 3,
|
retryAttempts: 3,
|
||||||
|
|||||||
@ -8,18 +8,18 @@ const ssoConfig: SSOConfig = {
|
|||||||
get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; },
|
get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; },
|
||||||
get sessionSecret() { return process.env.SESSION_SECRET || ''; },
|
get sessionSecret() { return process.env.SESSION_SECRET || ''; },
|
||||||
// Use only FRONTEND_URL from environment - no fallbacks
|
// Use only FRONTEND_URL from environment - no fallbacks
|
||||||
get allowedOrigins() {
|
get allowedOrigins() {
|
||||||
return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [];
|
return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [];
|
||||||
},
|
},
|
||||||
// Okta/Auth0 configuration for token exchange
|
// Okta/Auth0 configuration for token exchange
|
||||||
get oktaDomain() { return process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com'; },
|
get oktaDomain() { return process.env.OKTA_DOMAIN || '{{IDP_DOMAIN}}'; },
|
||||||
get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; },
|
get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; },
|
||||||
get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
|
get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
|
||||||
get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API
|
get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API
|
||||||
// Tanflow configuration for token exchange
|
// Tanflow configuration for token exchange
|
||||||
get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || 'https://ssodev.rebridge.co.in/realms/RE'; },
|
get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE'; },
|
||||||
get tanflowClientId() { return process.env.TANFLOW_CLIENT_ID || 'REFLOW'; },
|
get tanflowClientId() { return process.env.TANFLOW_CLIENT_ID || 'REFLOW'; },
|
||||||
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || 'cfIzMlwAMF1m4QWAP5StzZbV47HIrCox'; },
|
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || '{{TANFLOW_CLIENT_SECRET}}'; },
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ssoConfig };
|
export { ssoConfig };
|
||||||
|
|||||||
@ -172,7 +172,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
|||||||
- Initiator (for record)
|
- Initiator (for record)
|
||||||
- Finance team
|
- Finance team
|
||||||
- **Template**: `creditNoteSent.template.ts` (NEW)
|
- **Template**: `creditNoteSent.template.ts` (NEW)
|
||||||
- **Status**: ❌ Not Implemented (TODO comment at line 2037-2044)
|
- **Status**: Required implementation
|
||||||
- **Notification Type**: `credit_note_sent`
|
- **Notification Type**: `credit_note_sent`
|
||||||
- **Data Needed**:
|
- **Data Needed**:
|
||||||
- Credit note number
|
- Credit note number
|
||||||
@ -184,7 +184,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
|||||||
- Reason for credit note
|
- Reason for credit note
|
||||||
- Download link (if available)
|
- Download link (if available)
|
||||||
- **Notes**:
|
- **Notes**:
|
||||||
- Currently has TODO comment for email implementation
|
- Planned for email implementation
|
||||||
- Critical for dealer notification
|
- Critical for dealer notification
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -246,7 +246,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
|||||||
5. **Credit Note Sent** (`creditNoteSent.template.ts`)
|
5. **Credit Note Sent** (`creditNoteSent.template.ts`)
|
||||||
- Priority: High
|
- Priority: High
|
||||||
- When: Credit note is sent to dealer (Step 8)
|
- When: Credit note is sent to dealer (Step 8)
|
||||||
- Currently has TODO comment
|
- Planned for implementation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -255,7 +255,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
|||||||
### High Priority (Critical for Workflow)
|
### High Priority (Critical for Workflow)
|
||||||
1. **Activity Created** - Currently using generic notification, should be branded
|
1. **Activity Created** - Currently using generic notification, should be branded
|
||||||
2. **E-Invoice Generated** - Important for financial tracking
|
2. **E-Invoice Generated** - Important for financial tracking
|
||||||
3. **Credit Note Sent** - Critical for dealer notification (currently TODO)
|
3. **Credit Note Sent** - Critical for dealer notification
|
||||||
|
|
||||||
### Medium Priority (Nice to Have)
|
### Medium Priority (Nice to Have)
|
||||||
4. **Proposal Submitted** - Better UX, but existing approval request works
|
4. **Proposal Submitted** - Better UX, but existing approval request works
|
||||||
|
|||||||
@ -991,9 +991,9 @@ Add to `.env`:
|
|||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
SMTP_USER=notifications@royalenfield.com
|
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||||
SMTP_PASSWORD=your-app-specific-password
|
SMTP_PASSWORD=your-app-specific-password
|
||||||
EMAIL_FROM=RE Flow <noreply@royalenfield.com>
|
EMAIL_FROM=RE Flow <noreply@{{API_DOMAIN}}>
|
||||||
|
|
||||||
# Email Settings
|
# Email Settings
|
||||||
EMAIL_ENABLED=true
|
EMAIL_ENABLED=true
|
||||||
@ -1002,10 +1002,10 @@ EMAIL_BATCH_SIZE=50
|
|||||||
EMAIL_RETRY_ATTEMPTS=3
|
EMAIL_RETRY_ATTEMPTS=3
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
BASE_URL=https://workflow.royalenfield.com
|
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||||
COMPANY_NAME=Royal Enfield
|
COMPANY_NAME=Royal Enfield
|
||||||
COMPANY_WEBSITE=https://www.royalenfield.com
|
COMPANY_WEBSITE=https://www.{{API_DOMAIN}}
|
||||||
SUPPORT_EMAIL=support@royalenfield.com
|
SUPPORT_EMAIL=support@{{API_DOMAIN}}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -65,7 +65,7 @@ Each template uses color-coded gradients to indicate the scenario:
|
|||||||
All templates feature a single action button:
|
All templates feature a single action button:
|
||||||
- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now"
|
- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now"
|
||||||
- **Link Format:** `{baseURL}/request/{requestNumber}`
|
- **Link Format:** `{baseURL}/request/{requestNumber}`
|
||||||
- **Example:** `https://workflow.royalenfield.com/request/REQ-2025-12-0013`
|
- **Example:** `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
|
||||||
|
|
||||||
No approval/rejection buttons in emails - all actions happen within the application.
|
No approval/rejection buttons in emails - all actions happen within the application.
|
||||||
|
|
||||||
@ -231,8 +231,8 @@ SMTP_USER=your-email@domain.com
|
|||||||
SMTP_PASSWORD=your-app-password
|
SMTP_PASSWORD=your-app-password
|
||||||
|
|
||||||
# Email Settings
|
# Email Settings
|
||||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
|
||||||
BASE_URL=https://workflow.royalenfield.com
|
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||||
COMPANY_NAME=Royal Enfield
|
COMPANY_NAME=Royal Enfield
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -361,7 +361,7 @@ All `[ViewDetailsLink]` placeholders should be replaced with:
|
|||||||
{baseURL}/request/{requestNumber}
|
{baseURL}/request/{requestNumber}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: `https://workflow.royalenfield.com/request/REQ-2025-12-0013`
|
Example: `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
|
||||||
|
|
||||||
### Company Name
|
### Company Name
|
||||||
Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield")
|
Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield")
|
||||||
|
|||||||
@ -12,15 +12,15 @@ emailtemplates/
|
|||||||
├── approvalRequest.template.ts ✅ Single approver email
|
├── approvalRequest.template.ts ✅ Single approver email
|
||||||
├── multiApproverRequest.template.ts ✅ Multi-approver email
|
├── multiApproverRequest.template.ts ✅ Multi-approver email
|
||||||
│
|
│
|
||||||
├── approvalConfirmation.template.ts 🔨 TODO
|
├── approvalConfirmation.template.ts ✅ DONE
|
||||||
├── rejectionNotification.template.ts 🔨 TODO
|
├── rejectionNotification.template.ts ✅ DONE
|
||||||
├── tatReminder.template.ts 🔨 TODO
|
├── tatReminder.template.ts ✅ DONE
|
||||||
├── tatBreached.template.ts 🔨 TODO
|
├── tatBreached.template.ts ✅ DONE
|
||||||
├── workflowPaused.template.ts 🔨 TODO
|
├── workflowPaused.template.ts ✅ DONE
|
||||||
├── workflowResumed.template.ts 🔨 TODO
|
├── workflowResumed.template.ts ✅ DONE
|
||||||
├── participantAdded.template.ts 🔨 TODO
|
├── participantAdded.template.ts ✅ DONE
|
||||||
├── approverSkipped.template.ts 🔨 TODO
|
├── approverSkipped.template.ts ✅ DONE
|
||||||
└── requestClosed.template.ts 🔨 TODO
|
└── requestClosed.template.ts ✅ DONE
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -53,7 +53,7 @@ const data: RequestCreatedData = {
|
|||||||
requestTime: '02:30 PM',
|
requestTime: '02:30 PM',
|
||||||
totalApprovers: 3,
|
totalApprovers: 3,
|
||||||
expectedTAT: 48,
|
expectedTAT: 48,
|
||||||
viewDetailsLink: 'https://workflow.royalenfield.com/request/REQ-2025-12-0013',
|
viewDetailsLink: 'https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013',
|
||||||
companyName: 'Royal Enfield'
|
companyName: 'Royal Enfield'
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@ -188,10 +188,10 @@ SMTP_USER=your-email@domain.com
|
|||||||
SMTP_PASSWORD=your-app-password
|
SMTP_PASSWORD=your-app-password
|
||||||
|
|
||||||
# Email Settings
|
# Email Settings
|
||||||
EMAIL_FROM=Royal Enfield Workflow <notifications@royalenfield.com>
|
EMAIL_FROM=Royal Enfield Workflow <notifications@{{API_DOMAIN}}>
|
||||||
|
|
||||||
# Application Settings
|
# Application Settings
|
||||||
BASE_URL=https://workflow.royalenfield.com
|
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||||
COMPANY_NAME=Royal Enfield
|
COMPANY_NAME=Royal Enfield
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -13,12 +13,12 @@ import { EmailHeaderConfig, EmailFooterConfig } from './helpers';
|
|||||||
export const CompanyInfo = {
|
export const CompanyInfo = {
|
||||||
name: 'Royal Enfield',
|
name: 'Royal Enfield',
|
||||||
productName: 'RE Flow', // Product name displayed in header
|
productName: 'RE Flow', // Product name displayed in header
|
||||||
website: 'https://www.royalenfield.com',
|
website: 'https://www.{{API_DOMAIN}}',
|
||||||
supportEmail: 'support@royalenfield.com',
|
supportEmail: 'support@{{API_DOMAIN}}',
|
||||||
|
|
||||||
// Logo configuration for email headers
|
// Logo configuration for email headers
|
||||||
logo: {
|
logo: {
|
||||||
url: 'https://www.royalenfield.com/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp',
|
url: 'https://www.{{API_DOMAIN}}/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp',
|
||||||
alt: 'Royal Enfield Logo',
|
alt: 'Royal Enfield Logo',
|
||||||
width: 220, // Logo width in pixels (wider for better visibility)
|
width: 220, // Logo width in pixels (wider for better visibility)
|
||||||
height: 65, // Logo height in pixels (proportional ratio ~3.4:1)
|
height: 65, // Logo height in pixels (proportional ratio ~3.4:1)
|
||||||
@ -88,7 +88,7 @@ export const CustomHeaderStyles = {
|
|||||||
* Usage in email service:
|
* Usage in email service:
|
||||||
* const link = getViewDetailsLink('REQ-2025-12-0013', process.env.FRONTEND_URL);
|
* const link = getViewDetailsLink('REQ-2025-12-0013', process.env.FRONTEND_URL);
|
||||||
*
|
*
|
||||||
* Result: https://workflow.royalenfield.com/request/REQ-2025-12-0013
|
* Result: https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013
|
||||||
*/
|
*/
|
||||||
export function getViewDetailsLink(requestNumber: string, frontendUrl: string): string {
|
export function getViewDetailsLink(requestNumber: string, frontendUrl: string): string {
|
||||||
return `${frontendUrl}/request/${requestNumber}`;
|
return `${frontendUrl}/request/${requestNumber}`;
|
||||||
|
|||||||
@ -14,13 +14,13 @@ async function generatePreviews() {
|
|||||||
// Sample data
|
// Sample data
|
||||||
const initiator = {
|
const initiator = {
|
||||||
userId: 'user-1',
|
userId: 'user-1',
|
||||||
email: 'john.doe@royalenfield.com',
|
email: 'john.doe@{{API_DOMAIN}}',
|
||||||
displayName: 'John Doe'
|
displayName: 'John Doe'
|
||||||
};
|
};
|
||||||
|
|
||||||
const approver = {
|
const approver = {
|
||||||
userId: 'user-2',
|
userId: 'user-2',
|
||||||
email: 'jane.smith@royalenfield.com',
|
email: 'jane.smith@{{API_DOMAIN}}',
|
||||||
displayName: 'Jane Smith'
|
displayName: 'Jane Smith'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import {
|
import {
|
||||||
getRequestCreatedEmail,
|
getRequestCreatedEmail,
|
||||||
getApprovalRequestEmail,
|
getApprovalRequestEmail,
|
||||||
getMultiApproverRequestEmail,
|
getMultiApproverRequestEmail,
|
||||||
@ -21,10 +21,10 @@ import {
|
|||||||
async function sendTestEmail() {
|
async function sendTestEmail() {
|
||||||
try {
|
try {
|
||||||
console.log('🚀 Creating nodemailer test account...');
|
console.log('🚀 Creating nodemailer test account...');
|
||||||
|
|
||||||
// Create a test account automatically (free, no signup needed)
|
// Create a test account automatically (free, no signup needed)
|
||||||
const testAccount = await nodemailer.createTestAccount();
|
const testAccount = await nodemailer.createTestAccount();
|
||||||
|
|
||||||
console.log('✅ Test account created:', testAccount.user);
|
console.log('✅ Test account created:', testAccount.user);
|
||||||
|
|
||||||
// Create transporter with test SMTP details
|
// Create transporter with test SMTP details
|
||||||
@ -100,12 +100,12 @@ async function sendTestEmail() {
|
|||||||
// Test 1: Request Created Email
|
// Test 1: Request Created Email
|
||||||
const html1 = getRequestCreatedEmail(requestCreatedData);
|
const html1 = getRequestCreatedEmail(requestCreatedData);
|
||||||
const info1 = await transporter.sendMail({
|
const info1 = await transporter.sendMail({
|
||||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||||
to: 'initiator@example.com',
|
to: 'initiator@example.com',
|
||||||
subject: '[REQ-2025-12-0013] Request Created Successfully',
|
subject: '[REQ-2025-12-0013] Request Created Successfully',
|
||||||
html: html1
|
html: html1
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Test 1: Request Created Email');
|
console.log('✅ Test 1: Request Created Email');
|
||||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info1));
|
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info1));
|
||||||
console.log('');
|
console.log('');
|
||||||
@ -113,12 +113,12 @@ async function sendTestEmail() {
|
|||||||
// Test 2: Approval Request Email (Single)
|
// Test 2: Approval Request Email (Single)
|
||||||
const html2 = getApprovalRequestEmail(approvalRequestData);
|
const html2 = getApprovalRequestEmail(approvalRequestData);
|
||||||
const info2 = await transporter.sendMail({
|
const info2 = await transporter.sendMail({
|
||||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||||
to: 'approver@example.com',
|
to: 'approver@example.com',
|
||||||
subject: '[REQ-2025-12-0013] Approval Request - Action Required',
|
subject: '[REQ-2025-12-0013] Approval Request - Action Required',
|
||||||
html: html2
|
html: html2
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Test 2: Approval Request Email');
|
console.log('✅ Test 2: Approval Request Email');
|
||||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info2));
|
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info2));
|
||||||
console.log('');
|
console.log('');
|
||||||
@ -126,12 +126,12 @@ async function sendTestEmail() {
|
|||||||
// Test 3: Multi-Approver Request Email
|
// Test 3: Multi-Approver Request Email
|
||||||
const html3 = getMultiApproverRequestEmail(multiApproverData);
|
const html3 = getMultiApproverRequestEmail(multiApproverData);
|
||||||
const info3 = await transporter.sendMail({
|
const info3 = await transporter.sendMail({
|
||||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||||
to: 'approver-level2@example.com',
|
to: 'approver-level2@example.com',
|
||||||
subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn',
|
subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn',
|
||||||
html: html3
|
html: html3
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Test 3: Multi-Approver Request Email');
|
console.log('✅ Test 3: Multi-Approver Request Email');
|
||||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info3));
|
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info3));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@ -18,7 +18,7 @@ async function testRealScenario() {
|
|||||||
// Mock user data (simulating real database records)
|
// Mock user data (simulating real database records)
|
||||||
const user10 = {
|
const user10 = {
|
||||||
userId: 'user-10-uuid',
|
userId: 'user-10-uuid',
|
||||||
email: 'john.doe@royalenfield.com',
|
email: 'john.doe@{{API_DOMAIN}}',
|
||||||
displayName: 'John Doe',
|
displayName: 'John Doe',
|
||||||
department: 'Engineering',
|
department: 'Engineering',
|
||||||
designation: 'Senior Engineer'
|
designation: 'Senior Engineer'
|
||||||
@ -26,7 +26,7 @@ async function testRealScenario() {
|
|||||||
|
|
||||||
const user12 = {
|
const user12 = {
|
||||||
userId: 'user-12-uuid',
|
userId: 'user-12-uuid',
|
||||||
email: 'jane.smith@royalenfield.com',
|
email: 'jane.smith@{{API_DOMAIN}}',
|
||||||
displayName: 'Jane Smith',
|
displayName: 'Jane Smith',
|
||||||
department: 'Management',
|
department: 'Management',
|
||||||
designation: 'Engineering Manager',
|
designation: 'Engineering Manager',
|
||||||
@ -52,7 +52,7 @@ async function testRealScenario() {
|
|||||||
This purchase is critical for our Q1 2025 testing schedule and has been pre-approved by the department head.
|
This purchase is critical for our Q1 2025 testing schedule and has been pre-approved by the department head.
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<p>Please review and approve at your earliest convenience.</p>
|
<p>Please review and approve at your earliest convenience.</p>
|
||||||
<p>For questions, contact: <a href="mailto:john.doe@royalenfield.com">john.doe@royalenfield.com</a></p>
|
<p>For questions, contact: <a href="mailto:john.doe@{{API_DOMAIN}}">john.doe@{{API_DOMAIN}}</a></p>
|
||||||
`,
|
`,
|
||||||
requestType: 'Purchase',
|
requestType: 'Purchase',
|
||||||
priority: 'HIGH',
|
priority: 'HIGH',
|
||||||
@ -68,21 +68,21 @@ async function testRealScenario() {
|
|||||||
{
|
{
|
||||||
levelNumber: 1,
|
levelNumber: 1,
|
||||||
approverName: 'Jane Smith',
|
approverName: 'Jane Smith',
|
||||||
approverEmail: 'jane.smith@royalenfield.com',
|
approverEmail: 'jane.smith@{{API_DOMAIN}}',
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
approvedAt: null
|
approvedAt: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
levelNumber: 2,
|
levelNumber: 2,
|
||||||
approverName: 'Michael Brown',
|
approverName: 'Michael Brown',
|
||||||
approverEmail: 'michael.brown@royalenfield.com',
|
approverEmail: 'michael.brown@{{API_DOMAIN}}',
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
approvedAt: null
|
approvedAt: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
levelNumber: 3,
|
levelNumber: 3,
|
||||||
approverName: 'Sarah Johnson',
|
approverName: 'Sarah Johnson',
|
||||||
approverEmail: 'sarah.johnson@royalenfield.com',
|
approverEmail: 'sarah.johnson@{{API_DOMAIN}}',
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
approvedAt: null
|
approvedAt: null
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ async function testRealScenario() {
|
|||||||
console.log('─'.repeat(80));
|
console.log('─'.repeat(80));
|
||||||
console.log('📧 Test 1: Request Created Email (to Initiator - User 10)');
|
console.log('📧 Test 1: Request Created Email (to Initiator - User 10)');
|
||||||
console.log('─'.repeat(80));
|
console.log('─'.repeat(80));
|
||||||
|
|
||||||
await emailNotificationService.sendRequestCreated(
|
await emailNotificationService.sendRequestCreated(
|
||||||
requestData,
|
requestData,
|
||||||
user10,
|
user10,
|
||||||
@ -168,7 +168,7 @@ async function testRealScenario() {
|
|||||||
approvedUser12,
|
approvedUser12,
|
||||||
user10,
|
user10,
|
||||||
false, // not final approval
|
false, // not final approval
|
||||||
{ displayName: 'Michael Brown', email: 'michael.brown@royalenfield.com' }
|
{ displayName: 'Michael Brown', email: 'michael.brown@{{API_DOMAIN}}' }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import dealerClaimRoutes from './dealerClaim.routes';
|
|||||||
import templateRoutes from './template.routes';
|
import templateRoutes from './template.routes';
|
||||||
import dealerRoutes from './dealer.routes';
|
import dealerRoutes from './dealer.routes';
|
||||||
import dmsWebhookRoutes from './dmsWebhook.routes';
|
import dmsWebhookRoutes from './dmsWebhook.routes';
|
||||||
|
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||||
|
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -38,7 +40,7 @@ router.use('/user/preferences', userPreferenceRoutes); // User preferences (auth
|
|||||||
router.use('/documents', documentRoutes);
|
router.use('/documents', documentRoutes);
|
||||||
router.use('/tat', tatRoutes);
|
router.use('/tat', tatRoutes);
|
||||||
router.use('/admin', adminRoutes);
|
router.use('/admin', adminRoutes);
|
||||||
router.use('/debug', debugRoutes);
|
router.use('/debug', authenticateToken, requireAdmin, debugRoutes);
|
||||||
router.use('/dashboard', dashboardRoutes);
|
router.use('/dashboard', dashboardRoutes);
|
||||||
router.use('/notifications', notificationRoutes);
|
router.use('/notifications', notificationRoutes);
|
||||||
router.use('/conclusions', conclusionRoutes);
|
router.use('/conclusions', conclusionRoutes);
|
||||||
|
|||||||
@ -323,7 +323,7 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log(' 1. Server will start automatically');
|
console.log(' 1. Server will start automatically');
|
||||||
console.log(' 2. Log in via SSO');
|
console.log(' 2. Log in via SSO');
|
||||||
console.log(' 3. Run this SQL to make yourself admin:');
|
console.log(' 3. Run this SQL to make yourself admin:');
|
||||||
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
|
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -77,7 +77,7 @@ const dealersData: DealerSeedData[] = [
|
|||||||
onBoardingCharges: null,
|
onBoardingCharges: null,
|
||||||
date: '2014-09-30',
|
date: '2014-09-30',
|
||||||
singleFormatMonthYear: 'Sep-2014',
|
singleFormatMonthYear: 'Sep-2014',
|
||||||
domainId: 'acceleratemotors.rrnagar@dealer.royalenfield.com',
|
domainId: 'acceleratemotors.rrnagar@dealer.{{API_DOMAIN}}',
|
||||||
replacement: null,
|
replacement: null,
|
||||||
terminationResignationStatus: null,
|
terminationResignationStatus: null,
|
||||||
dateOfTerminationResignation: null,
|
dateOfTerminationResignation: null,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ interface DealerData {
|
|||||||
|
|
||||||
const dealers: DealerData[] = [
|
const dealers: DealerData[] = [
|
||||||
{
|
{
|
||||||
email: 'test.2@royalenfield.com',
|
email: 'test.2@{{API_DOMAIN}}',
|
||||||
dealerCode: 'RE-MH-001',
|
dealerCode: 'RE-MH-001',
|
||||||
dealerName: 'Royal Motors Mumbai',
|
dealerName: 'Royal Motors Mumbai',
|
||||||
displayName: 'Royal Motors Mumbai',
|
displayName: 'Royal Motors Mumbai',
|
||||||
@ -31,7 +31,7 @@ const dealers: DealerData[] = [
|
|||||||
role: 'USER',
|
role: 'USER',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: 'test.4@royalenfield.com',
|
email: 'test.4@{{API_DOMAIN}}',
|
||||||
dealerCode: 'RE-DL-002',
|
dealerCode: 'RE-DL-002',
|
||||||
dealerName: 'Delhi enfield center',
|
dealerName: 'Delhi enfield center',
|
||||||
displayName: 'Delhi Enfield Center',
|
displayName: 'Delhi Enfield Center',
|
||||||
@ -55,13 +55,13 @@ async function seedDealers(): Promise<void> {
|
|||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
// User already exists (likely from Okta SSO login)
|
// User already exists (likely from Okta SSO login)
|
||||||
const isOktaUser = existingUser.oktaSub && !existingUser.oktaSub.startsWith('dealer-');
|
const isOktaUser = existingUser.oktaSub && !existingUser.oktaSub.startsWith('dealer-');
|
||||||
|
|
||||||
if (isOktaUser) {
|
if (isOktaUser) {
|
||||||
logger.info(`[Seed Dealers] User ${dealer.email} already exists as Okta user (oktaSub: ${existingUser.oktaSub}), updating dealer-specific fields only...`);
|
logger.info(`[Seed Dealers] User ${dealer.email} already exists as Okta user (oktaSub: ${existingUser.oktaSub}), updating dealer-specific fields only...`);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`[Seed Dealers] User ${dealer.email} already exists, updating dealer information...`);
|
logger.info(`[Seed Dealers] User ${dealer.email} already exists, updating dealer information...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update existing user with dealer information
|
// Update existing user with dealer information
|
||||||
// IMPORTANT: Preserve Okta data (oktaSub, role from Okta, etc.) and only update dealer-specific fields
|
// IMPORTANT: Preserve Okta data (oktaSub, role from Okta, etc.) and only update dealer-specific fields
|
||||||
const nameParts = dealer.dealerName.split(' ');
|
const nameParts = dealer.dealerName.split(' ');
|
||||||
@ -117,7 +117,7 @@ async function seedDealers(): Promise<void> {
|
|||||||
logger.warn(`[Seed Dealers] User ${dealer.email} not found in database. Creating placeholder user...`);
|
logger.warn(`[Seed Dealers] User ${dealer.email} not found in database. Creating placeholder user...`);
|
||||||
logger.warn(`[Seed Dealers] ⚠️ If this user is an Okta user, they should login via SSO first to be created automatically.`);
|
logger.warn(`[Seed Dealers] ⚠️ If this user is an Okta user, they should login via SSO first to be created automatically.`);
|
||||||
logger.warn(`[Seed Dealers] ⚠️ The oktaSub will be updated when they login via SSO.`);
|
logger.warn(`[Seed Dealers] ⚠️ The oktaSub will be updated when they login via SSO.`);
|
||||||
|
|
||||||
// Generate a UUID for userId
|
// Generate a UUID for userId
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const userId = uuidv4();
|
const userId = uuidv4();
|
||||||
|
|||||||
@ -23,14 +23,14 @@ export class ApprovalService {
|
|||||||
// Get workflow to determine priority for working hours calculation
|
// Get workflow to determine priority for working hours calculation
|
||||||
const wf = await WorkflowRequest.findByPk(level.requestId);
|
const wf = await WorkflowRequest.findByPk(level.requestId);
|
||||||
if (!wf) return null;
|
if (!wf) return null;
|
||||||
|
|
||||||
// Verify this is NOT a claim management workflow (should use DealerClaimApprovalService)
|
// Verify this is NOT a claim management workflow (should use DealerClaimApprovalService)
|
||||||
const workflowType = (wf as any)?.workflowType;
|
const workflowType = (wf as any)?.workflowType;
|
||||||
if (workflowType === 'CLAIM_MANAGEMENT') {
|
if (workflowType === 'CLAIM_MANAGEMENT') {
|
||||||
logger.error(`[Approval] Attempted to use ApprovalService for CLAIM_MANAGEMENT workflow ${level.requestId}. Use DealerClaimApprovalService instead.`);
|
logger.error(`[Approval] Attempted to use ApprovalService for CLAIM_MANAGEMENT workflow ${level.requestId}. Use DealerClaimApprovalService instead.`);
|
||||||
throw new Error('ApprovalService cannot be used for CLAIM_MANAGEMENT workflows. Use DealerClaimApprovalService instead.');
|
throw new Error('ApprovalService cannot be used for CLAIM_MANAGEMENT workflows. Use DealerClaimApprovalService instead.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
|
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
|
||||||
const isPaused = (wf as any).isPaused || (level as any).isPaused;
|
const isPaused = (wf as any).isPaused || (level as any).isPaused;
|
||||||
|
|
||||||
@ -47,16 +47,16 @@ export class ApprovalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Calculate elapsed hours using working hours logic (with pause handling)
|
// Calculate elapsed hours using working hours logic (with pause handling)
|
||||||
// Case 1: Level is currently paused (isPaused = true)
|
// Case 1: Level is currently paused (isPaused = true)
|
||||||
// Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist)
|
// Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist)
|
||||||
const isPausedLevel = (level as any).isPaused;
|
const isPausedLevel = (level as any).isPaused;
|
||||||
const wasResumed = !isPausedLevel &&
|
const wasResumed = !isPausedLevel &&
|
||||||
(level as any).pauseElapsedHours !== null &&
|
(level as any).pauseElapsedHours !== null &&
|
||||||
(level as any).pauseElapsedHours !== undefined &&
|
(level as any).pauseElapsedHours !== undefined &&
|
||||||
(level as any).pauseResumeDate !== null;
|
(level as any).pauseResumeDate !== null;
|
||||||
|
|
||||||
const pauseInfo = isPausedLevel ? {
|
const pauseInfo = isPausedLevel ? {
|
||||||
// Level is currently paused - return frozen elapsed hours at pause time
|
// Level is currently paused - return frozen elapsed hours at pause time
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
@ -70,10 +70,10 @@ export class ApprovalService {
|
|||||||
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
||||||
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const elapsedHours = await calculateElapsedWorkingHours(
|
const elapsedHours = await calculateElapsedWorkingHours(
|
||||||
level.levelStartTime || level.createdAt,
|
level.levelStartTime || level.createdAt,
|
||||||
now,
|
now,
|
||||||
priority,
|
priority,
|
||||||
pauseInfo
|
pauseInfo
|
||||||
);
|
);
|
||||||
@ -130,12 +130,12 @@ export class ApprovalService {
|
|||||||
const totalLevels = allLevels.length;
|
const totalLevels = allLevels.length;
|
||||||
const isAllLevelsApproved = approvedLevelsCount === totalLevels;
|
const isAllLevelsApproved = approvedLevelsCount === totalLevels;
|
||||||
const isFinalApproval = level.isFinalApprover || isAllLevelsApproved;
|
const isFinalApproval = level.isFinalApprover || isAllLevelsApproved;
|
||||||
|
|
||||||
if (isFinalApproval) {
|
if (isFinalApproval) {
|
||||||
// Final approver - close workflow as APPROVED
|
// Final approver - close workflow as APPROVED
|
||||||
await WorkflowRequest.update(
|
await WorkflowRequest.update(
|
||||||
{
|
{
|
||||||
status: WorkflowStatus.APPROVED,
|
status: WorkflowStatus.APPROVED,
|
||||||
closureDate: now,
|
closureDate: now,
|
||||||
currentLevel: (level.levelNumber || 0) + 1
|
currentLevel: (level.levelNumber || 0) + 1
|
||||||
},
|
},
|
||||||
@ -147,7 +147,7 @@ export class ApprovalService {
|
|||||||
status: 'APPROVED',
|
status: 'APPROVED',
|
||||||
detectedBy: level.isFinalApprover ? 'isFinalApprover flag' : 'all levels approved check'
|
detectedBy: level.isFinalApprover ? 'isFinalApprover flag' : 'all levels approved check'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log final approval activity first (so it's included in AI context)
|
// Log final approval activity first (so it's included in AI context)
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -159,7 +159,7 @@ export class ApprovalService {
|
|||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate AI conclusion remark ASYNCHRONOUSLY (don't wait)
|
// Generate AI conclusion remark ASYNCHRONOUSLY (don't wait)
|
||||||
// This runs in the background without blocking the approval response
|
// This runs in the background without blocking the approval response
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -171,17 +171,17 @@ export class ApprovalService {
|
|||||||
const { Document } = await import('@models/Document');
|
const { Document } = await import('@models/Document');
|
||||||
const { Activity } = await import('@models/Activity');
|
const { Activity } = await import('@models/Activity');
|
||||||
const { getConfigValue } = await import('./configReader.service');
|
const { getConfigValue } = await import('./configReader.service');
|
||||||
|
|
||||||
// Check if AI features and remark generation are enabled in admin config
|
// Check if AI features and remark generation are enabled in admin config
|
||||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||||
|
|
||||||
if (aiEnabled && remarkGenerationEnabled && aiService.isAvailable()) {
|
if (aiEnabled && remarkGenerationEnabled && aiService.isAvailable()) {
|
||||||
logAIEvent('request', {
|
logAIEvent('request', {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
action: 'conclusion_generation_started',
|
action: 'conclusion_generation_started',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gather context for AI generation
|
// Gather context for AI generation
|
||||||
const approvalLevels = await ApprovalLevel.findAll({
|
const approvalLevels = await ApprovalLevel.findAll({
|
||||||
where: { requestId: level.requestId },
|
where: { requestId: level.requestId },
|
||||||
@ -212,8 +212,8 @@ export class ApprovalService {
|
|||||||
requestNumber: (wf as any).requestNumber,
|
requestNumber: (wf as any).requestNumber,
|
||||||
priority: (wf as any).priority,
|
priority: (wf as any).priority,
|
||||||
approvalFlow: approvalLevels.map((l: any) => {
|
approvalFlow: approvalLevels.map((l: any) => {
|
||||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||||
? Number(l.tatPercentageUsed)
|
? Number(l.tatPercentageUsed)
|
||||||
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
||||||
return {
|
return {
|
||||||
levelNumber: l.levelNumber,
|
levelNumber: l.levelNumber,
|
||||||
@ -245,12 +245,12 @@ export class ApprovalService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const aiResult = await aiService.generateConclusionRemark(context);
|
const aiResult = await aiService.generateConclusionRemark(context);
|
||||||
|
|
||||||
// Check if conclusion already exists (e.g., from previous final approval before additional approver was added)
|
// Check if conclusion already exists (e.g., from previous final approval before additional approver was added)
|
||||||
const existingConclusion = await ConclusionRemark.findOne({
|
const existingConclusion = await ConclusionRemark.findOne({
|
||||||
where: { requestId: level.requestId }
|
where: { requestId: level.requestId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingConclusion) {
|
if (existingConclusion) {
|
||||||
// Update existing conclusion with new AI-generated remark (regenerated with updated context)
|
// Update existing conclusion with new AI-generated remark (regenerated with updated context)
|
||||||
await existingConclusion.update({
|
await existingConclusion.update({
|
||||||
@ -266,7 +266,7 @@ export class ApprovalService {
|
|||||||
approvalSummary: {
|
approvalSummary: {
|
||||||
totalLevels: approvalLevels.length,
|
totalLevels: approvalLevels.length,
|
||||||
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
|
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
|
||||||
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
|
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
|
||||||
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
|
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
|
||||||
},
|
},
|
||||||
documentSummary: {
|
documentSummary: {
|
||||||
@ -293,7 +293,7 @@ export class ApprovalService {
|
|||||||
approvalSummary: {
|
approvalSummary: {
|
||||||
totalLevels: approvalLevels.length,
|
totalLevels: approvalLevels.length,
|
||||||
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
|
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
|
||||||
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
|
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
|
||||||
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
|
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
|
||||||
},
|
},
|
||||||
documentSummary: {
|
documentSummary: {
|
||||||
@ -305,12 +305,12 @@ export class ApprovalService {
|
|||||||
finalizedAt: null
|
finalizedAt: null
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
logAIEvent('response', {
|
logAIEvent('response', {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
action: 'conclusion_generation_completed',
|
action: 'conclusion_generation_completed',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -332,16 +332,16 @@ export class ApprovalService {
|
|||||||
logger.warn(`[Approval] AI service unavailable for ${level.requestId}, skipping conclusion generation`);
|
logger.warn(`[Approval] AI service unavailable for ${level.requestId}, skipping conclusion generation`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generate RequestSummary after final approval (system-level generation)
|
// Auto-generate RequestSummary after final approval (system-level generation)
|
||||||
// This makes the summary immediately available when user views the approved request
|
// This makes the summary immediately available when user views the approved request
|
||||||
try {
|
try {
|
||||||
const { summaryService } = await import('./summary.service');
|
const { summaryService } = await import('./summary.service');
|
||||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||||
isSystemGeneration: true
|
isSystemGeneration: true
|
||||||
});
|
});
|
||||||
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId}`);
|
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId}`);
|
||||||
|
|
||||||
// Log summary generation activity
|
// Log summary generation activity
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -357,7 +357,7 @@ export class ApprovalService {
|
|||||||
// Log but don't fail - initiator can regenerate later
|
// Log but don't fail - initiator can regenerate later
|
||||||
logger.error(`[Approval] Failed to auto-generate summary for ${level.requestId}:`, summaryError.message);
|
logger.error(`[Approval] Failed to auto-generate summary for ${level.requestId}:`, summaryError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (aiError) {
|
} catch (aiError) {
|
||||||
logAIEvent('error', {
|
logAIEvent('error', {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -365,12 +365,12 @@ export class ApprovalService {
|
|||||||
error: aiError,
|
error: aiError,
|
||||||
});
|
});
|
||||||
// Silent failure - initiator can write manually
|
// Silent failure - initiator can write manually
|
||||||
|
|
||||||
// Still try to generate summary even if AI conclusion failed
|
// Still try to generate summary even if AI conclusion failed
|
||||||
try {
|
try {
|
||||||
const { summaryService } = await import('./summary.service');
|
const { summaryService } = await import('./summary.service');
|
||||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||||
isSystemGeneration: true
|
isSystemGeneration: true
|
||||||
});
|
});
|
||||||
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId} (without AI conclusion)`);
|
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId} (without AI conclusion)`);
|
||||||
} catch (summaryError: any) {
|
} catch (summaryError: any) {
|
||||||
@ -381,19 +381,19 @@ export class ApprovalService {
|
|||||||
// Catch any unhandled promise rejections
|
// Catch any unhandled promise rejections
|
||||||
logger.error(`[Approval] Unhandled error in background AI generation:`, err);
|
logger.error(`[Approval] Unhandled error in background AI generation:`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify initiator and all participants (including spectators) about approval
|
// Notify initiator and all participants (including spectators) about approval
|
||||||
// Spectators are CC'd for transparency, similar to email CC
|
// Spectators are CC'd for transparency, similar to email CC
|
||||||
if (wf) {
|
if (wf) {
|
||||||
const participants = await Participant.findAll({
|
const participants = await Participant.findAll({
|
||||||
where: { requestId: level.requestId }
|
where: { requestId: level.requestId }
|
||||||
});
|
});
|
||||||
const targetUserIds = new Set<string>();
|
const targetUserIds = new Set<string>();
|
||||||
targetUserIds.add((wf as any).initiatorId);
|
targetUserIds.add((wf as any).initiatorId);
|
||||||
for (const p of participants as any[]) {
|
for (const p of participants as any[]) {
|
||||||
targetUserIds.add(p.userId); // Includes spectators
|
targetUserIds.add(p.userId); // Includes spectators
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification to initiator about final approval (triggers email)
|
// Send notification to initiator about final approval (triggers email)
|
||||||
const initiatorId = (wf as any).initiatorId;
|
const initiatorId = (wf as any).initiatorId;
|
||||||
await notificationService.sendToUsers([initiatorId], {
|
await notificationService.sendToUsers([initiatorId], {
|
||||||
@ -406,7 +406,7 @@ export class ApprovalService {
|
|||||||
priority: 'HIGH',
|
priority: 'HIGH',
|
||||||
actionRequired: true
|
actionRequired: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send notification to all participants/spectators (for transparency, no action required)
|
// Send notification to all participants/spectators (for transparency, no action required)
|
||||||
const participantUserIds = Array.from(targetUserIds).filter(id => id !== initiatorId);
|
const participantUserIds = Array.from(targetUserIds).filter(id => id !== initiatorId);
|
||||||
if (participantUserIds.length > 0) {
|
if (participantUserIds.length > 0) {
|
||||||
@ -421,7 +421,7 @@ export class ApprovalService {
|
|||||||
actionRequired: false
|
actionRequired: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[Approval] ✅ Final approval complete for ${level.requestId}. Initiator and ${participants.length} participant(s) notified.`);
|
logger.info(`[Approval] ✅ Final approval complete for ${level.requestId}. Initiator and ${participants.length} participant(s) notified.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -431,30 +431,30 @@ export class ApprovalService {
|
|||||||
logger.warn(`[Approval] Cannot advance workflow ${level.requestId} - workflow is paused`);
|
logger.warn(`[Approval] Cannot advance workflow ${level.requestId} - workflow is paused`);
|
||||||
throw new Error('Cannot advance workflow - workflow is currently paused. Please resume the workflow first.');
|
throw new Error('Cannot advance workflow - workflow is currently paused. Please resume the workflow first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the next PENDING level
|
// Find the next PENDING level
|
||||||
// Custom workflows use strict sequential ordering (levelNumber + 1) to maintain intended order
|
// Custom workflows use strict sequential ordering (levelNumber + 1) to maintain intended order
|
||||||
// This ensures custom workflows work predictably and don't skip levels
|
// This ensures custom workflows work predictably and don't skip levels
|
||||||
const currentLevelNumber = level.levelNumber || 0;
|
const currentLevelNumber = level.levelNumber || 0;
|
||||||
logger.info(`[Approval] Finding next level after level ${currentLevelNumber} for request ${level.requestId} (Custom workflow)`);
|
logger.info(`[Approval] Finding next level after level ${currentLevelNumber} for request ${level.requestId} (Custom workflow)`);
|
||||||
|
|
||||||
// Use strict sequential approach for custom workflows
|
// Use strict sequential approach for custom workflows
|
||||||
const nextLevel = await ApprovalLevel.findOne({
|
const nextLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
levelNumber: currentLevelNumber + 1
|
levelNumber: currentLevelNumber + 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!nextLevel) {
|
if (!nextLevel) {
|
||||||
logger.info(`[Approval] Sequential level ${currentLevelNumber + 1} not found for custom workflow - this may be the final approval`);
|
logger.info(`[Approval] Sequential level ${currentLevelNumber + 1} not found for custom workflow - this may be the final approval`);
|
||||||
} else if (nextLevel.status !== ApprovalStatus.PENDING) {
|
} else if (nextLevel.status !== ApprovalStatus.PENDING) {
|
||||||
// Sequential level exists but not PENDING - log warning but proceed
|
// Sequential level exists but not PENDING - log warning but proceed
|
||||||
logger.warn(`[Approval] Sequential level ${currentLevelNumber + 1} exists but status is ${nextLevel.status}, expected PENDING. Proceeding with sequential level to maintain workflow order.`);
|
logger.warn(`[Approval] Sequential level ${currentLevelNumber + 1} exists but status is ${nextLevel.status}, expected PENDING. Proceeding with sequential level to maintain workflow order.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
|
const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
|
||||||
|
|
||||||
if (nextLevel) {
|
if (nextLevel) {
|
||||||
logger.info(`[Approval] Found next level: ${nextLevelNumber} (${(nextLevel as any).levelName || 'unnamed'}), approver: ${(nextLevel as any).approverName || (nextLevel as any).approverEmail || 'unknown'}, status: ${nextLevel.status}`);
|
logger.info(`[Approval] Found next level: ${nextLevelNumber} (${(nextLevel as any).levelName || 'unnamed'}), approver: ${(nextLevel as any).approverName || (nextLevel as any).approverEmail || 'unknown'}, status: ${nextLevel.status}`);
|
||||||
} else {
|
} else {
|
||||||
@ -467,19 +467,19 @@ export class ApprovalService {
|
|||||||
logger.warn(`[Approval] Cannot activate next level ${nextLevelNumber} - level is paused`);
|
logger.warn(`[Approval] Cannot activate next level ${nextLevelNumber} - level is paused`);
|
||||||
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
|
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate next level
|
// Activate next level
|
||||||
await nextLevel.update({
|
await nextLevel.update({
|
||||||
status: ApprovalStatus.IN_PROGRESS,
|
status: ApprovalStatus.IN_PROGRESS,
|
||||||
levelStartTime: now,
|
levelStartTime: now,
|
||||||
tatStartTime: now
|
tatStartTime: now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schedule TAT jobs for the next level
|
// Schedule TAT jobs for the next level
|
||||||
try {
|
try {
|
||||||
// Get workflow priority for TAT calculation
|
// Get workflow priority for TAT calculation
|
||||||
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
||||||
|
|
||||||
await tatSchedulerService.scheduleTatJobs(
|
await tatSchedulerService.scheduleTatJobs(
|
||||||
level.requestId,
|
level.requestId,
|
||||||
(nextLevel as any).levelId,
|
(nextLevel as any).levelId,
|
||||||
@ -493,7 +493,7 @@ export class ApprovalService {
|
|||||||
logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError);
|
logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError);
|
||||||
// Don't fail the approval if TAT scheduling fails
|
// Don't fail the approval if TAT scheduling fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update workflow current level (only if nextLevelNumber is not null)
|
// Update workflow current level (only if nextLevelNumber is not null)
|
||||||
if (nextLevelNumber !== null) {
|
if (nextLevelNumber !== null) {
|
||||||
await WorkflowRequest.update(
|
await WorkflowRequest.update(
|
||||||
@ -504,10 +504,10 @@ export class ApprovalService {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn(`Approved level ${level.levelNumber} but no next level found - workflow may be complete`);
|
logger.warn(`Approved level ${level.levelNumber} but no next level found - workflow may be complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Dealer claim-specific logic (Activity Creation, E-Invoice) is handled by DealerClaimApprovalService
|
// Note: Dealer claim-specific logic (Activity Creation, E-Invoice) is handled by DealerClaimApprovalService
|
||||||
// This service is for custom workflows only
|
// This service is for custom workflows only
|
||||||
|
|
||||||
// Log approval activity
|
// Log approval activity
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -519,7 +519,7 @@ export class ApprovalService {
|
|||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify initiator about the approval (triggers email for regular workflows)
|
// Notify initiator about the approval (triggers email for regular workflows)
|
||||||
if (wf) {
|
if (wf) {
|
||||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||||
@ -532,29 +532,29 @@ export class ApprovalService {
|
|||||||
priority: 'MEDIUM'
|
priority: 'MEDIUM'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify next approver
|
// Notify next approver
|
||||||
if (wf && nextLevel) {
|
if (wf && nextLevel) {
|
||||||
// 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 now activity logs only, not approval steps
|
// 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
|
||||||
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com'
|
const isAutoStep = (nextLevel as any).approverEmail === 'system@{{API_DOMAIN}}'
|
||||||
|| (nextLevel as any).approverName === 'System Auto-Process'
|
|| (nextLevel as any).approverName === 'System Auto-Process'
|
||||||
|| (nextLevel as any).approverId === 'system';
|
|| (nextLevel as any).approverId === 'system';
|
||||||
|
|
||||||
// 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 any step with system@{{API_DOMAIN}}
|
||||||
// Only send notifications to real users, NOT system processes
|
// Only send notifications to real users, NOT system processes
|
||||||
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
|
||||||
// This prevents notifications to system accounts even if they pass other checks
|
// This prevents notifications to system accounts even if they pass other checks
|
||||||
const approverEmail = (nextLevel as any).approverEmail || '';
|
const approverEmail = (nextLevel as any).approverEmail || '';
|
||||||
const approverName = (nextLevel as any).approverName || '';
|
const approverName = (nextLevel as any).approverName || '';
|
||||||
const isSystemEmail = approverEmail.toLowerCase() === 'system@royalenfield.com'
|
const isSystemEmail = approverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|
||||||
|| approverEmail.toLowerCase().includes('system');
|
|| approverEmail.toLowerCase().includes('system');
|
||||||
const isSystemName = approverName.toLowerCase() === 'system auto-process'
|
const isSystemName = approverName.toLowerCase() === 'system auto-process'
|
||||||
|| approverName.toLowerCase().includes('system');
|
|| approverName.toLowerCase().includes('system');
|
||||||
|
|
||||||
// EXCLUDE all system-related steps from notifications
|
// EXCLUDE all system-related steps from notifications
|
||||||
// Only send notifications to real users, NOT system processes
|
// Only send notifications to real users, NOT system processes
|
||||||
if (!isSystemEmail && !isSystemName) {
|
if (!isSystemEmail && !isSystemName) {
|
||||||
@ -562,10 +562,10 @@ export class ApprovalService {
|
|||||||
// This will send both in-app and email notifications
|
// This will send both in-app and email notifications
|
||||||
const nextApproverId = (nextLevel as any).approverId;
|
const nextApproverId = (nextLevel as any).approverId;
|
||||||
const nextApproverName = (nextLevel as any).approverName || (nextLevel as any).approverEmail || 'approver';
|
const nextApproverName = (nextLevel as any).approverName || (nextLevel as any).approverEmail || 'approver';
|
||||||
|
|
||||||
logger.info(`[Approval] Sending assignment notification to next approver: ${nextApproverName} (${nextApproverId}) at level ${nextLevelNumber} for request ${(wf as any).requestNumber}`);
|
logger.info(`[Approval] Sending assignment notification to next approver: ${nextApproverName} (${nextApproverId}) at level ${nextLevelNumber} for request ${(wf as any).requestNumber}`);
|
||||||
|
|
||||||
await notificationService.sendToUsers([ nextApproverId ], {
|
await notificationService.sendToUsers([nextApproverId], {
|
||||||
title: `Action required: ${(wf as any).requestNumber}`,
|
title: `Action required: ${(wf as any).requestNumber}`,
|
||||||
body: `${(wf as any).title}`,
|
body: `${(wf as any).title}`,
|
||||||
requestNumber: (wf as any).requestNumber,
|
requestNumber: (wf as any).requestNumber,
|
||||||
@ -595,7 +595,7 @@ export class ApprovalService {
|
|||||||
} else {
|
} else {
|
||||||
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
|
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Dealer-specific notifications (proposal/completion submissions) are handled by DealerClaimApprovalService
|
// Note: Dealer-specific notifications (proposal/completion submissions) are handled by DealerClaimApprovalService
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -603,15 +603,15 @@ export class ApprovalService {
|
|||||||
logger.warn(`No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`);
|
logger.warn(`No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`);
|
||||||
// Use current level number since there's no next level (workflow is complete)
|
// Use current level number since there's no next level (workflow is complete)
|
||||||
await WorkflowRequest.update(
|
await WorkflowRequest.update(
|
||||||
{
|
{
|
||||||
status: WorkflowStatus.APPROVED,
|
status: WorkflowStatus.APPROVED,
|
||||||
closureDate: now,
|
closureDate: now,
|
||||||
currentLevel: level.levelNumber || 0
|
currentLevel: level.levelNumber || 0
|
||||||
},
|
},
|
||||||
{ where: { requestId: level.requestId } }
|
{ where: { requestId: level.requestId } }
|
||||||
);
|
);
|
||||||
if (wf) {
|
if (wf) {
|
||||||
await notificationService.sendToUsers([ (wf as any).initiatorId ], {
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||||
title: `Approved: ${(wf as any).requestNumber}`,
|
title: `Approved: ${(wf as any).requestNumber}`,
|
||||||
body: `${(wf as any).title}`,
|
body: `${(wf as any).title}`,
|
||||||
requestNumber: (wf as any).requestNumber,
|
requestNumber: (wf as any).requestNumber,
|
||||||
@ -633,7 +633,7 @@ export class ApprovalService {
|
|||||||
} else if (action.action === 'REJECT') {
|
} else if (action.action === 'REJECT') {
|
||||||
// Rejection - mark workflow as REJECTED (closure will happen when initiator finalizes conclusion)
|
// Rejection - mark workflow as REJECTED (closure will happen when initiator finalizes conclusion)
|
||||||
await WorkflowRequest.update(
|
await WorkflowRequest.update(
|
||||||
{
|
{
|
||||||
status: WorkflowStatus.REJECTED
|
status: WorkflowStatus.REJECTED
|
||||||
// Note: closureDate will be set when initiator finalizes the conclusion
|
// Note: closureDate will be set when initiator finalizes the conclusion
|
||||||
},
|
},
|
||||||
@ -642,16 +642,16 @@ export class ApprovalService {
|
|||||||
|
|
||||||
// Mark all pending levels as skipped
|
// Mark all pending levels as skipped
|
||||||
await ApprovalLevel.update(
|
await ApprovalLevel.update(
|
||||||
{
|
{
|
||||||
status: ApprovalStatus.SKIPPED,
|
status: ApprovalStatus.SKIPPED,
|
||||||
levelEndTime: now
|
levelEndTime: now
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
status: ApprovalStatus.PENDING,
|
status: ApprovalStatus.PENDING,
|
||||||
levelNumber: { [Op.gt]: level.levelNumber }
|
levelNumber: { [Op.gt]: level.levelNumber }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -660,7 +660,7 @@ export class ApprovalService {
|
|||||||
status: 'REJECTED',
|
status: 'REJECTED',
|
||||||
message: 'Awaiting closure from initiator',
|
message: 'Awaiting closure from initiator',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log rejection activity first (so it's included in AI context)
|
// Log rejection activity first (so it's included in AI context)
|
||||||
if (wf) {
|
if (wf) {
|
||||||
activityService.log({
|
activityService.log({
|
||||||
@ -674,7 +674,7 @@ export class ApprovalService {
|
|||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify initiator and all participants
|
// Notify initiator and all participants
|
||||||
if (wf) {
|
if (wf) {
|
||||||
const participants = await Participant.findAll({ where: { requestId: level.requestId } });
|
const participants = await Participant.findAll({ where: { requestId: level.requestId } });
|
||||||
@ -683,7 +683,7 @@ export class ApprovalService {
|
|||||||
for (const p of participants as any[]) {
|
for (const p of participants as any[]) {
|
||||||
targetUserIds.add(p.userId);
|
targetUserIds.add(p.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification to initiator with type 'rejection' to trigger email
|
// Send notification to initiator with type 'rejection' to trigger email
|
||||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||||
title: `Rejected: ${(wf as any).requestNumber}`,
|
title: `Rejected: ${(wf as any).requestNumber}`,
|
||||||
@ -697,7 +697,7 @@ export class ApprovalService {
|
|||||||
rejectionReason: action.rejectionReason || action.comments || 'No reason provided'
|
rejectionReason: action.rejectionReason || action.comments || 'No reason provided'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send notification to other participants (spectators) for transparency (no email, just in-app)
|
// Send notification to other participants (spectators) for transparency (no email, just in-app)
|
||||||
const participantUserIds = Array.from(targetUserIds).filter(id => id !== (wf as any).initiatorId);
|
const participantUserIds = Array.from(targetUserIds).filter(id => id !== (wf as any).initiatorId);
|
||||||
if (participantUserIds.length > 0) {
|
if (participantUserIds.length > 0) {
|
||||||
@ -712,7 +712,7 @@ export class ApprovalService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved)
|
// Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved)
|
||||||
// This runs in the background without blocking the rejection response
|
// This runs in the background without blocking the rejection response
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -724,46 +724,46 @@ export class ApprovalService {
|
|||||||
const { Document } = await import('@models/Document');
|
const { Document } = await import('@models/Document');
|
||||||
const { Activity } = await import('@models/Activity');
|
const { Activity } = await import('@models/Activity');
|
||||||
const { getConfigValue } = await import('./configReader.service');
|
const { getConfigValue } = await import('./configReader.service');
|
||||||
|
|
||||||
// Check if AI features and remark generation are enabled in admin config
|
// Check if AI features and remark generation are enabled in admin config
|
||||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||||
|
|
||||||
if (!aiEnabled || !remarkGenerationEnabled) {
|
if (!aiEnabled || !remarkGenerationEnabled) {
|
||||||
logger.info(`[Approval] AI conclusion generation skipped for rejected request ${level.requestId} (AI disabled)`);
|
logger.info(`[Approval] AI conclusion generation skipped for rejected request ${level.requestId} (AI disabled)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if AI service is available
|
// Check if AI service is available
|
||||||
const { aiService: aiSvc } = await import('./ai.service');
|
const { aiService: aiSvc } = await import('./ai.service');
|
||||||
if (!aiSvc.isAvailable()) {
|
if (!aiSvc.isAvailable()) {
|
||||||
logger.warn(`[Approval] AI service unavailable for rejected request ${level.requestId}`);
|
logger.warn(`[Approval] AI service unavailable for rejected request ${level.requestId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather context for AI generation (similar to approved flow)
|
// Gather context for AI generation (similar to approved flow)
|
||||||
const approvalLevels = await ApprovalLevel.findAll({
|
const approvalLevels = await ApprovalLevel.findAll({
|
||||||
where: { requestId: level.requestId },
|
where: { requestId: level.requestId },
|
||||||
order: [['levelNumber', 'ASC']]
|
order: [['levelNumber', 'ASC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
const workNotes = await WorkNote.findAll({
|
const workNotes = await WorkNote.findAll({
|
||||||
where: { requestId: level.requestId },
|
where: { requestId: level.requestId },
|
||||||
order: [['createdAt', 'ASC']],
|
order: [['createdAt', 'ASC']],
|
||||||
limit: 20
|
limit: 20
|
||||||
});
|
});
|
||||||
|
|
||||||
const documents = await Document.findAll({
|
const documents = await Document.findAll({
|
||||||
where: { requestId: level.requestId },
|
where: { requestId: level.requestId },
|
||||||
order: [['uploadedAt', 'DESC']]
|
order: [['uploadedAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
const activities = await Activity.findAll({
|
const activities = await Activity.findAll({
|
||||||
where: { requestId: level.requestId },
|
where: { requestId: level.requestId },
|
||||||
order: [['createdAt', 'ASC']],
|
order: [['createdAt', 'ASC']],
|
||||||
limit: 50
|
limit: 50
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build context object (include rejection reason)
|
// Build context object (include rejection reason)
|
||||||
const context = {
|
const context = {
|
||||||
requestTitle: (wf as any).title,
|
requestTitle: (wf as any).title,
|
||||||
@ -773,8 +773,8 @@ export class ApprovalService {
|
|||||||
rejectionReason: action.rejectionReason || action.comments || 'No reason provided',
|
rejectionReason: action.rejectionReason || action.comments || 'No reason provided',
|
||||||
rejectedBy: level.approverName || level.approverEmail,
|
rejectedBy: level.approverName || level.approverEmail,
|
||||||
approvalFlow: approvalLevels.map((l: any) => {
|
approvalFlow: approvalLevels.map((l: any) => {
|
||||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||||
? Number(l.tatPercentageUsed)
|
? Number(l.tatPercentageUsed)
|
||||||
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
||||||
return {
|
return {
|
||||||
levelNumber: l.levelNumber,
|
levelNumber: l.levelNumber,
|
||||||
@ -804,15 +804,15 @@ export class ApprovalService {
|
|||||||
timestamp: activity.createdAt
|
timestamp: activity.createdAt
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`[Approval] Generating AI conclusion for rejected request ${level.requestId}...`);
|
logger.info(`[Approval] Generating AI conclusion for rejected request ${level.requestId}...`);
|
||||||
|
|
||||||
// Generate AI conclusion (will adapt to rejection context)
|
// Generate AI conclusion (will adapt to rejection context)
|
||||||
const aiResult = await aiSvc.generateConclusionRemark(context);
|
const aiResult = await aiSvc.generateConclusionRemark(context);
|
||||||
|
|
||||||
// Create or update conclusion remark
|
// Create or update conclusion remark
|
||||||
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId: level.requestId } });
|
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId: level.requestId } });
|
||||||
|
|
||||||
const conclusionData = {
|
const conclusionData = {
|
||||||
aiGeneratedRemark: aiResult.remark,
|
aiGeneratedRemark: aiResult.remark,
|
||||||
aiModelUsed: aiResult.provider,
|
aiModelUsed: aiResult.provider,
|
||||||
@ -830,7 +830,7 @@ export class ApprovalService {
|
|||||||
keyDiscussionPoints: aiResult.keyPoints,
|
keyDiscussionPoints: aiResult.keyPoints,
|
||||||
generatedAt: new Date()
|
generatedAt: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (conclusionInstance) {
|
if (conclusionInstance) {
|
||||||
await conclusionInstance.update(conclusionData as any);
|
await conclusionInstance.update(conclusionData as any);
|
||||||
logger.info(`[Approval] ✅ AI conclusion updated for rejected request ${level.requestId}`);
|
logger.info(`[Approval] ✅ AI conclusion updated for rejected request ${level.requestId}`);
|
||||||
@ -854,7 +854,7 @@ export class ApprovalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Approval level ${levelId} ${action.action.toLowerCase()}ed`);
|
logger.info(`Approval level ${levelId} ${action.action.toLowerCase()}ed`);
|
||||||
|
|
||||||
// Emit real-time update to all users viewing this request
|
// Emit real-time update to all users viewing this request
|
||||||
emitToRequestRoom(level.requestId, 'request:updated', {
|
emitToRequestRoom(level.requestId, 'request:updated', {
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
@ -863,7 +863,7 @@ export class ApprovalService {
|
|||||||
levelNumber: level.levelNumber,
|
levelNumber: level.levelNumber,
|
||||||
timestamp: now.toISOString()
|
timestamp: now.toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedLevel;
|
return updatedLevel;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error);
|
logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error);
|
||||||
|
|||||||
@ -330,7 +330,7 @@ export class DealerClaimService {
|
|||||||
let stepDef = null;
|
let stepDef = null;
|
||||||
|
|
||||||
// Check if this is a system step by email (for backwards compatibility)
|
// Check if this is a system step by email (for backwards compatibility)
|
||||||
const isSystemEmail = approver.email === 'system@royalenfield.com' || approver.email === 'finance@royalenfield.com';
|
const isSystemEmail = approver.email === 'system@{{API_DOMAIN}}' || approver.email === 'finance@{{API_DOMAIN}}';
|
||||||
|
|
||||||
if (approver.isAdditional) {
|
if (approver.isAdditional) {
|
||||||
// Additional approver - use stepName from frontend
|
// Additional approver - use stepName from frontend
|
||||||
@ -594,7 +594,7 @@ export class DealerClaimService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed)
|
// 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed)
|
||||||
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@royalenfield.com') {
|
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@{{API_DOMAIN}}') {
|
||||||
let dealerUser = await User.findOne({
|
let dealerUser = await User.findOne({
|
||||||
where: { email: dealerEmail.toLowerCase() },
|
where: { email: dealerEmail.toLowerCase() },
|
||||||
});
|
});
|
||||||
@ -626,7 +626,7 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
// 3. Add all approvers from approval levels (excluding system and duplicates)
|
// 3. Add all approvers from approval levels (excluding system and duplicates)
|
||||||
const addedUserIds = new Set<string>([initiatorId]);
|
const addedUserIds = new Set<string>([initiatorId]);
|
||||||
const systemEmails = ['system@royalenfield.com'];
|
const systemEmails = ['system@{{API_DOMAIN}}'];
|
||||||
|
|
||||||
for (const level of approvalLevels) {
|
for (const level of approvalLevels) {
|
||||||
const approverEmail = (level as any).approverEmail?.toLowerCase();
|
const approverEmail = (level as any).approverEmail?.toLowerCase();
|
||||||
|
|||||||
@ -399,11 +399,11 @@ export class DealerClaimApprovalService {
|
|||||||
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
|
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
|
||||||
|
|
||||||
// Check if it's an auto-step or system process
|
// Check if it's an auto-step or system process
|
||||||
const isAutoStep = nextApproverEmail === 'system@royalenfield.com'
|
const isAutoStep = nextApproverEmail === 'system@{{API_DOMAIN}}'
|
||||||
|| (nextLevel as any).approverName === 'System Auto-Process'
|
|| (nextLevel as any).approverName === 'System Auto-Process'
|
||||||
|| nextApproverId === 'system';
|
|| nextApproverId === 'system';
|
||||||
|
|
||||||
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@royalenfield.com'
|
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|
||||||
|| nextApproverEmail.toLowerCase().includes('system');
|
|| nextApproverEmail.toLowerCase().includes('system');
|
||||||
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|
||||||
|| nextApproverName.toLowerCase().includes('system');
|
|| nextApproverName.toLowerCase().includes('system');
|
||||||
|
|||||||
@ -19,7 +19,7 @@ interface EmailOptions {
|
|||||||
|
|
||||||
// Hardcoded BCC addresses (temporary - for time being)
|
// Hardcoded BCC addresses (temporary - for time being)
|
||||||
const HARDCODED_BCC: string[] = [
|
const HARDCODED_BCC: string[] = [
|
||||||
'rohitm_ext@royalenfield.com',
|
'{{USER_EMAIL}}',
|
||||||
// Add your BCC email addresses here
|
// Add your BCC email addresses here
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
||||||
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
|
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@{{API_DOMAIN}}>';
|
||||||
|
|
||||||
// Merge hardcoded BCC with provided BCC
|
// Merge hardcoded BCC with provided BCC
|
||||||
let bccRecipients: string[] = [];
|
let bccRecipients: string[] = [];
|
||||||
@ -166,17 +166,6 @@ export class EmailService {
|
|||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
result.previewUrl = previewUrl;
|
result.previewUrl = previewUrl;
|
||||||
|
|
||||||
// Always log to console for visibility
|
|
||||||
console.log('\n' + '='.repeat(80));
|
|
||||||
console.log(`📧 EMAIL PREVIEW (${options.subject})`);
|
|
||||||
console.log(`To: ${recipients}`);
|
|
||||||
if (finalBcc && finalBcc.length > 0) {
|
|
||||||
console.log(`BCC: ${finalBcc.join(', ')}`);
|
|
||||||
}
|
|
||||||
console.log(`Preview URL: ${previewUrl}`);
|
|
||||||
console.log(`Message ID: ${info.messageId}`);
|
|
||||||
console.log('='.repeat(80) + '\n');
|
|
||||||
|
|
||||||
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
|
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
|
||||||
logger.info(`📧 Preview URL: ${previewUrl}`);
|
logger.info(`📧 Preview URL: ${previewUrl}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -270,13 +270,7 @@ class GoogleSecretManagerService {
|
|||||||
loadedSecrets[envVarName] = secretValue;
|
loadedSecrets[envVarName] = secretValue;
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
|
|
||||||
// Print masked value for verification as requested by user
|
logger.info(`[Secret Manager] ✅ Loaded: ${secretNameToFetch} -> process.env.${envVarName}`);
|
||||||
// Note: Trailing/leading whitespace is now trimmed to prevent auth errors
|
|
||||||
const maskedValue = secretValue.length > 8
|
|
||||||
? `${secretValue.substring(0, 3)}...${secretValue.substring(secretValue.length - 3)}`
|
|
||||||
: '***';
|
|
||||||
|
|
||||||
logger.info(`[Secret Manager] ✅ Loaded: ${secretNameToFetch} -> process.env.${envVarName} (Value: ${maskedValue})`);
|
|
||||||
} else {
|
} else {
|
||||||
// Track which secrets weren't found for better logging
|
// Track which secrets weren't found for better logging
|
||||||
notFoundSecrets.push(fullSecretName);
|
notFoundSecrets.push(fullSecretName);
|
||||||
|
|||||||
@ -250,7 +250,7 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Send email notification for applicable types (async, don't wait)
|
// 4. Send email notification for applicable types (async, don't wait)
|
||||||
console.log(`[DEBUG] Checking email for notification type: ${payload.type}`);
|
// Check email notification preferences
|
||||||
this.sendEmailNotification(userId, user, payload).catch(emailError => {
|
this.sendEmailNotification(userId, user, payload).catch(emailError => {
|
||||||
console.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
|
console.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
|
||||||
logger.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
|
logger.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
|
||||||
@ -269,7 +269,6 @@ class NotificationService {
|
|||||||
* Only sends for notification types that warrant email
|
* Only sends for notification types that warrant email
|
||||||
*/
|
*/
|
||||||
private async sendEmailNotification(userId: string, user: any, payload: NotificationPayload): Promise<void> {
|
private async sendEmailNotification(userId: string, user: any, payload: NotificationPayload): Promise<void> {
|
||||||
console.log(`[DEBUG Email] Notification type: ${payload.type}, userId: ${userId}`);
|
|
||||||
|
|
||||||
// Import email service (lazy load to avoid circular dependencies)
|
// Import email service (lazy load to avoid circular dependencies)
|
||||||
const { emailNotificationService } = await import('./emailNotification.service');
|
const { emailNotificationService } = await import('./emailNotification.service');
|
||||||
@ -311,13 +310,10 @@ class NotificationService {
|
|||||||
|
|
||||||
const emailType = emailTypeMap[payload.type || ''];
|
const emailType = emailTypeMap[payload.type || ''];
|
||||||
|
|
||||||
console.log(`[DEBUG Email] Email type mapped: ${emailType}`);
|
|
||||||
|
|
||||||
if (!emailType) {
|
if (!emailType) {
|
||||||
// This notification type doesn't warrant email
|
// This notification type doesn't warrant email
|
||||||
// Note: 'document_added' emails are handled separately via emailNotificationService
|
// Note: 'document_added' emails are handled separately via emailNotificationService
|
||||||
if (payload.type !== 'document_added') {
|
if (payload.type !== 'document_added') {
|
||||||
console.log(`[DEBUG Email] No email for notification type: ${payload.type}`);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -333,10 +329,7 @@ class NotificationService {
|
|||||||
? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery
|
? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery
|
||||||
: await shouldSendEmail(userId, emailType); // Regular emails
|
: await shouldSendEmail(userId, emailType); // Regular emails
|
||||||
|
|
||||||
console.log(`[DEBUG Email] Should send email: ${shouldSend} for type: ${payload.type}, userId: ${userId}`);
|
|
||||||
|
|
||||||
if (!shouldSend) {
|
if (!shouldSend) {
|
||||||
console.log(`[DEBUG Email] Email skipped for user ${userId}, type: ${payload.type} (preferences)`);
|
|
||||||
logger.warn(`[Email] Email skipped for user ${userId}, type: ${payload.type} (preferences or admin disabled)`);
|
logger.warn(`[Email] Email skipped for user ${userId}, type: ${payload.type} (preferences or admin disabled)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -345,11 +338,9 @@ class NotificationService {
|
|||||||
|
|
||||||
// Trigger email based on notification type
|
// Trigger email based on notification type
|
||||||
// Email service will fetch additional data as needed
|
// Email service will fetch additional data as needed
|
||||||
console.log(`[DEBUG Email] Triggering email for type: ${payload.type}`);
|
|
||||||
try {
|
try {
|
||||||
await this.triggerEmailByType(payload.type || '', userId, payload, user);
|
await this.triggerEmailByType(payload.type || '', userId, payload, user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[DEBUG Email] Error triggering email:`, error);
|
|
||||||
logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error);
|
logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -578,7 +569,7 @@ class NotificationService {
|
|||||||
approverData = {
|
approverData = {
|
||||||
userId: (rejectedLevel as any).approverId,
|
userId: (rejectedLevel as any).approverId,
|
||||||
displayName: (rejectedLevel as any).approverName || 'Unknown Approver',
|
displayName: (rejectedLevel as any).approverName || 'Unknown Approver',
|
||||||
email: (rejectedLevel as any).approverEmail || 'unknown@royalenfield.com',
|
email: (rejectedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}',
|
||||||
rejectedAt: (rejectedLevel as any).actionDate,
|
rejectedAt: (rejectedLevel as any).actionDate,
|
||||||
comments: (rejectedLevel as any).comments
|
comments: (rejectedLevel as any).comments
|
||||||
};
|
};
|
||||||
@ -621,7 +612,7 @@ class NotificationService {
|
|||||||
approverData = {
|
approverData = {
|
||||||
userId: (currentLevel as any).approverId,
|
userId: (currentLevel as any).approverId,
|
||||||
displayName: (currentLevel as any).approverName || 'Unknown Approver',
|
displayName: (currentLevel as any).approverName || 'Unknown Approver',
|
||||||
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com'
|
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -708,7 +699,7 @@ class NotificationService {
|
|||||||
approverData = {
|
approverData = {
|
||||||
userId: (currentLevel as any).approverId,
|
userId: (currentLevel as any).approverId,
|
||||||
displayName: (currentLevel as any).approverName || 'Unknown Approver',
|
displayName: (currentLevel as any).approverName || 'Unknown Approver',
|
||||||
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com'
|
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -842,7 +833,7 @@ class NotificationService {
|
|||||||
recipientData = {
|
recipientData = {
|
||||||
userId: (pausedLevel as any).approverId,
|
userId: (pausedLevel as any).approverId,
|
||||||
displayName: (pausedLevel as any).approverName || 'Unknown Approver',
|
displayName: (pausedLevel as any).approverName || 'Unknown Approver',
|
||||||
email: (pausedLevel as any).approverEmail || 'unknown@royalenfield.com'
|
email: (pausedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -894,7 +885,7 @@ class NotificationService {
|
|||||||
recipientData = {
|
recipientData = {
|
||||||
userId: (currentLevel as any).approverId,
|
userId: (currentLevel as any).approverId,
|
||||||
displayName: (currentLevel as any).approverName || 'Unknown User',
|
displayName: (currentLevel as any).approverName || 'Unknown User',
|
||||||
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com'
|
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export class PdfService {
|
|||||||
private getInvoiceHtmlTemplate(data: any): string {
|
private getInvoiceHtmlTemplate(data: any): string {
|
||||||
const { request, invoice, dealer, claimDetails } = data;
|
const { request, invoice, dealer, claimDetails } = data;
|
||||||
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
|
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
|
||||||
const logoUrl = 'https://www.royalenfield.com/content/dam/royal-enfield/india/logos/logo.png';
|
const logoUrl = '{{LOGO_URL}}';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -119,7 +119,7 @@ export class PdfService {
|
|||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
|
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
|
||||||
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">33AAACE3882D1ZZ</div></div>
|
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">{{BUYER_GSTIN}}</div></div>
|
||||||
<div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div>
|
<div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div>
|
<div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div>
|
||||||
|
|||||||
@ -123,7 +123,7 @@ export class PWCIntegrationService {
|
|||||||
SourceSystem: "RE_WORKFLOW",
|
SourceSystem: "RE_WORKFLOW",
|
||||||
is_irn: "Y",
|
is_irn: "Y",
|
||||||
is_ewb: "N",
|
is_ewb: "N",
|
||||||
email: (request as any).initiator?.email || "system@royalenfield.com",
|
email: (request as any).initiator?.email || "system@{{API_DOMAIN}}",
|
||||||
TranDtls: {
|
TranDtls: {
|
||||||
TaxSch: "GST",
|
TaxSch: "GST",
|
||||||
SubType: "SUPPLY",
|
SubType: "SUPPLY",
|
||||||
@ -155,7 +155,7 @@ export class PWCIntegrationService {
|
|||||||
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
|
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
|
||||||
},
|
},
|
||||||
BuyerDtls: {
|
BuyerDtls: {
|
||||||
Gstin: "33AAACE3882D1ZZ", // Royal Enfield GST
|
Gstin: "{{BUYER_GSTIN}}", // Royal Enfield GST
|
||||||
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
|
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
|
||||||
TrdNm: "ROYAL ENFIELD",
|
TrdNm: "ROYAL ENFIELD",
|
||||||
Addr1: "No. 2, Thiruvottiyur High Road",
|
Addr1: "No. 2, Thiruvottiyur High Road",
|
||||||
@ -201,7 +201,6 @@ export class PWCIntegrationService {
|
|||||||
'token': this.token
|
'token': this.token
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('PWC Response:', JSON.stringify(response.data));
|
|
||||||
|
|
||||||
// Parse PWC Response based on provided structure
|
// Parse PWC Response based on provided structure
|
||||||
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]
|
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]
|
||||||
|
|||||||
@ -34,7 +34,6 @@ export class SAPIntegrationService {
|
|||||||
* Check if SAP integration is configured
|
* Check if SAP integration is configured
|
||||||
*/
|
*/
|
||||||
private isConfigured(): boolean {
|
private isConfigured(): boolean {
|
||||||
// Check if SAP bypass is explicitly enabled
|
|
||||||
if (process.env.SAP_BYPASS === 'true') {
|
if (process.env.SAP_BYPASS === 'true') {
|
||||||
logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable');
|
logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable');
|
||||||
return false;
|
return false;
|
||||||
@ -188,12 +187,8 @@ export class SAPIntegrationService {
|
|||||||
}) : undefined
|
}) : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add request interceptor for debugging
|
|
||||||
client.interceptors.request.use(
|
client.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
logger.debug(`[SAP] Making ${config.method?.toUpperCase()} request to: ${config.baseURL}${config.url}`);
|
|
||||||
logger.debug(`[SAP] Auth username: ${this.sapUsername}`);
|
|
||||||
// Don't log password for security
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -219,7 +214,7 @@ export class SAPIntegrationService {
|
|||||||
} else if (error.code === 'ENOTFOUND') {
|
} else if (error.code === 'ENOTFOUND') {
|
||||||
logger.error('[SAP] Host not found - check SAP_BASE_URL');
|
logger.error('[SAP] Host not found - check SAP_BASE_URL');
|
||||||
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
||||||
logger.error('[SAP] SSL certificate error - set SAP_DISABLE_SSL_VERIFY=true to bypass (testing only)');
|
logger.error('[SAP] SSL certificate error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@ -597,9 +592,10 @@ export class SAPIntegrationService {
|
|||||||
logger.debug(`[SAP] POST request config prepared (auth included)`);
|
logger.debug(`[SAP] POST request config prepared (auth included)`);
|
||||||
const response = await sapClient.post(urlWithParams, requestPayload, postConfig);
|
const response = await sapClient.post(urlWithParams, requestPayload, postConfig);
|
||||||
|
|
||||||
// Log full response for debugging
|
// Log response status for visibility
|
||||||
logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`);
|
logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`);
|
||||||
logger.info(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2));
|
// Headers relegated to debug level
|
||||||
|
logger.debug(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2));
|
||||||
|
|
||||||
// Check if response is XML (SAP returns XML/Atom by default for POST)
|
// Check if response is XML (SAP returns XML/Atom by default for POST)
|
||||||
const contentType = response.headers['content-type'] || '';
|
const contentType = response.headers['content-type'] || '';
|
||||||
@ -640,9 +636,10 @@ export class SAPIntegrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also log the request that was sent
|
// Log the request URL
|
||||||
logger.info(`[SAP] POST Request URL: ${urlWithParams}`);
|
logger.info(`[SAP] POST Request URL: ${urlWithParams}`);
|
||||||
logger.info(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2));
|
// Payload relegated to debug level
|
||||||
|
logger.debug(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2));
|
||||||
|
|
||||||
if (response.status === 200 || response.status === 201) {
|
if (response.status === 200 || response.status === 201) {
|
||||||
// Parse SAP response
|
// Parse SAP response
|
||||||
|
|||||||
@ -133,7 +133,6 @@ if (process.env.LOKI_HOST) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transports.push(new LokiTransport(lokiTransportOptions));
|
transports.push(new LokiTransport(lokiTransportOptions));
|
||||||
console.log(`[Logger] ✅ Loki transport enabled: ${process.env.LOKI_HOST}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Logger] ⚠️ Failed to initialize Loki transport:', (error as Error).message);
|
console.warn('[Logger] ⚠️ Failed to initialize Loki transport:', (error as Error).message);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user