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_PORT=587
SMTP_SECURE=false
SMTP_USER=notifications@royalenfield.com
SMTP_USER=notifications@{{API_DOMAIN}}
SMTP_PASSWORD=your_smtp_password
EMAIL_FROM=RE Workflow System <notifications@royalenfield.com>
EMAIL_FROM=RE Workflow System <notifications@{{API_DOMAIN}}>
# AI Service (for conclusion generation)
AI_API_KEY=your_ai_api_key

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-7F7W4LDI.js"></script>
<script type="module" crossorigin src="/assets/index-y_ojbF9T.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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