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();

View File

@ -9,7 +9,7 @@ export const emailConfig = {
}, },
}, },
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: {

View File

@ -12,14 +12,14 @@ const ssoConfig: SSOConfig = {
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

@ -100,7 +100,7 @@ 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
@ -113,7 +113,7 @@ 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
@ -126,7 +126,7 @@ 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

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
} }
@ -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',

View File

@ -538,19 +538,19 @@ export class ApprovalService {
// 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');

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);
} }