sanitized code removed url and mails

This commit is contained in:
laxmanhalaki 2026-02-12 20:57:41 +05:30
parent 81afd7ec96
commit b32a3505ac
54 changed files with 1149 additions and 549 deletions

View File

@ -1326,9 +1326,9 @@ GCP_KEY_FILE=./config/gcp-key.json
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_USER=notifications@royalenfield.com SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_PASSWORD=your_smtp_password SMTP_PASSWORD=your_smtp_password
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com> EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
# AI Service (for conclusion generation) # AI Service (for conclusion generation)
AI_API_KEY=your_ai_api_key AI_API_KEY=your_ai_api_key

View File

@ -155,13 +155,13 @@ export async function calculateBusinessDays(
2. ✅ Imported `calculateElapsedWorkingHours`, `addWorkingHours`, `addWorkingHoursExpress` from `@utils/tatTimeUtils` 2. ✅ Imported `calculateElapsedWorkingHours`, `addWorkingHours`, `addWorkingHoursExpress` from `@utils/tatTimeUtils`
3. ✅ Replaced lines 64-65 with proper working hours calculation (now lines 66-77) 3. ✅ Replaced lines 64-65 with proper working hours calculation (now lines 66-77)
4. ✅ Gets priority from workflow 4. ✅ Gets priority from workflow
5. **TODO:** Test TAT breach alerts 5. Done: Test TAT breach alerts
### Step 2: Add Business Days Function ✅ **DONE** ### Step 2: Add Business Days Function ✅ **DONE**
1. ✅ Opened `Re_Backend/src/utils/tatTimeUtils.ts` 1. ✅ Opened `Re_Backend/src/utils/tatTimeUtils.ts`
2. ✅ Added `calculateBusinessDays()` function (lines 697-758) 2. ✅ Added `calculateBusinessDays()` function (lines 697-758)
3. ✅ Exported the function 3. ✅ Exported the function
4. **TODO:** Test with various date ranges 4. Done: Test with various date ranges
### Step 3: Update Workflow Aging Report ✅ **DONE** ### Step 3: Update Workflow Aging Report ✅ **DONE**
1. ✅ Built report endpoint using `calculateBusinessDays()` 1. ✅ Built report endpoint using `calculateBusinessDays()`

View File

@ -19,10 +19,10 @@ This command will output something like:
``` ```
======================================= =======================================
Public Key: Public Key:
BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6 {{VAPID_PUBLIC_KEY}}
Private Key: Private Key:
aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz {{VAPID_PRIVATE_KEY}}
======================================= =======================================
``` ```
@ -59,9 +59,9 @@ Add the generated keys to your backend `.env` file:
```env ```env
# Notification Service Worker credentials (Web Push / VAPID) # Notification Service Worker credentials (Web Push / VAPID)
VAPID_PUBLIC_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6 VAPID_PUBLIC_KEY={{VAPID_PUBLIC_KEY}}
VAPID_PRIVATE_KEY=aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCdEfGhIjKlMnOpQrStUvWxYz VAPID_PRIVATE_KEY={{VAPID_PRIVATE_KEY}}
VAPID_CONTACT=mailto:admin@royalenfield.com VAPID_CONTACT=mailto:{{ADMIN_EMAIL}}
``` ```
**Important Notes:** **Important Notes:**
@ -75,7 +75,7 @@ Add the **SAME** `VAPID_PUBLIC_KEY` to your frontend `.env` file:
```env ```env
# Push Notifications (Web Push / VAPID) # Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=BEl62iUYgUivxIkvpY5kXK3t3b9i5X8YzA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6 VITE_PUBLIC_VAPID_KEY={{VAPID_PUBLIC_KEY}}
``` ```
**Important:** **Important:**

View File

@ -98,7 +98,7 @@ npm run dev
1. Server will start automatically 1. Server will start automatically
2. Log in via SSO 2. Log in via SSO
3. Run this SQL to make yourself admin: 3. Run this SQL to make yourself admin:
UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com'; UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';
[Config Seed] ✅ Default configurations seeded successfully (30 settings) [Config Seed] ✅ Default configurations seeded successfully (30 settings)
info: ✅ Server started successfully on port 5000 info: ✅ Server started successfully on port 5000
@ -112,7 +112,7 @@ psql -d royal_enfield_workflow
UPDATE users UPDATE users
SET role = 'ADMIN' SET role = 'ADMIN'
WHERE email = 'your-email@royalenfield.com'; WHERE email = 'your-email@{{API_DOMAIN}}';
\q \q
``` ```

View File

@ -471,7 +471,7 @@ The backend supports web push notifications via VAPID (Voluntary Application Ser
``` ```
VAPID_PUBLIC_KEY=<your-public-key> VAPID_PUBLIC_KEY=<your-public-key>
VAPID_PRIVATE_KEY=<your-private-key> VAPID_PRIVATE_KEY=<your-private-key>
VAPID_CONTACT=mailto:admin@royalenfield.com VAPID_CONTACT=mailto:admin@{{API_DOMAIN}}
``` ```
3. **Add to Frontend `.env`:** 3. **Add to Frontend `.env`:**

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,2 @@
import{a as s}from"./index-7F7W4LDI.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion}; import{a as s}from"./index-y_ojbF9T.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DfwWW08H.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
//# sourceMappingURL=conclusionApi-BJO_6JLT.js.map //# sourceMappingURL=conclusionApi-DoX_H3Tk.js.map

View File

@ -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

View File

@ -13,7 +13,7 @@
<!-- Preload critical fonts and icons --> <!-- Preload critical fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-7F7W4LDI.js"></script> <script type="module" crossorigin src="/assets/index-y_ojbF9T.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js"> <link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">

View File

@ -34,7 +34,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
- **Approver Type**: System (Auto-processed) - **Approver Type**: System (Auto-processed)
- **Action Type**: **AUTO** (System automatically creates activity) - **Action Type**: **AUTO** (System automatically creates activity)
- **TAT**: 1 hour - **TAT**: 1 hour
- **Mapping**: System user (`system@royalenfield.com`) - **Mapping**: System user (`system@{{API_DOMAIN}}`)
- **Status**: Auto-approved when triggered - **Status**: Auto-approved when triggered
### Step 5: Dealer Completion Documents ### Step 5: Dealer Completion Documents
@ -55,7 +55,7 @@ The Claim Management workflow has **8 fixed steps** with specific approvers and
- **Approver Type**: System (Auto-processed via DMS) - **Approver Type**: System (Auto-processed via DMS)
- **Action Type**: **AUTO** (System generates e-invoice via DMS integration) - **Action Type**: **AUTO** (System generates e-invoice via DMS integration)
- **TAT**: 1 hour - **TAT**: 1 hour
- **Mapping**: System user (`system@royalenfield.com`) - **Mapping**: System user (`system@{{API_DOMAIN}}`)
- **Status**: Auto-approved when triggered - **Status**: Auto-approved when triggered
### Step 8: Credit Note Confirmation ### Step 8: Credit Note Confirmation
@ -121,7 +121,7 @@ const dealerUser = await User.findOne({ where: { email: dealerEmail } });
1. Find user with department containing "Finance" and role = 'MANAGEMENT' 1. Find user with department containing "Finance" and role = 'MANAGEMENT'
2. Find user with designation containing "Finance" or "Accountant" 2. Find user with designation containing "Finance" or "Accountant"
3. Use configured finance team email from admin_configurations table 3. Use configured finance team email from admin_configurations table
4. Fallback: Use default finance email (e.g., finance@royalenfield.com) 4. Fallback: Use default finance email (e.g., finance@{{API_DOMAIN}})
``` ```
## Next Steps ## Next Steps

View File

@ -112,7 +112,7 @@ Your CSV file must have these **44 columns** in the following order:
| `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) | | `on_boarding_charges` | Decimal | No | Numeric value (e.g., 1000.50) |
| `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) | | `date` | Date | No | Format: YYYY-MM-DD (e.g., 2014-09-30) |
| `single_format_month_year` | String(50) | No | Format: Sep-2014 | | `single_format_month_year` | String(50) | No | Format: Sep-2014 |
| `domain_id` | String(255) | No | Email domain (e.g., dealer@royalenfield.com) | | `domain_id` | String(255) | No | Email domain (e.g., dealer@{{API_DOMAIN}}) |
| `replacement` | String(50) | No | Replacement status | | `replacement` | String(50) | No | Replacement status |
| `termination_resignation_status` | String(255) | No | Termination/Resignation status | | `termination_resignation_status` | String(255) | No | Termination/Resignation status |
| `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD | | `date_of_termination_resignation` | Date | No | Format: YYYY-MM-DD |
@ -183,7 +183,7 @@ Ensure dates are in `YYYY-MM-DD` format:
```csv ```csv
sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode sales_code,service_code,gear_code,gma_code,region,dealership,state,district,city,location,city_category_pst,layout_format,tier_city_category,on_boarding_charges,date,single_format_month_year,domain_id,replacement,termination_resignation_status,date_of_termination_resignation,last_date_of_operations,old_codes,branch_details,dealer_principal_name,dealer_principal_email_id,dp_contact_number,dp_contacts,showroom_address,showroom_pincode,workshop_address,workshop_pincode,location_district,state_workshop,no_of_studios,website_update,gst,pan,firm_type,prop_managing_partners_directors,total_prop_partners_directors,docs_folder_link,workshop_gma_codes,existing_new,dlrcode
5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.royalenfield.com,,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386 5124,5125,5573,9430,S3,Accelerate Motors,Karnataka,Bengaluru,Bengaluru,RAJA RAJESHWARI NAGAR,A+,A+,Tier 1 City,,2014-09-30,Sep-2014,acceleratemotors.rrnagar@dealer.{{API_DOMAIN}},,,,,,,N. Shyam Charmanna,shyamcharmanna@yahoo.co.in,7022049621,7022049621,"No.335, HVP RR Nagar Sector B, Ideal Homes Town Ship, Bangalore - 560098, Dist Bangalore, Karnataka",560098,"Works Shop No.460, 80ft Road, 2nd Phase R R Nagar, Bangalore - 560098, Dist Bangalore, Karnataka",560098,Bangalore,Karnataka,0,Yes,29ARCPS1311D1Z6,ARCPS1311D,Proprietorship,CHARMANNA SHYAM NELLAMAKADA,CHARMANNA SHYAM NELLAMAKADA,https://drive.google.com/drive/folders/1sGtg3s1h9aBXX9fhxJufYuBWar8gVvnb,,,3386
``` ```
**What gets auto-generated:** **What gets auto-generated:**

View File

@ -56,7 +56,7 @@ users {
```json ```json
{ {
"userId": "uuid-1", "userId": "uuid-1",
"email": "john.doe@royalenfield.com", "email": "john.doe@{{API_DOMAIN}}",
"employeeId": "E12345", // Regular employee ID "employeeId": "E12345", // Regular employee ID
"designation": "Software Engineer", "designation": "Software Engineer",
"department": "IT", "department": "IT",
@ -68,7 +68,7 @@ users {
```json ```json
{ {
"userId": "uuid-2", "userId": "uuid-2",
"email": "test.2@royalenfield.com", "email": "test.2@{{API_DOMAIN}}",
"employeeId": "RE-MH-001", // Dealer code stored here "employeeId": "RE-MH-001", // Dealer code stored here
"designation": "Dealer", "designation": "Dealer",
"department": "Dealer Operations", "department": "Dealer Operations",

View File

@ -98,8 +98,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
**Base URL Examples:** **Base URL Examples:**
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice` - Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice` - UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice` - Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice`
### 3.2 Request Headers ### 3.2 Request Headers
@ -205,8 +205,8 @@ User-Agent: DMS-Webhook-Client/1.0
**Base URL Examples:** **Base URL Examples:**
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note` - Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note` - UAT: `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note` - Production: `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note`
### 4.2 Request Headers ### 4.2 Request Headers
@ -563,8 +563,8 @@ DMS_WEBHOOK_SECRET=your_shared_secret_key_here
| Environment | Invoice Webhook URL | Credit Note Webhook URL | | Environment | Invoice Webhook URL | Credit Note Webhook URL |
|-------------|---------------------|-------------------------| |-------------|---------------------|-------------------------|
| Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` | | Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` |
| UAT | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note` | | UAT | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow-uat.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
| Production | `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note` | | Production | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/invoice` | `https://reflow.{{API_DOMAIN}}/api/v1/webhooks/dms/credit-note` |
--- ---

View File

@ -157,7 +157,7 @@ npm run seed:config
```bash ```bash
# Edit the script # Edit the script
nano scripts/assign-admin-user.sql nano scripts/assign-admin-user.sql
# Change: YOUR_EMAIL@royalenfield.com # Change: YOUR_EMAIL@{{API_DOMAIN}}
# Run it # Run it
psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql psql -d royal_enfield_workflow -f scripts/assign-admin-user.sql
@ -170,7 +170,7 @@ psql -d royal_enfield_workflow
UPDATE users UPDATE users
SET role = 'ADMIN' SET role = 'ADMIN'
WHERE email = 'your-email@royalenfield.com'; WHERE email = 'your-email@{{API_DOMAIN}}';
-- Verify -- Verify
SELECT email, role FROM users WHERE role = 'ADMIN'; SELECT email, role FROM users WHERE role = 'ADMIN';
@ -188,7 +188,7 @@ psql -d royal_enfield_workflow -c "\dt"
psql -d royal_enfield_workflow -c "\dT+ user_role_enum" psql -d royal_enfield_workflow -c "\dT+ user_role_enum"
# Check your user # Check your user
psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@royalenfield.com';" psql -d royal_enfield_workflow -c "SELECT email, role FROM users WHERE email = 'your-email@{{API_DOMAIN}}';"
``` ```
--- ---
@ -241,13 +241,13 @@ Expected output:
```sql ```sql
-- Single user -- Single user
UPDATE users SET role = 'MANAGEMENT' UPDATE users SET role = 'MANAGEMENT'
WHERE email = 'manager@royalenfield.com'; WHERE email = 'manager@{{API_DOMAIN}}';
-- Multiple users -- Multiple users
UPDATE users SET role = 'MANAGEMENT' UPDATE users SET role = 'MANAGEMENT'
WHERE email IN ( WHERE email IN (
'manager1@royalenfield.com', 'manager1@{{API_DOMAIN}}',
'manager2@royalenfield.com' 'manager2@{{API_DOMAIN}}'
); );
-- By department -- By department
@ -260,13 +260,13 @@ WHERE department = 'Management' AND is_active = true;
```sql ```sql
-- Single user -- Single user
UPDATE users SET role = 'ADMIN' UPDATE users SET role = 'ADMIN'
WHERE email = 'admin@royalenfield.com'; WHERE email = 'admin@{{API_DOMAIN}}';
-- Multiple admins -- Multiple admins
UPDATE users SET role = 'ADMIN' UPDATE users SET role = 'ADMIN'
WHERE email IN ( WHERE email IN (
'admin1@royalenfield.com', 'admin1@{{API_DOMAIN}}',
'admin2@royalenfield.com' 'admin2@{{API_DOMAIN}}'
); );
-- By department -- By department
@ -331,7 +331,7 @@ SELECT
mobile_phone, mobile_phone,
array_length(ad_groups, 1) as ad_group_count array_length(ad_groups, 1) as ad_group_count
FROM users FROM users
WHERE email = 'your-email@royalenfield.com'; WHERE email = 'your-email@{{API_DOMAIN}}';
``` ```
--- ---
@ -344,7 +344,7 @@ WHERE email = 'your-email@royalenfield.com';
curl -X POST http://localhost:5000/api/v1/auth/okta/callback \ curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"email": "test@royalenfield.com", "email": "test@{{API_DOMAIN}}",
"displayName": "Test User", "displayName": "Test User",
"oktaSub": "test-sub-123" "oktaSub": "test-sub-123"
}' }'
@ -353,14 +353,14 @@ curl -X POST http://localhost:5000/api/v1/auth/okta/callback \
### 2. Check User Created with Default Role ### 2. Check User Created with Default Role
```sql ```sql
SELECT email, role FROM users WHERE email = 'test@royalenfield.com'; SELECT email, role FROM users WHERE email = 'test@{{API_DOMAIN}}';
-- Expected: role = 'USER' -- Expected: role = 'USER'
``` ```
### 3. Update to ADMIN ### 3. Update to ADMIN
```sql ```sql
UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com'; UPDATE users SET role = 'ADMIN' WHERE email = 'test@{{API_DOMAIN}}';
``` ```
### 4. Verify API Access ### 4. Verify API Access
@ -369,7 +369,7 @@ UPDATE users SET role = 'ADMIN' WHERE email = 'test@royalenfield.com';
# Login and get token # Login and get token
curl -X POST http://localhost:5000/api/v1/auth/login \ curl -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"email": "test@royalenfield.com", ...}' -d '{"email": "test@{{API_DOMAIN}}", ...}'
# Try admin endpoint (should work if ADMIN role) # Try admin endpoint (should work if ADMIN role)
curl http://localhost:5000/api/v1/admin/configurations \ curl http://localhost:5000/api/v1/admin/configurations \
@ -449,7 +449,7 @@ npm run migrate
```sql ```sql
-- Check if user exists -- Check if user exists
SELECT * FROM users WHERE email = 'your-email@royalenfield.com'; SELECT * FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
-- Check Okta sub -- Check Okta sub
SELECT * FROM users WHERE okta_sub = 'your-okta-sub'; SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
@ -459,7 +459,7 @@ SELECT * FROM users WHERE okta_sub = 'your-okta-sub';
```sql ```sql
-- Verify role -- Verify role
SELECT email, role, is_active FROM users WHERE email = 'your-email@royalenfield.com'; SELECT email, role, is_active FROM users WHERE email = 'your-email@{{API_DOMAIN}}';
-- Check role enum -- Check role enum
\dT+ user_role_enum \dT+ user_role_enum

