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_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=notifications@royalenfield.com
|
||||
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||
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_API_KEY=your_ai_api_key
|
||||
|
||||
@ -155,13 +155,13 @@ export async function calculateBusinessDays(
|
||||
2. ✅ Imported `calculateElapsedWorkingHours`, `addWorkingHours`, `addWorkingHoursExpress` from `@utils/tatTimeUtils`
|
||||
3. ✅ Replaced lines 64-65 with proper working hours calculation (now lines 66-77)
|
||||
4. ✅ Gets priority from workflow
|
||||
5. ⏳ **TODO:** Test TAT breach alerts
|
||||
5. Done: Test TAT breach alerts
|
||||
|
||||
### Step 2: Add Business Days Function ✅ **DONE**
|
||||
1. ✅ Opened `Re_Backend/src/utils/tatTimeUtils.ts`
|
||||
2. ✅ Added `calculateBusinessDays()` function (lines 697-758)
|
||||
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**
|
||||
1. ✅ Built report endpoint using `calculateBusinessDays()`
|
||||
|
||||
@ -19,10 +19,10 @@ This command will output something like:
|
||||
```
|
||||
=======================================
|
||||
Public Key:
|
||||
BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
||||
{{VAPID_PUBLIC_KEY}}
|
||||
|
||||
Private Key:
|
||||
aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz
|
||||
{{VAPID_PRIVATE_KEY}}
|
||||
|
||||
=======================================
|
||||
```
|
||||
@ -59,9 +59,9 @@ Add the generated keys to your backend `.env` file:
|
||||
|
||||
```env
|
||||
# Notification Service Worker credentials (Web Push / VAPID)
|
||||
VAPID_PUBLIC_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
||||
VAPID_PRIVATE_KEY=aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz
|
||||
VAPID_CONTACT=mailto:admin@royalenfield.com
|
||||
VAPID_PUBLIC_KEY={{VAPID_PUBLIC_KEY}}
|
||||
VAPID_PRIVATE_KEY={{VAPID_PRIVATE_KEY}}
|
||||
VAPID_CONTACT=mailto:{{ADMIN_EMAIL}}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
@ -75,7 +75,7 @@ Add the **SAME** `VAPID_PUBLIC_KEY` to your frontend `.env` file:
|
||||
|
||||
```env
|
||||
# Push Notifications (Web Push / VAPID)
|
||||
VITE_PUBLIC_VAPID_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
|
||||
VITE_PUBLIC_VAPID_KEY={{VAPID_PUBLIC_KEY}}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
|
||||
@ -98,7 +98,7 @@ npm run dev
|
||||
1. Server will start automatically
|
||||
2. Log in via SSO
|
||||
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)
|
||||
info: ✅ Server started successfully on port 5000
|
||||
@ -112,7 +112,7 @@ psql -d royal_enfield_workflow
|
||||
|
||||
UPDATE users
|
||||
SET role = 'ADMIN'
|
||||
WHERE email = 'your-email@royalenfield.com';
|
||||
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||
|
||||
\q
|
||||
```
|
||||
|
||||
@ -471,7 +471,7 @@ The backend supports web push notifications via VAPID (Voluntary Application Ser
|
||||
```
|
||||
VAPID_PUBLIC_KEY=<your-public-key>
|
||||
VAPID_PRIVATE_KEY=<your-private-key>
|
||||
VAPID_CONTACT=mailto:admin@royalenfield.com
|
||||
VAPID_CONTACT=mailto:admin@{{API_DOMAIN}}
|
||||
```
|
||||
|
||||
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};
|
||||
//# sourceMappingURL=conclusionApi-BJO_6JLT.js.map
|
||||
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-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 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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/radix-vendor-CYvDqP9X.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)
|
||||
- **Action Type**: **AUTO** (System automatically creates activity)
|
||||
- **TAT**: 1 hour
|
||||
- **Mapping**: System user (`system@royalenfield.com`)
|
||||
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
|
||||
- **Status**: Auto-approved when triggered
|
||||
|
||||
### 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)
|
||||
- **Action Type**: **AUTO** (System generates e-invoice via DMS integration)
|
||||
- **TAT**: 1 hour
|
||||
- **Mapping**: System user (`system@royalenfield.com`)
|
||||
- **Mapping**: System user (`system@{{API_DOMAIN}}`)
|
||||
- **Status**: Auto-approved when triggered
|
||||
|
||||
### 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'
|
||||
2. Find user with designation containing "Finance" or "Accountant"
|
||||
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
|
||||
|
||||
@ -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) |
|
||||
| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) |
|
||||
| `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 |
|
||||
| `termination_resignation_status` | String(255) | No | Termination/Resignation status |
|
||||
| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD |
|
||||
@ -183,7 +183,7 @@ Ensure dates are in `YYYY-MM-DD` format:
|
||||
|
||||
```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
|
||||
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:**
|
||||
|
||||
@ -56,7 +56,7 @@ users {
|
||||
```json
|
||||
{
|
||||
"userId": "uuid-1",
|
||||
"email": "john.doe@royalenfield.com",
|
||||
"email": "john.doe@{{API_DOMAIN}}",
|
||||
"employeeId": "E12345", // Regular employee ID
|
||||
"designation": "Software Engineer",
|
||||
"department": "IT",
|
||||
@ -68,7 +68,7 @@ users {
|
||||
```json
|
||||
{
|
||||
"userId": "uuid-2",
|
||||
"email": "test.2@royalenfield.com",
|
||||
"email": "test.2@{{API_DOMAIN}}",
|
||||
"employeeId": "RE-MH-001", // Dealer code stored here
|
||||
"designation": "Dealer",
|
||||
"department": "Dealer Operations",
|
||||
|
||||
@ -98,8 +98,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
||||
|
||||
**Base URL Examples:**
|
||||
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
|
||||
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice`
|
||||
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice`
|
||||
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
|
||||
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
|
||||
|
||||
### 3.2 Request Headers
|
||||
|
||||
@ -205,8 +205,8 @@ User-Agent: DMS-Webhook-Client/1.0
|
||||
|
||||
**Base URL Examples:**
|
||||
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
|
||||
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
||||
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
||||
- UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
|
||||
- Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
|
||||
|
||||
### 4.2 Request Headers
|
||||
|
||||
@ -563,8 +563,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
||||
| 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` |
|
||||
| UAT | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow-uat.royalenfield.com/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` |
|
||||
| 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.{{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
|
||||
# Edit the script
|
||||
nano scripts/assign-admin-user.sql
|
||||
# Change: YOUR_EMAIL@royalenfield.com
|
||||
# Change: YOUR_EMAIL@{{API_DOMAIN}}
|
||||
|
||||
# Run it
|
||||
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
|
||||
@ -170,7 +170,7 @@ psql -d royal_enfield_workflow
|
||||
|
||||
UPDATE users
|
||||
SET role = 'ADMIN'
|
||||
WHERE email = 'your-email@royalenfield.com';
|
||||
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||
|
||||
-- Verify
|
||||
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"
|
||||
|
||||
# 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
|
||||
-- Single user
|
||||
UPDATE users SET role = 'MANAGEMENT'
|
||||
WHERE email = 'manager@royalenfield.com';
|
||||
WHERE email = 'manager@{{API_DOMAIN}}';
|
||||
|
||||
-- Multiple users
|
||||
UPDATE users SET role = 'MANAGEMENT'
|
||||
WHERE email IN (
|
||||
'manager1@royalenfield.com',
|
||||
'manager2@royalenfield.com'
|
||||
'manager1@{{API_DOMAIN}}',
|
||||
'manager2@{{API_DOMAIN}}'
|
||||
);
|
||||
|
||||
-- By department
|
||||
@ -260,13 +260,13 @@ WHERE department = 'Management' AND is_active = true;
|
||||
```sql
|
||||
-- Single user
|
||||
UPDATE users SET role = 'ADMIN'
|
||||
WHERE email = 'admin@royalenfield.com';
|
||||
WHERE email = 'admin@{{API_DOMAIN}}';
|
||||
|
||||
-- Multiple admins
|
||||
UPDATE users SET role = 'ADMIN'
|
||||
WHERE email IN (
|
||||
'admin1@royalenfield.com',
|
||||
'admin2@royalenfield.com'
|
||||
'admin1@{{API_DOMAIN}}',
|
||||
'admin2@{{API_DOMAIN}}'
|
||||
);
|
||||
|
||||
-- By department
|
||||
@ -331,7 +331,7 @@ SELECT
|
||||
mobile_phone,
|
||||
array_length(ad_groups, 1) as ad_group_count
|
||||
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 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@royalenfield.com",
|
||||
"email": "test@{{API_DOMAIN}}",
|
||||
"displayName": "Test User",
|
||||
"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
|
||||
|
||||
```sql
|
||||
SELECT email, role FROM users WHERE email = 'test@royalenfield.com';
|
||||
SELECT email, role FROM users WHERE email = 'test@{{API_DOMAIN}}';
|
||||
-- Expected: role = 'USER'
|
||||
```
|
||||
|
||||
### 3. Update to ADMIN
|
||||
|
||||
```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
|
||||
@ -369,7 +369,7 @@ UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com';
|
||||
# Login and get token
|
||||
curl -X POST http://localhost:5000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "test@royalenfield.com", ...}'
|
||||
-d '{"email": "test@{{API_DOMAIN}}", ...}'
|
||||
|
||||
# Try admin endpoint (should work if ADMIN role)
|
||||
curl http://localhost:5000/api/v1/admin/configurations \
|
||||
@ -449,7 +449,7 @@ npm run migrate
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
|
||||
@ -459,7 +459,7 @@ SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
\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 |
|
||||
| **Environment** | Production |
|
||||
| **Domain** | `https://reflow.royalenfield.com` |
|
||||
| **Domain** | `https://reflow.{{API_DOMAIN}}` |
|
||||
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
|
||||
| **Storage Type** | Google Cloud Storage (GCS) |
|
||||
| **Region** | `asia-south1` (Mumbai) |
|
||||
@ -325,8 +325,8 @@ Create `cors-config-prod.json`:
|
||||
[
|
||||
{
|
||||
"origin": [
|
||||
"https://reflow.royalenfield.com",
|
||||
"https://www.royalenfield.com"
|
||||
"https://reflow.{{API_DOMAIN}}",
|
||||
"https://www.{{API_DOMAIN}}"
|
||||
],
|
||||
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
||||
"responseHeader": [
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|------|-------|
|
||||
| **Application** | RE Workflow System |
|
||||
| **Environment** | UAT |
|
||||
| **Domain** | https://reflow-uat.royalenfield.com |
|
||||
| **Domain** | https://reflow-uat.{{API_DOMAIN}} |
|
||||
| **Purpose** | Store workflow documents and attachments |
|
||||
|
||||
---
|
||||
@ -131,8 +131,8 @@ Apply this CORS policy to allow browser uploads:
|
||||
[
|
||||
{
|
||||
"origin": [
|
||||
"https://reflow-uat.royalenfield.com",
|
||||
"https://reflow.royalenfield.com"
|
||||
"https://reflow-uat.{{API_DOMAIN}}",
|
||||
"https://reflow.{{API_DOMAIN}}"
|
||||
],
|
||||
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
||||
"responseHeader": [
|
||||
|
||||
@ -72,8 +72,8 @@ The Users API returns a complete user object:
|
||||
"employeeID": "E09994",
|
||||
"title": "Supports Business Applications (SAP) portfolio",
|
||||
"department": "Deputy Manager - Digital & IT",
|
||||
"login": "sanjaysahu@Royalenfield.com",
|
||||
"email": "sanjaysahu@royalenfield.com"
|
||||
"login": "sanjaysahu@{{API_DOMAIN}}",
|
||||
"email": "sanjaysahu@{{API_DOMAIN}}"
|
||||
},
|
||||
...
|
||||
}
|
||||
@ -127,7 +127,7 @@ Example log:
|
||||
### Test with curl
|
||||
|
||||
```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 'Accept: application/json'
|
||||
```
|
||||
|
||||
@ -450,16 +450,16 @@ Before Migration:
|
||||
+-------------------------+-----------+
|
||||
| email | is_admin |
|
||||
+-------------------------+-----------+
|
||||
| admin@royalenfield.com | true |
|
||||
| user1@royalenfield.com | false |
|
||||
| admin@{{API_DOMAIN}} | true |
|
||||
| user1@{{API_DOMAIN}} | false |
|
||||
+-------------------------+-----------+
|
||||
|
||||
After Migration:
|
||||
+-------------------------+-----------+-----------+
|
||||
| email | role | is_admin |
|
||||
+-------------------------+-----------+-----------+
|
||||
| admin@royalenfield.com | ADMIN | true |
|
||||
| user1@royalenfield.com | USER | false |
|
||||
| admin@{{API_DOMAIN}} | ADMIN | true |
|
||||
| user1@{{API_DOMAIN}} | USER | false |
|
||||
+-------------------------+-----------+-----------+
|
||||
```
|
||||
|
||||
@ -473,17 +473,17 @@ After Migration:
|
||||
-- Make user a MANAGEMENT role
|
||||
UPDATE users
|
||||
SET role = 'MANAGEMENT', is_admin = false
|
||||
WHERE email = 'manager@royalenfield.com';
|
||||
WHERE email = 'manager@{{API_DOMAIN}}';
|
||||
|
||||
-- Make user an ADMIN role
|
||||
UPDATE users
|
||||
SET role = 'ADMIN', is_admin = true
|
||||
WHERE email = 'admin@royalenfield.com';
|
||||
WHERE email = 'admin@{{API_DOMAIN}}';
|
||||
|
||||
-- Revert to USER role
|
||||
UPDATE users
|
||||
SET role = 'USER', is_admin = false
|
||||
WHERE email = 'user@royalenfield.com';
|
||||
WHERE email = 'user@{{API_DOMAIN}}';
|
||||
```
|
||||
|
||||
### Via API (Admin Endpoint)
|
||||
|
||||
@ -47,12 +47,12 @@ psql -d royal_enfield_db -f scripts/assign-user-roles.sql
|
||||
-- Make specific users ADMIN
|
||||
UPDATE users
|
||||
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
|
||||
UPDATE users
|
||||
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
|
||||
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
|
||||
@ -219,7 +219,7 @@ GROUP BY role;
|
||||
-- Check specific user
|
||||
SELECT email, role, is_admin
|
||||
FROM users
|
||||
WHERE email = 'your-email@royalenfield.com';
|
||||
WHERE email = 'your-email@{{API_DOMAIN}}';
|
||||
```
|
||||
|
||||
### Test 2: Test API Access
|
||||
@ -356,7 +356,7 @@ WHERE designation ILIKE '%manager%' OR designation ILIKE '%head%';
|
||||
```sql
|
||||
SELECT email, role, is_admin
|
||||
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
|
||||
|
||||
# Okta Configuration
|
||||
OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
||||
OKTA_DOMAIN=https://{{IDP_DOMAIN}}
|
||||
OKTA_CLIENT_ID=your-client-id
|
||||
OKTA_CLIENT_SECRET=your-client-secret
|
||||
|
||||
@ -334,7 +334,7 @@ GCP_BUCKET_PUBLIC=true
|
||||
|
||||
**Identity Provider**: Okta
|
||||
- **Domain**: Configurable via `OKTA_DOMAIN` environment variable
|
||||
- **Default**: `https://dev-830839.oktapreview.com`
|
||||
- **Default**: `https://{{IDP_DOMAIN}}`
|
||||
- **Protocol**: OAuth 2.0 / OpenID Connect (OIDC)
|
||||
- **Grant Types**: Authorization Code, Resource Owner Password Credentials
|
||||
|
||||
@ -650,7 +650,7 @@ graph LR
|
||||
{
|
||||
"userId": "uuid",
|
||||
"employeeId": "EMP001",
|
||||
"email": "user@royalenfield.com",
|
||||
"email": "user@{{API_DOMAIN}}",
|
||||
"role": "USER" | "MANAGEMENT" | "ADMIN",
|
||||
"iat": 1234567890,
|
||||
"exp": 1234654290
|
||||
@ -1048,7 +1048,7 @@ JWT_EXPIRY=24h
|
||||
REFRESH_TOKEN_EXPIRY=7d
|
||||
|
||||
# Okta
|
||||
OKTA_DOMAIN=https://dev-830839.oktapreview.com
|
||||
OKTA_DOMAIN=https://{{IDP_DOMAIN}}
|
||||
OKTA_CLIENT_ID=your-client-id
|
||||
OKTA_CLIENT_SECRET=your-client-secret
|
||||
|
||||
@ -1063,7 +1063,7 @@ GCP_BUCKET_PUBLIC=true
|
||||
**Frontend (.env):**
|
||||
```env
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ await this.createClaimApprovalLevels(
|
||||
isAuto: false,
|
||||
approverType: 'department_lead' as const,
|
||||
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
|
||||
|
||||
{
|
||||
"username": "john.doe@royalenfield.com",
|
||||
"username": "john.doe@{{API_DOMAIN}}",
|
||||
"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
|
||||
|
||||
# Cloud Storage (GCP)
|
||||
GCP_PROJECT_ID=re-workflow-project
|
||||
GCP_BUCKET_NAME=re-workflow-documents
|
||||
GCP_PROJECT_ID={{GCP_PROJECT_ID}}
|
||||
GCP_BUCKET_NAME={{GCP_BUCKET_NAME}}
|
||||
GCP_KEY_FILE=./config/gcp-key.json
|
||||
|
||||
# Google Secret Manager (Optional - for production)
|
||||
@ -41,9 +41,9 @@ USE_GOOGLE_SECRET_MANAGER=false
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=notifications@royalenfield.com
|
||||
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||
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
|
||||
# Uses service account credentials from GCP_KEY_FILE
|
||||
@ -55,7 +55,7 @@ VERTEX_AI_LOCATION=asia-south1
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE_PATH=./logs
|
||||
APP_VERSION=1.2.0
|
||||
APP_VERSION={{APP_VERSION}}
|
||||
|
||||
# ============ Loki Configuration (Grafana Log Aggregation) ============
|
||||
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="*"
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# File Upload
|
||||
@ -83,16 +83,16 @@ OKTA_CLIENT_ID={{okta_client_id}}
|
||||
OKTA_CLIENT_SECRET={{okta_client_secret}}
|
||||
|
||||
# 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_CONTACT=mailto:you@example.com
|
||||
|
||||
#Redis
|
||||
REDIS_URL={{REDIS_URL_FOR DELAY JoBS create redis setup and add url here}}
|
||||
TAT_TEST_MODE=false (on true it will consider 1 hour==1min)
|
||||
REDIS_URL={{REDIS_URL}}
|
||||
TAT_TEST_MODE=false # Set to true to accelerate TAT for testing
|
||||
|
||||
# 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_PASSWORD={{SAP_PASSWORD}}
|
||||
SAP_TIMEOUT_MS=30000
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
UPDATE users
|
||||
SET role = 'ADMIN'
|
||||
WHERE email = 'YOUR_EMAIL@royalenfield.com' -- ← CHANGE THIS
|
||||
WHERE email = 'YOUR_EMAIL@{{API_DOMAIN}}' -- ← CHANGE THIS
|
||||
RETURNING
|
||||
user_id,
|
||||
email,
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
UPDATE users
|
||||
SET role = 'ADMIN'
|
||||
WHERE email IN (
|
||||
'admin@royalenfield.com',
|
||||
'it.admin@royalenfield.com',
|
||||
'system.admin@royalenfield.com'
|
||||
'admin@{{API_DOMAIN}}',
|
||||
'it.admin@{{API_DOMAIN}}',
|
||||
'system.admin@{{API_DOMAIN}}'
|
||||
-- Add more admin emails here
|
||||
);
|
||||
|
||||
@ -45,9 +45,9 @@ ORDER BY email;
|
||||
UPDATE users
|
||||
SET role = 'MANAGEMENT'
|
||||
WHERE email IN (
|
||||
'manager1@royalenfield.com',
|
||||
'dept.head@royalenfield.com',
|
||||
'auditor@royalenfield.com'
|
||||
'manager1@{{API_DOMAIN}}',
|
||||
'dept.head@{{API_DOMAIN}}',
|
||||
'auditor@{{API_DOMAIN}}'
|
||||
-- Add more management emails here
|
||||
);
|
||||
|
||||
|
||||
@ -162,7 +162,7 @@ SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=${SMTP_USER}
|
||||
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)
|
||||
# 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 ""
|
||||
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 "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!"
|
||||
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 { sequelize } from './config/database';
|
||||
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 routes from './routes/index';
|
||||
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-elem 'self'",
|
||||
"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:",
|
||||
"font-src 'self' https://fonts.gstatic.com data:",
|
||||
"object-src 'none'",
|
||||
@ -117,7 +119,7 @@ app.use(morgan('combined'));
|
||||
app.use(metricsMiddleware);
|
||||
|
||||
// Prometheus metrics endpoint - expose metrics for scraping
|
||||
app.use(createMetricsRouter());
|
||||
app.use('/metrics', authenticateToken, requireAdmin, createMetricsRouter());
|
||||
|
||||
// Health check endpoint (before API routes)
|
||||
app.get('/health', (_req: express.Request, res: express.Response) => {
|
||||
@ -134,7 +136,7 @@ app.use('/api/v1', routes);
|
||||
|
||||
// Serve uploaded files statically
|
||||
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)
|
||||
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
|
||||
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 {
|
||||
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 || '',
|
||||
},
|
||||
},
|
||||
|
||||
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@royalenfield.com>',
|
||||
|
||||
|
||||
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@{{API_DOMAIN}}>',
|
||||
|
||||
// Email templates
|
||||
templates: {
|
||||
workflowCreated: 'workflow-created',
|
||||
@ -20,7 +20,7 @@ export const emailConfig = {
|
||||
tatReminder: 'tat-reminder',
|
||||
tatBreached: 'tat-breached',
|
||||
},
|
||||
|
||||
|
||||
// Email settings
|
||||
settings: {
|
||||
retryAttempts: 3,
|
||||
|
||||
@ -8,18 +8,18 @@ const ssoConfig: SSOConfig = {
|
||||
get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; },
|
||||
get sessionSecret() { return process.env.SESSION_SECRET || ''; },
|
||||
// Use only FRONTEND_URL from environment - no fallbacks
|
||||
get allowedOrigins() {
|
||||
get allowedOrigins() {
|
||||
return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [];
|
||||
},
|
||||
// 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 oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
|
||||
get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API
|
||||
// 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 tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || 'cfIzMlwAMF1m4QWAP5StzZbV47HIrCox'; },
|
||||
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || '{{TANFLOW_CLIENT_SECRET}}'; },
|
||||
};
|
||||
|
||||
export { ssoConfig };
|
||||
|
||||
@ -172,7 +172,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
||||
- Initiator (for record)
|
||||
- Finance team
|
||||
- **Template**: `creditNoteSent.template.ts` (NEW)
|
||||
- **Status**: ❌ Not Implemented (TODO comment at line 2037-2044)
|
||||
- **Status**: Required implementation
|
||||
- **Notification Type**: `credit_note_sent`
|
||||
- **Data Needed**:
|
||||
- Credit note number
|
||||
@ -184,7 +184,7 @@ This document outlines all email templates required for the Dealer Claim Managem
|
||||
- Reason for credit note
|
||||
- Download link (if available)
|
||||
- **Notes**:
|
||||
- Currently has TODO comment for email implementation
|
||||
- Planned for email implementation
|
||||
- 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`)
|
||||
- Priority: High
|
||||
- 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)
|
||||
1. **Activity Created** - Currently using generic notification, should be branded
|
||||
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)
|
||||
4. **Proposal Submitted** - Better UX, but existing approval request works
|
||||
|
||||
@ -991,9 +991,9 @@ Add to `.env`:
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=notifications@royalenfield.com
|
||||
SMTP_USER=notifications@{{API_DOMAIN}}
|
||||
SMTP_PASSWORD=your-app-specific-password
|
||||
EMAIL_FROM=RE Flow <noreply@royalenfield.com>
|
||||
EMAIL_FROM=RE Flow <noreply@{{API_DOMAIN}}>
|
||||
|
||||
# Email Settings
|
||||
EMAIL_ENABLED=true
|
||||
@ -1002,10 +1002,10 @@ EMAIL_BATCH_SIZE=50
|
||||
EMAIL_RETRY_ATTEMPTS=3
|
||||
|
||||
# Application
|
||||
BASE_URL=https://workflow.royalenfield.com
|
||||
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||
COMPANY_NAME=Royal Enfield
|
||||
COMPANY_WEBSITE=https://www.royalenfield.com
|
||||
SUPPORT_EMAIL=support@royalenfield.com
|
||||
COMPANY_WEBSITE=https://www.{{API_DOMAIN}}
|
||||
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:
|
||||
- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now"
|
||||
- **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.
|
||||
|
||||
@ -231,8 +231,8 @@ SMTP_USER=your-email@domain.com
|
||||
SMTP_PASSWORD=your-app-password
|
||||
|
||||
# Email Settings
|
||||
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
|
||||
BASE_URL=https://workflow.royalenfield.com
|
||||
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
|
||||
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||
COMPANY_NAME=Royal Enfield
|
||||
```
|
||||
|
||||
|
||||
@ -361,7 +361,7 @@ All `[ViewDetailsLink]` placeholders should be replaced with:
|
||||
{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
|
||||
Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield")
|
||||
|
||||
@ -12,15 +12,15 @@ emailtemplates/
|
||||
├── approvalRequest.template.ts ✅ Single approver email
|
||||
├── multiApproverRequest.template.ts ✅ Multi-approver email
|
||||
│
|
||||
├── approvalConfirmation.template.ts 🔨 TODO
|
||||
├── rejectionNotification.template.ts 🔨 TODO
|
||||
├── tatReminder.template.ts 🔨 TODO
|
||||
├── tatBreached.template.ts 🔨 TODO
|
||||
├── workflowPaused.template.ts 🔨 TODO
|
||||
├── workflowResumed.template.ts 🔨 TODO
|
||||
├── participantAdded.template.ts 🔨 TODO
|
||||
├── approverSkipped.template.ts 🔨 TODO
|
||||
└── requestClosed.template.ts 🔨 TODO
|
||||
├── approvalConfirmation.template.ts ✅ DONE
|
||||
├── rejectionNotification.template.ts ✅ DONE
|
||||
├── tatReminder.template.ts ✅ DONE
|
||||
├── tatBreached.template.ts ✅ DONE
|
||||
├── workflowPaused.template.ts ✅ DONE
|
||||
├── workflowResumed.template.ts ✅ DONE
|
||||
├── participantAdded.template.ts ✅ DONE
|
||||
├── approverSkipped.template.ts ✅ DONE
|
||||
└── requestClosed.template.ts ✅ DONE
|
||||
```
|
||||
|
||||
---
|
||||
@ -53,7 +53,7 @@ const data: RequestCreatedData = {
|
||||
requestTime: '02:30 PM',
|
||||
totalApprovers: 3,
|
||||
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'
|
||||
};
|
||||
```
|
||||
@ -188,10 +188,10 @@ SMTP_USER=your-email@domain.com
|
||||
SMTP_PASSWORD=your-app-password
|
||||
|
||||
# Email Settings
|
||||
EMAIL_FROM=Royal Enfield Workflow <notifications@royalenfield.com>
|
||||
EMAIL_FROM=Royal Enfield Workflow <notifications@{{API_DOMAIN}}>
|
||||
|
||||
# Application Settings
|
||||
BASE_URL=https://workflow.royalenfield.com
|
||||
BASE_URL=https://workflow.{{API_DOMAIN}}
|
||||
COMPANY_NAME=Royal Enfield
|
||||
```
|
||||
|
||||
|
||||
@ -13,12 +13,12 @@ import { EmailHeaderConfig, EmailFooterConfig } from './helpers';
|
||||
export const CompanyInfo = {
|
||||
name: 'Royal Enfield',
|
||||
productName: 'RE Flow', // Product name displayed in header
|
||||
website: 'https://www.royalenfield.com',
|
||||
supportEmail: 'support@royalenfield.com',
|
||||
|
||||
website: 'https://www.{{API_DOMAIN}}',
|
||||
supportEmail: 'support@{{API_DOMAIN}}',
|
||||
|
||||
// Logo configuration for email headers
|
||||
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',
|
||||
width: 220, // Logo width in pixels (wider for better visibility)
|
||||
height: 65, // Logo height in pixels (proportional ratio ~3.4:1)
|
||||
@ -88,7 +88,7 @@ export const CustomHeaderStyles = {
|
||||
* Usage in email service:
|
||||
* 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 {
|
||||
return `${frontendUrl}/request/${requestNumber}`;
|
||||
|
||||
@ -14,13 +14,13 @@ async function generatePreviews() {
|
||||
// Sample data
|
||||
const initiator = {
|
||||
userId: 'user-1',
|
||||
email: 'john.doe@royalenfield.com',
|
||||
email: 'john.doe@{{API_DOMAIN}}',
|
||||
displayName: 'John Doe'
|
||||
};
|
||||
|
||||
const approver = {
|
||||
userId: 'user-2',
|
||||
email: 'jane.smith@royalenfield.com',
|
||||
email: 'jane.smith@{{API_DOMAIN}}',
|
||||
displayName: 'Jane Smith'
|
||||
};
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
import {
|
||||
import {
|
||||
getRequestCreatedEmail,
|
||||
getApprovalRequestEmail,
|
||||
getMultiApproverRequestEmail,
|
||||
@ -21,10 +21,10 @@ import {
|
||||
async function sendTestEmail() {
|
||||
try {
|
||||
console.log('🚀 Creating nodemailer test account...');
|
||||
|
||||
|
||||
// Create a test account automatically (free, no signup needed)
|
||||
const testAccount = await nodemailer.createTestAccount();
|
||||
|
||||
|
||||
console.log('✅ Test account created:', testAccount.user);
|
||||
|
||||
// Create transporter with test SMTP details
|
||||
@ -100,12 +100,12 @@ async function sendTestEmail() {
|
||||
// Test 1: Request Created Email
|
||||
const html1 = getRequestCreatedEmail(requestCreatedData);
|
||||
const info1 = await transporter.sendMail({
|
||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
||||
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||
to: 'initiator@example.com',
|
||||
subject: '[REQ-2025-12-0013] Request Created Successfully',
|
||||
html: html1
|
||||
});
|
||||
|
||||
|
||||
console.log('✅ Test 1: Request Created Email');
|
||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info1));
|
||||
console.log('');
|
||||
@ -113,12 +113,12 @@ async function sendTestEmail() {
|
||||
// Test 2: Approval Request Email (Single)
|
||||
const html2 = getApprovalRequestEmail(approvalRequestData);
|
||||
const info2 = await transporter.sendMail({
|
||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
||||
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||
to: 'approver@example.com',
|
||||
subject: '[REQ-2025-12-0013] Approval Request - Action Required',
|
||||
html: html2
|
||||
});
|
||||
|
||||
|
||||
console.log('✅ Test 2: Approval Request Email');
|
||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info2));
|
||||
console.log('');
|
||||
@ -126,12 +126,12 @@ async function sendTestEmail() {
|
||||
// Test 3: Multi-Approver Request Email
|
||||
const html3 = getMultiApproverRequestEmail(multiApproverData);
|
||||
const info3 = await transporter.sendMail({
|
||||
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>',
|
||||
from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
|
||||
to: 'approver-level2@example.com',
|
||||
subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn',
|
||||
html: html3
|
||||
});
|
||||
|
||||
|
||||
console.log('✅ Test 3: Multi-Approver Request Email');
|
||||
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info3));
|
||||
console.log('');
|
||||
|
||||
@ -18,7 +18,7 @@ async function testRealScenario() {
|
||||
// Mock user data (simulating real database records)
|
||||
const user10 = {
|
||||
userId: 'user-10-uuid',
|
||||
email: 'john.doe@royalenfield.com',
|
||||
email: 'john.doe@{{API_DOMAIN}}',
|
||||
displayName: 'John Doe',
|
||||
department: 'Engineering',
|
||||
designation: 'Senior Engineer'
|
||||
@ -26,7 +26,7 @@ async function testRealScenario() {
|
||||
|
||||
const user12 = {
|
||||
userId: 'user-12-uuid',
|
||||
email: 'jane.smith@royalenfield.com',
|
||||
email: 'jane.smith@{{API_DOMAIN}}',
|
||||
displayName: 'Jane Smith',
|
||||
department: 'Management',
|
||||
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.
|
||||
</blockquote>
|
||||
<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',
|
||||
priority: 'HIGH',
|
||||
@ -68,21 +68,21 @@ async function testRealScenario() {
|
||||
{
|
||||
levelNumber: 1,
|
||||
approverName: 'Jane Smith',
|
||||
approverEmail: 'jane.smith@royalenfield.com',
|
||||
approverEmail: 'jane.smith@{{API_DOMAIN}}',
|
||||
status: 'PENDING',
|
||||
approvedAt: null
|
||||
},
|
||||
{
|
||||
levelNumber: 2,
|
||||
approverName: 'Michael Brown',
|
||||
approverEmail: 'michael.brown@royalenfield.com',
|
||||
approverEmail: 'michael.brown@{{API_DOMAIN}}',
|
||||
status: 'PENDING',
|
||||
approvedAt: null
|
||||
},
|
||||
{
|
||||
levelNumber: 3,
|
||||
approverName: 'Sarah Johnson',
|
||||
approverEmail: 'sarah.johnson@royalenfield.com',
|
||||
approverEmail: 'sarah.johnson@{{API_DOMAIN}}',
|
||||
status: 'PENDING',
|
||||
approvedAt: null
|
||||
}
|
||||
@ -92,7 +92,7 @@ async function testRealScenario() {
|
||||
console.log('─'.repeat(80));
|
||||
console.log('📧 Test 1: Request Created Email (to Initiator - User 10)');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
|
||||
await emailNotificationService.sendRequestCreated(
|
||||
requestData,
|
||||
user10,
|
||||
@ -168,7 +168,7 @@ async function testRealScenario() {
|
||||
approvedUser12,
|
||||
user10,
|
||||
false, // not final approval
|
||||
{ displayName: 'Michael Brown', email: 'michael.brown@royalenfield.com' }
|
||||
{ displayName: 'Michael Brown', email: 'michael.brown@{{API_DOMAIN}}' }
|
||||
);
|
||||
|
||||
console.log('\n');
|
||||
|
||||
@ -17,6 +17,8 @@ import dealerClaimRoutes from './dealerClaim.routes';
|
||||
import templateRoutes from './template.routes';
|
||||
import dealerRoutes from './dealer.routes';
|
||||
import dmsWebhookRoutes from './dmsWebhook.routes';
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -38,7 +40,7 @@ router.use('/user/preferences', userPreferenceRoutes); // User preferences (auth
|
||||
router.use('/documents', documentRoutes);
|
||||
router.use('/tat', tatRoutes);
|
||||
router.use('/admin', adminRoutes);
|
||||
router.use('/debug', debugRoutes);
|
||||
router.use('/debug', authenticateToken, requireAdmin, debugRoutes);
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
router.use('/notifications', notificationRoutes);
|
||||
router.use('/conclusions', conclusionRoutes);
|
||||
|
||||
@ -323,7 +323,7 @@ async function autoSetup(): Promise<void> {
|
||||
console.log(' 1. Server will start automatically');
|
||||
console.log(' 2. Log in via SSO');
|
||||
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) {
|
||||
|
||||
@ -77,7 +77,7 @@ const dealersData: DealerSeedData[] = [
|
||||
onBoardingCharges: null,
|
||||
date: '2014-09-30',
|
||||
singleFormatMonthYear: 'Sep-2014',
|
||||
domainId: 'acceleratemotors.rrnagar@dealer.royalenfield.com',
|
||||
domainId: 'acceleratemotors.rrnagar@dealer.{{API_DOMAIN}}',
|
||||
replacement: null,
|
||||
terminationResignationStatus: null,
|
||||
dateOfTerminationResignation: null,
|
||||
|
||||
@ -21,7 +21,7 @@ interface DealerData {
|
||||
|
||||
const dealers: DealerData[] = [
|
||||
{
|
||||
email: 'test.2@royalenfield.com',
|
||||
email: 'test.2@{{API_DOMAIN}}',
|
||||
dealerCode: 'RE-MH-001',
|
||||
dealerName: 'Royal Motors Mumbai',
|
||||
displayName: 'Royal Motors Mumbai',
|
||||
@ -31,7 +31,7 @@ const dealers: DealerData[] = [
|
||||
role: 'USER',
|
||||
},
|
||||
{
|
||||
email: 'test.4@royalenfield.com',
|
||||
email: 'test.4@{{API_DOMAIN}}',
|
||||
dealerCode: 'RE-DL-002',
|
||||
dealerName: 'Delhi enfield center',
|
||||
displayName: 'Delhi Enfield Center',
|
||||
@ -55,13 +55,13 @@ async function seedDealers(): Promise<void> {
|
||||
if (existingUser) {
|
||||
// User already exists (likely from Okta SSO login)
|
||||
const isOktaUser = existingUser.oktaSub && !existingUser.oktaSub.startsWith('dealer-');
|
||||
|
||||
|
||||
if (isOktaUser) {
|
||||
logger.info(`[Seed Dealers] User ${dealer.email} already exists as Okta user (oktaSub: ${existingUser.oktaSub}), updating dealer-specific fields only...`);
|
||||
} else {
|
||||
logger.info(`[Seed Dealers] User ${dealer.email} already exists, updating dealer information...`);
|
||||
}
|
||||
|
||||
|
||||
// Update existing user with dealer information
|
||||
// IMPORTANT: Preserve Okta data (oktaSub, role from Okta, etc.) and only update dealer-specific fields
|
||||
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] ⚠️ 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.`);
|
||||
|
||||
|
||||
// Generate a UUID for userId
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const userId = uuidv4();
|
||||
|
||||
@ -23,14 +23,14 @@ export class ApprovalService {
|
||||
// Get workflow to determine priority for working hours calculation
|
||||
const wf = await WorkflowRequest.findByPk(level.requestId);
|
||||
if (!wf) return null;
|
||||
|
||||
|
||||
// Verify this is NOT a claim management workflow (should use DealerClaimApprovalService)
|
||||
const workflowType = (wf as any)?.workflowType;
|
||||
if (workflowType === 'CLAIM_MANAGEMENT') {
|
||||
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.');
|
||||
}
|
||||
|
||||
|
||||
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
|
||||
const isPaused = (wf as any).isPaused || (level as any).isPaused;
|
||||
|
||||
@ -47,16 +47,16 @@ export class ApprovalService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
|
||||
// Calculate elapsed hours using working hours logic (with pause handling)
|
||||
// Case 1: Level is currently paused (isPaused = true)
|
||||
// Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist)
|
||||
const isPausedLevel = (level as any).isPaused;
|
||||
const wasResumed = !isPausedLevel &&
|
||||
(level as any).pauseElapsedHours !== null &&
|
||||
const wasResumed = !isPausedLevel &&
|
||||
(level as any).pauseElapsedHours !== null &&
|
||||
(level as any).pauseElapsedHours !== undefined &&
|
||||
(level as any).pauseResumeDate !== null;
|
||||
|
||||
|
||||
const pauseInfo = isPausedLevel ? {
|
||||
// Level is currently paused - return frozen elapsed hours at pause time
|
||||
isPaused: true,
|
||||
@ -70,10 +70,10 @@ export class ApprovalService {
|
||||
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
||||
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
||||
} : undefined;
|
||||
|
||||
|
||||
const elapsedHours = await calculateElapsedWorkingHours(
|
||||
level.levelStartTime || level.createdAt,
|
||||
now,
|
||||
level.levelStartTime || level.createdAt,
|
||||
now,
|
||||
priority,
|
||||
pauseInfo
|
||||
);
|
||||
@ -130,12 +130,12 @@ export class ApprovalService {
|
||||
const totalLevels = allLevels.length;
|
||||
const isAllLevelsApproved = approvedLevelsCount === totalLevels;
|
||||
const isFinalApproval = level.isFinalApprover || isAllLevelsApproved;
|
||||
|
||||
|
||||
if (isFinalApproval) {
|
||||
// Final approver - close workflow as APPROVED
|
||||
await WorkflowRequest.update(
|
||||
{
|
||||
status: WorkflowStatus.APPROVED,
|
||||
{
|
||||
status: WorkflowStatus.APPROVED,
|
||||
closureDate: now,
|
||||
currentLevel: (level.levelNumber || 0) + 1
|
||||
},
|
||||
@ -147,7 +147,7 @@ export class ApprovalService {
|
||||
status: 'APPROVED',
|
||||
detectedBy: level.isFinalApprover ? 'isFinalApprover flag' : 'all levels approved check'
|
||||
});
|
||||
|
||||
|
||||
// Log final approval activity first (so it's included in AI context)
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
@ -159,7 +159,7 @@ export class ApprovalService {
|
||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
|
||||
|
||||
// Generate AI conclusion remark ASYNCHRONOUSLY (don't wait)
|
||||
// This runs in the background without blocking the approval response
|
||||
(async () => {
|
||||
@ -171,17 +171,17 @@ export class ApprovalService {
|
||||
const { Document } = await import('@models/Document');
|
||||
const { Activity } = await import('@models/Activity');
|
||||
const { getConfigValue } = await import('./configReader.service');
|
||||
|
||||
|
||||
// Check if AI features and remark generation are enabled in admin config
|
||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
|
||||
|
||||
if (aiEnabled && remarkGenerationEnabled && aiService.isAvailable()) {
|
||||
logAIEvent('request', {
|
||||
requestId: level.requestId,
|
||||
action: 'conclusion_generation_started',
|
||||
});
|
||||
|
||||
|
||||
// Gather context for AI generation
|
||||
const approvalLevels = await ApprovalLevel.findAll({
|
||||
where: { requestId: level.requestId },
|
||||
@ -212,8 +212,8 @@ export class ApprovalService {
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
priority: (wf as any).priority,
|
||||
approvalFlow: approvalLevels.map((l: any) => {
|
||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||
? Number(l.tatPercentageUsed)
|
||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||
? Number(l.tatPercentageUsed)
|
||||
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
||||
return {
|
||||
levelNumber: l.levelNumber,
|
||||
@ -245,12 +245,12 @@ export class ApprovalService {
|
||||
};
|
||||
|
||||
const aiResult = await aiService.generateConclusionRemark(context);
|
||||
|
||||
|
||||
// Check if conclusion already exists (e.g., from previous final approval before additional approver was added)
|
||||
const existingConclusion = await ConclusionRemark.findOne({
|
||||
where: { requestId: level.requestId }
|
||||
const existingConclusion = await ConclusionRemark.findOne({
|
||||
where: { requestId: level.requestId }
|
||||
});
|
||||
|
||||
|
||||
if (existingConclusion) {
|
||||
// Update existing conclusion with new AI-generated remark (regenerated with updated context)
|
||||
await existingConclusion.update({
|
||||
@ -266,7 +266,7 @@ export class ApprovalService {
|
||||
approvalSummary: {
|
||||
totalLevels: approvalLevels.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)
|
||||
},
|
||||
documentSummary: {
|
||||
@ -293,7 +293,7 @@ export class ApprovalService {
|
||||
approvalSummary: {
|
||||
totalLevels: approvalLevels.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)
|
||||
},
|
||||
documentSummary: {
|
||||
@ -305,12 +305,12 @@ export class ApprovalService {
|
||||
finalizedAt: null
|
||||
} as any);
|
||||
}
|
||||
|
||||
|
||||
logAIEvent('response', {
|
||||
requestId: level.requestId,
|
||||
action: 'conclusion_generation_completed',
|
||||
});
|
||||
|
||||
|
||||
// Log activity
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
@ -332,16 +332,16 @@ export class ApprovalService {
|
||||
logger.warn(`[Approval] AI service unavailable for ${level.requestId}, skipping conclusion generation`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Auto-generate RequestSummary after final approval (system-level generation)
|
||||
// This makes the summary immediately available when user views the approved request
|
||||
try {
|
||||
const { summaryService } = await import('./summary.service');
|
||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||
isSystemGeneration: true
|
||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||
isSystemGeneration: true
|
||||
});
|
||||
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId}`);
|
||||
|
||||
|
||||
// Log summary generation activity
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
@ -357,7 +357,7 @@ export class ApprovalService {
|
||||
// Log but don't fail - initiator can regenerate later
|
||||
logger.error(`[Approval] Failed to auto-generate summary for ${level.requestId}:`, summaryError.message);
|
||||
}
|
||||
|
||||
|
||||
} catch (aiError) {
|
||||
logAIEvent('error', {
|
||||
requestId: level.requestId,
|
||||
@ -365,12 +365,12 @@ export class ApprovalService {
|
||||
error: aiError,
|
||||
});
|
||||
// Silent failure - initiator can write manually
|
||||
|
||||
|
||||
// Still try to generate summary even if AI conclusion failed
|
||||
try {
|
||||
const { summaryService } = await import('./summary.service');
|
||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||
isSystemGeneration: true
|
||||
const summary = await summaryService.createSummary(level.requestId, 'system', {
|
||||
isSystemGeneration: true
|
||||
});
|
||||
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId} (without AI conclusion)`);
|
||||
} catch (summaryError: any) {
|
||||
@ -381,19 +381,19 @@ export class ApprovalService {
|
||||
// Catch any unhandled promise rejections
|
||||
logger.error(`[Approval] Unhandled error in background AI generation:`, err);
|
||||
});
|
||||
|
||||
|
||||
// Notify initiator and all participants (including spectators) about approval
|
||||
// Spectators are CC'd for transparency, similar to email CC
|
||||
if (wf) {
|
||||
const participants = await Participant.findAll({
|
||||
where: { requestId: level.requestId }
|
||||
const participants = await Participant.findAll({
|
||||
where: { requestId: level.requestId }
|
||||
});
|
||||
const targetUserIds = new Set<string>();
|
||||
targetUserIds.add((wf as any).initiatorId);
|
||||
for (const p of participants as any[]) {
|
||||
targetUserIds.add(p.userId); // Includes spectators
|
||||
}
|
||||
|
||||
|
||||
// Send notification to initiator about final approval (triggers email)
|
||||
const initiatorId = (wf as any).initiatorId;
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
@ -406,7 +406,7 @@ export class ApprovalService {
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
|
||||
|
||||
// Send notification to all participants/spectators (for transparency, no action required)
|
||||
const participantUserIds = Array.from(targetUserIds).filter(id => id !== initiatorId);
|
||||
if (participantUserIds.length > 0) {
|
||||
@ -421,7 +421,7 @@ export class ApprovalService {
|
||||
actionRequired: false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger.info(`[Approval] ✅ Final approval complete for ${level.requestId}. Initiator and ${participants.length} participant(s) notified.`);
|
||||
}
|
||||
} else {
|
||||
@ -431,30 +431,30 @@ export class ApprovalService {
|
||||
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.');
|
||||
}
|
||||
|
||||
|
||||
// Find the next PENDING level
|
||||
// Custom workflows use strict sequential ordering (levelNumber + 1) to maintain intended order
|
||||
// This ensures custom workflows work predictably and don't skip levels
|
||||
const currentLevelNumber = level.levelNumber || 0;
|
||||
logger.info(`[Approval] Finding next level after level ${currentLevelNumber} for request ${level.requestId} (Custom workflow)`);
|
||||
|
||||
|
||||
// Use strict sequential approach for custom workflows
|
||||
const nextLevel = await ApprovalLevel.findOne({
|
||||
where: {
|
||||
where: {
|
||||
requestId: level.requestId,
|
||||
levelNumber: currentLevelNumber + 1
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!nextLevel) {
|
||||
logger.info(`[Approval] Sequential level ${currentLevelNumber + 1} not found for custom workflow - this may be the final approval`);
|
||||
} else if (nextLevel.status !== ApprovalStatus.PENDING) {
|
||||
// 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.`);
|
||||
}
|
||||
|
||||
|
||||
const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
|
||||
|
||||
|
||||
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}`);
|
||||
} else {
|
||||
@ -467,19 +467,19 @@ export class ApprovalService {
|
||||
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.');
|
||||
}
|
||||
|
||||
|
||||
// Activate next level
|
||||
await nextLevel.update({
|
||||
status: ApprovalStatus.IN_PROGRESS,
|
||||
levelStartTime: now,
|
||||
tatStartTime: now
|
||||
});
|
||||
|
||||
|
||||
// Schedule TAT jobs for the next level
|
||||
try {
|
||||
// Get workflow priority for TAT calculation
|
||||
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
||||
|
||||
|
||||
await tatSchedulerService.scheduleTatJobs(
|
||||
level.requestId,
|
||||
(nextLevel as any).levelId,
|
||||
@ -493,7 +493,7 @@ export class ApprovalService {
|
||||
logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError);
|
||||
// Don't fail the approval if TAT scheduling fails
|
||||
}
|
||||
|
||||
|
||||
// Update workflow current level (only if nextLevelNumber is not null)
|
||||
if (nextLevelNumber !== null) {
|
||||
await WorkflowRequest.update(
|
||||
@ -504,10 +504,10 @@ export class ApprovalService {
|
||||
} else {
|
||||
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
|
||||
// This service is for custom workflows only
|
||||
|
||||
|
||||
// Log approval activity
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
@ -519,7 +519,7 @@ export class ApprovalService {
|
||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
|
||||
|
||||
// Notify initiator about the approval (triggers email for regular workflows)
|
||||
if (wf) {
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
@ -532,29 +532,29 @@ export class ApprovalService {
|
||||
priority: 'MEDIUM'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Notify next approver
|
||||
if (wf && nextLevel) {
|
||||
// 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
|
||||
// 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).approverId === 'system';
|
||||
|
||||
|
||||
// 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
|
||||
if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') {
|
||||
// Additional checks: ensure approverEmail and approverName are not system-related
|
||||
// This prevents notifications to system accounts even if they pass other checks
|
||||
const approverEmail = (nextLevel as any).approverEmail || '';
|
||||
const approverName = (nextLevel as any).approverName || '';
|
||||
const isSystemEmail = approverEmail.toLowerCase() === 'system@royalenfield.com'
|
||||
const isSystemEmail = approverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|
||||
|| approverEmail.toLowerCase().includes('system');
|
||||
const isSystemName = approverName.toLowerCase() === 'system auto-process'
|
||||
|| approverName.toLowerCase().includes('system');
|
||||
|
||||
|
||||
// EXCLUDE all system-related steps from notifications
|
||||
// Only send notifications to real users, NOT system processes
|
||||
if (!isSystemEmail && !isSystemName) {
|
||||
@ -562,10 +562,10 @@ export class ApprovalService {
|
||||
// This will send both in-app and email notifications
|
||||
const nextApproverId = (nextLevel as any).approverId;
|
||||
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}`);
|
||||
|
||||
await notificationService.sendToUsers([ nextApproverId ], {
|
||||
|
||||
await notificationService.sendToUsers([nextApproverId], {
|
||||
title: `Action required: ${(wf as any).requestNumber}`,
|
||||
body: `${(wf as any).title}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
@ -595,7 +595,7 @@ export class ApprovalService {
|
||||
} else {
|
||||
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
|
||||
}
|
||||
|
||||
|
||||
// Note: Dealer-specific notifications (proposal/completion submissions) are handled by DealerClaimApprovalService
|
||||
}
|
||||
} else {
|
||||
@ -603,15 +603,15 @@ export class ApprovalService {
|
||||
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)
|
||||
await WorkflowRequest.update(
|
||||
{
|
||||
status: WorkflowStatus.APPROVED,
|
||||
{
|
||||
status: WorkflowStatus.APPROVED,
|
||||
closureDate: now,
|
||||
currentLevel: level.levelNumber || 0
|
||||
},
|
||||
{ where: { requestId: level.requestId } }
|
||||
);
|
||||
if (wf) {
|
||||
await notificationService.sendToUsers([ (wf as any).initiatorId ], {
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
title: `Approved: ${(wf as any).requestNumber}`,
|
||||
body: `${(wf as any).title}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
@ -633,7 +633,7 @@ export class ApprovalService {
|
||||
} else if (action.action === 'REJECT') {
|
||||
// Rejection - mark workflow as REJECTED (closure will happen when initiator finalizes conclusion)
|
||||
await WorkflowRequest.update(
|
||||
{
|
||||
{
|
||||
status: WorkflowStatus.REJECTED
|
||||
// Note: closureDate will be set when initiator finalizes the conclusion
|
||||
},
|
||||
@ -642,16 +642,16 @@ export class ApprovalService {
|
||||
|
||||
// Mark all pending levels as skipped
|
||||
await ApprovalLevel.update(
|
||||
{
|
||||
{
|
||||
status: ApprovalStatus.SKIPPED,
|
||||
levelEndTime: now
|
||||
},
|
||||
{
|
||||
where: {
|
||||
{
|
||||
where: {
|
||||
requestId: level.requestId,
|
||||
status: ApprovalStatus.PENDING,
|
||||
levelNumber: { [Op.gt]: level.levelNumber }
|
||||
}
|
||||
levelNumber: { [Op.gt]: level.levelNumber }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -660,7 +660,7 @@ export class ApprovalService {
|
||||
status: 'REJECTED',
|
||||
message: 'Awaiting closure from initiator',
|
||||
});
|
||||
|
||||
|
||||
// Log rejection activity first (so it's included in AI context)
|
||||
if (wf) {
|
||||
activityService.log({
|
||||
@ -674,7 +674,7 @@ export class ApprovalService {
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Notify initiator and all participants
|
||||
if (wf) {
|
||||
const participants = await Participant.findAll({ where: { requestId: level.requestId } });
|
||||
@ -683,7 +683,7 @@ export class ApprovalService {
|
||||
for (const p of participants as any[]) {
|
||||
targetUserIds.add(p.userId);
|
||||
}
|
||||
|
||||
|
||||
// Send notification to initiator with type 'rejection' to trigger email
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
title: `Rejected: ${(wf as any).requestNumber}`,
|
||||
@ -697,7 +697,7 @@ export class ApprovalService {
|
||||
rejectionReason: action.rejectionReason || action.comments || 'No reason provided'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 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);
|
||||
if (participantUserIds.length > 0) {
|
||||
@ -712,7 +712,7 @@ export class ApprovalService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved)
|
||||
// This runs in the background without blocking the rejection response
|
||||
(async () => {
|
||||
@ -724,46 +724,46 @@ export class ApprovalService {
|
||||
const { Document } = await import('@models/Document');
|
||||
const { Activity } = await import('@models/Activity');
|
||||
const { getConfigValue } = await import('./configReader.service');
|
||||
|
||||
|
||||
// Check if AI features and remark generation are enabled in admin config
|
||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
|
||||
|
||||
if (!aiEnabled || !remarkGenerationEnabled) {
|
||||
logger.info(`[Approval] AI conclusion generation skipped for rejected request ${level.requestId} (AI disabled)`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if AI service is available
|
||||
const { aiService: aiSvc } = await import('./ai.service');
|
||||
if (!aiSvc.isAvailable()) {
|
||||
logger.warn(`[Approval] AI service unavailable for rejected request ${level.requestId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Gather context for AI generation (similar to approved flow)
|
||||
const approvalLevels = await ApprovalLevel.findAll({
|
||||
where: { requestId: level.requestId },
|
||||
order: [['levelNumber', 'ASC']]
|
||||
});
|
||||
|
||||
|
||||
const workNotes = await WorkNote.findAll({
|
||||
where: { requestId: level.requestId },
|
||||
order: [['createdAt', 'ASC']],
|
||||
limit: 20
|
||||
});
|
||||
|
||||
|
||||
const documents = await Document.findAll({
|
||||
where: { requestId: level.requestId },
|
||||
order: [['uploadedAt', 'DESC']]
|
||||
});
|
||||
|
||||
|
||||
const activities = await Activity.findAll({
|
||||
where: { requestId: level.requestId },
|
||||
order: [['createdAt', 'ASC']],
|
||||
limit: 50
|
||||
});
|
||||
|
||||
|
||||
// Build context object (include rejection reason)
|
||||
const context = {
|
||||
requestTitle: (wf as any).title,
|
||||
@ -773,8 +773,8 @@ export class ApprovalService {
|
||||
rejectionReason: action.rejectionReason || action.comments || 'No reason provided',
|
||||
rejectedBy: level.approverName || level.approverEmail,
|
||||
approvalFlow: approvalLevels.map((l: any) => {
|
||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||
? Number(l.tatPercentageUsed)
|
||||
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
|
||||
? Number(l.tatPercentageUsed)
|
||||
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
|
||||
return {
|
||||
levelNumber: l.levelNumber,
|
||||
@ -804,15 +804,15 @@ export class ApprovalService {
|
||||
timestamp: activity.createdAt
|
||||
}))
|
||||
};
|
||||
|
||||
|
||||
logger.info(`[Approval] Generating AI conclusion for rejected request ${level.requestId}...`);
|
||||
|
||||
|
||||
// Generate AI conclusion (will adapt to rejection context)
|
||||
const aiResult = await aiSvc.generateConclusionRemark(context);
|
||||
|
||||
|
||||
// Create or update conclusion remark
|
||||
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId: level.requestId } });
|
||||
|
||||
|
||||
const conclusionData = {
|
||||
aiGeneratedRemark: aiResult.remark,
|
||||
aiModelUsed: aiResult.provider,
|
||||
@ -830,7 +830,7 @@ export class ApprovalService {
|
||||
keyDiscussionPoints: aiResult.keyPoints,
|
||||
generatedAt: new Date()
|
||||
};
|
||||
|
||||
|
||||
if (conclusionInstance) {
|
||||
await conclusionInstance.update(conclusionData as any);
|
||||
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`);
|
||||
|
||||
|
||||
// Emit real-time update to all users viewing this request
|
||||
emitToRequestRoom(level.requestId, 'request:updated', {
|
||||
requestId: level.requestId,
|
||||
@ -863,7 +863,7 @@ export class ApprovalService {
|
||||
levelNumber: level.levelNumber,
|
||||
timestamp: now.toISOString()
|
||||
});
|
||||
|
||||
|
||||
return updatedLevel;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error);
|
||||
|
||||
@ -330,7 +330,7 @@ export class DealerClaimService {
|
||||
let stepDef = null;
|
||||
|
||||
// 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) {
|
||||
// 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)
|
||||
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@royalenfield.com') {
|
||||
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@{{API_DOMAIN}}') {
|
||||
let dealerUser = await User.findOne({
|
||||
where: { email: dealerEmail.toLowerCase() },
|
||||
});
|
||||
@ -626,7 +626,7 @@ export class DealerClaimService {
|
||||
|
||||
// 3. Add all approvers from approval levels (excluding system and duplicates)
|
||||
const addedUserIds = new Set<string>([initiatorId]);
|
||||
const systemEmails = ['system@royalenfield.com'];
|
||||
const systemEmails = ['system@{{API_DOMAIN}}'];
|
||||
|
||||
for (const level of approvalLevels) {
|
||||
const approverEmail = (level as any).approverEmail?.toLowerCase();
|
||||
|
||||
@ -399,11 +399,11 @@ export class DealerClaimApprovalService {
|
||||
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
|
||||
|
||||
// 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'
|
||||
|| nextApproverId === 'system';
|
||||
|
||||
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@royalenfield.com'
|
||||
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|
||||
|| nextApproverEmail.toLowerCase().includes('system');
|
||||
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|
||||
|| nextApproverName.toLowerCase().includes('system');
|
||||
|
||||
@ -19,7 +19,7 @@ interface EmailOptions {
|
||||
|
||||
// Hardcoded BCC addresses (temporary - for time being)
|
||||
const HARDCODED_BCC: string[] = [
|
||||
'rohitm_ext@royalenfield.com',
|
||||
'{{USER_EMAIL}}',
|
||||
// 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 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
|
||||
let bccRecipients: string[] = [];
|
||||
@ -166,17 +166,6 @@ export class EmailService {
|
||||
if (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(`📧 Preview URL: ${previewUrl}`);
|
||||
} else {
|
||||
|
||||
@ -270,13 +270,7 @@ class GoogleSecretManagerService {
|
||||
loadedSecrets[envVarName] = secretValue;
|
||||
loadedCount++;
|
||||
|
||||
// Print masked value for verification as requested by user
|
||||
// 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})`);
|
||||
logger.info(`[Secret Manager] ✅ Loaded: ${secretNameToFetch} -> process.env.${envVarName}`);
|
||||
} else {
|
||||
// Track which secrets weren't found for better logging
|
||||
notFoundSecrets.push(fullSecretName);
|
||||
|
||||
@ -250,7 +250,7 @@ class NotificationService {
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
console.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
|
||||
*/
|
||||
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)
|
||||
const { emailNotificationService } = await import('./emailNotification.service');
|
||||
@ -311,13 +310,10 @@ class NotificationService {
|
||||
|
||||
const emailType = emailTypeMap[payload.type || ''];
|
||||
|
||||
console.log(`[DEBUG Email] Email type mapped: ${emailType}`);
|
||||
|
||||
if (!emailType) {
|
||||
// This notification type doesn't warrant email
|
||||
// Note: 'document_added' emails are handled separately via emailNotificationService
|
||||
if (payload.type !== 'document_added') {
|
||||
console.log(`[DEBUG Email] No email for notification type: ${payload.type}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -333,10 +329,7 @@ class NotificationService {
|
||||
? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery
|
||||
: await shouldSendEmail(userId, emailType); // Regular emails
|
||||
|
||||
console.log(`[DEBUG Email] Should send email: ${shouldSend} for type: ${payload.type}, userId: ${userId}`);
|
||||
|
||||
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)`);
|
||||
return;
|
||||
}
|
||||
@ -345,11 +338,9 @@ class NotificationService {
|
||||
|
||||
// Trigger email based on notification type
|
||||
// Email service will fetch additional data as needed
|
||||
console.log(`[DEBUG Email] Triggering email for type: ${payload.type}`);
|
||||
try {
|
||||
await this.triggerEmailByType(payload.type || '', userId, payload, user);
|
||||
} catch (error) {
|
||||
console.error(`[DEBUG Email] Error triggering email:`, error);
|
||||
logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error);
|
||||
}
|
||||
}
|
||||
@ -578,7 +569,7 @@ class NotificationService {
|
||||
approverData = {
|
||||
userId: (rejectedLevel as any).approverId,
|
||||
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,
|
||||
comments: (rejectedLevel as any).comments
|
||||
};
|
||||
@ -621,7 +612,7 @@ class NotificationService {
|
||||
approverData = {
|
||||
userId: (currentLevel as any).approverId,
|
||||
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 = {
|
||||
userId: (currentLevel as any).approverId,
|
||||
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 = {
|
||||
userId: (pausedLevel as any).approverId,
|
||||
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 = {
|
||||
userId: (currentLevel as any).approverId,
|
||||
displayName: (currentLevel as any).approverName || 'Unknown User',
|
||||
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com'
|
||||
email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -71,7 +71,7 @@ export class PdfService {
|
||||
private getInvoiceHtmlTemplate(data: any): string {
|
||||
const { request, invoice, dealer, claimDetails } = data;
|
||||
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
|
||||
const logoUrl = 'https://www.royalenfield.com/content/dam/royal-enfield/india/logos/logo.png';
|
||||
const logoUrl = '{{LOGO_URL}}';
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
@ -119,7 +119,7 @@ export class PdfService {
|
||||
<div class="info-grid">
|
||||
<div class="info-section">
|
||||
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
|
||||
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">33AAACE3882D1ZZ</div></div>
|
||||
<div class="info-row"><div class="info-label">Customer 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>
|
||||
<br/>
|
||||
<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",
|
||||
is_irn: "Y",
|
||||
is_ewb: "N",
|
||||
email: (request as any).initiator?.email || "system@royalenfield.com",
|
||||
email: (request as any).initiator?.email || "system@{{API_DOMAIN}}",
|
||||
TranDtls: {
|
||||
TaxSch: "GST",
|
||||
SubType: "SUPPLY",
|
||||
@ -155,7 +155,7 @@ export class PWCIntegrationService {
|
||||
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
|
||||
},
|
||||
BuyerDtls: {
|
||||
Gstin: "33AAACE3882D1ZZ", // Royal Enfield GST
|
||||
Gstin: "{{BUYER_GSTIN}}", // Royal Enfield GST
|
||||
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
|
||||
TrdNm: "ROYAL ENFIELD",
|
||||
Addr1: "No. 2, Thiruvottiyur High Road",
|
||||
@ -201,7 +201,6 @@ export class PWCIntegrationService {
|
||||
'token': this.token
|
||||
}
|
||||
});
|
||||
console.log('PWC Response:', JSON.stringify(response.data));
|
||||
|
||||
// Parse PWC Response based on provided structure
|
||||
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]
|
||||
|
||||
@ -34,7 +34,6 @@ export class SAPIntegrationService {
|
||||
* Check if SAP integration is configured
|
||||
*/
|
||||
private isConfigured(): boolean {
|
||||
// Check if SAP bypass is explicitly enabled
|
||||
if (process.env.SAP_BYPASS === 'true') {
|
||||
logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable');
|
||||
return false;
|
||||
@ -188,12 +187,8 @@ export class SAPIntegrationService {
|
||||
}) : undefined
|
||||
});
|
||||
|
||||
// Add request interceptor for debugging
|
||||
client.interceptors.request.use(
|
||||
(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;
|
||||
},
|
||||
(error) => {
|
||||
@ -219,7 +214,7 @@ export class SAPIntegrationService {
|
||||
} else if (error.code === 'ENOTFOUND') {
|
||||
logger.error('[SAP] Host not found - check SAP_BASE_URL');
|
||||
} 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);
|
||||
@ -597,9 +592,10 @@ export class SAPIntegrationService {
|
||||
logger.debug(`[SAP] POST request config prepared (auth included)`);
|
||||
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 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)
|
||||
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 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) {
|
||||
// Parse SAP response
|
||||
|
||||
@ -133,7 +133,6 @@ if (process.env.LOKI_HOST) {
|
||||
}
|
||||
|
||||
transports.push(new LokiTransport(lokiTransportOptions));
|
||||
console.log(`[Logger] ✅ Loki transport enabled: ${process.env.LOKI_HOST}`);
|
||||
} catch (error) {
|
||||
console.warn('[Logger] ⚠️ Failed to initialize Loki transport:', (error as Error).message);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user