View File

@ -29,7 +29,7 @@ This guide provides step-by-step instructions for setting up Google Cloud Storag
|------|------------------| |------|------------------|
| **Application** | Royal Enfield Workflow System | | **Application** | Royal Enfield Workflow System |
| **Environment** | Production | | **Environment** | Production |
| **Domain** | `https://reflow.royalenfield.com` | | **Domain** | `https://reflow.{{API_DOMAIN}}` |
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes | | **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
| **Storage Type** | Google Cloud Storage (GCS) | | **Storage Type** | Google Cloud Storage (GCS) |
| **Region** | `asia-south1` (Mumbai) | | **Region** | `asia-south1` (Mumbai) |
@ -325,8 +325,8 @@ Create `cors-config-prod.json`:
[ [
{ {
"origin": [ "origin": [
"https://reflow.royalenfield.com", "https://reflow.{{API_DOMAIN}}",
"https://www.royalenfield.com" "https://www.{{API_DOMAIN}}"
], ],
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"], "method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
"responseHeader": [ "responseHeader": [

View File

@ -6,7 +6,7 @@
|------|-------| |------|-------|
| **Application** | RE Workflow System | | **Application** | RE Workflow System |
| **Environment** | UAT | | **Environment** | UAT |
| **Domain** | https://reflow-uat.royalenfield.com | | **Domain** | https://reflow-uat.{{API_DOMAIN}} |
| **Purpose** | Store workflow documents and attachments | | **Purpose** | Store workflow documents and attachments |
--- ---
@ -131,8 +131,8 @@ Apply this CORS policy to allow browser uploads:
[ [
{ {
"origin": [ "origin": [
"https://reflow-uat.royalenfield.com", "https://reflow-uat.{{API_DOMAIN}}",
"https://reflow.royalenfield.com" "https://reflow.{{API_DOMAIN}}"
], ],
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"], "method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
"responseHeader": [ "responseHeader": [

View File

@ -72,8 +72,8 @@ The Users API returns a complete user object:
"employeeID": "E09994", "employeeID": "E09994",
"title": "Supports Business Applications (SAP) portfolio", "title": "Supports Business Applications (SAP) portfolio",
"department": "Deputy Manager - Digital & IT", "department": "Deputy Manager - Digital & IT",
"login": "sanjaysahu@Royalenfield.com", "login": "sanjaysahu@{{API_DOMAIN}}",
"email": "sanjaysahu@royalenfield.com" "email": "sanjaysahu@{{API_DOMAIN}}"
}, },
... ...
} }
@ -127,7 +127,7 @@ Example log:
### Test with curl ### Test with curl
```bash ```bash
curl --location 'https://dev-830839.oktapreview.com/api/v1/users/testuser10@eichergroup.com' \ curl --location 'https://{{IDP_DOMAIN}}/api/v1/users/testuser10@eichergroup.com' \
--header 'Authorization: SSWS YOUR_OKTA_API_TOKEN' \ --header 'Authorization: SSWS YOUR_OKTA_API_TOKEN' \
--header 'Accept: application/json' --header 'Accept: application/json'
``` ```

View File

@ -450,16 +450,16 @@ Before Migration:
+-------------------------+-----------+ +-------------------------+-----------+
| email | is_admin | | email | is_admin |
+-------------------------+-----------+ +-------------------------+-----------+
| admin@royalenfield.com | true | | admin@{{API_DOMAIN}} | true |
| user1@royalenfield.com | false | | user1@{{API_DOMAIN}} | false |
+-------------------------+-----------+ +-------------------------+-----------+
After Migration: After Migration:
+-------------------------+-----------+-----------+ +-------------------------+-----------+-----------+
| email | role | is_admin | | email | role | is_admin |
+-------------------------+-----------+-----------+ +-------------------------+-----------+-----------+
| admin@royalenfield.com | ADMIN | true | | admin@{{API_DOMAIN}} | ADMIN | true |
| user1@royalenfield.com | USER | false | | user1@{{API_DOMAIN}} | USER | false |
+-------------------------+-----------+-----------+ +-------------------------+-----------+-----------+
``` ```
@ -473,17 +473,17 @@ After Migration:
-- Make user a MANAGEMENT role -- Make user a MANAGEMENT role
UPDATE users UPDATE users
SET role = 'MANAGEMENT', is_admin = false SET role = 'MANAGEMENT', is_admin = false
WHERE email = 'manager@royalenfield.com'; WHERE email = 'manager@{{API_DOMAIN}}';
-- Make user an ADMIN role -- Make user an ADMIN role
UPDATE users UPDATE users
SET role = 'ADMIN', is_admin = true SET role = 'ADMIN', is_admin = true
WHERE email = 'admin@royalenfield.com'; WHERE email = 'admin@{{API_DOMAIN}}';
-- Revert to USER role -- Revert to USER role
UPDATE users UPDATE users
SET role = 'USER', is_admin = false SET role = 'USER', is_admin = false
WHERE email = 'user@royalenfield.com'; WHERE email = 'user@{{API_DOMAIN}}';
``` ```
### Via API (Admin Endpoint) ### Via API (Admin Endpoint)

View File

@ -47,12 +47,12 @@ psql -d royal_enfield_db -f scripts/assign-user-roles.sql
-- Make specific users ADMIN -- Make specific users ADMIN
UPDATE users UPDATE users
SET role = 'ADMIN', is_admin = true SET role = 'ADMIN', is_admin = true
WHERE email IN ('admin@royalenfield.com', 'it.admin@royalenfield.com'); WHERE email IN ('admin@{{API_DOMAIN}}', 'it.admin@{{API_DOMAIN}}');
-- Make specific users MANAGEMENT -- Make specific users MANAGEMENT
UPDATE users UPDATE users
SET role = 'MANAGEMENT', is_admin = false SET role = 'MANAGEMENT', is_admin = false
WHERE email IN ('manager@royalenfield.com', 'auditor@royalenfield.com'); WHERE email IN ('manager@{{API_DOMAIN}}', 'auditor@{{API_DOMAIN}}');
-- Verify roles -- Verify roles
SELECT email, display_name, role, is_admin FROM users ORDER BY role, email; SELECT email, display_name, role, is_admin FROM users ORDER BY role, email;
@ -219,7 +219,7 @@ GROUP BY role;
-- Check specific user -- Check specific user
SELECT email, role, is_admin SELECT email, role, is_admin
FROM users FROM users
WHERE email = 'your-email@royalenfield.com'; WHERE email = 'your-email@{{API_DOMAIN}}';
``` ```
### Test 2: Test API Access ### Test 2: Test API Access
@ -356,7 +356,7 @@ WHERE designation ILIKE '%manager%' OR designation ILIKE '%head%';
```sql ```sql
SELECT email, role, is_admin SELECT email, role, is_admin
FROM users FROM users
WHERE email = 'your-email@royalenfield.com'; WHERE email = 'your-email@{{API_DOMAIN}}';
``` ```
--- ---

View File

@ -314,7 +314,7 @@ JWT_EXPIRY=24h
REFRESH_TOKEN_EXPIRY=7d REFRESH_TOKEN_EXPIRY=7d
# Okta Configuration # Okta Configuration
OKTA_DOMAIN=https://dev-830839.oktapreview.com OKTA_DOMAIN=https://{{IDP_DOMAIN}}
OKTA_CLIENT_ID=your-client-id OKTA_CLIENT_ID=your-client-id
OKTA_CLIENT_SECRET=your-client-secret OKTA_CLIENT_SECRET=your-client-secret
@ -334,7 +334,7 @@ GCP_BUCKET_PUBLIC=true
**Identity Provider**: Okta **Identity Provider**: Okta
- **Domain**: Configurable via `OKTA_DOMAIN` environment variable - **Domain**: Configurable via `OKTA_DOMAIN` environment variable
- **Default**: `https://dev-830839.oktapreview.com` - **Default**: `https://{{IDP_DOMAIN}}`
- **Protocol**: OAuth 2.0 / OpenID Connect (OIDC) - **Protocol**: OAuth 2.0 / OpenID Connect (OIDC)
- **Grant Types**: Authorization Code, Resource Owner Password Credentials - **Grant Types**: Authorization Code, Resource Owner Password Credentials
@ -650,7 +650,7 @@ graph LR
{ {
"userId": "uuid", "userId": "uuid",
"employeeId": "EMP001", "employeeId": "EMP001",
"email": "user@royalenfield.com", "email": "user@{{API_DOMAIN}}",
"role": "USER" | "MANAGEMENT" | "ADMIN", "role": "USER" | "MANAGEMENT" | "ADMIN",
"iat": 1234567890, "iat": 1234567890,
"exp": 1234654290 "exp": 1234654290
@ -1048,7 +1048,7 @@ JWT_EXPIRY=24h
REFRESH_TOKEN_EXPIRY=7d REFRESH_TOKEN_EXPIRY=7d
# Okta # Okta
OKTA_DOMAIN=https://dev-830839.oktapreview.com OKTA_DOMAIN=https://{{IDP_DOMAIN}}
OKTA_CLIENT_ID=your-client-id OKTA_CLIENT_ID=your-client-id
OKTA_CLIENT_SECRET=your-client-secret OKTA_CLIENT_SECRET=your-client-secret
@ -1063,7 +1063,7 @@ GCP_BUCKET_PUBLIC=true
**Frontend (.env):** **Frontend (.env):**
```env ```env
VITE_API_BASE_URL=https://api.rebridge.co.in/api/v1 VITE_API_BASE_URL=https://api.rebridge.co.in/api/v1
VITE_OKTA_DOMAIN=https://dev-830839.oktapreview.com VITE_OKTA_DOMAIN=https://{{IDP_DOMAIN}}
VITE_OKTA_CLIENT_ID=your-client-id VITE_OKTA_CLIENT_ID=your-client-id
``` ```

View File

@ -64,7 +64,7 @@ await this.createClaimApprovalLevels(
isAuto: false, isAuto: false,
approverType: 'department_lead' as const, approverType: 'department_lead' as const,
approverId: departmentLead?.userId || null, approverId: departmentLead?.userId || null,
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@royalenfield.com', approverEmail: departmentLead?.email || initiator.manager || 'deptlead@{{API_DOMAIN}}',
} }
``` ```

View File

@ -181,7 +181,7 @@ POST http://localhost:5000/api/v1/auth/login
Content-Type: application/json Content-Type: application/json
{ {
"username": "john.doe@royalenfield.com", "username": "john.doe@{{API_DOMAIN}}",
"password": "SecurePassword123!" "password": "SecurePassword123!"
} }
``` ```

View File

@ -26,8 +26,8 @@ REFRESH_TOKEN_EXPIRY=7d
SESSION_SECRET=your_session_secret_here_min_32_chars SESSION_SECRET=your_session_secret_here_min_32_chars
# Cloud Storage (GCP) # Cloud Storage (GCP)
GCP_PROJECT_ID=re-workflow-project GCP_PROJECT_ID={{GCP_PROJECT_ID}}
GCP_BUCKET_NAME=re-workflow-documents GCP_BUCKET_NAME={{GCP_BUCKET_NAME}}
GCP_KEY_FILE=./config/gcp-key.json GCP_KEY_FILE=./config/gcp-key.json
# Google Secret Manager (Optional - for production) # Google Secret Manager (Optional - for production)
@ -41,9 +41,9 @@ USE_GOOGLE_SECRET_MANAGER=false
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_USER=notifications@royalenfield.com SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_PASSWORD=your_smtp_password SMTP_PASSWORD=your_smtp_password
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com> EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
# AI Service (for conclusion generation) - Vertex AI Gemini # AI Service (for conclusion generation) - Vertex AI Gemini
# Uses service account credentials from GCP_KEY_FILE # Uses service account credentials from GCP_KEY_FILE
@ -55,7 +55,7 @@ VERTEX_AI_LOCATION=asia-south1
# Logging # Logging
LOG_LEVEL=info LOG_LEVEL=info
LOG_FILE_PATH=./logs LOG_FILE_PATH=./logs
APP_VERSION=1.2.0 APP_VERSION={{APP_VERSION}}
# ============ Loki Configuration (Grafana Log Aggregation) ============ # ============ Loki Configuration (Grafana Log Aggregation) ============
LOKI_HOST= # e.g., http://loki:3100 or http://monitoring.cloudtopiaa.com:3100 LOKI_HOST= # e.g., http://loki:3100 or http://monitoring.cloudtopiaa.com:3100
@ -66,7 +66,7 @@ LOKI_PASSWORD= # Optional: Basic auth password
CORS_ORIGIN="*" CORS_ORIGIN="*"
# Rate Limiting # Rate Limiting
RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_MAX_REQUESTS=100
# File Upload # File Upload
@ -83,16 +83,16 @@ OKTA_CLIENT_ID={{okta_client_id}}
OKTA_CLIENT_SECRET={{okta_client_secret}} OKTA_CLIENT_SECRET={{okta_client_secret}}
# Notificaton Service Worker credentials # Notificaton Service Worker credentials
VAPID_PUBLIC_KEY={{vapid_public_key}} note: same key need to add on front end for web push VAPID_PUBLIC_KEY={{VAPID_PUBLIC_KEY}}
VAPID_PRIVATE_KEY={{vapid_private_key}} VAPID_PRIVATE_KEY={{vapid_private_key}}
VAPID_CONTACT=mailto:you@example.com VAPID_CONTACT=mailto:you@example.com
#Redis #Redis
REDIS_URL={{REDIS_URL_FOR DELAY JoBS create redis setup and add url here}} REDIS_URL={{REDIS_URL}}
TAT_TEST_MODE=false (on true it will consider 1 hour==1min) TAT_TEST_MODE=false # Set to true to accelerate TAT for testing
# SAP Integration (OData Service via Zscaler) # SAP Integration (OData Service via Zscaler)
SAP_BASE_URL=https://RENOIHND01.Eichergroup.com:1443 SAP_BASE_URL=https://{{SAP_DOMAIN_HERE}}:{{PORT}}
SAP_USERNAME={{SAP_USERNAME}} SAP_USERNAME={{SAP_USERNAME}}
SAP_PASSWORD={{SAP_PASSWORD}} SAP_PASSWORD={{SAP_PASSWORD}}
SAP_TIMEOUT_MS=30000 SAP_TIMEOUT_MS=30000

View File

@ -16,7 +16,7 @@
UPDATE users UPDATE users
SET role = 'ADMIN' SET role = 'ADMIN'
WHERE email = 'YOUR_EMAIL@royalenfield.com' -- ← CHANGE THIS WHERE email = 'YOUR_EMAIL@{{API_DOMAIN}}' -- ← CHANGE THIS
RETURNING RETURNING
user_id, user_id,
email, email,

View File

@ -21,9 +21,9 @@
UPDATE users UPDATE users
SET role = 'ADMIN' SET role = 'ADMIN'
WHERE email IN ( WHERE email IN (
'admin@royalenfield.com', 'admin@{{API_DOMAIN}}',
'it.admin@royalenfield.com', 'it.admin@{{API_DOMAIN}}',
'system.admin@royalenfield.com' 'system.admin@{{API_DOMAIN}}'
-- Add more admin emails here -- Add more admin emails here
); );
@ -45,9 +45,9 @@ ORDER BY email;
UPDATE users UPDATE users
SET role = 'MANAGEMENT' SET role = 'MANAGEMENT'
WHERE email IN ( WHERE email IN (
'manager1@royalenfield.com', 'manager1@{{API_DOMAIN}}',
'dept.head@royalenfield.com', 'dept.head@{{API_DOMAIN}}',
'auditor@royalenfield.com' 'auditor@{{API_DOMAIN}}'
-- Add more management emails here -- Add more management emails here
); );

View File

@ -162,7 +162,7 @@ SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_USER=${SMTP_USER} SMTP_USER=${SMTP_USER}
SMTP_PASSWORD=${SMTP_PASSWORD} SMTP_PASSWORD=${SMTP_PASSWORD}
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com> EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
# Vertex AI Gemini Configuration (for conclusion generation) # Vertex AI Gemini Configuration (for conclusion generation)
# Service account credentials should be placed in ./credentials/ folder # Service account credentials should be placed in ./credentials/ folder
@ -232,7 +232,7 @@ show_vapid_instructions() {
echo " VITE_PUBLIC_VAPID_KEY=<your-public-key>" echo " VITE_PUBLIC_VAPID_KEY=<your-public-key>"
echo "" echo ""
echo "5. The VAPID_CONTACT should be a valid mailto: URL" echo "5. The VAPID_CONTACT should be a valid mailto: URL"
echo " Example: mailto:admin@royalenfield.com" echo " Example: mailto:admin@{{API_DOMAIN}}"
echo "" echo ""
echo "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!" echo "Note: Keep your VAPID_PRIVATE_KEY secure and never commit it to version control!"
echo "" echo ""

View File

@ -7,6 +7,8 @@ import { UserService } from './services/user.service';
import { SSOUserData } from './types/auth.types'; import { SSOUserData } from './types/auth.types';
import { sequelize } from './config/database'; import { sequelize } from './config/database';
import { corsMiddleware } from './middlewares/cors.middleware'; import { corsMiddleware } from './middlewares/cors.middleware';
import { authenticateToken } from './middlewares/auth.middleware';
import { requireAdmin } from './middlewares/authorization.middleware';
import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware'; import { metricsMiddleware, createMetricsRouter } from './middlewares/metrics.middleware';
import routes from './routes/index'; import routes from './routes/index';
import { ensureUploadDir, UPLOAD_DIR } from './config/storage'; import { ensureUploadDir, UPLOAD_DIR } from './config/storage';
@ -51,7 +53,7 @@ app.use((req: express.Request, res: express.Response, next: express.NextFunction
"script-src 'self'", "script-src 'self'",
"script-src-elem 'self'", "script-src-elem 'self'",
"script-src-attr 'none'", "script-src-attr 'none'",
"img-src 'self' data: blob: https://*.royalenfield.com https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com", "img-src 'self' data: blob: https://*.{{API_DOMAIN}} https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com",
"frame-src 'self' blob: data:", "frame-src 'self' blob: data:",
"font-src 'self' https://fonts.gstatic.com data:", "font-src 'self' https://fonts.gstatic.com data:",
"object-src 'none'", "object-src 'none'",
@ -117,7 +119,7 @@ app.use(morgan('combined'));
app.use(metricsMiddleware); app.use(metricsMiddleware);
// Prometheus metrics endpoint - expose metrics for scraping // Prometheus metrics endpoint - expose metrics for scraping
app.use(createMetricsRouter()); app.use('/metrics', authenticateToken, requireAdmin, createMetricsRouter());
// Health check endpoint (before API routes) // Health check endpoint (before API routes)
app.get('/health', (_req: express.Request, res: express.Response) => { app.get('/health', (_req: express.Request, res: express.Response) => {
@ -134,7 +136,7 @@ app.use('/api/v1', routes);
// Serve uploaded files statically // Serve uploaded files statically
ensureUploadDir(); ensureUploadDir();
app.use('/uploads', express.static(UPLOAD_DIR)); app.use('/uploads', authenticateToken, express.static(UPLOAD_DIR));
// Legacy SSO Callback endpoint for user creation/update (kept for backward compatibility) // Legacy SSO Callback endpoint for user creation/update (kept for backward compatibility)
app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => { app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => {
@ -188,7 +190,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.
}); });
// Get all users endpoint // Get all users endpoint
app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise<void> => { app.get('/api/v1/users', authenticateToken, requireAdmin, async (_req: express.Request, res: express.Response): Promise<void> => {
try { try {
const users = await userService.getAllUsers(); const users = await userService.getAllUsers();
@ -293,4 +295,4 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
}); });
} }
export default app; export default app;

View File

@ -8,9 +8,9 @@ export const emailConfig = {
pass: process.env.SMTP_PASSWORD || '', pass: process.env.SMTP_PASSWORD || '',
}, },
}, },
from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@royalenfield.com>', from: process.env.EMAIL_FROM || 'RE Workflow System <notifications@{{API_DOMAIN}}>',
// Email templates // Email templates
templates: { templates: {
workflowCreated: 'workflow-created', workflowCreated: 'workflow-created',
@ -20,7 +20,7 @@ export const emailConfig = {
tatReminder: 'tat-reminder', tatReminder: 'tat-reminder',
tatBreached: 'tat-breached', tatBreached: 'tat-breached',
}, },
// Email settings // Email settings
settings: { settings: {
retryAttempts: 3, retryAttempts: 3,

View File

@ -8,18 +8,18 @@ const ssoConfig: SSOConfig = {
get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; }, get refreshTokenExpiry() { return process.env.REFRESH_TOKEN_EXPIRY || '7d'; },
get sessionSecret() { return process.env.SESSION_SECRET || ''; }, get sessionSecret() { return process.env.SESSION_SECRET || ''; },
// Use only FRONTEND_URL from environment - no fallbacks // Use only FRONTEND_URL from environment - no fallbacks
get allowedOrigins() { get allowedOrigins() {
return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || []; return process.env.FRONTEND_URL?.split(',').map(s => s.trim()).filter(Boolean) || [];
}, },
// Okta/Auth0 configuration for token exchange // Okta/Auth0 configuration for token exchange
get oktaDomain() { return process.env.OKTA_DOMAIN || 'https://dev-830839.oktapreview.com'; }, get oktaDomain() { return process.env.OKTA_DOMAIN || '{{IDP_DOMAIN}}'; },
get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; }, get oktaClientId() { return process.env.OKTA_CLIENT_ID || ''; },
get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; }, get oktaClientSecret() { return process.env.OKTA_CLIENT_SECRET || ''; },
get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API get oktaApiToken() { return process.env.OKTA_API_TOKEN || ''; }, // SSWS token for Users API
// Tanflow configuration for token exchange // Tanflow configuration for token exchange
get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || 'https://ssodev.rebridge.co.in/realms/RE'; }, get tanflowBaseUrl() { return process.env.TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE'; },
get tanflowClientId() { return process.env.TANFLOW_CLIENT_ID || 'REFLOW'; }, get tanflowClientId() { return process.env.TANFLOW_CLIENT_ID || 'REFLOW'; },
get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || 'cfIzMlwAMF1m4QWAP5StzZbV47HIrCox'; }, get tanflowClientSecret() { return process.env.TANFLOW_CLIENT_SECRET || '{{TANFLOW_CLIENT_SECRET}}'; },
}; };
export { ssoConfig }; export { ssoConfig };

View File

@ -172,7 +172,7 @@ This document outlines all email templates required for the Dealer Claim Managem
- Initiator (for record) - Initiator (for record)
- Finance team - Finance team
- **Template**: `creditNoteSent.template.ts` (NEW) - **Template**: `creditNoteSent.template.ts` (NEW)
- **Status**: ❌ Not Implemented (TODO comment at line 2037-2044) - **Status**: Required implementation
- **Notification Type**: `credit_note_sent` - **Notification Type**: `credit_note_sent`
- **Data Needed**: - **Data Needed**:
- Credit note number - Credit note number
@ -184,7 +184,7 @@ This document outlines all email templates required for the Dealer Claim Managem
- Reason for credit note - Reason for credit note
- Download link (if available) - Download link (if available)
- **Notes**: - **Notes**:
- Currently has TODO comment for email implementation - Planned for email implementation
- Critical for dealer notification - Critical for dealer notification
--- ---
@ -246,7 +246,7 @@ This document outlines all email templates required for the Dealer Claim Managem
5. **Credit Note Sent** (`creditNoteSent.template.ts`) 5. **Credit Note Sent** (`creditNoteSent.template.ts`)
- Priority: High - Priority: High
- When: Credit note is sent to dealer (Step 8) - When: Credit note is sent to dealer (Step 8)
- Currently has TODO comment - Planned for implementation
--- ---
@ -255,7 +255,7 @@ This document outlines all email templates required for the Dealer Claim Managem
### High Priority (Critical for Workflow) ### High Priority (Critical for Workflow)
1. **Activity Created** - Currently using generic notification, should be branded 1. **Activity Created** - Currently using generic notification, should be branded
2. **E-Invoice Generated** - Important for financial tracking 2. **E-Invoice Generated** - Important for financial tracking
3. **Credit Note Sent** - Critical for dealer notification (currently TODO) 3. **Credit Note Sent** - Critical for dealer notification
### Medium Priority (Nice to Have) ### Medium Priority (Nice to Have)
4. **Proposal Submitted** - Better UX, but existing approval request works 4. **Proposal Submitted** - Better UX, but existing approval request works

View File

@ -991,9 +991,9 @@ Add to `.env`:
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_SECURE=false SMTP_SECURE=false
SMTP_USER=notifications@royalenfield.com SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_PASSWORD=your-app-specific-password SMTP_PASSWORD=your-app-specific-password
EMAIL_FROM=RE Flow <noreply@royalenfield.com> EMAIL_FROM=RE Flow <noreply@{{API_DOMAIN}}>
# Email Settings # Email Settings
EMAIL_ENABLED=true EMAIL_ENABLED=true
@ -1002,10 +1002,10 @@ EMAIL_BATCH_SIZE=50
EMAIL_RETRY_ATTEMPTS=3 EMAIL_RETRY_ATTEMPTS=3
# Application # Application
BASE_URL=https://workflow.royalenfield.com BASE_URL=https://workflow.{{API_DOMAIN}}
COMPANY_NAME=Royal Enfield COMPANY_NAME=Royal Enfield
COMPANY_WEBSITE=https://www.royalenfield.com COMPANY_WEBSITE=https://www.{{API_DOMAIN}}
SUPPORT_EMAIL=support@royalenfield.com SUPPORT_EMAIL=support@{{API_DOMAIN}}
``` ```
--- ---

View File

@ -65,7 +65,7 @@ Each template uses color-coded gradients to indicate the scenario:
All templates feature a single action button: All templates feature a single action button:
- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now" - **Text:** "View Request Details" / "Review Request Now" / "Take Action Now"
- **Link Format:** `{baseURL}/request/{requestNumber}` - **Link Format:** `{baseURL}/request/{requestNumber}`
- **Example:** `https://workflow.royalenfield.com/request/REQ-2025-12-0013` - **Example:** `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
No approval/rejection buttons in emails - all actions happen within the application. No approval/rejection buttons in emails - all actions happen within the application.
@ -231,8 +231,8 @@ SMTP_USER=your-email@domain.com
SMTP_PASSWORD=your-app-password SMTP_PASSWORD=your-app-password
# Email Settings # Email Settings
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com> EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
BASE_URL=https://workflow.royalenfield.com BASE_URL=https://workflow.{{API_DOMAIN}}
COMPANY_NAME=Royal Enfield COMPANY_NAME=Royal Enfield
``` ```

View File

@ -361,7 +361,7 @@ All `[ViewDetailsLink]` placeholders should be replaced with:
{baseURL}/request/{requestNumber} {baseURL}/request/{requestNumber}
``` ```
Example: `https://workflow.royalenfield.com/request/REQ-2025-12-0013` Example: `https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013`
### Company Name ### Company Name
Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield") Replace `[CompanyName]` with your organization name (e.g., "Royal Enfield")

View File

@ -12,15 +12,15 @@ emailtemplates/
├── approvalRequest.template.ts ✅ Single approver email ├── approvalRequest.template.ts ✅ Single approver email
├── multiApproverRequest.template.ts ✅ Multi-approver email ├── multiApproverRequest.template.ts ✅ Multi-approver email
├── approvalConfirmation.template.ts 🔨 TODO ├── approvalConfirmation.template.ts ✅ DONE
├── rejectionNotification.template.ts 🔨 TODO ├── rejectionNotification.template.ts ✅ DONE
├── tatReminder.template.ts 🔨 TODO ├── tatReminder.template.ts ✅ DONE
├── tatBreached.template.ts 🔨 TODO ├── tatBreached.template.ts ✅ DONE
├── workflowPaused.template.ts 🔨 TODO ├── workflowPaused.template.ts ✅ DONE
├── workflowResumed.template.ts 🔨 TODO ├── workflowResumed.template.ts ✅ DONE
├── participantAdded.template.ts 🔨 TODO ├── participantAdded.template.ts ✅ DONE
├── approverSkipped.template.ts 🔨 TODO ├── approverSkipped.template.ts ✅ DONE
└── requestClosed.template.ts 🔨 TODO └── requestClosed.template.ts ✅ DONE
``` ```
--- ---
@ -53,7 +53,7 @@ const data: RequestCreatedData = {
requestTime: '02:30 PM', requestTime: '02:30 PM',
totalApprovers: 3, totalApprovers: 3,
expectedTAT: 48, expectedTAT: 48,
viewDetailsLink: 'https://workflow.royalenfield.com/request/REQ-2025-12-0013', viewDetailsLink: 'https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013',
companyName: 'Royal Enfield' companyName: 'Royal Enfield'
}; };
``` ```
@ -188,10 +188,10 @@ SMTP_USER=your-email@domain.com
SMTP_PASSWORD=your-app-password SMTP_PASSWORD=your-app-password
# Email Settings # Email Settings
EMAIL_FROM=Royal Enfield Workflow <notifications@royalenfield.com> EMAIL_FROM=Royal Enfield Workflow <notifications@{{API_DOMAIN}}>
# Application Settings # Application Settings
BASE_URL=https://workflow.royalenfield.com BASE_URL=https://workflow.{{API_DOMAIN}}
COMPANY_NAME=Royal Enfield COMPANY_NAME=Royal Enfield
``` ```

View File

@ -13,12 +13,12 @@ import { EmailHeaderConfig, EmailFooterConfig } from './helpers';
export const CompanyInfo = { export const CompanyInfo = {
name: 'Royal Enfield', name: 'Royal Enfield',
productName: 'RE Flow', // Product name displayed in header productName: 'RE Flow', // Product name displayed in header
website: 'https://www.royalenfield.com', website: 'https://www.{{API_DOMAIN}}',
supportEmail: 'support@royalenfield.com', supportEmail: 'support@{{API_DOMAIN}}',
// Logo configuration for email headers // Logo configuration for email headers
logo: { logo: {
url: 'https://www.royalenfield.com/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp', url: 'https://www.{{API_DOMAIN}}/content/dam/RE-Platform-Revamp/re-revamp-commons/logo.webp',
alt: 'Royal Enfield Logo', alt: 'Royal Enfield Logo',
width: 220, // Logo width in pixels (wider for better visibility) width: 220, // Logo width in pixels (wider for better visibility)
height: 65, // Logo height in pixels (proportional ratio ~3.4:1) height: 65, // Logo height in pixels (proportional ratio ~3.4:1)
@ -88,7 +88,7 @@ export const CustomHeaderStyles = {
* Usage in email service: * Usage in email service:
* const link = getViewDetailsLink('REQ-2025-12-0013', process.env.FRONTEND_URL); * const link = getViewDetailsLink('REQ-2025-12-0013', process.env.FRONTEND_URL);
* *
* Result: https://workflow.royalenfield.com/request/REQ-2025-12-0013 * Result: https://workflow.{{API_DOMAIN}}/request/REQ-2025-12-0013
*/ */
export function getViewDetailsLink(requestNumber: string, frontendUrl: string): string { export function getViewDetailsLink(requestNumber: string, frontendUrl: string): string {
return `${frontendUrl}/request/${requestNumber}`; return `${frontendUrl}/request/${requestNumber}`;

View File

@ -14,13 +14,13 @@ async function generatePreviews() {
// Sample data // Sample data
const initiator = { const initiator = {
userId: 'user-1', userId: 'user-1',
email: 'john.doe@royalenfield.com', email: 'john.doe@{{API_DOMAIN}}',
displayName: 'John Doe' displayName: 'John Doe'
}; };
const approver = { const approver = {
userId: 'user-2', userId: 'user-2',
email: 'jane.smith@royalenfield.com', email: 'jane.smith@{{API_DOMAIN}}',
displayName: 'Jane Smith' displayName: 'Jane Smith'
}; };

View File

@ -6,7 +6,7 @@
*/ */
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { import {
getRequestCreatedEmail, getRequestCreatedEmail,
getApprovalRequestEmail, getApprovalRequestEmail,
getMultiApproverRequestEmail, getMultiApproverRequestEmail,
@ -21,10 +21,10 @@ import {
async function sendTestEmail() { async function sendTestEmail() {
try { try {
console.log('🚀 Creating nodemailer test account...'); console.log('🚀 Creating nodemailer test account...');
// Create a test account automatically (free, no signup needed) // Create a test account automatically (free, no signup needed)
const testAccount = await nodemailer.createTestAccount(); const testAccount = await nodemailer.createTestAccount();
console.log('✅ Test account created:', testAccount.user); console.log('✅ Test account created:', testAccount.user);
// Create transporter with test SMTP details // Create transporter with test SMTP details
@ -100,12 +100,12 @@ async function sendTestEmail() {
// Test 1: Request Created Email // Test 1: Request Created Email
const html1 = getRequestCreatedEmail(requestCreatedData); const html1 = getRequestCreatedEmail(requestCreatedData);
const info1 = await transporter.sendMail({ const info1 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>', from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
to: 'initiator@example.com', to: 'initiator@example.com',
subject: '[REQ-2025-12-0013] Request Created Successfully', subject: '[REQ-2025-12-0013] Request Created Successfully',
html: html1 html: html1
}); });
console.log('✅ Test 1: Request Created Email'); console.log('✅ Test 1: Request Created Email');
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info1)); console.log(' Preview URL:', nodemailer.getTestMessageUrl(info1));
console.log(''); console.log('');
@ -113,12 +113,12 @@ async function sendTestEmail() {
// Test 2: Approval Request Email (Single) // Test 2: Approval Request Email (Single)
const html2 = getApprovalRequestEmail(approvalRequestData); const html2 = getApprovalRequestEmail(approvalRequestData);
const info2 = await transporter.sendMail({ const info2 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>', from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
to: 'approver@example.com', to: 'approver@example.com',
subject: '[REQ-2025-12-0013] Approval Request - Action Required', subject: '[REQ-2025-12-0013] Approval Request - Action Required',
html: html2 html: html2
}); });
console.log('✅ Test 2: Approval Request Email'); console.log('✅ Test 2: Approval Request Email');
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info2)); console.log(' Preview URL:', nodemailer.getTestMessageUrl(info2));
console.log(''); console.log('');
@ -126,12 +126,12 @@ async function sendTestEmail() {
// Test 3: Multi-Approver Request Email // Test 3: Multi-Approver Request Email
const html3 = getMultiApproverRequestEmail(multiApproverData); const html3 = getMultiApproverRequestEmail(multiApproverData);
const info3 = await transporter.sendMail({ const info3 = await transporter.sendMail({
from: '"Royal Enfield Workflow" <noreply@royalenfield.com>', from: '"Royal Enfield Workflow" <noreply@{{API_DOMAIN}}>',
to: 'approver-level2@example.com', to: 'approver-level2@example.com',
subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn', subject: '[REQ-2025-12-0013] Multi-Level Approval Request - Your Turn',
html: html3 html: html3
}); });
console.log('✅ Test 3: Multi-Approver Request Email'); console.log('✅ Test 3: Multi-Approver Request Email');
console.log(' Preview URL:', nodemailer.getTestMessageUrl(info3)); console.log(' Preview URL:', nodemailer.getTestMessageUrl(info3));
console.log(''); console.log('');

View File

@ -18,7 +18,7 @@ async function testRealScenario() {
// Mock user data (simulating real database records) // Mock user data (simulating real database records)
const user10 = { const user10 = {
userId: 'user-10-uuid', userId: 'user-10-uuid',
email: 'john.doe@royalenfield.com', email: 'john.doe@{{API_DOMAIN}}',
displayName: 'John Doe', displayName: 'John Doe',
department: 'Engineering', department: 'Engineering',
designation: 'Senior Engineer' designation: 'Senior Engineer'
@ -26,7 +26,7 @@ async function testRealScenario() {
const user12 = { const user12 = {
userId: 'user-12-uuid', userId: 'user-12-uuid',
email: 'jane.smith@royalenfield.com', email: 'jane.smith@{{API_DOMAIN}}',
displayName: 'Jane Smith', displayName: 'Jane Smith',
department: 'Management', department: 'Management',
designation: 'Engineering Manager', designation: 'Engineering Manager',
@ -52,7 +52,7 @@ async function testRealScenario() {
This purchase is critical for our Q1 2025 testing schedule and has been pre-approved by the department head. This purchase is critical for our Q1 2025 testing schedule and has been pre-approved by the department head.
</blockquote> </blockquote>
<p>Please review and approve at your earliest convenience.</p> <p>Please review and approve at your earliest convenience.</p>
<p>For questions, contact: <a href="mailto:john.doe@royalenfield.com">john.doe@royalenfield.com</a></p> <p>For questions, contact: <a href="mailto:john.doe@{{API_DOMAIN}}">john.doe@{{API_DOMAIN}}</a></p>
`, `,
requestType: 'Purchase', requestType: 'Purchase',
priority: 'HIGH', priority: 'HIGH',
@ -68,21 +68,21 @@ async function testRealScenario() {
{ {
levelNumber: 1, levelNumber: 1,
approverName: 'Jane Smith', approverName: 'Jane Smith',
approverEmail: 'jane.smith@royalenfield.com', approverEmail: 'jane.smith@{{API_DOMAIN}}',
status: 'PENDING', status: 'PENDING',
approvedAt: null approvedAt: null
}, },
{ {
levelNumber: 2, levelNumber: 2,
approverName: 'Michael Brown', approverName: 'Michael Brown',
approverEmail: 'michael.brown@royalenfield.com', approverEmail: 'michael.brown@{{API_DOMAIN}}',
status: 'PENDING', status: 'PENDING',
approvedAt: null approvedAt: null
}, },
{ {
levelNumber: 3, levelNumber: 3,
approverName: 'Sarah Johnson', approverName: 'Sarah Johnson',
approverEmail: 'sarah.johnson@royalenfield.com', approverEmail: 'sarah.johnson@{{API_DOMAIN}}',
status: 'PENDING', status: 'PENDING',
approvedAt: null approvedAt: null
} }
@ -92,7 +92,7 @@ async function testRealScenario() {
console.log('─'.repeat(80)); console.log('─'.repeat(80));
console.log('📧 Test 1: Request Created Email (to Initiator - User 10)'); console.log('📧 Test 1: Request Created Email (to Initiator - User 10)');
console.log('─'.repeat(80)); console.log('─'.repeat(80));
await emailNotificationService.sendRequestCreated( await emailNotificationService.sendRequestCreated(
requestData, requestData,
user10, user10,
@ -168,7 +168,7 @@ async function testRealScenario() {
approvedUser12, approvedUser12,
user10, user10,
false, // not final approval false, // not final approval
{ displayName: 'Michael Brown', email: 'michael.brown@royalenfield.com' } { displayName: 'Michael Brown', email: 'michael.brown@{{API_DOMAIN}}' }
); );
console.log('\n'); console.log('\n');

View File

@ -17,6 +17,8 @@ import dealerClaimRoutes from './dealerClaim.routes';
import templateRoutes from './template.routes'; import templateRoutes from './template.routes';
import dealerRoutes from './dealer.routes'; import dealerRoutes from './dealer.routes';
import dmsWebhookRoutes from './dmsWebhook.routes'; import dmsWebhookRoutes from './dmsWebhook.routes';
import { authenticateToken } from '../middlewares/auth.middleware';
import { requireAdmin } from '../middlewares/authorization.middleware';
const router = Router(); const router = Router();
@ -38,7 +40,7 @@ router.use('/user/preferences', userPreferenceRoutes); // User preferences (auth
router.use('/documents', documentRoutes); router.use('/documents', documentRoutes);
router.use('/tat', tatRoutes); router.use('/tat', tatRoutes);
router.use('/admin', adminRoutes); router.use('/admin', adminRoutes);
router.use('/debug', debugRoutes); router.use('/debug', authenticateToken, requireAdmin, debugRoutes);
router.use('/dashboard', dashboardRoutes); router.use('/dashboard', dashboardRoutes);
router.use('/notifications', notificationRoutes); router.use('/notifications', notificationRoutes);
router.use('/conclusions', conclusionRoutes); router.use('/conclusions', conclusionRoutes);

View File

@ -323,7 +323,7 @@ async function autoSetup(): Promise<void> {
console.log(' 1. Server will start automatically'); console.log(' 1. Server will start automatically');
console.log(' 2. Log in via SSO'); console.log(' 2. Log in via SSO');
console.log(' 3. Run this SQL to make yourself admin:'); console.log(' 3. Run this SQL to make yourself admin:');
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`); console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@{{API_DOMAIN}}';\n`);
} }
} catch (error: any) { } catch (error: any) {

View File

@ -77,7 +77,7 @@ const dealersData: DealerSeedData[] = [
onBoardingCharges: null, onBoardingCharges: null,
date: '2014-09-30', date: '2014-09-30',
singleFormatMonthYear: 'Sep-2014', singleFormatMonthYear: 'Sep-2014',
domainId: 'acceleratemotors.rrnagar@dealer.royalenfield.com', domainId: 'acceleratemotors.rrnagar@dealer.{{API_DOMAIN}}',
replacement: null, replacement: null,
terminationResignationStatus: null, terminationResignationStatus: null,
dateOfTerminationResignation: null, dateOfTerminationResignation: null,

View File

@ -21,7 +21,7 @@ interface DealerData {
const dealers: DealerData[] = [ const dealers: DealerData[] = [
{ {
email: 'test.2@royalenfield.com', email: 'test.2@{{API_DOMAIN}}',
dealerCode: 'RE-MH-001', dealerCode: 'RE-MH-001',
dealerName: 'Royal Motors Mumbai', dealerName: 'Royal Motors Mumbai',
displayName: 'Royal Motors Mumbai', displayName: 'Royal Motors Mumbai',
@ -31,7 +31,7 @@ const dealers: DealerData[] = [
role: 'USER', role: 'USER',
}, },
{ {
email: 'test.4@royalenfield.com', email: 'test.4@{{API_DOMAIN}}',
dealerCode: 'RE-DL-002', dealerCode: 'RE-DL-002',
dealerName: 'Delhi enfield center', dealerName: 'Delhi enfield center',
displayName: 'Delhi Enfield Center', displayName: 'Delhi Enfield Center',
@ -55,13 +55,13 @@ async function seedDealers(): Promise<void> {
if (existingUser) { if (existingUser) {
// User already exists (likely from Okta SSO login) // User already exists (likely from Okta SSO login)
const isOktaUser = existingUser.oktaSub && !existingUser.oktaSub.startsWith('dealer-'); const isOktaUser = existingUser.oktaSub && !existingUser.oktaSub.startsWith('dealer-');
if (isOktaUser) { if (isOktaUser) {
logger.info(`[Seed Dealers] User ${dealer.email} already exists as Okta user (oktaSub: ${existingUser.oktaSub}), updating dealer-specific fields only...`); logger.info(`[Seed Dealers] User ${dealer.email} already exists as Okta user (oktaSub: ${existingUser.oktaSub}), updating dealer-specific fields only...`);
} else { } else {
logger.info(`[Seed Dealers] User ${dealer.email} already exists, updating dealer information...`); logger.info(`[Seed Dealers] User ${dealer.email} already exists, updating dealer information...`);
} }
// Update existing user with dealer information // Update existing user with dealer information
// IMPORTANT: Preserve Okta data (oktaSub, role from Okta, etc.) and only update dealer-specific fields // IMPORTANT: Preserve Okta data (oktaSub, role from Okta, etc.) and only update dealer-specific fields
const nameParts = dealer.dealerName.split(' '); const nameParts = dealer.dealerName.split(' ');
@ -117,7 +117,7 @@ async function seedDealers(): Promise<void> {
logger.warn(`[Seed Dealers] User ${dealer.email} not found in database. Creating placeholder user...`); logger.warn(`[Seed Dealers] User ${dealer.email} not found in database. Creating placeholder user...`);
logger.warn(`[Seed Dealers] ⚠️ If this user is an Okta user, they should login via SSO first to be created automatically.`); logger.warn(`[Seed Dealers] ⚠️ If this user is an Okta user, they should login via SSO first to be created automatically.`);
logger.warn(`[Seed Dealers] ⚠️ The oktaSub will be updated when they login via SSO.`); logger.warn(`[Seed Dealers] ⚠️ The oktaSub will be updated when they login via SSO.`);
// Generate a UUID for userId // Generate a UUID for userId
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const userId = uuidv4(); const userId = uuidv4();

View File

@ -23,14 +23,14 @@ export class ApprovalService {
// Get workflow to determine priority for working hours calculation // Get workflow to determine priority for working hours calculation
const wf = await WorkflowRequest.findByPk(level.requestId); const wf = await WorkflowRequest.findByPk(level.requestId);
if (!wf) return null; if (!wf) return null;
// Verify this is NOT a claim management workflow (should use DealerClaimApprovalService) // Verify this is NOT a claim management workflow (should use DealerClaimApprovalService)
const workflowType = (wf as any)?.workflowType; const workflowType = (wf as any)?.workflowType;
if (workflowType === 'CLAIM_MANAGEMENT') { if (workflowType === 'CLAIM_MANAGEMENT') {
logger.error(`[Approval] Attempted to use ApprovalService for CLAIM_MANAGEMENT workflow ${level.requestId}. Use DealerClaimApprovalService instead.`); logger.error(`[Approval] Attempted to use ApprovalService for CLAIM_MANAGEMENT workflow ${level.requestId}. Use DealerClaimApprovalService instead.`);
throw new Error('ApprovalService cannot be used for CLAIM_MANAGEMENT workflows. Use DealerClaimApprovalService instead.'); throw new Error('ApprovalService cannot be used for CLAIM_MANAGEMENT workflows. Use DealerClaimApprovalService instead.');
} }
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase(); const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
const isPaused = (wf as any).isPaused || (level as any).isPaused; const isPaused = (wf as any).isPaused || (level as any).isPaused;
@ -47,16 +47,16 @@ export class ApprovalService {
} }
const now = new Date(); const now = new Date();
// Calculate elapsed hours using working hours logic (with pause handling) // Calculate elapsed hours using working hours logic (with pause handling)
// Case 1: Level is currently paused (isPaused = true) // Case 1: Level is currently paused (isPaused = true)
// Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist) // Case 2: Level was paused and resumed (isPaused = false but pauseElapsedHours and pauseResumeDate exist)
const isPausedLevel = (level as any).isPaused; const isPausedLevel = (level as any).isPaused;
const wasResumed = !isPausedLevel && const wasResumed = !isPausedLevel &&
(level as any).pauseElapsedHours !== null && (level as any).pauseElapsedHours !== null &&
(level as any).pauseElapsedHours !== undefined && (level as any).pauseElapsedHours !== undefined &&
(level as any).pauseResumeDate !== null; (level as any).pauseResumeDate !== null;
const pauseInfo = isPausedLevel ? { const pauseInfo = isPausedLevel ? {
// Level is currently paused - return frozen elapsed hours at pause time // Level is currently paused - return frozen elapsed hours at pause time
isPaused: true, isPaused: true,
@ -70,10 +70,10 @@ export class ApprovalService {
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
} : undefined; } : undefined;
const elapsedHours = await calculateElapsedWorkingHours( const elapsedHours = await calculateElapsedWorkingHours(
level.levelStartTime || level.createdAt, level.levelStartTime || level.createdAt,
now, now,
priority, priority,
pauseInfo pauseInfo
); );
@ -130,12 +130,12 @@ export class ApprovalService {
const totalLevels = allLevels.length; const totalLevels = allLevels.length;
const isAllLevelsApproved = approvedLevelsCount === totalLevels; const isAllLevelsApproved = approvedLevelsCount === totalLevels;
const isFinalApproval = level.isFinalApprover || isAllLevelsApproved; const isFinalApproval = level.isFinalApprover || isAllLevelsApproved;
if (isFinalApproval) { if (isFinalApproval) {
// Final approver - close workflow as APPROVED // Final approver - close workflow as APPROVED
await WorkflowRequest.update( await WorkflowRequest.update(
{ {
status: WorkflowStatus.APPROVED, status: WorkflowStatus.APPROVED,
closureDate: now, closureDate: now,
currentLevel: (level.levelNumber || 0) + 1 currentLevel: (level.levelNumber || 0) + 1
}, },
@ -147,7 +147,7 @@ export class ApprovalService {
status: 'APPROVED', status: 'APPROVED',
detectedBy: level.isFinalApprover ? 'isFinalApprover flag' : 'all levels approved check' detectedBy: level.isFinalApprover ? 'isFinalApprover flag' : 'all levels approved check'
}); });
// Log final approval activity first (so it's included in AI context) // Log final approval activity first (so it's included in AI context)
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
@ -159,7 +159,7 @@ export class ApprovalService {
ipAddress: requestMetadata?.ipAddress || undefined, ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined userAgent: requestMetadata?.userAgent || undefined
}); });
// Generate AI conclusion remark ASYNCHRONOUSLY (don't wait) // Generate AI conclusion remark ASYNCHRONOUSLY (don't wait)
// This runs in the background without blocking the approval response // This runs in the background without blocking the approval response
(async () => { (async () => {
@ -171,17 +171,17 @@ export class ApprovalService {
const { Document } = await import('@models/Document'); const { Document } = await import('@models/Document');
const { Activity } = await import('@models/Activity'); const { Activity } = await import('@models/Activity');
const { getConfigValue } = await import('./configReader.service'); const { getConfigValue } = await import('./configReader.service');
// Check if AI features and remark generation are enabled in admin config // Check if AI features and remark generation are enabled in admin config
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true'; const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true'; const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
if (aiEnabled && remarkGenerationEnabled && aiService.isAvailable()) { if (aiEnabled && remarkGenerationEnabled && aiService.isAvailable()) {
logAIEvent('request', { logAIEvent('request', {
requestId: level.requestId, requestId: level.requestId,
action: 'conclusion_generation_started', action: 'conclusion_generation_started',
}); });
// Gather context for AI generation // Gather context for AI generation
const approvalLevels = await ApprovalLevel.findAll({ const approvalLevels = await ApprovalLevel.findAll({
where: { requestId: level.requestId }, where: { requestId: level.requestId },
@ -212,8 +212,8 @@ export class ApprovalService {
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
priority: (wf as any).priority, priority: (wf as any).priority,
approvalFlow: approvalLevels.map((l: any) => { approvalFlow: approvalLevels.map((l: any) => {
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
? Number(l.tatPercentageUsed) ? Number(l.tatPercentageUsed)
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0); : (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
return { return {
levelNumber: l.levelNumber, levelNumber: l.levelNumber,
@ -245,12 +245,12 @@ export class ApprovalService {
}; };
const aiResult = await aiService.generateConclusionRemark(context); const aiResult = await aiService.generateConclusionRemark(context);
// Check if conclusion already exists (e.g., from previous final approval before additional approver was added) // Check if conclusion already exists (e.g., from previous final approval before additional approver was added)
const existingConclusion = await ConclusionRemark.findOne({ const existingConclusion = await ConclusionRemark.findOne({
where: { requestId: level.requestId } where: { requestId: level.requestId }
}); });
if (existingConclusion) { if (existingConclusion) {
// Update existing conclusion with new AI-generated remark (regenerated with updated context) // Update existing conclusion with new AI-generated remark (regenerated with updated context)
await existingConclusion.update({ await existingConclusion.update({
@ -266,7 +266,7 @@ export class ApprovalService {
approvalSummary: { approvalSummary: {
totalLevels: approvalLevels.length, totalLevels: approvalLevels.length,
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length, approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
averageTatUsage: approvalLevels.reduce((sum: number, l: any) => averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1) sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
}, },
documentSummary: { documentSummary: {
@ -293,7 +293,7 @@ export class ApprovalService {
approvalSummary: { approvalSummary: {
totalLevels: approvalLevels.length, totalLevels: approvalLevels.length,
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length, approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
averageTatUsage: approvalLevels.reduce((sum: number, l: any) => averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1) sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
}, },
documentSummary: { documentSummary: {
@ -305,12 +305,12 @@ export class ApprovalService {
finalizedAt: null finalizedAt: null
} as any); } as any);
} }
logAIEvent('response', { logAIEvent('response', {
requestId: level.requestId, requestId: level.requestId,
action: 'conclusion_generation_completed', action: 'conclusion_generation_completed',
}); });
// Log activity // Log activity
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
@ -332,16 +332,16 @@ export class ApprovalService {
logger.warn(`[Approval] AI service unavailable for ${level.requestId}, skipping conclusion generation`); logger.warn(`[Approval] AI service unavailable for ${level.requestId}, skipping conclusion generation`);
} }
} }
// Auto-generate RequestSummary after final approval (system-level generation) // Auto-generate RequestSummary after final approval (system-level generation)
// This makes the summary immediately available when user views the approved request // This makes the summary immediately available when user views the approved request
try { try {
const { summaryService } = await import('./summary.service'); const { summaryService } = await import('./summary.service');
const summary = await summaryService.createSummary(level.requestId, 'system', { const summary = await summaryService.createSummary(level.requestId, 'system', {
isSystemGeneration: true isSystemGeneration: true
}); });
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId}`); logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId}`);
// Log summary generation activity // Log summary generation activity
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
@ -357,7 +357,7 @@ export class ApprovalService {
// Log but don't fail - initiator can regenerate later // Log but don't fail - initiator can regenerate later
logger.error(`[Approval] Failed to auto-generate summary for ${level.requestId}:`, summaryError.message); logger.error(`[Approval] Failed to auto-generate summary for ${level.requestId}:`, summaryError.message);
} }
} catch (aiError) { } catch (aiError) {
logAIEvent('error', { logAIEvent('error', {
requestId: level.requestId, requestId: level.requestId,
@ -365,12 +365,12 @@ export class ApprovalService {
error: aiError, error: aiError,
}); });
// Silent failure - initiator can write manually // Silent failure - initiator can write manually
// Still try to generate summary even if AI conclusion failed // Still try to generate summary even if AI conclusion failed
try { try {
const { summaryService } = await import('./summary.service'); const { summaryService } = await import('./summary.service');
const summary = await summaryService.createSummary(level.requestId, 'system', { const summary = await summaryService.createSummary(level.requestId, 'system', {
isSystemGeneration: true isSystemGeneration: true
}); });
logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId} (without AI conclusion)`); logger.info(`[Approval] ✅ Auto-generated summary ${(summary as any).summaryId} for approved request ${level.requestId} (without AI conclusion)`);
} catch (summaryError: any) { } catch (summaryError: any) {
@ -381,19 +381,19 @@ export class ApprovalService {
// Catch any unhandled promise rejections // Catch any unhandled promise rejections
logger.error(`[Approval] Unhandled error in background AI generation:`, err); logger.error(`[Approval] Unhandled error in background AI generation:`, err);
}); });
// Notify initiator and all participants (including spectators) about approval // Notify initiator and all participants (including spectators) about approval
// Spectators are CC'd for transparency, similar to email CC // Spectators are CC'd for transparency, similar to email CC
if (wf) { if (wf) {
const participants = await Participant.findAll({ const participants = await Participant.findAll({
where: { requestId: level.requestId } where: { requestId: level.requestId }
}); });
const targetUserIds = new Set<string>(); const targetUserIds = new Set<string>();
targetUserIds.add((wf as any).initiatorId); targetUserIds.add((wf as any).initiatorId);
for (const p of participants as any[]) { for (const p of participants as any[]) {
targetUserIds.add(p.userId); // Includes spectators targetUserIds.add(p.userId); // Includes spectators
} }
// Send notification to initiator about final approval (triggers email) // Send notification to initiator about final approval (triggers email)
const initiatorId = (wf as any).initiatorId; const initiatorId = (wf as any).initiatorId;
await notificationService.sendToUsers([initiatorId], { await notificationService.sendToUsers([initiatorId], {
@ -406,7 +406,7 @@ export class ApprovalService {
priority: 'HIGH', priority: 'HIGH',
actionRequired: true actionRequired: true
}); });
// Send notification to all participants/spectators (for transparency, no action required) // Send notification to all participants/spectators (for transparency, no action required)
const participantUserIds = Array.from(targetUserIds).filter(id => id !== initiatorId); const participantUserIds = Array.from(targetUserIds).filter(id => id !== initiatorId);
if (participantUserIds.length > 0) { if (participantUserIds.length > 0) {
@ -421,7 +421,7 @@ export class ApprovalService {
actionRequired: false actionRequired: false
}); });
} }
logger.info(`[Approval] ✅ Final approval complete for ${level.requestId}. Initiator and ${participants.length} participant(s) notified.`); logger.info(`[Approval] ✅ Final approval complete for ${level.requestId}. Initiator and ${participants.length} participant(s) notified.`);
} }
} else { } else {
@ -431,30 +431,30 @@ export class ApprovalService {
logger.warn(`[Approval] Cannot advance workflow ${level.requestId} - workflow is paused`); logger.warn(`[Approval] Cannot advance workflow ${level.requestId} - workflow is paused`);
throw new Error('Cannot advance workflow - workflow is currently paused. Please resume the workflow first.'); throw new Error('Cannot advance workflow - workflow is currently paused. Please resume the workflow first.');
} }
// Find the next PENDING level // Find the next PENDING level
// Custom workflows use strict sequential ordering (levelNumber + 1) to maintain intended order // Custom workflows use strict sequential ordering (levelNumber + 1) to maintain intended order
// This ensures custom workflows work predictably and don't skip levels // This ensures custom workflows work predictably and don't skip levels
const currentLevelNumber = level.levelNumber || 0; const currentLevelNumber = level.levelNumber || 0;
logger.info(`[Approval] Finding next level after level ${currentLevelNumber} for request ${level.requestId} (Custom workflow)`); logger.info(`[Approval] Finding next level after level ${currentLevelNumber} for request ${level.requestId} (Custom workflow)`);
// Use strict sequential approach for custom workflows // Use strict sequential approach for custom workflows
const nextLevel = await ApprovalLevel.findOne({ const nextLevel = await ApprovalLevel.findOne({
where: { where: {
requestId: level.requestId, requestId: level.requestId,
levelNumber: currentLevelNumber + 1 levelNumber: currentLevelNumber + 1
} }
}); });
if (!nextLevel) { if (!nextLevel) {
logger.info(`[Approval] Sequential level ${currentLevelNumber + 1} not found for custom workflow - this may be the final approval`); logger.info(`[Approval] Sequential level ${currentLevelNumber + 1} not found for custom workflow - this may be the final approval`);
} else if (nextLevel.status !== ApprovalStatus.PENDING) { } else if (nextLevel.status !== ApprovalStatus.PENDING) {
// Sequential level exists but not PENDING - log warning but proceed // Sequential level exists but not PENDING - log warning but proceed
logger.warn(`[Approval] Sequential level ${currentLevelNumber + 1} exists but status is ${nextLevel.status}, expected PENDING. Proceeding with sequential level to maintain workflow order.`); logger.warn(`[Approval] Sequential level ${currentLevelNumber + 1} exists but status is ${nextLevel.status}, expected PENDING. Proceeding with sequential level to maintain workflow order.`);
} }
const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null; const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
if (nextLevel) { if (nextLevel) {
logger.info(`[Approval] Found next level: ${nextLevelNumber} (${(nextLevel as any).levelName || 'unnamed'}), approver: ${(nextLevel as any).approverName || (nextLevel as any).approverEmail || 'unknown'}, status: ${nextLevel.status}`); logger.info(`[Approval] Found next level: ${nextLevelNumber} (${(nextLevel as any).levelName || 'unnamed'}), approver: ${(nextLevel as any).approverName || (nextLevel as any).approverEmail || 'unknown'}, status: ${nextLevel.status}`);
} else { } else {
@ -467,19 +467,19 @@ export class ApprovalService {
logger.warn(`[Approval] Cannot activate next level ${nextLevelNumber} - level is paused`); logger.warn(`[Approval] Cannot activate next level ${nextLevelNumber} - level is paused`);
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.'); throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
} }
// Activate next level // Activate next level
await nextLevel.update({ await nextLevel.update({
status: ApprovalStatus.IN_PROGRESS, status: ApprovalStatus.IN_PROGRESS,
levelStartTime: now, levelStartTime: now,
tatStartTime: now tatStartTime: now
}); });
// Schedule TAT jobs for the next level // Schedule TAT jobs for the next level
try { try {
// Get workflow priority for TAT calculation // Get workflow priority for TAT calculation
const workflowPriority = (wf as any)?.priority || 'STANDARD'; const workflowPriority = (wf as any)?.priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs( await tatSchedulerService.scheduleTatJobs(
level.requestId, level.requestId,
(nextLevel as any).levelId, (nextLevel as any).levelId,
@ -493,7 +493,7 @@ export class ApprovalService {
logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError); logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError);
// Don't fail the approval if TAT scheduling fails // Don't fail the approval if TAT scheduling fails
} }
// Update workflow current level (only if nextLevelNumber is not null) // Update workflow current level (only if nextLevelNumber is not null)
if (nextLevelNumber !== null) { if (nextLevelNumber !== null) {
await WorkflowRequest.update( await WorkflowRequest.update(
@ -504,10 +504,10 @@ export class ApprovalService {
} else { } else {
logger.warn(`Approved level ${level.levelNumber} but no next level found - workflow may be complete`); logger.warn(`Approved level ${level.levelNumber} but no next level found - workflow may be complete`);
} }
// Note: Dealer claim-specific logic (Activity Creation, E-Invoice) is handled by DealerClaimApprovalService // Note: Dealer claim-specific logic (Activity Creation, E-Invoice) is handled by DealerClaimApprovalService
// This service is for custom workflows only // This service is for custom workflows only
// Log approval activity // Log approval activity
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
@ -519,7 +519,7 @@ export class ApprovalService {
ipAddress: requestMetadata?.ipAddress || undefined, ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined userAgent: requestMetadata?.userAgent || undefined
}); });
// Notify initiator about the approval (triggers email for regular workflows) // Notify initiator about the approval (triggers email for regular workflows)
if (wf) { if (wf) {
await notificationService.sendToUsers([(wf as any).initiatorId], { await notificationService.sendToUsers([(wf as any).initiatorId], {
@ -532,29 +532,29 @@ export class ApprovalService {
priority: 'MEDIUM' priority: 'MEDIUM'
}); });
} }
// Notify next approver // Notify next approver
if (wf && nextLevel) { if (wf && nextLevel) {
// Check if it's an auto-step by checking approverEmail or levelName // Check if it's an auto-step by checking approverEmail or levelName
// Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps // Note: Activity Creation, E-Invoice Generation, and Credit Note Confirmation are now activity logs only, not approval steps
// These steps are processed automatically and should NOT trigger notifications // These steps are processed automatically and should NOT trigger notifications
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com' const isAutoStep = (nextLevel as any).approverEmail === 'system@{{API_DOMAIN}}'
|| (nextLevel as any).approverName === 'System Auto-Process' || (nextLevel as any).approverName === 'System Auto-Process'
|| (nextLevel as any).approverId === 'system'; || (nextLevel as any).approverId === 'system';
// IMPORTANT: Skip notifications and assignment logging for system/auto-steps // IMPORTANT: Skip notifications and assignment logging for system/auto-steps
// System steps are any step with system@royalenfield.com // System steps are any step with system@{{API_DOMAIN}}
// Only send notifications to real users, NOT system processes // Only send notifications to real users, NOT system processes
if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') { if (!isAutoStep && (nextLevel as any).approverId && (nextLevel as any).approverId !== 'system') {
// Additional checks: ensure approverEmail and approverName are not system-related // Additional checks: ensure approverEmail and approverName are not system-related
// This prevents notifications to system accounts even if they pass other checks // This prevents notifications to system accounts even if they pass other checks
const approverEmail = (nextLevel as any).approverEmail || ''; const approverEmail = (nextLevel as any).approverEmail || '';
const approverName = (nextLevel as any).approverName || ''; const approverName = (nextLevel as any).approverName || '';
const isSystemEmail = approverEmail.toLowerCase() === 'system@royalenfield.com' const isSystemEmail = approverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|| approverEmail.toLowerCase().includes('system'); || approverEmail.toLowerCase().includes('system');
const isSystemName = approverName.toLowerCase() === 'system auto-process' const isSystemName = approverName.toLowerCase() === 'system auto-process'
|| approverName.toLowerCase().includes('system'); || approverName.toLowerCase().includes('system');
// EXCLUDE all system-related steps from notifications // EXCLUDE all system-related steps from notifications
// Only send notifications to real users, NOT system processes // Only send notifications to real users, NOT system processes
if (!isSystemEmail && !isSystemName) { if (!isSystemEmail && !isSystemName) {
@ -562,10 +562,10 @@ export class ApprovalService {
// This will send both in-app and email notifications // This will send both in-app and email notifications
const nextApproverId = (nextLevel as any).approverId; const nextApproverId = (nextLevel as any).approverId;
const nextApproverName = (nextLevel as any).approverName || (nextLevel as any).approverEmail || 'approver'; const nextApproverName = (nextLevel as any).approverName || (nextLevel as any).approverEmail || 'approver';
logger.info(`[Approval] Sending assignment notification to next approver: ${nextApproverName} (${nextApproverId}) at level ${nextLevelNumber} for request ${(wf as any).requestNumber}`); logger.info(`[Approval] Sending assignment notification to next approver: ${nextApproverName} (${nextApproverId}) at level ${nextLevelNumber} for request ${(wf as any).requestNumber}`);
await notificationService.sendToUsers([ nextApproverId ], { await notificationService.sendToUsers([nextApproverId], {
title: `Action required: ${(wf as any).requestNumber}`, title: `Action required: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`, body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
@ -595,7 +595,7 @@ export class ApprovalService {
} else { } else {
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`); logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
} }
// Note: Dealer-specific notifications (proposal/completion submissions) are handled by DealerClaimApprovalService // Note: Dealer-specific notifications (proposal/completion submissions) are handled by DealerClaimApprovalService
} }
} else { } else {
@ -603,15 +603,15 @@ export class ApprovalService {
logger.warn(`No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`); logger.warn(`No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`);
// Use current level number since there's no next level (workflow is complete) // Use current level number since there's no next level (workflow is complete)
await WorkflowRequest.update( await WorkflowRequest.update(
{ {
status: WorkflowStatus.APPROVED, status: WorkflowStatus.APPROVED,
closureDate: now, closureDate: now,
currentLevel: level.levelNumber || 0 currentLevel: level.levelNumber || 0
}, },
{ where: { requestId: level.requestId } } { where: { requestId: level.requestId } }
); );
if (wf) { if (wf) {
await notificationService.sendToUsers([ (wf as any).initiatorId ], { await notificationService.sendToUsers([(wf as any).initiatorId], {
title: `Approved: ${(wf as any).requestNumber}`, title: `Approved: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`, body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
@ -633,7 +633,7 @@ export class ApprovalService {
} else if (action.action === 'REJECT') { } else if (action.action === 'REJECT') {
// Rejection - mark workflow as REJECTED (closure will happen when initiator finalizes conclusion) // Rejection - mark workflow as REJECTED (closure will happen when initiator finalizes conclusion)
await WorkflowRequest.update( await WorkflowRequest.update(
{ {
status: WorkflowStatus.REJECTED status: WorkflowStatus.REJECTED
// Note: closureDate will be set when initiator finalizes the conclusion // Note: closureDate will be set when initiator finalizes the conclusion
}, },
@ -642,16 +642,16 @@ export class ApprovalService {
// Mark all pending levels as skipped // Mark all pending levels as skipped
await ApprovalLevel.update( await ApprovalLevel.update(
{ {
status: ApprovalStatus.SKIPPED, status: ApprovalStatus.SKIPPED,
levelEndTime: now levelEndTime: now
}, },
{ {
where: { where: {
requestId: level.requestId, requestId: level.requestId,
status: ApprovalStatus.PENDING, status: ApprovalStatus.PENDING,
levelNumber: { [Op.gt]: level.levelNumber } levelNumber: { [Op.gt]: level.levelNumber }
} }
} }
); );
@ -660,7 +660,7 @@ export class ApprovalService {
status: 'REJECTED', status: 'REJECTED',
message: 'Awaiting closure from initiator', message: 'Awaiting closure from initiator',
}); });
// Log rejection activity first (so it's included in AI context) // Log rejection activity first (so it's included in AI context)
if (wf) { if (wf) {
activityService.log({ activityService.log({
@ -674,7 +674,7 @@ export class ApprovalService {
userAgent: requestMetadata?.userAgent || undefined userAgent: requestMetadata?.userAgent || undefined
}); });
} }
// Notify initiator and all participants // Notify initiator and all participants
if (wf) { if (wf) {
const participants = await Participant.findAll({ where: { requestId: level.requestId } }); const participants = await Participant.findAll({ where: { requestId: level.requestId } });
@ -683,7 +683,7 @@ export class ApprovalService {
for (const p of participants as any[]) { for (const p of participants as any[]) {
targetUserIds.add(p.userId); targetUserIds.add(p.userId);
} }
// Send notification to initiator with type 'rejection' to trigger email // Send notification to initiator with type 'rejection' to trigger email
await notificationService.sendToUsers([(wf as any).initiatorId], { await notificationService.sendToUsers([(wf as any).initiatorId], {
title: `Rejected: ${(wf as any).requestNumber}`, title: `Rejected: ${(wf as any).requestNumber}`,
@ -697,7 +697,7 @@ export class ApprovalService {
rejectionReason: action.rejectionReason || action.comments || 'No reason provided' rejectionReason: action.rejectionReason || action.comments || 'No reason provided'
} }
}); });
// Send notification to other participants (spectators) for transparency (no email, just in-app) // Send notification to other participants (spectators) for transparency (no email, just in-app)
const participantUserIds = Array.from(targetUserIds).filter(id => id !== (wf as any).initiatorId); const participantUserIds = Array.from(targetUserIds).filter(id => id !== (wf as any).initiatorId);
if (participantUserIds.length > 0) { if (participantUserIds.length > 0) {
@ -712,7 +712,7 @@ export class ApprovalService {
}); });
} }
} }
// Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved) // Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved)
// This runs in the background without blocking the rejection response // This runs in the background without blocking the rejection response
(async () => { (async () => {
@ -724,46 +724,46 @@ export class ApprovalService {
const { Document } = await import('@models/Document'); const { Document } = await import('@models/Document');
const { Activity } = await import('@models/Activity'); const { Activity } = await import('@models/Activity');
const { getConfigValue } = await import('./configReader.service'); const { getConfigValue } = await import('./configReader.service');
// Check if AI features and remark generation are enabled in admin config // Check if AI features and remark generation are enabled in admin config
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true'; const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true'; const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
if (!aiEnabled || !remarkGenerationEnabled) { if (!aiEnabled || !remarkGenerationEnabled) {
logger.info(`[Approval] AI conclusion generation skipped for rejected request ${level.requestId} (AI disabled)`); logger.info(`[Approval] AI conclusion generation skipped for rejected request ${level.requestId} (AI disabled)`);
return; return;
} }
// Check if AI service is available // Check if AI service is available
const { aiService: aiSvc } = await import('./ai.service'); const { aiService: aiSvc } = await import('./ai.service');
if (!aiSvc.isAvailable()) { if (!aiSvc.isAvailable()) {
logger.warn(`[Approval] AI service unavailable for rejected request ${level.requestId}`); logger.warn(`[Approval] AI service unavailable for rejected request ${level.requestId}`);
return; return;
} }
// Gather context for AI generation (similar to approved flow) // Gather context for AI generation (similar to approved flow)
const approvalLevels = await ApprovalLevel.findAll({ const approvalLevels = await ApprovalLevel.findAll({
where: { requestId: level.requestId }, where: { requestId: level.requestId },
order: [['levelNumber', 'ASC']] order: [['levelNumber', 'ASC']]
}); });
const workNotes = await WorkNote.findAll({ const workNotes = await WorkNote.findAll({
where: { requestId: level.requestId }, where: { requestId: level.requestId },
order: [['createdAt', 'ASC']], order: [['createdAt', 'ASC']],
limit: 20 limit: 20
}); });
const documents = await Document.findAll({ const documents = await Document.findAll({
where: { requestId: level.requestId }, where: { requestId: level.requestId },
order: [['uploadedAt', 'DESC']] order: [['uploadedAt', 'DESC']]
}); });
const activities = await Activity.findAll({ const activities = await Activity.findAll({
where: { requestId: level.requestId }, where: { requestId: level.requestId },
order: [['createdAt', 'ASC']], order: [['createdAt', 'ASC']],
limit: 50 limit: 50
}); });
// Build context object (include rejection reason) // Build context object (include rejection reason)
const context = { const context = {
requestTitle: (wf as any).title, requestTitle: (wf as any).title,
@ -773,8 +773,8 @@ export class ApprovalService {
rejectionReason: action.rejectionReason || action.comments || 'No reason provided', rejectionReason: action.rejectionReason || action.comments || 'No reason provided',
rejectedBy: level.approverName || level.approverEmail, rejectedBy: level.approverName || level.approverEmail,
approvalFlow: approvalLevels.map((l: any) => { approvalFlow: approvalLevels.map((l: any) => {
const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null const tatPercentage = l.tatPercentageUsed !== undefined && l.tatPercentageUsed !== null
? Number(l.tatPercentageUsed) ? Number(l.tatPercentageUsed)
: (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0); : (l.elapsedHours && l.tatHours ? (Number(l.elapsedHours) / Number(l.tatHours)) * 100 : 0);
return { return {
levelNumber: l.levelNumber, levelNumber: l.levelNumber,
@ -804,15 +804,15 @@ export class ApprovalService {
timestamp: activity.createdAt timestamp: activity.createdAt
})) }))
}; };
logger.info(`[Approval] Generating AI conclusion for rejected request ${level.requestId}...`); logger.info(`[Approval] Generating AI conclusion for rejected request ${level.requestId}...`);
// Generate AI conclusion (will adapt to rejection context) // Generate AI conclusion (will adapt to rejection context)
const aiResult = await aiSvc.generateConclusionRemark(context); const aiResult = await aiSvc.generateConclusionRemark(context);
// Create or update conclusion remark // Create or update conclusion remark
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId: level.requestId } }); let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId: level.requestId } });
const conclusionData = { const conclusionData = {
aiGeneratedRemark: aiResult.remark, aiGeneratedRemark: aiResult.remark,
aiModelUsed: aiResult.provider, aiModelUsed: aiResult.provider,
@ -830,7 +830,7 @@ export class ApprovalService {
keyDiscussionPoints: aiResult.keyPoints, keyDiscussionPoints: aiResult.keyPoints,
generatedAt: new Date() generatedAt: new Date()
}; };
if (conclusionInstance) { if (conclusionInstance) {
await conclusionInstance.update(conclusionData as any); await conclusionInstance.update(conclusionData as any);
logger.info(`[Approval] ✅ AI conclusion updated for rejected request ${level.requestId}`); logger.info(`[Approval] ✅ AI conclusion updated for rejected request ${level.requestId}`);
@ -854,7 +854,7 @@ export class ApprovalService {
} }
logger.info(`Approval level ${levelId} ${action.action.toLowerCase()}ed`); logger.info(`Approval level ${levelId} ${action.action.toLowerCase()}ed`);
// Emit real-time update to all users viewing this request // Emit real-time update to all users viewing this request
emitToRequestRoom(level.requestId, 'request:updated', { emitToRequestRoom(level.requestId, 'request:updated', {
requestId: level.requestId, requestId: level.requestId,
@ -863,7 +863,7 @@ export class ApprovalService {
levelNumber: level.levelNumber, levelNumber: level.levelNumber,
timestamp: now.toISOString() timestamp: now.toISOString()
}); });
return updatedLevel; return updatedLevel;
} catch (error) { } catch (error) {
logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error); logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error);

View File

@ -330,7 +330,7 @@ export class DealerClaimService {
let stepDef = null; let stepDef = null;
// Check if this is a system step by email (for backwards compatibility) // Check if this is a system step by email (for backwards compatibility)
const isSystemEmail = approver.email === 'system@royalenfield.com' || approver.email === 'finance@royalenfield.com'; const isSystemEmail = approver.email === 'system@{{API_DOMAIN}}' || approver.email === 'finance@{{API_DOMAIN}}';
if (approver.isAdditional) { if (approver.isAdditional) {
// Additional approver - use stepName from frontend // Additional approver - use stepName from frontend
@ -594,7 +594,7 @@ export class DealerClaimService {
}); });
// 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed) // 2. Add Dealer (treated as Okta/internal user - sync from Okta if needed)
if (dealerEmail && dealerEmail.toLowerCase() !== 'system@royalenfield.com') { if (dealerEmail && dealerEmail.toLowerCase() !== 'system@{{API_DOMAIN}}') {
let dealerUser = await User.findOne({ let dealerUser = await User.findOne({
where: { email: dealerEmail.toLowerCase() }, where: { email: dealerEmail.toLowerCase() },
}); });
@ -626,7 +626,7 @@ export class DealerClaimService {
// 3. Add all approvers from approval levels (excluding system and duplicates) // 3. Add all approvers from approval levels (excluding system and duplicates)
const addedUserIds = new Set<string>([initiatorId]); const addedUserIds = new Set<string>([initiatorId]);
const systemEmails = ['system@royalenfield.com']; const systemEmails = ['system@{{API_DOMAIN}}'];
for (const level of approvalLevels) { for (const level of approvalLevels) {
const approverEmail = (level as any).approverEmail?.toLowerCase(); const approverEmail = (level as any).approverEmail?.toLowerCase();

View File

@ -399,11 +399,11 @@ export class DealerClaimApprovalService {
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver'; const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
// Check if it's an auto-step or system process // Check if it's an auto-step or system process
const isAutoStep = nextApproverEmail === 'system@royalenfield.com' const isAutoStep = nextApproverEmail === 'system@{{API_DOMAIN}}'
|| (nextLevel as any).approverName === 'System Auto-Process' || (nextLevel as any).approverName === 'System Auto-Process'
|| nextApproverId === 'system'; || nextApproverId === 'system';
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@royalenfield.com' const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@{{API_DOMAIN}}'
|| nextApproverEmail.toLowerCase().includes('system'); || nextApproverEmail.toLowerCase().includes('system');
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process' const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|| nextApproverName.toLowerCase().includes('system'); || nextApproverName.toLowerCase().includes('system');

View File

@ -19,7 +19,7 @@ interface EmailOptions {
// Hardcoded BCC addresses (temporary - for time being) // Hardcoded BCC addresses (temporary - for time being)
const HARDCODED_BCC: string[] = [ const HARDCODED_BCC: string[] = [
'rohitm_ext@royalenfield.com', '{{USER_EMAIL}}',
// Add your BCC email addresses here // Add your BCC email addresses here
]; ];
@ -119,7 +119,7 @@ export class EmailService {
} }
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to; const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>'; const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@{{API_DOMAIN}}>';
// Merge hardcoded BCC with provided BCC // Merge hardcoded BCC with provided BCC
let bccRecipients: string[] = []; let bccRecipients: string[] = [];
@ -166,17 +166,6 @@ export class EmailService {
if (previewUrl) { if (previewUrl) {
result.previewUrl = previewUrl; result.previewUrl = previewUrl;
// Always log to console for visibility
console.log('\n' + '='.repeat(80));
console.log(`📧 EMAIL PREVIEW (${options.subject})`);
console.log(`To: ${recipients}`);
if (finalBcc && finalBcc.length > 0) {
console.log(`BCC: ${finalBcc.join(', ')}`);
}
console.log(`Preview URL: ${previewUrl}`);
console.log(`Message ID: ${info.messageId}`);
console.log('='.repeat(80) + '\n');
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`); logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
logger.info(`📧 Preview URL: ${previewUrl}`); logger.info(`📧 Preview URL: ${previewUrl}`);
} else { } else {

View File

@ -270,13 +270,7 @@ class GoogleSecretManagerService {
loadedSecrets[envVarName] = secretValue; loadedSecrets[envVarName] = secretValue;
loadedCount++; loadedCount++;
// Print masked value for verification as requested by user logger.info(`[Secret Manager] ✅ Loaded: ${secretNameToFetch} -> process.env.${envVarName}`);
// Note: Trailing/leading whitespace is now trimmed to prevent auth errors
const maskedValue = secretValue.length > 8
? `${secretValue.substring(0, 3)}...${secretValue.substring(secretValue.length - 3)}`
: '***';
logger.info(`[Secret Manager] ✅ Loaded: ${secretNameToFetch} -> process.env.${envVarName} (Value: ${maskedValue})`);
} else { } else {
// Track which secrets weren't found for better logging // Track which secrets weren't found for better logging
notFoundSecrets.push(fullSecretName); notFoundSecrets.push(fullSecretName);

View File

@ -250,7 +250,7 @@ class NotificationService {
} }
// 4. Send email notification for applicable types (async, don't wait) // 4. Send email notification for applicable types (async, don't wait)
console.log(`[DEBUG] Checking email for notification type: ${payload.type}`); // Check email notification preferences
this.sendEmailNotification(userId, user, payload).catch(emailError => { this.sendEmailNotification(userId, user, payload).catch(emailError => {
console.error(`[Notification] Email sending failed for user ${userId}:`, emailError); console.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
logger.error(`[Notification] Email sending failed for user ${userId}:`, emailError); logger.error(`[Notification] Email sending failed for user ${userId}:`, emailError);
@ -269,7 +269,6 @@ class NotificationService {
* Only sends for notification types that warrant email * Only sends for notification types that warrant email
*/ */
private async sendEmailNotification(userId: string, user: any, payload: NotificationPayload): Promise<void> { private async sendEmailNotification(userId: string, user: any, payload: NotificationPayload): Promise<void> {
console.log(`[DEBUG Email] Notification type: ${payload.type}, userId: ${userId}`);
// Import email service (lazy load to avoid circular dependencies) // Import email service (lazy load to avoid circular dependencies)
const { emailNotificationService } = await import('./emailNotification.service'); const { emailNotificationService } = await import('./emailNotification.service');
@ -311,13 +310,10 @@ class NotificationService {
const emailType = emailTypeMap[payload.type || '']; const emailType = emailTypeMap[payload.type || ''];
console.log(`[DEBUG Email] Email type mapped: ${emailType}`);
if (!emailType) { if (!emailType) {
// This notification type doesn't warrant email // This notification type doesn't warrant email
// Note: 'document_added' emails are handled separately via emailNotificationService // Note: 'document_added' emails are handled separately via emailNotificationService
if (payload.type !== 'document_added') { if (payload.type !== 'document_added') {
console.log(`[DEBUG Email] No email for notification type: ${payload.type}`);
} }
return; return;
} }
@ -333,10 +329,7 @@ class NotificationService {
? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery ? await shouldSendEmailWithOverride(userId, emailType) // Assignment emails - use override to ensure delivery
: await shouldSendEmail(userId, emailType); // Regular emails : await shouldSendEmail(userId, emailType); // Regular emails
console.log(`[DEBUG Email] Should send email: ${shouldSend} for type: ${payload.type}, userId: ${userId}`);
if (!shouldSend) { if (!shouldSend) {
console.log(`[DEBUG Email] Email skipped for user ${userId}, type: ${payload.type} (preferences)`);
logger.warn(`[Email] Email skipped for user ${userId}, type: ${payload.type} (preferences or admin disabled)`); logger.warn(`[Email] Email skipped for user ${userId}, type: ${payload.type} (preferences or admin disabled)`);
return; return;
} }
@ -345,11 +338,9 @@ class NotificationService {
// Trigger email based on notification type // Trigger email based on notification type
// Email service will fetch additional data as needed // Email service will fetch additional data as needed
console.log(`[DEBUG Email] Triggering email for type: ${payload.type}`);
try { try {
await this.triggerEmailByType(payload.type || '', userId, payload, user); await this.triggerEmailByType(payload.type || '', userId, payload, user);
} catch (error) { } catch (error) {
console.error(`[DEBUG Email] Error triggering email:`, error);
logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error); logger.error(`[Email] Failed to trigger email for type ${payload.type}:`, error);
} }
} }
@ -578,7 +569,7 @@ class NotificationService {
approverData = { approverData = {
userId: (rejectedLevel as any).approverId, userId: (rejectedLevel as any).approverId,
displayName: (rejectedLevel as any).approverName || 'Unknown Approver', displayName: (rejectedLevel as any).approverName || 'Unknown Approver',
email: (rejectedLevel as any).approverEmail || 'unknown@royalenfield.com', email: (rejectedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}',
rejectedAt: (rejectedLevel as any).actionDate, rejectedAt: (rejectedLevel as any).actionDate,
comments: (rejectedLevel as any).comments comments: (rejectedLevel as any).comments
}; };
@ -621,7 +612,7 @@ class NotificationService {
approverData = { approverData = {
userId: (currentLevel as any).approverId, userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown Approver', displayName: (currentLevel as any).approverName || 'Unknown Approver',
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
}; };
} }
} }
@ -708,7 +699,7 @@ class NotificationService {
approverData = { approverData = {
userId: (currentLevel as any).approverId, userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown Approver', displayName: (currentLevel as any).approverName || 'Unknown Approver',
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
}; };
} }
} }
@ -842,7 +833,7 @@ class NotificationService {
recipientData = { recipientData = {
userId: (pausedLevel as any).approverId, userId: (pausedLevel as any).approverId,
displayName: (pausedLevel as any).approverName || 'Unknown Approver', displayName: (pausedLevel as any).approverName || 'Unknown Approver',
email: (pausedLevel as any).approverEmail || 'unknown@royalenfield.com' email: (pausedLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
}; };
} }
} }
@ -894,7 +885,7 @@ class NotificationService {
recipientData = { recipientData = {
userId: (currentLevel as any).approverId, userId: (currentLevel as any).approverId,
displayName: (currentLevel as any).approverName || 'Unknown User', displayName: (currentLevel as any).approverName || 'Unknown User',
email: (currentLevel as any).approverEmail || 'unknown@royalenfield.com' email: (currentLevel as any).approverEmail || 'unknown@{{API_DOMAIN}}'
}; };
} }
} else { } else {

View File

@ -71,7 +71,7 @@ export class PdfService {
private getInvoiceHtmlTemplate(data: any): string { private getInvoiceHtmlTemplate(data: any): string {
const { request, invoice, dealer, claimDetails } = data; const { request, invoice, dealer, claimDetails } = data;
const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : ''; const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : '';
const logoUrl = 'https://www.royalenfield.com/content/dam/royal-enfield/india/logos/logo.png'; const logoUrl = '{{LOGO_URL}}';
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
@ -119,7 +119,7 @@ export class PdfService {
<div class="info-grid"> <div class="info-grid">
<div class="info-section"> <div class="info-section">
<div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div> <div class="info-row"><div class="info-label">Customer Name</div><div class="info-value">Royal Enfield</div></div>
<div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">33AAACE3882D1ZZ</div></div> <div class="info-row"><div class="info-label">Customer GSTIN</div><div class="info-value">{{BUYER_GSTIN}}</div></div>
<div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div> <div class="info-row"><div class="info-label">Customer Address</div><div class="info-value">State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604</div></div>
<br/> <br/>
<div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div> <div class="info-row"><div class="info-label">Vehicle Owner</div><div class="info-value">N/A</div></div>

View File

@ -123,7 +123,7 @@ export class PWCIntegrationService {
SourceSystem: "RE_WORKFLOW", SourceSystem: "RE_WORKFLOW",
is_irn: "Y", is_irn: "Y",
is_ewb: "N", is_ewb: "N",
email: (request as any).initiator?.email || "system@royalenfield.com", email: (request as any).initiator?.email || "system@{{API_DOMAIN}}",
TranDtls: { TranDtls: {
TaxSch: "GST", TaxSch: "GST",
SubType: "SUPPLY", SubType: "SUPPLY",
@ -155,7 +155,7 @@ export class PWCIntegrationService {
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com" Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
}, },
BuyerDtls: { BuyerDtls: {
Gstin: "33AAACE3882D1ZZ", // Royal Enfield GST Gstin: "{{BUYER_GSTIN}}", // Royal Enfield GST
LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)", LglNm: "ROYAL ENFIELD (A UNIT OF EICHER MOTORS LTD)",
TrdNm: "ROYAL ENFIELD", TrdNm: "ROYAL ENFIELD",
Addr1: "No. 2, Thiruvottiyur High Road", Addr1: "No. 2, Thiruvottiyur High Road",
@ -201,7 +201,6 @@ export class PWCIntegrationService {
'token': this.token 'token': this.token
} }
}); });
console.log('PWC Response:', JSON.stringify(response.data));
// Parse PWC Response based on provided structure // Parse PWC Response based on provided structure
// Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }] // Sample response is an array: [{ pwc_response, irp_response, qr_b64_encoded }]

View File

@ -34,7 +34,6 @@ export class SAPIntegrationService {
* Check if SAP integration is configured * Check if SAP integration is configured
*/ */
private isConfigured(): boolean { private isConfigured(): boolean {
// Check if SAP bypass is explicitly enabled
if (process.env.SAP_BYPASS === 'true') { if (process.env.SAP_BYPASS === 'true') {
logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable'); logger.info('[SAP] SAP integration explicitly bypassed via SAP_BYPASS env variable');
return false; return false;
@ -188,12 +187,8 @@ export class SAPIntegrationService {
}) : undefined }) : undefined
}); });
// Add request interceptor for debugging
client.interceptors.request.use( client.interceptors.request.use(
(config) => { (config) => {
logger.debug(`[SAP] Making ${config.method?.toUpperCase()} request to: ${config.baseURL}${config.url}`);
logger.debug(`[SAP] Auth username: ${this.sapUsername}`);
// Don't log password for security
return config; return config;
}, },
(error) => { (error) => {
@ -219,7 +214,7 @@ export class SAPIntegrationService {
} else if (error.code === 'ENOTFOUND') { } else if (error.code === 'ENOTFOUND') {
logger.error('[SAP] Host not found - check SAP_BASE_URL'); logger.error('[SAP] Host not found - check SAP_BASE_URL');
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') { } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
logger.error('[SAP] SSL certificate error - set SAP_DISABLE_SSL_VERIFY=true to bypass (testing only)'); logger.error('[SAP] SSL certificate error');
} }
} }
return Promise.reject(error); return Promise.reject(error);
@ -597,9 +592,10 @@ export class SAPIntegrationService {
logger.debug(`[SAP] POST request config prepared (auth included)`); logger.debug(`[SAP] POST request config prepared (auth included)`);
const response = await sapClient.post(urlWithParams, requestPayload, postConfig); const response = await sapClient.post(urlWithParams, requestPayload, postConfig);
// Log full response for debugging // Log response status for visibility
logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`); logger.info(`[SAP] POST Response Status: ${response.status} ${response.statusText || ''}`);
logger.info(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2)); // Headers relegated to debug level
logger.debug(`[SAP] POST Response Headers:`, JSON.stringify(response.headers, null, 2));
// Check if response is XML (SAP returns XML/Atom by default for POST) // Check if response is XML (SAP returns XML/Atom by default for POST)
const contentType = response.headers['content-type'] || ''; const contentType = response.headers['content-type'] || '';
@ -640,9 +636,10 @@ export class SAPIntegrationService {
} }
} }
// Also log the request that was sent // Log the request URL
logger.info(`[SAP] POST Request URL: ${urlWithParams}`); logger.info(`[SAP] POST Request URL: ${urlWithParams}`);
logger.info(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2)); // Payload relegated to debug level
logger.debug(`[SAP] POST Request Payload:`, JSON.stringify(requestPayload, null, 2));
if (response.status === 200 || response.status === 201) { if (response.status === 200 || response.status === 201) {
// Parse SAP response // Parse SAP response

View File

@ -133,7 +133,6 @@ if (process.env.LOKI_HOST) {
} }
transports.push(new LokiTransport(lokiTransportOptions)); transports.push(new LokiTransport(lokiTransportOptions));
console.log(`[Logger] ✅ Loki transport enabled: ${process.env.LOKI_HOST}`);
} catch (error) { } catch (error) {
console.warn('[Logger] ⚠️ Failed to initialize Loki transport:', (error as Error).message); console.warn('[Logger] ⚠️ Failed to initialize Loki transport:', (error as Error).message);
} }