we have aded the apad approvar in dealer claim
This commit is contained in:
parent
1ac169dc7f
commit
fbbde44d46
@ -2,10 +2,70 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document describes the data exchange between the Royal Enfield Workflow System and the DMS (Document Management System) for:
|
This document describes the data exchange between the Royal Enfield Workflow System (RE-Flow) and the DMS (Document Management System) for:
|
||||||
1. **E-Invoice Generation** - Submitting claim data to DMS for e-invoice creation
|
1. **E-Invoice Generation** - Submitting claim data to DMS for e-invoice creation
|
||||||
2. **Credit Note Generation** - Fetching/Generating credit note from DMS
|
2. **Credit Note Generation** - Fetching/Generating credit note from DMS
|
||||||
|
|
||||||
|
## Data Flow Overview
|
||||||
|
|
||||||
|
### Inputs from RE-Flow System
|
||||||
|
|
||||||
|
The following data is sent **FROM** RE-Flow System **TO** DMS:
|
||||||
|
|
||||||
|
1. **Dealer Code** - Unique dealer identifier
|
||||||
|
2. **Dealer Name** - Dealer business name
|
||||||
|
3. **Activity Name** - Name of the activity/claim type (see Activity Types below)
|
||||||
|
4. **Activity Description** - Detailed description of the activity
|
||||||
|
5. **Claim Amount** - Total claim amount (before taxes)
|
||||||
|
6. **Request Number** - Unique request identifier from RE-Flow (e.g., "REQ-2025-12-0001")
|
||||||
|
7. **IO Number** - Internal Order number (if available)
|
||||||
|
|
||||||
|
### Inputs from DMS Team
|
||||||
|
|
||||||
|
The following data is **PROVIDED BY** DMS Team **TO** RE-Flow System (via webhook):
|
||||||
|
|
||||||
|
3. **Document No** - Generated invoice/credit note number
|
||||||
|
4. **Document Type** - Type of document ("E-INVOICE", "INVOICE", or "CREDIT_NOTE")
|
||||||
|
10. **Item Code No** - Item code number (same as provided in request, used for GST calculation)
|
||||||
|
11. **HSN/SAC Code** - HSN/SAC code for tax calculation (determined by DMS based on Item Code No)
|
||||||
|
12. **CGST %** - CGST percentage (e.g., 9.0 for 9%) - calculated by DMS based on Item Code No and dealer location
|
||||||
|
13. **SGST %** - SGST percentage (e.g., 9.0 for 9%) - calculated by DMS based on Item Code No and dealer location
|
||||||
|
14. **IGST %** - IGST percentage (0.0 for intra-state, >0 for inter-state) - calculated by DMS based on Item Code No and dealer location
|
||||||
|
15. **CGST Amount** - CGST amount in INR - calculated by DMS
|
||||||
|
16. **SGST Amount** - SGST amount in INR - calculated by DMS
|
||||||
|
17. **IGST Amount** - IGST amount in INR - calculated by DMS
|
||||||
|
18. **Credit Type** - Type of credit: "GST" or "Commercial Credit" (for credit notes only)
|
||||||
|
19. **IRN No** - Invoice Reference Number from GST portal (response from GST system)
|
||||||
|
20. **SAP Credit Note No** - SAP Credit Note Number (response from SAP system, for credit notes only)
|
||||||
|
|
||||||
|
**Important:** Item Code No is used by DMS for GST calculation. DMS determines HSN/SAC code, tax percentages, and tax amounts based on the Item Code No and dealer location.
|
||||||
|
|
||||||
|
### Predefined Activity Types
|
||||||
|
|
||||||
|
The following is the complete list of predefined Activity Types that RE-Flow System uses. DMS Team must provide **Item Code No** mapping for each Activity Type:
|
||||||
|
|
||||||
|
- **Riders Mania Claims**
|
||||||
|
- **Marketing Cost – Bike to Vendor**
|
||||||
|
- **Media Bike Service**
|
||||||
|
- **ARAI Motorcycle Liquidation**
|
||||||
|
- **ARAI Certification – STA Approval CNR**
|
||||||
|
- **Procurement of Spares/Apparel/GMA for Events**
|
||||||
|
- **Fuel for Media Bike Used for Event**
|
||||||
|
- **Motorcycle Buyback and Goodwill Support**
|
||||||
|
- **Liquidation of Used Motorcycle**
|
||||||
|
- **Motorcycle Registration CNR (Owned or Gifted by RE)**
|
||||||
|
- **Legal Claims Reimbursement**
|
||||||
|
- **Service Camp Claims**
|
||||||
|
- **Corporate Claims – Institutional Sales PDI**
|
||||||
|
|
||||||
|
**Item Code No Lookup Process:**
|
||||||
|
1. RE-Flow sends `activity_name` to DMS
|
||||||
|
2. DMS responds with corresponding `item_code_no` based on activity type mapping
|
||||||
|
3. RE-Flow includes the `item_code_no` in invoice/credit note generation payload
|
||||||
|
4. DMS uses `item_code_no` to determine HSN/SAC code and calculate GST (CGST/SGST/IGST percentages and amounts)
|
||||||
|
|
||||||
|
**Note:** DMS Team must configure the Activity Type → Item Code No mapping in their system. This mapping is used for GST calculation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. E-Invoice Generation (DMS Push)
|
## 1. E-Invoice Generation (DMS Push)
|
||||||
@ -27,46 +87,84 @@ Authorization: Bearer {DMS_API_KEY}
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body (Complete Payload):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"request_number": "REQ-2025-001234",
|
"request_number": "REQ-2025-12-0001",
|
||||||
"dealer_code": "DLR001",
|
"dealer_code": "DLR001",
|
||||||
"dealer_name": "ABC Motors",
|
"dealer_name": "ABC Motors",
|
||||||
"amount": 150000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"description": "E-Invoice for claim request REQ-2025-001234",
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
|
"claim_amount": 150000.00,
|
||||||
"io_number": "IO-2025-001",
|
"io_number": "IO-2025-001",
|
||||||
"tax_details": {
|
"item_code_no": "ITEM-001"
|
||||||
"cgst": 0,
|
|
||||||
"sgst": 0,
|
|
||||||
"igst": 0,
|
|
||||||
"total_tax": 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Complete Webhook Response Payload (from DMS to RE-Flow):**
|
||||||
|
|
||||||
|
After processing, DMS will send the following complete payload to RE-Flow webhook endpoint `POST /api/v1/webhooks/dms/invoice`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_number": "REQ-2025-12-0001",
|
||||||
|
"document_no": "EINV-2025-001234",
|
||||||
|
"document_type": "E-INVOICE",
|
||||||
|
"document_date": "2025-12-17T10:30:00.000Z",
|
||||||
|
"dealer_code": "DLR001",
|
||||||
|
"dealer_name": "ABC Motors",
|
||||||
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
|
"claim_amount": 150000.00,
|
||||||
|
"io_number": "IO-2025-001",
|
||||||
|
"item_code_no": "ITEM-001",
|
||||||
|
"hsn_sac_code": "998314",
|
||||||
|
"cgst_percentage": 9.0,
|
||||||
|
"sgst_percentage": 9.0,
|
||||||
|
"igst_percentage": 0.0,
|
||||||
|
"cgst_amount": 13500.00,
|
||||||
|
"sgst_amount": 13500.00,
|
||||||
|
"igst_amount": 0.00,
|
||||||
|
"total_amount": 177000.00,
|
||||||
|
"irn_no": "IRN123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
"invoice_file_path": "https://dms.example.com/invoices/EINV-2025-001234.pdf",
|
||||||
|
"error_message": null,
|
||||||
|
"timestamp": "2025-12-17T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- RE-Flow sends all required details including `item_code_no` (determined by DMS based on `activity_name` mapping)
|
||||||
|
- DMS processes the invoice generation **asynchronously**
|
||||||
|
- DMS responds with acknowledgment only
|
||||||
|
- **Status Verification (Primary Method):** DMS sends webhook to RE-Flow webhook URL `POST /api/v1/webhooks/dms/invoice` (see DMS_WEBHOOK_API.md) to notify when invoice is generated with complete details
|
||||||
|
- `item_code_no` is used by DMS for GST calculation (HSN/SAC code, tax percentages, tax amounts)
|
||||||
|
- **Status Verification (Backup Method):** If webhook fails, RE-Flow can use backup status check API (see section "Backup: Status Check API" below)
|
||||||
|
|
||||||
### Request Field Descriptions
|
### Request Field Descriptions
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `request_number` | string | Yes | Unique request number from RE Workflow System (e.g., "REQ-2025-001234") |
|
| `request_number` | string | ✅ Yes | Unique request number from RE-Flow System (e.g., "REQ-2025-12-0001") |
|
||||||
| `dealer_code` | string | Yes | Dealer's unique code/identifier |
|
| `dealer_code` | string | ✅ Yes | Dealer's unique code/identifier |
|
||||||
| `dealer_name` | string | Yes | Dealer's business name |
|
| `dealer_name` | string | ✅ Yes | Dealer's business name |
|
||||||
| `amount` | number | Yes | Total invoice amount (in INR, decimal format) |
|
| `activity_name` | string | ✅ Yes | Activity type name (must match one of the predefined Activity Types) |
|
||||||
| `description` | string | Yes | Description of the invoice/claim |
|
| `activity_description` | string | ✅ Yes | Detailed description of the activity/claim |
|
||||||
|
| `claim_amount` | number | ✅ Yes | Total claim amount before taxes (in INR, decimal format) |
|
||||||
| `io_number` | string | No | Internal Order (IO) number if available |
|
| `io_number` | string | No | Internal Order (IO) number if available |
|
||||||
| `tax_details` | object | No | Tax breakdown (CGST, SGST, IGST, Total Tax) |
|
| `item_code_no` | string | ✅ Yes | Item code number determined by DMS based on `activity_name` mapping. RE-Flow includes this in the request. Used by DMS for GST calculation. |
|
||||||
|
|
||||||
### Expected Response
|
### Expected Response
|
||||||
|
|
||||||
**Success Response (200 OK):**
|
**Success Response (200 OK):**
|
||||||
|
|
||||||
|
**Note:** DMS should respond with a simple acknowledgment. The actual invoice details (document number, tax calculations, IRN, etc.) will be sent back to RE-Flow via **webhook** (see DMS_WEBHOOK_API.md).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"e_invoice_number": "EINV-2025-001234",
|
"message": "Invoice generation request received and queued for processing",
|
||||||
"dms_number": "DMS-2025-001234",
|
"request_number": "REQ-2025-12-0001"
|
||||||
"invoice_date": "2025-12-17T10:30:00.000Z",
|
|
||||||
"invoice_url": "https://dms.example.com/invoices/EINV-2025-001234"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -74,11 +172,15 @@ Content-Type: application/json
|
|||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `success` | boolean | Indicates if the request was successful |
|
| `success` | boolean | Indicates if the request was accepted |
|
||||||
| `e_invoice_number` | string | Generated e-invoice number (unique identifier) |
|
| `message` | string | Status message |
|
||||||
| `dms_number` | string | DMS internal tracking number |
|
| `request_number` | string | Echo of the request number for reference |
|
||||||
| `invoice_date` | string (ISO 8601) | Date when the invoice was generated |
|
|
||||||
| `invoice_url` | string | URL to view/download the invoice document |
|
**Important:**
|
||||||
|
- The actual invoice generation happens **asynchronously**
|
||||||
|
- DMS will send the complete invoice details (including document number, tax calculations, IRN, file path, `item_code_no`, etc.) via **webhook** to RE-Flow System once processing is complete
|
||||||
|
- Webhook endpoint: `POST /api/v1/webhooks/dms/invoice` (see DMS_WEBHOOK_API.md for details)
|
||||||
|
- If webhook delivery fails, RE-Flow can use the backup status check API (see section "Backup: Status Check API" below)
|
||||||
|
|
||||||
### Error Response
|
### Error Response
|
||||||
|
|
||||||
@ -107,12 +209,14 @@ curl -X POST "https://dms.example.com/api/invoices/generate" \
|
|||||||
-H "Authorization: Bearer YOUR_DMS_API_KEY" \
|
-H "Authorization: Bearer YOUR_DMS_API_KEY" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"request_number": "REQ-2025-001234",
|
"request_number": "REQ-2025-12-0001",
|
||||||
"dealer_code": "DLR001",
|
"dealer_code": "DLR001",
|
||||||
"dealer_name": "ABC Motors",
|
"dealer_name": "ABC Motors",
|
||||||
"amount": 150000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"description": "E-Invoice for claim request REQ-2025-001234",
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
"io_number": "IO-2025-001"
|
"claim_amount": 150000.00,
|
||||||
|
"io_number": "IO-2025-001",
|
||||||
|
"item_code_no": "ITEM-001"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -137,41 +241,86 @@ Authorization: Bearer {DMS_API_KEY}
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body (Complete Payload):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"request_number": "REQ-2025-001234",
|
"request_number": "REQ-2025-12-0001",
|
||||||
"e_invoice_number": "EINV-2025-001234",
|
"e_invoice_number": "EINV-2025-001234",
|
||||||
"dealer_code": "DLR001",
|
"dealer_code": "DLR001",
|
||||||
"dealer_name": "ABC Motors",
|
"dealer_name": "ABC Motors",
|
||||||
"amount": 150000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"reason": "Claim settlement",
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
"description": "Credit note for claim request REQ-2025-001234"
|
"claim_amount": 150000.00,
|
||||||
|
"io_number": "IO-2025-001",
|
||||||
|
"item_code_no": "ITEM-001"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Complete Webhook Response Payload (from DMS to RE-Flow):**
|
||||||
|
|
||||||
|
After processing, DMS will send the following complete payload to RE-Flow webhook endpoint `POST /api/v1/webhooks/dms/credit-note`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_number": "REQ-2025-12-0001",
|
||||||
|
"document_no": "CN-2025-001234",
|
||||||
|
"document_type": "CREDIT_NOTE",
|
||||||
|
"document_date": "2025-12-17T11:00:00.000Z",
|
||||||
|
"dealer_code": "DLR001",
|
||||||
|
"dealer_name": "ABC Motors",
|
||||||
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
|
"claim_amount": 150000.00,
|
||||||
|
"io_number": "IO-2025-001",
|
||||||
|
"item_code_no": "ITEM-001",
|
||||||
|
"hsn_sac_code": "998314",
|
||||||
|
"cgst_percentage": 9.0,
|
||||||
|
"sgst_percentage": 9.0,
|
||||||
|
"igst_percentage": 0.0,
|
||||||
|
"cgst_amount": 13500.00,
|
||||||
|
"sgst_amount": 13500.00,
|
||||||
|
"igst_amount": 0.00,
|
||||||
|
"total_amount": 177000.00,
|
||||||
|
"credit_type": "GST",
|
||||||
|
"irn_no": "IRN987654321098765432109876543210987654321098765432109876543210",
|
||||||
|
"sap_credit_note_no": "SAP-CN-2025-001234",
|
||||||
|
"credit_note_file_path": "https://dms.example.com/credit-notes/CN-2025-001234.pdf",
|
||||||
|
"error_message": null,
|
||||||
|
"timestamp": "2025-12-17T11:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- RE-Flow sends `activity_name` in the request
|
||||||
|
- DMS should use the same Item Code No from the original invoice (determined by `activity_name`)
|
||||||
|
- DMS returns `item_code_no` in the webhook response (see DMS_WEBHOOK_API.md)
|
||||||
|
- `item_code_no` is used by DMS for GST calculation (HSN/SAC code, tax percentages, tax amounts)
|
||||||
|
|
||||||
### Request Field Descriptions
|
### Request Field Descriptions
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `request_number` | string | Yes | Original request number from RE Workflow System |
|
| `request_number` | string | ✅ Yes | Original request number from RE-Flow System |
|
||||||
| `e_invoice_number` | string | Yes | E-invoice number that was generated earlier (must exist in DMS) |
|
| `e_invoice_number` | string | ✅ Yes | E-invoice number that was generated earlier (must exist in DMS) |
|
||||||
| `dealer_code` | string | Yes | Dealer's unique code/identifier (must match invoice) |
|
| `dealer_code` | string | ✅ Yes | Dealer's unique code/identifier (must match invoice) |
|
||||||
| `dealer_name` | string | Yes | Dealer's business name |
|
| `dealer_name` | string | ✅ Yes | Dealer's business name |
|
||||||
| `amount` | number | Yes | Credit note amount (in INR, decimal format) - typically matches invoice amount |
|
| `activity_name` | string | ✅ Yes | Activity type name (must match original invoice) |
|
||||||
| `reason` | string | Yes | Reason for credit note (e.g., "Claim settlement", "Return", "Adjustment") |
|
| `activity_description` | string | ✅ Yes | Activity description (must match original invoice) |
|
||||||
| `description` | string | No | Additional description/details about the credit note |
|
| `claim_amount` | number | ✅ Yes | Credit note amount (in INR, decimal format) - typically matches invoice amount |
|
||||||
|
| `io_number` | string | No | Internal Order (IO) number if available |
|
||||||
|
| `item_code_no` | string | ✅ Yes | Item code number (same as original invoice, determined by `activity_name` mapping). RE-Flow includes this in the request. Used by DMS for GST calculation. |
|
||||||
|
|
||||||
### Expected Response
|
### Expected Response
|
||||||
|
|
||||||
**Success Response (200 OK):**
|
**Success Response (200 OK):**
|
||||||
|
|
||||||
|
**Note:** DMS should respond with a simple acknowledgment. The actual credit note details (document number, tax calculations, SAP credit note number, IRN, etc.) will be sent back to RE-Flow via **webhook** (see DMS_WEBHOOK_API.md).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"credit_note_number": "CN-2025-001234",
|
"message": "Credit note generation request received and queued for processing",
|
||||||
"credit_note_date": "2025-12-17T10:30:00.000Z",
|
"request_number": "REQ-2025-12-0001"
|
||||||
"credit_note_amount": 150000.00,
|
|
||||||
"credit_note_url": "https://dms.example.com/credit-notes/CN-2025-001234"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -179,11 +328,11 @@ Content-Type: application/json
|
|||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `success` | boolean | Indicates if the request was successful |
|
| `success` | boolean | Indicates if the request was accepted |
|
||||||
| `credit_note_number` | string | Generated credit note number (unique identifier) |
|
| `message` | string | Status message |
|
||||||
| `credit_note_date` | string (ISO 8601) | Date when the credit note was generated |
|
| `request_number` | string | Echo of the request number for reference |
|
||||||
| `credit_note_amount` | number | Credit note amount (should match request amount) |
|
|
||||||
| `credit_note_url` | string | URL to view/download the credit note document |
|
**Important:** The actual credit note generation happens asynchronously. DMS will send the complete credit note details (including document number, tax calculations, SAP credit note number, IRN, file path, etc.) via webhook to RE-Flow System once processing is complete.
|
||||||
|
|
||||||
### Error Response
|
### Error Response
|
||||||
|
|
||||||
@ -213,13 +362,15 @@ curl -X POST "https://dms.example.com/api/credit-notes/generate" \
|
|||||||
-H "Authorization: Bearer YOUR_DMS_API_KEY" \
|
-H "Authorization: Bearer YOUR_DMS_API_KEY" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"request_number": "REQ-2025-001234",
|
"request_number": "REQ-2025-12-0001",
|
||||||
"e_invoice_number": "EINV-2025-001234",
|
"e_invoice_number": "EINV-2025-001234",
|
||||||
"dealer_code": "DLR001",
|
"dealer_code": "DLR001",
|
||||||
"dealer_name": "ABC Motors",
|
"dealer_name": "ABC Motors",
|
||||||
"amount": 150000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"reason": "Claim settlement",
|
"activity_description": "Q4 Marketing Campaign for Royal Enfield",
|
||||||
"description": "Credit note for claim request REQ-2025-001234"
|
"claim_amount": 150000.00,
|
||||||
|
"io_number": "IO-2025-001",
|
||||||
|
"item_code_no": "ITEM-001"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -261,34 +412,65 @@ DMS supports two authentication methods:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ RE Workflow │
|
│ RE-Flow System │
|
||||||
│ System │
|
|
||||||
│ (Step 6) │
|
│ (Step 6) │
|
||||||
└────────┬────────┘
|
└────────┬────────┘
|
||||||
│
|
│
|
||||||
│ POST /api/invoices/generate
|
│ POST /api/invoices/generate
|
||||||
│ { request_number, dealer_code, amount, ... }
|
│ { request_number, dealer_code, activity_name,
|
||||||
|
│ claim_amount, item_code_no, ... }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ DMS System │
|
│ DMS System │
|
||||||
│ │
|
│ │
|
||||||
│ - Validates │
|
│ - Validates │
|
||||||
│ - Generates │
|
│ - Queues for │
|
||||||
│ E-Invoice │
|
│ processing │
|
||||||
|
│ │
|
||||||
|
│ Response: │
|
||||||
|
│ { success: true }│
|
||||||
└────────┬────────┘
|
└────────┬────────┘
|
||||||
│
|
│
|
||||||
│ Response: { e_invoice_number, dms_number, ... }
|
│ (Asynchronous Processing)
|
||||||
|
│
|
||||||
|
│ - Determines Item Code No
|
||||||
|
│ - Calculates GST
|
||||||
|
│ - Generates E-Invoice
|
||||||
|
│ - Gets IRN from GST
|
||||||
|
│
|
||||||
|
│ POST /api/v1/webhooks/dms/invoice
|
||||||
|
│ { document_no, item_code_no,
|
||||||
|
│ hsn_sac_code, tax details,
|
||||||
|
│ irn_no, invoice_file_path, ... }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ RE Workflow │
|
│ RE-Flow System │
|
||||||
│ System │
|
|
||||||
│ │
|
│ │
|
||||||
|
│ - Receives │
|
||||||
|
│ webhook │
|
||||||
│ - Stores │
|
│ - Stores │
|
||||||
│ invoice data │
|
│ invoice data │
|
||||||
│ - Updates │
|
│ - Updates │
|
||||||
│ workflow │
|
│ workflow │
|
||||||
|
│ - Moves to │
|
||||||
|
│ Step 8 │
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
Backup (if webhook fails):
|
||||||
|
┌─────────────────┐
|
||||||
|
│ RE-Flow System │
|
||||||
|
│ │
|
||||||
|
│ GET /api/invoices/status/{request_number}
|
||||||
|
│ │
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ DMS System │
|
||||||
|
│ │
|
||||||
|
│ Returns current │
|
||||||
|
│ invoice status │
|
||||||
|
│ and details │
|
||||||
└─────────────────┘
|
└─────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -296,13 +478,14 @@ DMS supports two authentication methods:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ RE Workflow │
|
│ RE-Flow System │
|
||||||
│ System │
|
|
||||||
│ (Step 8) │
|
│ (Step 8) │
|
||||||
└────────┬────────┘
|
└────────┬────────┘
|
||||||
│
|
│
|
||||||
│ POST /api/credit-notes/generate
|
│ POST /api/credit-notes/generate
|
||||||
│ { e_invoice_number, request_number, amount, ... }
|
│ { e_invoice_number, request_number,
|
||||||
|
│ activity_name, claim_amount,
|
||||||
|
│ item_code_no, ... }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
@ -310,21 +493,55 @@ DMS supports two authentication methods:
|
|||||||
│ │
|
│ │
|
||||||
│ - Validates │
|
│ - Validates │
|
||||||
│ invoice │
|
│ invoice │
|
||||||
│ - Generates │
|
│ - Queues for │
|
||||||
│ Credit Note │
|
│ processing │
|
||||||
|
│ │
|
||||||
|
│ Response: │
|
||||||
|
│ { success: true }│
|
||||||
└────────┬────────┘
|
└────────┬────────┘
|
||||||
│
|
│
|
||||||
│ Response: { credit_note_number, credit_note_date, ... }
|
│ (Asynchronous Processing)
|
||||||
|
│
|
||||||
|
│ - Uses Item Code No from invoice
|
||||||
|
│ - Calculates GST
|
||||||
|
│ - Generates Credit Note
|
||||||
|
│ - Gets IRN from GST
|
||||||
|
│ - Gets SAP Credit Note No
|
||||||
|
│
|
||||||
|
│ POST /api/v1/webhooks/dms/credit-note
|
||||||
|
│ { document_no, item_code_no,
|
||||||
|
│ hsn_sac_code, tax details,
|
||||||
|
│ irn_no, sap_credit_note_no,
|
||||||
|
│ credit_note_file_path, ... }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ RE Workflow │
|
│ RE-Flow System │
|
||||||
│ System │
|
|
||||||
│ │
|
│ │
|
||||||
|
│ - Receives │
|
||||||
|
│ webhook │
|
||||||
│ - Stores │
|
│ - Stores │
|
||||||
│ credit note │
|
│ credit note │
|
||||||
│ - Updates │
|
│ - Updates │
|
||||||
│ workflow │
|
│ workflow │
|
||||||
|
│ - Completes │
|
||||||
|
│ request │
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
Backup (if webhook fails):
|
||||||
|
┌─────────────────┐
|
||||||
|
│ RE-Flow System │
|
||||||
|
│ │
|
||||||
|
│ GET /api/credit-notes/status/{request_number}
|
||||||
|
│ │
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ DMS System │
|
||||||
|
│ │
|
||||||
|
│ Returns current │
|
||||||
|
│ credit note │
|
||||||
|
│ status and │
|
||||||
|
│ details │
|
||||||
└─────────────────┘
|
└─────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -332,27 +549,37 @@ DMS supports two authentication methods:
|
|||||||
|
|
||||||
## Data Mapping
|
## Data Mapping
|
||||||
|
|
||||||
### RE Workflow System → DMS
|
### RE-Flow System → DMS (API Request)
|
||||||
|
|
||||||
| RE Workflow Field | DMS Request Field | Notes |
|
| RE-Flow Field | DMS Request Field | Notes |
|
||||||
|-------------------|-------------------|-------|
|
|----------------|-------------------|-------|
|
||||||
| `request.requestNumber` | `request_number` | Direct mapping |
|
| `request.requestNumber` | `request_number` | Direct mapping |
|
||||||
| `claimDetails.dealerCode` | `dealer_code` | Direct mapping |
|
| `claimDetails.dealerCode` | `dealer_code` | Direct mapping |
|
||||||
| `claimDetails.dealerName` | `dealer_name` | Direct mapping |
|
| `claimDetails.dealerName` | `dealer_name` | Direct mapping |
|
||||||
| `budgetTracking.closedExpenses` | `amount` | Total claim amount |
|
| `claimDetails.activityName` | `activity_name` | Must match predefined Activity Types |
|
||||||
|
| `claimDetails.activityDescription` | `activity_description` | Direct mapping |
|
||||||
|
| `budgetTracking.closedExpenses` | `claim_amount` | Total claim amount (before taxes) |
|
||||||
| `internalOrder.ioNumber` | `io_number` | Optional, if available |
|
| `internalOrder.ioNumber` | `io_number` | Optional, if available |
|
||||||
| `claimInvoice.invoiceNumber` | `e_invoice_number` | For credit note only |
|
| `itemCodeNo` (determined by DMS) | `item_code_no` | Included in payload. DMS determines this based on `activity_name` mapping. Used by DMS for GST calculation. |
|
||||||
|
| `claimInvoice.invoiceNumber` | `e_invoice_number` | For credit note request only |
|
||||||
|
|
||||||
### DMS → RE Workflow System
|
### DMS → RE-Flow System (Webhook Response)
|
||||||
|
|
||||||
| DMS Response Field | RE Workflow Field | Notes |
|
**Note:** All invoice and credit note details are sent via webhook (see DMS_WEBHOOK_API.md), not in the API response.
|
||||||
|-------------------|-------------------|-------|
|
|
||||||
| `e_invoice_number` | `ClaimInvoice.invoiceNumber` | Stored in database |
|
| DMS Webhook Field | RE-Flow Database Field | Table | Notes |
|
||||||
| `dms_number` | `ClaimInvoice.dmsNumber` | Stored in database |
|
|-------------------|------------------------|-------|-------|
|
||||||
| `invoice_date` | `ClaimInvoice.invoiceDate` | Converted to Date object |
|
| `document_no` | `invoice_number` / `credit_note_number` | `claim_invoices` / `claim_credit_notes` | Generated by DMS |
|
||||||
| `credit_note_number` | `ClaimCreditNote.creditNoteNumber` | Stored in database |
|
| `document_date` | `invoice_date` / `credit_note_date` | `claim_invoices` / `claim_credit_notes` | Converted to Date object |
|
||||||
| `credit_note_date` | `ClaimCreditNote.creditNoteDate` | Converted to Date object |
|
| `total_amount` | `invoice_amount` / `credit_amount` | `claim_invoices` / `claim_credit_notes` | Includes taxes |
|
||||||
| `credit_note_amount` | `ClaimCreditNote.creditNoteAmount` | Stored in database |
|
| `invoice_file_path` | `invoice_file_path` | `claim_invoices` | URL/path to PDF |
|
||||||
|
| `credit_note_file_path` | `credit_note_file_path` | `claim_credit_notes` | URL/path to PDF |
|
||||||
|
| `irn_no` | Stored in `description` field | Both tables | From GST portal |
|
||||||
|
| `sap_credit_note_no` | `sap_document_number` | `claim_credit_notes` | From SAP system |
|
||||||
|
| `item_code_no` | Stored in `description` field | Both tables | Provided by DMS based on activity |
|
||||||
|
| `hsn_sac_code` | Stored in `description` field | Both tables | Provided by DMS |
|
||||||
|
| `cgst_amount`, `sgst_amount`, `igst_amount` | Stored in `description` field | Both tables | Tax breakdown |
|
||||||
|
| `credit_type` | Stored in `description` field | `claim_credit_notes` | "GST" or "Commercial Credit" |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -373,8 +600,11 @@ When DMS is not configured, the system operates in **mock mode**:
|
|||||||
"request_number": "REQ-TEST-001",
|
"request_number": "REQ-TEST-001",
|
||||||
"dealer_code": "TEST-DLR-001",
|
"dealer_code": "TEST-DLR-001",
|
||||||
"dealer_name": "Test Dealer",
|
"dealer_name": "Test Dealer",
|
||||||
"amount": 10000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"description": "Test invoice generation"
|
"activity_description": "Test invoice generation for marketing activity",
|
||||||
|
"claim_amount": 10000.00,
|
||||||
|
"io_number": "IO-TEST-001",
|
||||||
|
"item_code_no": "ITEM-001"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -385,9 +615,11 @@ When DMS is not configured, the system operates in **mock mode**:
|
|||||||
"e_invoice_number": "EINV-TEST-001",
|
"e_invoice_number": "EINV-TEST-001",
|
||||||
"dealer_code": "TEST-DLR-001",
|
"dealer_code": "TEST-DLR-001",
|
||||||
"dealer_name": "Test Dealer",
|
"dealer_name": "Test Dealer",
|
||||||
"amount": 10000.00,
|
"activity_name": "Marketing Cost – Bike to Vendor",
|
||||||
"reason": "Test credit note",
|
"activity_description": "Test credit note generation for marketing activity",
|
||||||
"description": "Test credit note generation"
|
"claim_amount": 10000.00,
|
||||||
|
"io_number": "IO-TEST-001",
|
||||||
|
"item_code_no": "ITEM-001"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -395,17 +627,43 @@ When DMS is not configured, the system operates in **mock mode**:
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
1. **Idempotency**: Both APIs should be idempotent - calling them multiple times with the same data should not create duplicates.
|
1. **Asynchronous Processing**: Invoice and credit note generation happens asynchronously. DMS should:
|
||||||
|
- Accept the request immediately and return a success acknowledgment
|
||||||
|
- Process the invoice/credit note in the background
|
||||||
|
- Send complete details via webhook once processing is complete
|
||||||
|
|
||||||
2. **Amount Validation**: DMS should validate that credit note amount matches or is less than the original invoice amount.
|
2. **Activity Type to Item Code No Mapping**:
|
||||||
|
- DMS Team must provide **Item Code No** mapping for each predefined Activity Type
|
||||||
|
- This mapping should be configured in DMS system
|
||||||
|
- RE-Flow includes `item_code_no` in the request payload (determined by DMS based on `activity_name` mapping)
|
||||||
|
- DMS uses Item Code No to determine HSN/SAC code and calculate GST (CGST/SGST/IGST percentages and amounts)
|
||||||
|
- DMS returns `item_code_no` in the webhook response for verification
|
||||||
|
|
||||||
3. **Invoice Dependency**: Credit note generation requires a valid e-invoice to exist in DMS first.
|
3. **Tax Calculation**: DMS is responsible for:
|
||||||
|
- Determining CGST/SGST/IGST percentages based on dealer location and activity type
|
||||||
|
- Calculating tax amounts
|
||||||
|
- Providing HSN/SAC codes
|
||||||
|
|
||||||
4. **Error Handling**: RE Workflow System handles DMS errors gracefully and allows manual entry if DMS is unavailable.
|
4. **Amount Validation**: DMS should validate that credit note amount matches or is less than the original invoice amount.
|
||||||
|
|
||||||
5. **Retry Logic**: Consider implementing retry logic for transient DMS failures.
|
5. **Invoice Dependency**: Credit note generation requires a valid e-invoice to exist in DMS first.
|
||||||
|
|
||||||
6. **Webhooks** (Optional): DMS can send webhooks to notify RE Workflow System when invoice/credit note status changes.
|
6. **Error Handling**: RE-Flow System handles DMS errors gracefully and allows manual entry if DMS is unavailable.
|
||||||
|
|
||||||
|
7. **Retry Logic**: Consider implementing retry logic for transient DMS failures.
|
||||||
|
|
||||||
|
8. **Webhooks (Primary Method)**: DMS **MUST** send webhooks to notify RE-Flow System when invoice/credit note processing is complete. See DMS_WEBHOOK_API.md for webhook specifications. This is the **primary method** for status verification.
|
||||||
|
|
||||||
|
9. **Status Check API (Backup Method)**: If webhook delivery fails, RE-Flow can use the backup status check API to verify invoice/credit note generation status. See section "Backup: Status Check API" above.
|
||||||
|
|
||||||
|
10. **IRN Generation**: DMS should generate IRN (Invoice Reference Number) from GST portal and include it in the webhook response.
|
||||||
|
|
||||||
|
11. **SAP Integration**: For credit notes, DMS should generate SAP Credit Note Number and include it in the webhook response.
|
||||||
|
|
||||||
|
12. **Webhook URL Configuration**: DMS must be configured with RE-Flow webhook URLs:
|
||||||
|
- Invoice Webhook: `POST /api/v1/webhooks/dms/invoice`
|
||||||
|
- Credit Note Webhook: `POST /api/v1/webhooks/dms/credit-note`
|
||||||
|
- See DMS_WEBHOOK_API.md for complete webhook specifications
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -418,6 +676,20 @@ For issues or questions regarding DMS integration:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** December 17, 2025
|
**Last Updated:** December 19, 2025
|
||||||
**Version:** 1.0
|
**Version:** 2.0
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### Version 2.0 (December 19, 2025)
|
||||||
|
- Added clear breakdown of inputs from RE-Flow vs DMS Team
|
||||||
|
- Added predefined Activity Types list
|
||||||
|
- Updated request/response structure to reflect asynchronous processing
|
||||||
|
- Clarified that detailed responses come via webhook, not API response
|
||||||
|
- Updated field names to match actual implementation (`claim_amount` instead of `amount`, `activity_name`, `activity_description`)
|
||||||
|
- Added notes about Item Code No mapping requirement for DMS Team
|
||||||
|
- Updated data mapping section with webhook fields
|
||||||
|
|
||||||
|
### Version 1.0 (December 17, 2025)
|
||||||
|
- Initial documentation
|
||||||
|
|
||||||
|
|||||||
299
docs/STEP3_APPROVER_ANALYSIS.md
Normal file
299
docs/STEP3_APPROVER_ANALYSIS.md
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# Step 3 (Department Lead Approval) - User Addition Flow Analysis
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document analyzes how Step 3 approvers (Department Lead) are added to the dealer claim workflow, covering both frontend and backend implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Implementation
|
||||||
|
|
||||||
|
### 1. Request Creation Flow (`dealerClaim.service.ts`)
|
||||||
|
|
||||||
|
#### Entry Point: `createClaimRequest()`
|
||||||
|
- **Location**: `Re_Backend/src/services/dealerClaim.service.ts:37`
|
||||||
|
- **Parameters**:
|
||||||
|
- `userId`: Initiator's user ID
|
||||||
|
- `claimData`: Includes optional `selectedManagerEmail` for user selection
|
||||||
|
|
||||||
|
#### Step 3 Approver Resolution Process:
|
||||||
|
|
||||||
|
**Phase 1: Pre-Validation (Before Creating Records)**
|
||||||
|
```typescript
|
||||||
|
// Lines 67-87: Resolve Department Lead BEFORE creating workflow
|
||||||
|
let departmentLead: User | null = null;
|
||||||
|
|
||||||
|
if (claimData.selectedManagerEmail) {
|
||||||
|
// User selected a manager from multiple options
|
||||||
|
departmentLead = await this.userService.ensureUserExists({
|
||||||
|
email: claimData.selectedManagerEmail,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Search Okta using manager displayName from initiator's user record
|
||||||
|
departmentLead = await this.resolveDepartmentLeadFromManager(initiator);
|
||||||
|
|
||||||
|
// If no manager found, throw error BEFORE creating any records
|
||||||
|
if (!departmentLead) {
|
||||||
|
throw new Error(`No reporting manager found...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2: Approval Level Creation**
|
||||||
|
```typescript
|
||||||
|
// Line 136: Create approval levels with pre-resolved department lead
|
||||||
|
await this.createClaimApprovalLevels(
|
||||||
|
workflowRequest.requestId,
|
||||||
|
userId,
|
||||||
|
claimData.dealerEmail,
|
||||||
|
claimData.selectedManagerEmail,
|
||||||
|
departmentLead // Pre-resolved to avoid re-searching
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Approval Level Creation (`createClaimApprovalLevels()`)
|
||||||
|
|
||||||
|
#### Location: `Re_Backend/src/services/dealerClaim.service.ts:253`
|
||||||
|
|
||||||
|
#### Step 3 Configuration:
|
||||||
|
```typescript
|
||||||
|
// Lines 310-318: Step 3 definition
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
name: 'Department Lead Approval',
|
||||||
|
tatHours: 72,
|
||||||
|
isAuto: false,
|
||||||
|
approverType: 'department_lead' as const,
|
||||||
|
approverId: departmentLead?.userId || null,
|
||||||
|
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@royalenfield.com',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Approver Resolution Logic:
|
||||||
|
```typescript
|
||||||
|
// Lines 405-417: Department Lead resolution
|
||||||
|
else if (step.approverType === 'department_lead') {
|
||||||
|
if (finalDepartmentLead) {
|
||||||
|
approverId = finalDepartmentLead.userId;
|
||||||
|
approverName = finalDepartmentLead.displayName || finalDepartmentLead.email || 'Department Lead';
|
||||||
|
approverEmail = finalDepartmentLead.email;
|
||||||
|
} else {
|
||||||
|
// This should never happen as we validate manager before creating records
|
||||||
|
throw new Error('Department lead not found...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Database Record Creation:
|
||||||
|
```typescript
|
||||||
|
// Lines 432-454: Create ApprovalLevel record
|
||||||
|
await ApprovalLevel.create({
|
||||||
|
requestId,
|
||||||
|
levelNumber: 3,
|
||||||
|
levelName: 'Department Lead Approval',
|
||||||
|
approverId: approverId, // Department Lead's userId
|
||||||
|
approverEmail,
|
||||||
|
approverName,
|
||||||
|
tatHours: 72,
|
||||||
|
status: ApprovalStatus.PENDING, // Will be activated when Step 2 is approved
|
||||||
|
isFinalApprover: false,
|
||||||
|
// ... other fields
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Department Lead Resolution Methods
|
||||||
|
|
||||||
|
#### Method 1: `resolveDepartmentLeadFromManager()` (Primary)
|
||||||
|
- **Location**: `Re_Backend/src/services/dealerClaim.service.ts:622`
|
||||||
|
- **Flow**:
|
||||||
|
1. Get `manager` displayName from initiator's User record
|
||||||
|
2. Search Okta directory by displayName using `userService.searchOktaByDisplayName()`
|
||||||
|
3. **If 0 matches**: Return `null` (fallback to legacy method)
|
||||||
|
4. **If 1 match**: Create user in DB if needed, return User object
|
||||||
|
5. **If multiple matches**: Throw error with `MULTIPLE_MANAGERS_FOUND` code and list of managers
|
||||||
|
|
||||||
|
#### Method 2: `resolveDepartmentLead()` (Fallback/Legacy)
|
||||||
|
- **Location**: `Re_Backend/src/services/dealerClaim.service.ts:699`
|
||||||
|
- **Priority Order**:
|
||||||
|
1. User with `MANAGEMENT` role in same department
|
||||||
|
2. User with designation containing "Lead"/"Head"/"Manager" in same department
|
||||||
|
3. User matching `initiator.manager` email field
|
||||||
|
4. Any user in same department (excluding initiator)
|
||||||
|
5. Any user with "Department Lead" designation (across all departments)
|
||||||
|
6. Any user with `MANAGEMENT` role (across all departments)
|
||||||
|
7. Any user with `ADMIN` role (across all departments)
|
||||||
|
|
||||||
|
### 4. Participant Creation
|
||||||
|
|
||||||
|
#### Location: `Re_Backend/src/services/dealerClaim.service.ts:463`
|
||||||
|
- Department Lead is automatically added as a participant when approval levels are created
|
||||||
|
- Participant type: `APPROVER`
|
||||||
|
- Allows department lead to view, comment, and approve the request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Implementation
|
||||||
|
|
||||||
|
### 1. Request Creation (`ClaimManagementWizard.tsx`)
|
||||||
|
|
||||||
|
#### Location: `Re_Figma_Code/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx`
|
||||||
|
|
||||||
|
#### Current Implementation:
|
||||||
|
- **No UI for selecting Step 3 approver during creation**
|
||||||
|
- Step 3 approver is automatically resolved by backend based on:
|
||||||
|
- Initiator's manager field
|
||||||
|
- Department hierarchy
|
||||||
|
- Role-based lookup
|
||||||
|
|
||||||
|
#### Form Data Structure:
|
||||||
|
```typescript
|
||||||
|
// Lines 61-75: Form data structure
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
activityName: '',
|
||||||
|
activityType: '',
|
||||||
|
dealerCode: '',
|
||||||
|
// ... other fields
|
||||||
|
// Note: No selectedManagerEmail field in wizard
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Submission:
|
||||||
|
```typescript
|
||||||
|
// Lines 152-216: handleSubmit()
|
||||||
|
const claimData = {
|
||||||
|
...formData,
|
||||||
|
templateType: 'claim-management',
|
||||||
|
// selectedManagerEmail is NOT included in current wizard
|
||||||
|
// Backend will auto-resolve department lead
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Request Detail View (`RequestDetail.tsx`)
|
||||||
|
|
||||||
|
#### Location: `Re_Figma_Code/src/dealer-claim/pages/RequestDetail.tsx`
|
||||||
|
|
||||||
|
#### Step 3 Approver Detection:
|
||||||
|
```typescript
|
||||||
|
// Lines 147-173: Finding Step 3 approver
|
||||||
|
const step3Level = approvalFlow.find((level: any) =>
|
||||||
|
(level.step || level.levelNumber || level.level_number) === 3
|
||||||
|
) || approvals.find((level: any) =>
|
||||||
|
(level.levelNumber || level.level_number) === 3
|
||||||
|
);
|
||||||
|
|
||||||
|
const deptLeadUserId = step3Level?.approverId || step3Level?.approver_id || step3Level?.approver?.userId;
|
||||||
|
const deptLeadEmail = (step3Level?.approverEmail || '').toLowerCase().trim();
|
||||||
|
|
||||||
|
// User is department lead if they match Step 3 approver
|
||||||
|
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||||
|
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add Approver Functionality:
|
||||||
|
- **Lines 203-217, 609, 621, 688, 701, 711**: References to `handleAddApprover` and `AddApproverModal`
|
||||||
|
- **Note**: This appears to be generic approver addition (for other workflow types), not specifically for Step 3
|
||||||
|
- Step 3 approver is **fixed** and cannot be changed after request creation
|
||||||
|
|
||||||
|
### 3. Workflow Tab (`WorkflowTab.tsx`)
|
||||||
|
|
||||||
|
#### Location: `Re_Figma_Code/src/dealer-claim/components/request-detail/WorkflowTab.tsx`
|
||||||
|
|
||||||
|
#### Step 3 Action Button Visibility:
|
||||||
|
```typescript
|
||||||
|
// Lines 1109-1126: Step 3 approval button
|
||||||
|
{step.step === 3 && (() => {
|
||||||
|
// Find step 3 from approvalFlow to get approverEmail
|
||||||
|
const step3Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 3);
|
||||||
|
const step3ApproverEmail = (step3Level?.approverEmail || '').toLowerCase();
|
||||||
|
const isStep3ApproverByEmail = step3ApproverEmail && userEmail === step3ApproverEmail;
|
||||||
|
return isStep3ApproverByEmail || isStep3Approver || isCurrentApprover;
|
||||||
|
})() && (
|
||||||
|
<Button onClick={() => setShowIOApprovalModal(true)}>
|
||||||
|
Approve and Organise IO
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3 Approval Handler:
|
||||||
|
```typescript
|
||||||
|
// Lines 535-583: handleIOApproval()
|
||||||
|
// 1. Finds Step 3 levelId from approval levels
|
||||||
|
// 2. Updates IO details (ioNumber, ioRemark)
|
||||||
|
// 3. Approves Step 3 using approveLevel() API
|
||||||
|
// 4. Moves workflow to Step 4 (auto-processed)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
### Current Flow Summary:
|
||||||
|
|
||||||
|
1. **Request Creation**:
|
||||||
|
- User creates claim request via `ClaimManagementWizard`
|
||||||
|
- **No UI for selecting Step 3 approver**
|
||||||
|
- Backend automatically resolves department lead using:
|
||||||
|
- Initiator's `manager` displayName → Okta search
|
||||||
|
- Fallback to legacy resolution methods
|
||||||
|
|
||||||
|
2. **Multiple Managers Scenario**:
|
||||||
|
- If Okta search returns multiple managers:
|
||||||
|
- Backend throws `MULTIPLE_MANAGERS_FOUND` error
|
||||||
|
- Error includes list of manager options
|
||||||
|
- **Frontend needs to handle this** (currently not implemented in wizard)
|
||||||
|
|
||||||
|
3. **Approval Level Creation**:
|
||||||
|
- Step 3 approver is **fixed** at request creation
|
||||||
|
- Stored in `ApprovalLevel` table with:
|
||||||
|
- `levelNumber: 3`
|
||||||
|
- `approverId`: Department Lead's userId
|
||||||
|
- `approverEmail`: Department Lead's email
|
||||||
|
- `status: PENDING` (activated when Step 2 is approved)
|
||||||
|
|
||||||
|
4. **After Request Creation**:
|
||||||
|
- Step 3 approver **cannot be changed** via UI
|
||||||
|
- Generic `AddApproverModal` exists but is not used for Step 3
|
||||||
|
- Step 3 approver is determined by backend logic only
|
||||||
|
|
||||||
|
### Limitations:
|
||||||
|
|
||||||
|
1. **No User Selection During Creation**:
|
||||||
|
- Wizard doesn't allow user to select/override Step 3 approver
|
||||||
|
- If multiple managers found, error handling not implemented in frontend
|
||||||
|
|
||||||
|
2. **No Post-Creation Modification**:
|
||||||
|
- No UI to change Step 3 approver after request is created
|
||||||
|
- Would require backend API to update `ApprovalLevel.approverId`
|
||||||
|
|
||||||
|
3. **Fixed Resolution Logic**:
|
||||||
|
- Department lead resolution is hardcoded in backend
|
||||||
|
- No configuration or override mechanism
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Potential Enhancement Areas
|
||||||
|
|
||||||
|
1. **Frontend**: Add manager selection UI in wizard when multiple managers found
|
||||||
|
2. **Frontend**: Add "Change Approver" option for Step 3 (if allowed by business rules)
|
||||||
|
3. **Backend**: Add API endpoint to update Step 3 approver after request creation
|
||||||
|
4. **Backend**: Add configuration for department lead resolution rules
|
||||||
|
5. **Both**: Handle `MULTIPLE_MANAGERS_FOUND` error gracefully in frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
### Backend:
|
||||||
|
- `Re_Backend/src/services/dealerClaim.service.ts` - Main service
|
||||||
|
- `Re_Backend/src/controllers/dealerClaim.controller.ts` - API endpoints
|
||||||
|
- `Re_Backend/src/services/user.service.ts` - User/Okta integration
|
||||||
|
- `Re_Backend/src/models/ApprovalLevel.ts` - Database model
|
||||||
|
|
||||||
|
### Frontend:
|
||||||
|
- `Re_Figma_Code/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx` - Request creation
|
||||||
|
- `Re_Figma_Code/src/dealer-claim/pages/RequestDetail.tsx` - Request detail view
|
||||||
|
- `Re_Figma_Code/src/dealer-claim/components/request-detail/WorkflowTab.tsx` - Workflow display
|
||||||
|
- `Re_Figma_Code/src/dealer-claim/components/request-detail/modals/DeptLeadIOApprovalModal.tsx` - Step 3 approval modal
|
||||||
|
|
||||||
|
### Documentation:
|
||||||
|
- `Re_Backend/docs/CLAIM_MANAGEMENT_APPROVER_MAPPING.md` - Approver mapping rules
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export class DealerClaimController {
|
|||||||
periodStartDate,
|
periodStartDate,
|
||||||
periodEndDate,
|
periodEndDate,
|
||||||
estimatedBudget,
|
estimatedBudget,
|
||||||
selectedManagerEmail, // Optional: When multiple managers found, user selects one
|
approvers, // Array of approvers for all 8 steps
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
@ -62,7 +62,7 @@ export class DealerClaimController {
|
|||||||
periodStartDate: periodStartDate ? new Date(periodStartDate) : undefined,
|
periodStartDate: periodStartDate ? new Date(periodStartDate) : undefined,
|
||||||
periodEndDate: periodEndDate ? new Date(periodEndDate) : undefined,
|
periodEndDate: periodEndDate ? new Date(periodEndDate) : undefined,
|
||||||
estimatedBudget: estimatedBudget ? parseFloat(estimatedBudget) : undefined,
|
estimatedBudget: estimatedBudget ? parseFloat(estimatedBudget) : undefined,
|
||||||
selectedManagerEmail, // Pass selected manager email if provided
|
approvers: approvers || [], // Pass approvers array for all 8 steps
|
||||||
});
|
});
|
||||||
|
|
||||||
return ResponseHandler.success(res, {
|
return ResponseHandler.success(res, {
|
||||||
@ -70,36 +70,10 @@ export class DealerClaimController {
|
|||||||
message: 'Claim request created successfully'
|
message: 'Claim request created successfully'
|
||||||
}, 'Claim request created');
|
}, 'Claim request created');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle multiple managers found error
|
// Handle approver validation errors
|
||||||
if (error.code === 'MULTIPLE_MANAGERS_FOUND') {
|
if (error.message && error.message.includes('Approver')) {
|
||||||
const response: any = {
|
logger.warn('[DealerClaimController] Approver validation error:', { message: error.message });
|
||||||
success: false,
|
return ResponseHandler.error(res, error.message, 400);
|
||||||
message: 'Multiple reporting managers found. Please select one.',
|
|
||||||
error: {
|
|
||||||
code: 'MULTIPLE_MANAGERS_FOUND',
|
|
||||||
managers: error.managers || []
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
logger.warn('[DealerClaimController] Multiple managers found:', { managers: error.managers });
|
|
||||||
res.status(400).json(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle no manager found error
|
|
||||||
if (error.code === 'NO_MANAGER_FOUND') {
|
|
||||||
const response: any = {
|
|
||||||
success: false,
|
|
||||||
message: error.message || 'No reporting manager found. Please ensure your manager is correctly configured in the system.',
|
|
||||||
error: {
|
|
||||||
code: 'NO_MANAGER_FOUND',
|
|
||||||
message: error.message
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
logger.warn('[DealerClaimController] No manager found:', { message: error.message });
|
|
||||||
res.status(400).json(response);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
@ -648,6 +622,16 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0;
|
const blockAmount = blockedAmount ? parseFloat(blockedAmount) : 0;
|
||||||
|
|
||||||
|
// Log received data for debugging
|
||||||
|
logger.info('[DealerClaimController] updateIODetails received:', {
|
||||||
|
requestId,
|
||||||
|
ioNumber,
|
||||||
|
availableBalance,
|
||||||
|
blockedAmount: blockAmount,
|
||||||
|
receivedBlockedAmount: blockedAmount, // Original value from request
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
// Store in database when blocking amount > 0 OR when ioNumber and ioRemark are provided (for Step 3 approval)
|
// Store in database when blocking amount > 0 OR when ioNumber and ioRemark are provided (for Step 3 approval)
|
||||||
if (blockAmount > 0) {
|
if (blockAmount > 0) {
|
||||||
if (availableBalance === undefined) {
|
if (availableBalance === undefined) {
|
||||||
@ -656,15 +640,19 @@ export class DealerClaimController {
|
|||||||
|
|
||||||
// Don't pass remainingBalance - let the service calculate it from SAP's response
|
// Don't pass remainingBalance - let the service calculate it from SAP's response
|
||||||
// This ensures we always use the actual remaining balance from SAP after blocking
|
// This ensures we always use the actual remaining balance from SAP after blocking
|
||||||
|
const ioData = {
|
||||||
|
ioNumber,
|
||||||
|
ioRemark: ioRemark || '',
|
||||||
|
availableBalance: parseFloat(availableBalance),
|
||||||
|
blockedAmount: blockAmount,
|
||||||
|
// remainingBalance will be calculated by the service from SAP's response
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('[DealerClaimController] Calling updateIODetails service with:', ioData);
|
||||||
|
|
||||||
await this.dealerClaimService.updateIODetails(
|
await this.dealerClaimService.updateIODetails(
|
||||||
requestId,
|
requestId,
|
||||||
{
|
ioData,
|
||||||
ioNumber,
|
|
||||||
ioRemark: ioRemark || '',
|
|
||||||
availableBalance: parseFloat(availableBalance),
|
|
||||||
blockedAmount: blockAmount,
|
|
||||||
// remainingBalance will be calculated by the service from SAP's response
|
|
||||||
},
|
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -427,31 +427,49 @@ export class ApprovalService {
|
|||||||
|
|
||||||
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||||
|
|
||||||
// Check if this is Step 3 approval in a claim management workflow, and next level is Step 4 (auto-step)
|
// Check if this is Department Lead approval in a claim management workflow, and next level is Activity Creation (auto-step)
|
||||||
const workflowType = (wf as any)?.workflowType;
|
const workflowType = (wf as any)?.workflowType;
|
||||||
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
||||||
const isStep3Approval = level.levelNumber === 3;
|
|
||||||
const isStep4Next = nextLevelNumber === 4;
|
|
||||||
const isStep6Approval = level.levelNumber === 6;
|
|
||||||
const isStep7Next = nextLevelNumber === 7;
|
|
||||||
|
|
||||||
if (isClaimManagement && isStep3Approval && isStep4Next && nextLevel) {
|
// Check if current level is Department Lead (by levelName, not hardcoded step number)
|
||||||
// Step 4 is an auto-step - process it automatically
|
const currentLevelName = (level.levelName || '').toLowerCase();
|
||||||
logger.info(`[Approval] Step 3 approved for claim management workflow. Auto-processing Step 4: Activity Creation`);
|
const isDeptLeadApproval = currentLevelName.includes('department lead') || level.levelNumber === 3;
|
||||||
|
|
||||||
|
// Check if next level is Activity Creation (by levelName or approverEmail, not hardcoded step number)
|
||||||
|
const nextLevelName = (nextLevel?.levelName || '').toLowerCase();
|
||||||
|
const nextLevelEmail = (nextLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isActivityCreationNext = nextLevelName.includes('activity creation') ||
|
||||||
|
(nextLevelEmail === 'system@royalenfield.com' && nextLevelNumber > level.levelNumber);
|
||||||
|
|
||||||
|
// Check if current level is Requestor Claim Approval (Step 6) and next is E-Invoice Generation (Step 7)
|
||||||
|
const currentLevelNameForStep6 = (level.levelName || '').toLowerCase();
|
||||||
|
const isRequestorClaimApproval = currentLevelNameForStep6.includes('requestor') &&
|
||||||
|
(currentLevelNameForStep6.includes('claim') || currentLevelNameForStep6.includes('approval')) ||
|
||||||
|
level.levelNumber === 6;
|
||||||
|
|
||||||
|
const nextLevelNameForStep7 = (nextLevel?.levelName || '').toLowerCase();
|
||||||
|
const nextLevelEmailForStep7 = (nextLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isEInvoiceGenerationNext = nextLevelNameForStep7.includes('e-invoice') ||
|
||||||
|
nextLevelNameForStep7.includes('invoice generation') ||
|
||||||
|
(nextLevelEmailForStep7 === 'system@royalenfield.com' && nextLevelNumber > level.levelNumber);
|
||||||
|
|
||||||
|
if (isClaimManagement && isDeptLeadApproval && isActivityCreationNext && nextLevel) {
|
||||||
|
// Activity Creation is an auto-step - process it automatically
|
||||||
|
logger.info(`[Approval] Department Lead approved for claim management workflow. Auto-processing Activity Creation (Level ${nextLevelNumber})`);
|
||||||
try {
|
try {
|
||||||
const dealerClaimService = new DealerClaimService();
|
const dealerClaimService = new DealerClaimService();
|
||||||
await dealerClaimService.processActivityCreation(level.requestId);
|
await dealerClaimService.processActivityCreation(level.requestId);
|
||||||
logger.info(`[Approval] Step 4 auto-processing completed for request ${level.requestId}`);
|
logger.info(`[Approval] Activity Creation auto-processing completed for request ${level.requestId}`);
|
||||||
} catch (step4Error) {
|
} catch (step4Error) {
|
||||||
logger.error(`[Approval] Error auto-processing Step 4 for request ${level.requestId}:`, step4Error);
|
logger.error(`[Approval] Error auto-processing Activity Creation for request ${level.requestId}:`, step4Error);
|
||||||
// Don't fail the Step 3 approval if Step 4 processing fails - log and continue
|
// Don't fail the Department Lead approval if Activity Creation processing fails - log and continue
|
||||||
}
|
}
|
||||||
} else if (isClaimManagement && isStep6Approval && isStep7Next && nextLevel && nextLevel.approverEmail === 'system@royalenfield.com') {
|
} else if (isClaimManagement && isRequestorClaimApproval && isEInvoiceGenerationNext && nextLevel && nextLevel.approverEmail === 'system@royalenfield.com') {
|
||||||
// Step 7 is an auto-step - activate it but don't process invoice generation
|
// E-Invoice Generation is an auto-step - activate it but don't process invoice generation
|
||||||
// Invoice generation will be handled by DMS webhook when invoice is created
|
// Invoice generation will be handled by DMS webhook when invoice is created
|
||||||
logger.info(`[Approval] Step 6 approved for claim management workflow. Step 7 activated. Waiting for DMS webhook to generate invoice.`);
|
logger.info(`[Approval] Requestor Claim Approval approved for claim management workflow. E-Invoice Generation (Level ${nextLevelNumber}) activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
// Step 7 will remain in IN_PROGRESS until webhook creates invoice and auto-approves it
|
// E-Invoice Generation will remain in IN_PROGRESS until webhook creates invoice and auto-approves it
|
||||||
// Continue with normal flow to activate Step 7
|
// Continue with normal flow to activate E-Invoice Generation
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wf && nextLevel) {
|
if (wf && nextLevel) {
|
||||||
@ -529,19 +547,20 @@ export class ApprovalService {
|
|||||||
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
|
logger.info(`[Approval] Skipping notification for auto-step at level ${nextLevelNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify initiator when dealer submits documents (Step 1 or Step 5 approval in claim management)
|
// Notify initiator when dealer submits documents (Dealer Proposal or Dealer Completion Documents approval in claim management)
|
||||||
const workflowType = (wf as any)?.workflowType;
|
const workflowType = (wf as any)?.workflowType;
|
||||||
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT';
|
||||||
const isStep1Approval = level.levelNumber === 1;
|
const levelName = (level.levelName || '').toLowerCase();
|
||||||
const isStep5Approval = level.levelNumber === 5;
|
const isDealerProposalApproval = levelName.includes('dealer') && levelName.includes('proposal') || level.levelNumber === 1;
|
||||||
|
const isDealerCompletionApproval = levelName.includes('dealer') && (levelName.includes('completion') || levelName.includes('documents')) || level.levelNumber === 5;
|
||||||
|
|
||||||
if (isClaimManagement && (isStep1Approval || isStep5Approval) && (wf as any).initiatorId) {
|
if (isClaimManagement && (isDealerProposalApproval || isDealerCompletionApproval) && (wf as any).initiatorId) {
|
||||||
const stepMessage = isStep1Approval
|
const stepMessage = isDealerProposalApproval
|
||||||
? 'Dealer proposal has been submitted and is now under review.'
|
? 'Dealer proposal has been submitted and is now under review.'
|
||||||
: 'Dealer completion documents have been submitted and are now under review.';
|
: 'Dealer completion documents have been submitted and are now under review.';
|
||||||
|
|
||||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||||
title: isStep1Approval ? 'Proposal Submitted' : 'Completion Documents Submitted',
|
title: isDealerProposalApproval ? 'Proposal Submitted' : 'Completion Documents Submitted',
|
||||||
body: `Your claim request "${(wf as any).title}" - ${stepMessage}`,
|
body: `Your claim request "${(wf as any).title}" - ${stepMessage}`,
|
||||||
requestNumber: (wf as any).requestNumber,
|
requestNumber: (wf as any).requestNumber,
|
||||||
requestId: (wf as any).requestId,
|
requestId: (wf as any).requestId,
|
||||||
@ -551,7 +570,7 @@ export class ApprovalService {
|
|||||||
actionRequired: false
|
actionRequired: false
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[Approval] Sent notification to initiator for ${isStep1Approval ? 'Step 1' : 'Step 5'} approval in claim management workflow`);
|
logger.info(`[Approval] Sent notification to initiator for ${isDealerProposalApproval ? 'Dealer Proposal Submission' : 'Dealer Completion Documents'} approval in claim management workflow`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -50,40 +50,29 @@ export class DealerClaimService {
|
|||||||
periodStartDate?: Date;
|
periodStartDate?: Date;
|
||||||
periodEndDate?: Date;
|
periodEndDate?: Date;
|
||||||
estimatedBudget?: number;
|
estimatedBudget?: number;
|
||||||
selectedManagerEmail?: string; // Optional: When multiple managers found, user selects one
|
approvers?: Array<{
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
userId?: string;
|
||||||
|
level: number;
|
||||||
|
tat?: number | string;
|
||||||
|
tatType?: 'hours' | 'days';
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
): Promise<WorkflowRequest> {
|
): Promise<WorkflowRequest> {
|
||||||
try {
|
try {
|
||||||
// Generate request number
|
// Generate request number
|
||||||
const requestNumber = await generateRequestNumber();
|
const requestNumber = await generateRequestNumber();
|
||||||
|
|
||||||
// First, validate that manager can be resolved BEFORE creating any records
|
// Validate initiator
|
||||||
// This ensures no partial records are created if manager is not found
|
|
||||||
const initiator = await User.findByPk(userId);
|
const initiator = await User.findByPk(userId);
|
||||||
if (!initiator) {
|
if (!initiator) {
|
||||||
throw new Error('Initiator not found');
|
throw new Error('Initiator not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve Department Lead/Manager BEFORE creating workflow
|
// Validate approvers array is provided
|
||||||
let departmentLead: User | null = null;
|
if (!claimData.approvers || !Array.isArray(claimData.approvers) || claimData.approvers.length === 0) {
|
||||||
|
throw new Error('Approvers array is required. Please assign approvers for all workflow steps.');
|
||||||
if (claimData.selectedManagerEmail) {
|
|
||||||
// User selected a manager from multiple options
|
|
||||||
logger.info(`[DealerClaimService] Using selected manager email: ${claimData.selectedManagerEmail}`);
|
|
||||||
departmentLead = await this.userService.ensureUserExists({
|
|
||||||
email: claimData.selectedManagerEmail,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Search Okta using manager displayName from initiator's user record
|
|
||||||
departmentLead = await this.resolveDepartmentLeadFromManager(initiator);
|
|
||||||
|
|
||||||
// If no manager found, throw error BEFORE creating any records
|
|
||||||
if (!departmentLead) {
|
|
||||||
const managerDisplayName = initiator.manager || 'Unknown';
|
|
||||||
const error: any = new Error(`No reporting manager found for displayName: "${managerDisplayName}". Please ensure your manager is correctly configured in the system.`);
|
|
||||||
error.code = 'NO_MANAGER_FOUND';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now create workflow request (manager is validated)
|
// Now create workflow request (manager is validated)
|
||||||
@ -131,9 +120,8 @@ export class DealerClaimService {
|
|||||||
currency: 'INR',
|
currency: 'INR',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create 8 approval levels for claim management workflow
|
// Create 8 approval levels for claim management workflow from approvers array
|
||||||
// Pass the already-resolved departmentLead to avoid re-searching
|
await this.createClaimApprovalLevelsFromApprovers(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.approvers || []);
|
||||||
await this.createClaimApprovalLevels(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.selectedManagerEmail, departmentLead);
|
|
||||||
|
|
||||||
// Schedule TAT jobs for Step 1 (Dealer Proposal Submission) - first active step
|
// Schedule TAT jobs for Step 1 (Dealer Proposal Submission) - first active step
|
||||||
// This ensures SLA tracking starts immediately from request creation
|
// This ensures SLA tracking starts immediately from request creation
|
||||||
@ -243,215 +231,144 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create 8-step approval levels for claim management
|
* Create 8-step approval levels for claim management from approvers array
|
||||||
* Maps approvers based on step requirements:
|
* Validates and creates approval levels based on user-provided approvers
|
||||||
* - Step 1 & 5: Dealer (treated as Okta/internal user - synced from Okta)
|
* Similar to custom request flow - all approvers are manually assigned
|
||||||
* - Step 2, 6 & 8: Initiator (requestor) - Step 8 is credit note action taken by initiator
|
|
||||||
* - Step 3: Department Lead/Manager (resolved from initiator's manager displayName via Okta search)
|
|
||||||
* - Step 4 & 7: System (auto-processed)
|
|
||||||
*/
|
*/
|
||||||
private async createClaimApprovalLevels(
|
private async createClaimApprovalLevelsFromApprovers(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
initiatorId: string,
|
initiatorId: string,
|
||||||
dealerEmail?: string,
|
dealerEmail?: string,
|
||||||
selectedManagerEmail?: string,
|
approvers: Array<{
|
||||||
departmentLead?: User | null // Pre-resolved department lead (to avoid re-searching)
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
userId?: string;
|
||||||
|
level: number;
|
||||||
|
tat?: number | string;
|
||||||
|
tatType?: 'hours' | 'days';
|
||||||
|
}> = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const initiator = await User.findByPk(initiatorId);
|
const initiator = await User.findByPk(initiatorId);
|
||||||
if (!initiator) {
|
if (!initiator) {
|
||||||
throw new Error('Initiator not found');
|
throw new Error('Initiator not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use pre-resolved department lead if provided, otherwise resolve it
|
// Step definitions with default TAT
|
||||||
let finalDepartmentLead: User | null = departmentLead || null;
|
const stepDefinitions = [
|
||||||
|
{ level: 1, name: 'Dealer Proposal Submission', defaultTat: 72, isAuto: false },
|
||||||
if (!finalDepartmentLead) {
|
{ level: 2, name: 'Requestor Evaluation', defaultTat: 48, isAuto: false },
|
||||||
if (selectedManagerEmail) {
|
{ level: 3, name: 'Department Lead Approval', defaultTat: 72, isAuto: false },
|
||||||
// User selected a manager from multiple options
|
{ level: 4, name: 'Activity Creation', defaultTat: 1, isAuto: true },
|
||||||
logger.info(`[DealerClaimService] Using selected manager email: ${selectedManagerEmail}`);
|
{ level: 5, name: 'Dealer Completion Documents', defaultTat: 120, isAuto: false },
|
||||||
const managerUser = await this.userService.ensureUserExists({
|
{ level: 6, name: 'Requestor Claim Approval', defaultTat: 48, isAuto: false },
|
||||||
email: selectedManagerEmail,
|
{ level: 7, name: 'E-Invoice Generation', defaultTat: 1, isAuto: true },
|
||||||
});
|
{ level: 8, name: 'Credit Note Confirmation', defaultTat: 48, isAuto: true }, // System/Finance step handled by webhook
|
||||||
finalDepartmentLead = managerUser;
|
|
||||||
} else {
|
|
||||||
// Search Okta using manager displayName from initiator's user record
|
|
||||||
finalDepartmentLead = await this.resolveDepartmentLeadFromManager(initiator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Dealer Proposal Submission (72 hours) - Dealer submits proposal
|
|
||||||
// Step 2: Requestor Evaluation (48 hours) - Initiator evaluates
|
|
||||||
// Step 3: Department Lead Approval (72 hours) - Department Lead/Manager approves and blocks IO
|
|
||||||
// Step 4: Activity Creation (Auto - 1 hour) - System auto-processes
|
|
||||||
// Step 5: Dealer Completion Documents (120 hours) - Dealer submits completion docs
|
|
||||||
// Step 6: Requestor Claim Approval (48 hours) - Initiator approves completion
|
|
||||||
// Step 7: E-Invoice Generation (Auto - 1 hour) - System generates via DMS
|
|
||||||
// Step 8: Credit Note Confirmation (48 hours) - Initiator confirms credit note (FINAL)
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
level: 1,
|
|
||||||
name: 'Dealer Proposal Submission',
|
|
||||||
tatHours: 72,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'dealer' as const,
|
|
||||||
approverEmail: dealerEmail,
|
|
||||||
approverId: null, // Will be resolved by syncing dealer from Okta
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 2,
|
|
||||||
name: 'Requestor Evaluation',
|
|
||||||
tatHours: 48,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'initiator' as const,
|
|
||||||
approverId: initiatorId,
|
|
||||||
approverEmail: initiator.email,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 3,
|
|
||||||
name: 'Department Lead Approval',
|
|
||||||
tatHours: 72,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'department_lead' as const,
|
|
||||||
approverId: departmentLead?.userId || null,
|
|
||||||
approverEmail: departmentLead?.email || initiator.manager || 'deptlead@royalenfield.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 4,
|
|
||||||
name: 'Activity Creation',
|
|
||||||
tatHours: 1,
|
|
||||||
isAuto: true,
|
|
||||||
approverType: 'system' as const,
|
|
||||||
approverId: null,
|
|
||||||
approverEmail: 'system@royalenfield.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 5,
|
|
||||||
name: 'Dealer Completion Documents',
|
|
||||||
tatHours: 120,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'dealer' as const,
|
|
||||||
approverEmail: dealerEmail,
|
|
||||||
approverId: null, // Will be resolved by syncing dealer from Okta
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 6,
|
|
||||||
name: 'Requestor Claim Approval',
|
|
||||||
tatHours: 48,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'initiator' as const,
|
|
||||||
approverId: initiatorId,
|
|
||||||
approverEmail: initiator.email,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 7,
|
|
||||||
name: 'E-Invoice Generation',
|
|
||||||
tatHours: 1,
|
|
||||||
isAuto: true,
|
|
||||||
approverType: 'system' as const,
|
|
||||||
approverId: null,
|
|
||||||
approverEmail: 'system@royalenfield.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
level: 8,
|
|
||||||
name: 'Credit Note Confirmation',
|
|
||||||
tatHours: 48,
|
|
||||||
isAuto: false,
|
|
||||||
approverType: 'initiator' as const,
|
|
||||||
approverId: initiatorId,
|
|
||||||
approverEmail: initiator.email,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const step of steps) {
|
// Process each step
|
||||||
// For dealer steps, try to find dealer user, but allow email-only if not found
|
for (const stepDef of stepDefinitions) {
|
||||||
let approverId = step.approverId;
|
const approver = approvers.find((a) => a.level === stepDef.level);
|
||||||
let approverEmail = step.approverEmail || '';
|
|
||||||
|
let approverId: string | null = null;
|
||||||
|
let approverEmail = '';
|
||||||
let approverName = 'System';
|
let approverName = 'System';
|
||||||
|
let tatHours = stepDef.defaultTat;
|
||||||
|
|
||||||
if (step.isAuto) {
|
if (stepDef.isAuto) {
|
||||||
// For system/auto steps, use initiator's ID (system steps don't need real approver)
|
// System steps - handled by webhooks, no user validation needed
|
||||||
approverId = initiatorId;
|
// Step 8 is System/Finance, others are System
|
||||||
approverName = 'System Auto-Process';
|
if (stepDef.level === 8) {
|
||||||
approverEmail = approverEmail || 'system@royalenfield.com';
|
approverId = initiatorId; // Use initiator ID as placeholder (no actual user needed)
|
||||||
} else if (step.approverType === 'dealer' && step.approverEmail) {
|
approverName = 'System/Finance';
|
||||||
// Treat dealer as Okta user - sync from Okta if not in database
|
approverEmail = 'finance@royalenfield.com';
|
||||||
let dealerUser = await User.findOne({ where: { email: step.approverEmail.toLowerCase() } });
|
} else {
|
||||||
if (!dealerUser) {
|
approverId = initiatorId; // System steps use initiator ID as placeholder
|
||||||
logger.info(`[DealerClaimService] Dealer ${step.approverEmail} not found in DB, syncing from Okta`);
|
approverName = 'System Auto-Process';
|
||||||
try {
|
approverEmail = 'system@royalenfield.com';
|
||||||
dealerUser = await this.userService.ensureUserExists({
|
}
|
||||||
email: step.approverEmail.toLowerCase(),
|
tatHours = stepDef.defaultTat;
|
||||||
}) as any;
|
} else if (approver) {
|
||||||
logger.info(`[DealerClaimService] Successfully synced dealer ${step.approverEmail} from Okta`);
|
// User-provided approver
|
||||||
} catch (oktaError: any) {
|
if (!approver.email) {
|
||||||
logger.error(`[DealerClaimService] Failed to sync dealer from Okta: ${step.approverEmail}`, oktaError);
|
throw new Error(`Approver email is required for Step ${stepDef.level}: ${stepDef.name}`);
|
||||||
throw new Error(`Dealer email '${step.approverEmail}' not found in organization directory. Please verify the email address.`);
|
}
|
||||||
|
|
||||||
|
// Calculate TAT in hours
|
||||||
|
if (approver.tat) {
|
||||||
|
const tat = Number(approver.tat);
|
||||||
|
if (isNaN(tat) || tat <= 0) {
|
||||||
|
throw new Error(`Invalid TAT for Step ${stepDef.level}. TAT must be a positive number.`);
|
||||||
|
}
|
||||||
|
tatHours = approver.tatType === 'days' ? tat * 24 : tat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user exists in database (create from Okta if needed)
|
||||||
|
let user: User | null = null;
|
||||||
|
|
||||||
|
if (approver.userId) {
|
||||||
|
// User ID provided - use it
|
||||||
|
user = await User.findByPk(approver.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user && approver.email) {
|
||||||
|
// User not found by ID, try to find or create by email
|
||||||
|
user = await User.findOne({ where: { email: approver.email.toLowerCase() } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// User doesn't exist - create from Okta
|
||||||
|
logger.info(`[DealerClaimService] User ${approver.email} not found in DB, syncing from Okta`);
|
||||||
|
try {
|
||||||
|
user = await this.userService.ensureUserExists({
|
||||||
|
email: approver.email.toLowerCase(),
|
||||||
|
}) as any;
|
||||||
|
logger.info(`[DealerClaimService] Successfully synced user ${approver.email} from Okta`);
|
||||||
|
} catch (oktaError: any) {
|
||||||
|
logger.error(`[DealerClaimService] Failed to sync user from Okta: ${approver.email}`, oktaError);
|
||||||
|
throw new Error(`User email '${approver.email}' not found in organization directory. Please verify the email address.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure dealerUser is not null (TypeScript check)
|
if (!user) {
|
||||||
if (!dealerUser) {
|
throw new Error(`Could not resolve user for Step ${stepDef.level}: ${approver.email}`);
|
||||||
throw new Error(`Dealer user could not be resolved for email: ${step.approverEmail}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
approverId = dealerUser.userId;
|
|
||||||
approverName = dealerUser.displayName || dealerUser.email || 'Dealer';
|
|
||||||
approverEmail = dealerUser.email;
|
|
||||||
} else if (step.approverType === 'initiator') {
|
|
||||||
approverId = initiatorId;
|
|
||||||
approverName = initiator.displayName || initiator.email || 'Requestor';
|
|
||||||
approverEmail = initiator.email;
|
|
||||||
} else if (step.approverType === 'department_lead') {
|
|
||||||
if (finalDepartmentLead) {
|
|
||||||
approverId = finalDepartmentLead.userId;
|
|
||||||
approverName = finalDepartmentLead.displayName || finalDepartmentLead.email || 'Department Lead';
|
|
||||||
approverEmail = finalDepartmentLead.email;
|
|
||||||
} else {
|
|
||||||
// This should never happen as we validate manager before creating records
|
|
||||||
// But keeping as safety check
|
|
||||||
const error: any = new Error('Department lead not found. This should have been validated before creating the request.');
|
|
||||||
error.code = 'DEPARTMENT_LEAD_NOT_FOUND';
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
approverId = user.userId;
|
||||||
|
approverEmail = user.email;
|
||||||
|
approverName = approver.name || user.displayName || user.email || 'Approver';
|
||||||
|
} else {
|
||||||
|
// No approver provided for required step
|
||||||
|
throw new Error(`Approver is required for Step ${stepDef.level}: ${stepDef.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we always have a valid approverId (should never be null at this point)
|
// Ensure we have a valid approverId
|
||||||
if (!approverId) {
|
if (!approverId) {
|
||||||
logger.error(`[DealerClaimService] No approverId resolved for step ${step.level}, using initiator as fallback`);
|
logger.error(`[DealerClaimService] No approverId resolved for step ${stepDef.level}, using initiator as fallback`);
|
||||||
approverId = initiatorId;
|
approverId = initiatorId;
|
||||||
approverEmail = approverEmail || initiator.email;
|
approverEmail = approverEmail || initiator.email;
|
||||||
approverName = approverName || 'Unknown Approver';
|
approverName = approverName || 'Unknown Approver';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1 is the first active step - set levelStartTime immediately so SLA tracking starts
|
// Create approval level
|
||||||
// For other steps, levelStartTime will be set when they become active
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isStep1 = step.level === 1;
|
const isStep1 = stepDef.level === 1;
|
||||||
|
|
||||||
await ApprovalLevel.create({
|
await ApprovalLevel.create({
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: step.level,
|
levelNumber: stepDef.level,
|
||||||
levelName: step.name,
|
levelName: stepDef.name,
|
||||||
approverId: approverId, // Always a valid user ID now
|
approverId: approverId,
|
||||||
approverEmail,
|
approverEmail,
|
||||||
approverName,
|
approverName,
|
||||||
tatHours: step.tatHours,
|
tatHours: tatHours,
|
||||||
// tatDays is calculated later when needed: 1 day = 8 working hours
|
status: stepDef.level === 1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING,
|
||||||
// Formula: Math.ceil(tatHours / 8)
|
isFinalApprover: stepDef.level === 8,
|
||||||
// Only step 1 should be PENDING initially, others should wait
|
|
||||||
// But ApprovalStatus doesn't have WAITING, so we'll use PENDING for all
|
|
||||||
// and the approval service will properly activate the next level
|
|
||||||
status: step.level === 1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING,
|
|
||||||
isFinalApprover: step.level === 8,
|
|
||||||
elapsedHours: 0,
|
elapsedHours: 0,
|
||||||
remainingHours: step.tatHours,
|
remainingHours: tatHours,
|
||||||
tatPercentageUsed: 0,
|
tatPercentageUsed: 0,
|
||||||
// CRITICAL: Set levelStartTime for Step 1 immediately so SLA tracking starts from request creation
|
|
||||||
// This ensures dealers see SLA tracker in Open Requests even before they take any action
|
|
||||||
levelStartTime: isStep1 ? now : undefined,
|
levelStartTime: isStep1 ? now : undefined,
|
||||||
tatStartTime: isStep1 ? now : undefined,
|
tatStartTime: isStep1 ? now : undefined,
|
||||||
});
|
} as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1157,14 +1074,24 @@ export class DealerClaimService {
|
|||||||
currency: 'INR',
|
currency: 'INR',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Approve step 1 and move to step 2
|
// Approve Dealer Proposal Submission step dynamically (by levelName, not hardcoded step number)
|
||||||
const level1 = await ApprovalLevel.findOne({
|
let dealerProposalLevel = await ApprovalLevel.findOne({
|
||||||
where: { requestId, levelNumber: 1 }
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelName: 'Dealer Proposal Submission'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (level1) {
|
// Fallback: try to find by levelNumber 1 (for backwards compatibility)
|
||||||
|
if (!dealerProposalLevel) {
|
||||||
|
dealerProposalLevel = await ApprovalLevel.findOne({
|
||||||
|
where: { requestId, levelNumber: 1 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealerProposalLevel) {
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
level1.levelId,
|
dealerProposalLevel.levelId,
|
||||||
{ action: 'APPROVE', comments: 'Dealer proposal submitted' },
|
{ action: 'APPROVE', comments: 'Dealer proposal submitted' },
|
||||||
'system', // System approval
|
'system', // System approval
|
||||||
{ ipAddress: null, userAgent: null }
|
{ ipAddress: null, userAgent: null }
|
||||||
@ -1236,14 +1163,24 @@ export class DealerClaimService {
|
|||||||
currency: 'INR',
|
currency: 'INR',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Approve step 5 and move to step 6
|
// Approve Dealer Completion Documents step dynamically (by levelName, not hardcoded step number)
|
||||||
const level5 = await ApprovalLevel.findOne({
|
let dealerCompletionLevel = await ApprovalLevel.findOne({
|
||||||
where: { requestId, levelNumber: 5 }
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelName: 'Dealer Completion Documents'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (level5) {
|
// Fallback: try to find by levelNumber 5 (for backwards compatibility)
|
||||||
|
if (!dealerCompletionLevel) {
|
||||||
|
dealerCompletionLevel = await ApprovalLevel.findOne({
|
||||||
|
where: { requestId, levelNumber: 5 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dealerCompletionLevel) {
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
level5.levelId,
|
dealerCompletionLevel.levelId,
|
||||||
{ action: 'APPROVE', comments: 'Completion documents submitted' },
|
{ action: 'APPROVE', comments: 'Completion documents submitted' },
|
||||||
'system',
|
'system',
|
||||||
{ ipAddress: null, userAgent: null }
|
{ ipAddress: null, userAgent: null }
|
||||||
@ -1348,6 +1285,14 @@ export class DealerClaimService {
|
|||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';
|
const requestNumber = request ? ((request as any).requestNumber || (request as any).request_number) : 'UNKNOWN';
|
||||||
|
|
||||||
|
logger.info(`[DealerClaimService] Blocking budget in SAP:`, {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
ioNumber: ioData.ioNumber,
|
||||||
|
amountToBlock: blockedAmount,
|
||||||
|
availableBalance: ioData.availableBalance || ioValidation.availableBalance,
|
||||||
|
});
|
||||||
|
|
||||||
const blockResult = await sapIntegrationService.blockBudget(
|
const blockResult = await sapIntegrationService.blockBudget(
|
||||||
ioData.ioNumber,
|
ioData.ioNumber,
|
||||||
blockedAmount,
|
blockedAmount,
|
||||||
@ -1359,21 +1304,60 @@ export class DealerClaimService {
|
|||||||
throw new Error(`Failed to block budget in SAP: ${blockResult.error}`);
|
throw new Error(`Failed to block budget in SAP: ${blockResult.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalBlockedAmount = blockResult.blockedAmount;
|
const sapReturnedBlockedAmount = blockResult.blockedAmount;
|
||||||
const availableBalance = ioData.availableBalance || ioValidation.availableBalance;
|
const availableBalance = ioData.availableBalance || ioValidation.availableBalance;
|
||||||
|
|
||||||
// Calculate remaining balance: availableBalance - blockedAmount
|
// Use the amount we REQUESTED for calculation, not what SAP returned
|
||||||
// Use SAP's returned value if available and valid (> 0), otherwise calculate it
|
// SAP might return a slightly different amount due to rounding, but we calculate based on what we requested
|
||||||
// This ensures we always have a valid remaining balance value
|
// Only use SAP's returned amount if it's significantly different (more than 1 rupee), which would indicate an actual issue
|
||||||
const sapRemainingBalance = blockResult.remainingBalance;
|
const amountDifference = Math.abs(sapReturnedBlockedAmount - blockedAmount);
|
||||||
|
const useSapAmount = amountDifference > 1.0; // Only use SAP's amount if difference is more than 1 rupee
|
||||||
|
const finalBlockedAmount = useSapAmount ? sapReturnedBlockedAmount : blockedAmount;
|
||||||
|
|
||||||
|
// Log SAP response vs what we sent
|
||||||
|
logger.info(`[DealerClaimService] SAP block result:`, {
|
||||||
|
requestedAmount: blockedAmount,
|
||||||
|
sapReturnedBlockedAmount: sapReturnedBlockedAmount,
|
||||||
|
sapReturnedRemainingBalance: blockResult.remainingBalance,
|
||||||
|
availableBalance,
|
||||||
|
amountDifference,
|
||||||
|
usingSapAmount: useSapAmount,
|
||||||
|
finalBlockedAmountUsed: finalBlockedAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warn if SAP blocked a significantly different amount than requested
|
||||||
|
if (amountDifference > 0.01) {
|
||||||
|
if (amountDifference > 1.0) {
|
||||||
|
logger.warn(`[DealerClaimService] ⚠️ Significant amount mismatch! Requested: ${blockedAmount}, SAP blocked: ${sapReturnedBlockedAmount}, Difference: ${amountDifference}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`[DealerClaimService] Minor amount difference (likely rounding): Requested: ${blockedAmount}, SAP returned: ${sapReturnedBlockedAmount}, Using requested amount for calculation`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate remaining balance: availableBalance - requestedAmount
|
||||||
|
// IMPORTANT: Use the amount we REQUESTED, not SAP's returned amount (unless SAP blocked significantly different amount)
|
||||||
|
// This ensures accuracy: remaining = available - requested
|
||||||
const calculatedRemainingBalance = availableBalance - finalBlockedAmount;
|
const calculatedRemainingBalance = availableBalance - finalBlockedAmount;
|
||||||
const remainingBalance = (sapRemainingBalance > 0 && sapRemainingBalance <= availableBalance)
|
|
||||||
|
// Only use SAP's value if it's valid AND matches our calculation (within 1 rupee tolerance)
|
||||||
|
// This is a safety check - if SAP's value is way off, use our calculation
|
||||||
|
const sapRemainingBalance = blockResult.remainingBalance;
|
||||||
|
const sapValueIsValid = sapRemainingBalance > 0 &&
|
||||||
|
sapRemainingBalance <= availableBalance &&
|
||||||
|
Math.abs(sapRemainingBalance - calculatedRemainingBalance) < 1;
|
||||||
|
|
||||||
|
const remainingBalance = sapValueIsValid
|
||||||
? sapRemainingBalance
|
? sapRemainingBalance
|
||||||
: calculatedRemainingBalance;
|
: calculatedRemainingBalance;
|
||||||
|
|
||||||
// Ensure remaining balance is not negative
|
// Ensure remaining balance is not negative
|
||||||
const finalRemainingBalance = Math.max(0, remainingBalance);
|
const finalRemainingBalance = Math.max(0, remainingBalance);
|
||||||
|
|
||||||
|
// Warn if SAP's value doesn't match our calculation
|
||||||
|
if (!sapValueIsValid && sapRemainingBalance !== calculatedRemainingBalance) {
|
||||||
|
logger.warn(`[DealerClaimService] ⚠️ SAP returned invalid remaining balance (${sapRemainingBalance}), using calculated value (${calculatedRemainingBalance})`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Budget blocking calculation:`, {
|
logger.info(`[DealerClaimService] Budget blocking calculation:`, {
|
||||||
availableBalance,
|
availableBalance,
|
||||||
blockedAmount: finalBlockedAmount,
|
blockedAmount: finalBlockedAmount,
|
||||||
@ -1385,14 +1369,19 @@ export class DealerClaimService {
|
|||||||
// Get the user who is blocking the IO (current user)
|
// Get the user who is blocking the IO (current user)
|
||||||
const organizedBy = organizedByUserId || null;
|
const organizedBy = organizedByUserId || null;
|
||||||
|
|
||||||
|
// Round amounts to 2 decimal places for database storage (avoid floating point precision issues)
|
||||||
|
const roundedAvailableBalance = Math.round(availableBalance * 100) / 100;
|
||||||
|
const roundedBlockedAmount = Math.round(finalBlockedAmount * 100) / 100;
|
||||||
|
const roundedRemainingBalance = Math.round(finalRemainingBalance * 100) / 100;
|
||||||
|
|
||||||
// Create or update Internal Order record (only when blocking)
|
// Create or update Internal Order record (only when blocking)
|
||||||
const ioRecordData = {
|
const ioRecordData = {
|
||||||
requestId,
|
requestId,
|
||||||
ioNumber: ioData.ioNumber,
|
ioNumber: ioData.ioNumber,
|
||||||
ioRemark: ioData.ioRemark || '',
|
ioRemark: ioData.ioRemark || '',
|
||||||
ioAvailableBalance: availableBalance,
|
ioAvailableBalance: roundedAvailableBalance,
|
||||||
ioBlockedAmount: finalBlockedAmount,
|
ioBlockedAmount: roundedBlockedAmount,
|
||||||
ioRemainingBalance: finalRemainingBalance,
|
ioRemainingBalance: roundedRemainingBalance,
|
||||||
organizedBy: organizedBy || undefined,
|
organizedBy: organizedBy || undefined,
|
||||||
organizedAt: new Date(),
|
organizedAt: new Date(),
|
||||||
status: IOStatus.BLOCKED,
|
status: IOStatus.BLOCKED,
|
||||||
@ -1568,25 +1557,36 @@ export class DealerClaimService {
|
|||||||
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
|
logger.info(`[DealerClaimService] E-Invoice details manually updated for request: ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Step 6 is approved - if not, approve it first
|
// Check if Requestor Claim Approval (Step 6) is approved - if not, approve it first
|
||||||
const step6Level = await ApprovalLevel.findOne({
|
// Find dynamically by levelName, not hardcoded step number
|
||||||
where: { requestId, levelNumber: 6 }
|
let requestorClaimLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelName: 'Requestor Claim Approval'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (step6Level && step6Level.status !== ApprovalStatus.APPROVED) {
|
// Fallback: try to find by levelNumber 6 (for backwards compatibility)
|
||||||
logger.info(`[DealerClaimService] Step 6 not approved yet. Auto-approving Step 6 for request ${requestId}`);
|
if (!requestorClaimLevel) {
|
||||||
// Auto-approve Step 6 - Step 7 will be activated but invoice generation will happen via webhook
|
requestorClaimLevel = await ApprovalLevel.findOne({
|
||||||
|
where: { requestId, levelNumber: 6 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestorClaimLevel && requestorClaimLevel.status !== ApprovalStatus.APPROVED) {
|
||||||
|
logger.info(`[DealerClaimService] Requestor Claim Approval not approved yet. Auto-approving for request ${requestId}`);
|
||||||
|
// Auto-approve Requestor Claim Approval - E-Invoice Generation will be activated but invoice generation will happen via webhook
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step6Level.levelId,
|
requestorClaimLevel.levelId,
|
||||||
{ action: 'APPROVE', comments: 'Auto-approved when pushing to DMS. Waiting for DMS webhook to generate invoice.' },
|
{ action: 'APPROVE', comments: 'Auto-approved when pushing to DMS. Waiting for DMS webhook to generate invoice.' },
|
||||||
'system',
|
'system',
|
||||||
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
||||||
);
|
);
|
||||||
// Note: Step 7 invoice generation will be handled by DMS webhook, not here
|
// Note: E-Invoice Generation will be handled by DMS webhook, not here
|
||||||
logger.info(`[DealerClaimService] Step 6 approved. Step 7 activated. Waiting for DMS webhook to generate invoice.`);
|
logger.info(`[DealerClaimService] Requestor Claim Approval approved. E-Invoice Generation activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
} else {
|
} else {
|
||||||
// Step 6 already approved - Step 7 should be active, waiting for webhook
|
// Requestor Claim Approval already approved - E-Invoice Generation should be active, waiting for webhook
|
||||||
logger.info(`[DealerClaimService] Step 6 already approved. Step 7 should be active. Invoice generation will happen via DMS webhook.`);
|
logger.info(`[DealerClaimService] Requestor Claim Approval already approved. E-Invoice Generation should be active. Invoice generation will happen via DMS webhook.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[DealerClaimService] Error updating e-invoice details:', error);
|
logger.error('[DealerClaimService] Error updating e-invoice details:', error);
|
||||||
@ -1620,34 +1620,45 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Step 7 approval level
|
// Get Step 7 approval level
|
||||||
const step7Level = await ApprovalLevel.findOne({
|
// Get E-Invoice Generation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
|
let eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: 7
|
levelName: 'E-Invoice Generation'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!step7Level) {
|
// Fallback: try to find by levelNumber 7 (for backwards compatibility)
|
||||||
throw new Error(`Step 7 approval level not found for request ${requestId}`);
|
if (!eInvoiceLevel) {
|
||||||
|
eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 7
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If e-invoice already generated, just auto-approve Step 7
|
if (!eInvoiceLevel) {
|
||||||
|
throw new Error(`E-Invoice Generation approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If e-invoice already generated, just auto-approve E-Invoice Generation
|
||||||
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
const claimInvoice = await ClaimInvoice.findOne({ where: { requestId } });
|
||||||
|
|
||||||
if (claimInvoice?.invoiceNumber) {
|
if (claimInvoice?.invoiceNumber) {
|
||||||
logger.info(`[DealerClaimService] E-Invoice already generated (${claimInvoice.invoiceNumber}). Auto-approving Step 7 for request ${requestId}`);
|
logger.info(`[DealerClaimService] E-Invoice already generated (${claimInvoice.invoiceNumber}). Auto-approving E-Invoice Generation for request ${requestId}`);
|
||||||
|
|
||||||
// Auto-approve Step 7
|
// Auto-approve E-Invoice Generation
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step7Level.levelId,
|
eInvoiceLevel.levelId,
|
||||||
{ action: 'APPROVE', comments: 'E-Invoice generated via DMS. Step 7 auto-approved.' },
|
{ action: 'APPROVE', comments: 'E-Invoice generated via DMS. E-Invoice Generation auto-approved.' },
|
||||||
'system',
|
'system',
|
||||||
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
{ ipAddress: null, userAgent: 'System Auto-Process' }
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Successfully auto-processed and approved Step 7 for request ${requestId}`);
|
logger.info(`[DealerClaimService] Successfully auto-processed and approved E-Invoice Generation (Level ${eInvoiceLevel.levelNumber}) for request ${requestId}`);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`[DealerClaimService] E-Invoice not generated yet for request ${requestId}. Step 7 will be processed when e-invoice is generated.`);
|
logger.warn(`[DealerClaimService] E-Invoice not generated yet for request ${requestId}. E-Invoice Generation will be processed when e-invoice is generated.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[DealerClaimService] Error processing Step 7 (E-Invoice Generation) for request ${requestId}:`, error);
|
logger.error(`[DealerClaimService] Error processing Step 7 (E-Invoice Generation) for request ${requestId}:`, error);
|
||||||
@ -1780,30 +1791,41 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Step 8 approval level
|
// Get Step 8 approval level
|
||||||
const step8Level = await ApprovalLevel.findOne({
|
// Get Credit Note Confirmation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
|
let creditNoteLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: 8
|
levelName: 'Credit Note Confirmation'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!step8Level) {
|
// Fallback: try to find by levelNumber 8 (for backwards compatibility)
|
||||||
throw new Error(`Step 8 approval level not found for request ${requestId}`);
|
if (!creditNoteLevel) {
|
||||||
|
creditNoteLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Step 8 is already approved
|
if (!creditNoteLevel) {
|
||||||
if (step8Level.status === 'APPROVED') {
|
throw new Error(`Credit Note Confirmation approval level not found for request ${requestId}`);
|
||||||
logger.info(`[DealerClaimService] Step 8 already approved for request ${requestId}`);
|
}
|
||||||
|
|
||||||
|
// Check if Credit Note Confirmation is already approved
|
||||||
|
if (creditNoteLevel.status === 'APPROVED') {
|
||||||
|
logger.info(`[DealerClaimService] Credit Note Confirmation already approved for request ${requestId}`);
|
||||||
// Still send notification to dealer if credit note wasn't sent before
|
// Still send notification to dealer if credit note wasn't sent before
|
||||||
// You can add a flag to track if credit note was already sent
|
// You can add a flag to track if credit note was already sent
|
||||||
} else {
|
} else {
|
||||||
// Auto-approve Step 8
|
// Auto-approve Credit Note Confirmation
|
||||||
logger.info(`[DealerClaimService] Auto-approving Step 8 for request ${requestId}`);
|
logger.info(`[DealerClaimService] Auto-approving Credit Note Confirmation (Level ${creditNoteLevel.levelNumber}) for request ${requestId}`);
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step8Level.levelId,
|
creditNoteLevel.levelId,
|
||||||
{
|
{
|
||||||
action: 'APPROVE',
|
action: 'APPROVE',
|
||||||
comments: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Step 8 auto-approved.`,
|
comments: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Credit Note Confirmation auto-approved.`,
|
||||||
},
|
},
|
||||||
userId,
|
userId,
|
||||||
{
|
{
|
||||||
@ -1812,7 +1834,7 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Step 8 auto-approved successfully for request ${requestId}`);
|
logger.info(`[DealerClaimService] Credit Note Confirmation (Level ${creditNoteLevel.levelNumber}) auto-approved successfully for request ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update credit note status to SENT
|
// Update credit note status to SENT
|
||||||
@ -1873,16 +1895,27 @@ export class DealerClaimService {
|
|||||||
throw new Error(`Claim details not found for request ${requestId}`);
|
throw new Error(`Claim details not found for request ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Step 4 approval level
|
// Get Activity Creation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
const step4Level = await ApprovalLevel.findOne({
|
// This handles cases where approvers are added between steps, causing Activity Creation to shift
|
||||||
|
let activityCreationLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: 4
|
levelName: 'Activity Creation'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!step4Level) {
|
if (!activityCreationLevel) {
|
||||||
throw new Error(`Step 4 approval level not found for request ${requestId}`);
|
// Fallback: try to find by levelNumber 4 (for backwards compatibility)
|
||||||
|
activityCreationLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activityCreationLevel) {
|
||||||
|
throw new Error(`Activity Creation approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get participants for email notifications
|
// Get participants for email notifications
|
||||||
@ -1891,15 +1924,25 @@ export class DealerClaimService {
|
|||||||
? await User.findOne({ where: { email: claimDetails.dealerEmail } })
|
? await User.findOne({ where: { email: claimDetails.dealerEmail } })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Get department lead from Step 3
|
// Get department lead dynamically (by levelName, not hardcoded step number)
|
||||||
const step3Level = await ApprovalLevel.findOne({
|
let deptLeadLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: 3
|
levelName: 'Department Lead Approval'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const departmentLead = step3Level?.approverId
|
|
||||||
? await User.findByPk(step3Level.approverId)
|
// Fallback: try to find by levelNumber 3 (for backwards compatibility)
|
||||||
|
if (!deptLeadLevel) {
|
||||||
|
deptLeadLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 3
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const departmentLead = deptLeadLevel?.approverId
|
||||||
|
? await User.findByPk(deptLeadLevel.approverId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
|
const requestNumber = (request as any).requestNumber || (request as any).request_number || 'UNKNOWN';
|
||||||
@ -1958,11 +2001,11 @@ export class DealerClaimService {
|
|||||||
details: `Activity "${activityName}" created. Activity confirmation email auto-triggered to dealer, requestor, and department lead. IO confirmation to be made.`,
|
details: `Activity "${activityName}" created. Activity confirmation email auto-triggered to dealer, requestor, and department lead. IO confirmation to be made.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 4 is already activated (IN_PROGRESS) by the approval service
|
// Activity Creation level is already activated (IN_PROGRESS) by the approval service
|
||||||
// Now auto-approve Step 4 immediately (since it's an auto-step)
|
// Now auto-approve Activity Creation immediately (since it's an auto-step)
|
||||||
const step4LevelId = step4Level.levelId;
|
const activityCreationLevelId = activityCreationLevel.levelId;
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step4LevelId,
|
activityCreationLevelId,
|
||||||
{ action: 'APPROVE', comments: 'Activity created automatically. Activity confirmation email sent to dealer, requestor, and department lead.' },
|
{ action: 'APPROVE', comments: 'Activity created automatically. Activity confirmation email sent to dealer, requestor, and department lead.' },
|
||||||
'system',
|
'system',
|
||||||
{
|
{
|
||||||
@ -1971,7 +2014,7 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Step 4 auto-approved for request ${requestId}. Activity creation completed.`);
|
logger.info(`[DealerClaimService] Activity Creation (Level ${activityCreationLevel.levelNumber}) auto-approved for request ${requestId}. Activity creation completed.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[DealerClaimService] Error processing Step 4 activity creation for request ${requestId}:`, error);
|
logger.error(`[DealerClaimService] Error processing Step 4 activity creation for request ${requestId}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -347,40 +347,51 @@ export class DMSWebhookService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Step 7 approval level
|
// Get E-Invoice Generation approval level dynamically (by levelName, not hardcoded step number)
|
||||||
const step7Level = await ApprovalLevel.findOne({
|
let eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId,
|
requestId,
|
||||||
levelNumber: 7,
|
levelName: 'E-Invoice Generation',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!step7Level) {
|
// Fallback: try to find by levelNumber 7 (for backwards compatibility)
|
||||||
logger.warn('[DMSWebhook] Step 7 approval level not found', { requestId, requestNumber });
|
if (!eInvoiceLevel) {
|
||||||
|
eInvoiceLevel = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eInvoiceLevel) {
|
||||||
|
logger.warn('[DMSWebhook] E-Invoice Generation approval level not found', { requestId, requestNumber });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Step 7 is already approved
|
// Check if E-Invoice Generation is already approved
|
||||||
if (step7Level.status === 'APPROVED') {
|
if (eInvoiceLevel.status === 'APPROVED') {
|
||||||
logger.info('[DMSWebhook] Step 7 already approved, skipping auto-approval', {
|
logger.info('[DMSWebhook] E-Invoice Generation already approved, skipping auto-approval', {
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-approve Step 7
|
// Auto-approve E-Invoice Generation
|
||||||
logger.info('[DMSWebhook] Auto-approving Step 7 (E-Invoice Generation)', {
|
logger.info(`[DMSWebhook] Auto-approving E-Invoice Generation (Level ${eInvoiceLevel.levelNumber})`, {
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
levelId: step7Level.levelId,
|
levelId: eInvoiceLevel.levelId,
|
||||||
|
levelNumber: eInvoiceLevel.levelNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step7Level.levelId,
|
eInvoiceLevel.levelId,
|
||||||
{
|
{
|
||||||
action: 'APPROVE',
|
action: 'APPROVE',
|
||||||
comments: `E-Invoice generated via DMS webhook. Invoice Number: ${(await ClaimInvoice.findOne({ where: { requestId } }))?.invoiceNumber || 'N/A'}. Step 7 auto-approved.`,
|
comments: `E-Invoice generated via DMS webhook. Invoice Number: ${(await ClaimInvoice.findOne({ where: { requestId } }))?.invoiceNumber || 'N/A'}. E-Invoice Generation auto-approved.`,
|
||||||
},
|
},
|
||||||
'system', // System user for auto-approval
|
'system', // System user for auto-approval
|
||||||
{
|
{
|
||||||
@ -389,9 +400,10 @@ export class DMSWebhookService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info('[DMSWebhook] Step 7 auto-approved successfully. Workflow moved to Step 8', {
|
logger.info('[DMSWebhook] E-Invoice Generation auto-approved successfully. Workflow moved to next step', {
|
||||||
requestId,
|
requestId,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
|
levelNumber: eInvoiceLevel.levelNumber,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|||||||
@ -612,6 +612,8 @@ export class SAPIntegrationService {
|
|||||||
textNodeName: '#text',
|
textNodeName: '#text',
|
||||||
parseAttributeValue: true,
|
parseAttributeValue: true,
|
||||||
trimValues: true
|
trimValues: true
|
||||||
|
// Note: fast-xml-parser preserves namespace prefixes by default
|
||||||
|
// So 'd:Available_Amount' should remain as 'd:Available_Amount' in the parsed object
|
||||||
});
|
});
|
||||||
responseData = parser.parse(response.data);
|
responseData = parser.parse(response.data);
|
||||||
logger.info(`[SAP] XML parsed successfully`);
|
logger.info(`[SAP] XML parsed successfully`);
|
||||||
@ -648,23 +650,65 @@ export class SAPIntegrationService {
|
|||||||
// For XML: Available_Amount in lt_io_output[0] (may be prefixed with 'd:' namespace)
|
// For XML: Available_Amount in lt_io_output[0] (may be prefixed with 'd:' namespace)
|
||||||
// For JSON: RemainingBalance, Remaining, Available_Amount, etc.
|
// For JSON: RemainingBalance, Remaining, Available_Amount, etc.
|
||||||
const extractRemainingBalance = (obj: any): number => {
|
const extractRemainingBalance = (obj: any): number => {
|
||||||
if (!obj) return 0;
|
if (!obj) {
|
||||||
|
logger.debug(`[SAP] extractRemainingBalance: obj is null/undefined`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to extract value from field (handles both direct values and nested #text nodes)
|
||||||
|
const getFieldValue = (fieldName: string): any => {
|
||||||
|
const field = obj[fieldName];
|
||||||
|
if (field === undefined || field === null) return null;
|
||||||
|
|
||||||
|
// If it's an object with #text property (XML parser sometimes does this)
|
||||||
|
if (typeof field === 'object' && field['#text'] !== undefined) {
|
||||||
|
return field['#text'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct value
|
||||||
|
return field;
|
||||||
|
};
|
||||||
|
|
||||||
// Try various field name variations (both JSON and XML formats)
|
// Try various field name variations (both JSON and XML formats)
|
||||||
// XML namespace prefixes: 'd:Available_Amount', 'd:RemainingBalance', etc.
|
// XML namespace prefixes: 'd:Available_Amount', 'd:RemainingBalance', etc.
|
||||||
const value = obj['d:Available_Amount'] || // XML format with namespace prefix
|
// IMPORTANT: Check 'd:Available_Amount' first as that's what SAP returns in XML
|
||||||
obj.Available_Amount || // XML format without prefix
|
const value = getFieldValue('d:Available_Amount') ?? // XML format with namespace prefix (PRIORITY)
|
||||||
obj.RemainingBalance ||
|
getFieldValue('Available_Amount') ?? // XML format without prefix
|
||||||
obj['d:RemainingBalance'] ||
|
getFieldValue('d:RemainingBalance') ??
|
||||||
obj.Remaining ||
|
getFieldValue('RemainingBalance') ??
|
||||||
obj.RemainingAmount ||
|
getFieldValue('RemainingAmount') ??
|
||||||
obj.AvailableBalance ||
|
getFieldValue('Remaining') ??
|
||||||
obj.Balance ||
|
getFieldValue('AvailableBalance') ??
|
||||||
obj.Available ||
|
getFieldValue('Balance') ??
|
||||||
0;
|
getFieldValue('Available') ??
|
||||||
|
null;
|
||||||
|
|
||||||
const parsed = parseFloat(value?.toString() || '0');
|
if (value === null || value === undefined) {
|
||||||
return isNaN(parsed) ? 0 : parsed;
|
logger.debug(`[SAP] extractRemainingBalance: No value found. Object keys:`, Object.keys(obj));
|
||||||
|
// Log all keys that might be relevant
|
||||||
|
const relevantKeys = Object.keys(obj).filter(k =>
|
||||||
|
k.toLowerCase().includes('available') ||
|
||||||
|
k.toLowerCase().includes('amount') ||
|
||||||
|
k.toLowerCase().includes('remaining') ||
|
||||||
|
k.toLowerCase().includes('balance')
|
||||||
|
);
|
||||||
|
if (relevantKeys.length > 0) {
|
||||||
|
logger.debug(`[SAP] extractRemainingBalance: Found relevant keys but no value:`, relevantKeys.map(k => ({ key: k, value: obj[k], type: typeof obj[k] })));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string first, then parse (handles both string "14291525.00" and number)
|
||||||
|
const valueStr = value?.toString().trim() || '0';
|
||||||
|
const parsed = parseFloat(valueStr);
|
||||||
|
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
logger.warn(`[SAP] extractRemainingBalance: Failed to parse value "${valueStr}" as number`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[SAP] extractRemainingBalance: Extracted value "${valueStr}" -> ${parsed}`);
|
||||||
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to extract blocked amount
|
// Helper function to extract blocked amount
|
||||||
@ -692,36 +736,141 @@ export class SAPIntegrationService {
|
|||||||
// Check for XML structure first (parsed XML from fast-xml-parser)
|
// Check for XML structure first (parsed XML from fast-xml-parser)
|
||||||
let ioOutputData: any = null;
|
let ioOutputData: any = null;
|
||||||
let message = '';
|
let message = '';
|
||||||
|
let mainEntryProperties: any = null;
|
||||||
|
|
||||||
// XML structure: entry.link (array) -> find link with @rel='lt_io_output' -> inline.feed.entry
|
// XML structure: entry.link (array) -> find link with @rel='lt_io_output' -> inline.feed.entry
|
||||||
if (responseData.entry) {
|
if (responseData.entry) {
|
||||||
const entry = responseData.entry;
|
const entry = responseData.entry;
|
||||||
|
|
||||||
// Find lt_io_output link
|
// Also check main entry properties (sometimes Available_Amount is here)
|
||||||
const links = Array.isArray(entry.link) ? entry.link : (entry.link ? [entry.link] : []);
|
const mainContent = entry.content || {};
|
||||||
const ioOutputLink = links.find((link: any) =>
|
mainEntryProperties = mainContent['m:properties'] || mainContent.properties || (mainContent['@_type'] === 'application/xml' ? mainContent : null);
|
||||||
link['@_rel']?.includes('lt_io_output') ||
|
|
||||||
link['@_title'] === 'IOOutputSet' ||
|
|
||||||
link.title === 'IOOutputSet'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ioOutputLink?.inline?.feed?.entry) {
|
if (mainEntryProperties) {
|
||||||
const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry)
|
logger.info(`[SAP] Found main entry properties, keys:`, Object.keys(mainEntryProperties));
|
||||||
? ioOutputLink.inline.feed.entry[0]
|
}
|
||||||
: ioOutputLink.inline.feed.entry;
|
|
||||||
|
// Find lt_io_output link
|
||||||
|
// The rel attribute can be a full URL like "http://schemas.microsoft.com/ado/2007/08/dataservices/related/lt_io_output"
|
||||||
|
// or just "lt_io_output", so we check if it includes "lt_io_output"
|
||||||
|
const links = Array.isArray(entry.link) ? entry.link : (entry.link ? [entry.link] : []);
|
||||||
|
|
||||||
|
logger.debug(`[SAP] All links in entry:`, links.map((l: any) => ({
|
||||||
|
rel: l['@_rel'] || l.rel,
|
||||||
|
title: l['@_title'] || l.title,
|
||||||
|
href: l['@_href'] || l.href
|
||||||
|
})));
|
||||||
|
|
||||||
|
const ioOutputLink = links.find((link: any) => {
|
||||||
|
const rel = link['@_rel'] || link.rel || '';
|
||||||
|
const title = link['@_title'] || link.title || '';
|
||||||
|
return rel.includes('lt_io_output') || title === 'IOOutputSet' || title === 'lt_io_output';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ioOutputLink) {
|
||||||
|
logger.info(`[SAP] Found lt_io_output link:`, {
|
||||||
|
rel: ioOutputLink['@_rel'] || ioOutputLink.rel,
|
||||||
|
title: ioOutputLink['@_title'] || ioOutputLink.title,
|
||||||
|
hasInline: !!ioOutputLink.inline,
|
||||||
|
hasFeed: !!ioOutputLink.inline?.feed,
|
||||||
|
hasEntry: !!ioOutputLink.inline?.feed?.entry
|
||||||
|
});
|
||||||
|
|
||||||
// Handle both namespace-prefixed and non-prefixed property names
|
if (ioOutputLink.inline?.feed?.entry) {
|
||||||
const content = ioEntry.content || {};
|
const ioEntry = Array.isArray(ioOutputLink.inline.feed.entry)
|
||||||
const properties = content['m:properties'] || content.properties || content['@_type'] === 'application/xml' ? content : null;
|
? ioOutputLink.inline.feed.entry[0]
|
||||||
|
: ioOutputLink.inline.feed.entry;
|
||||||
if (properties) {
|
|
||||||
ioOutputData = properties;
|
|
||||||
// Try both namespace-prefixed and non-prefixed field names
|
|
||||||
message = ioOutputData['d:Message'] || ioOutputData.Message || ioOutputData['#text'] || '';
|
|
||||||
|
|
||||||
logger.info(`[SAP] Found XML structure - lt_io_output data extracted`);
|
logger.debug(`[SAP] IO Entry structure:`, {
|
||||||
logger.debug(`[SAP] XML properties keys:`, Object.keys(ioOutputData));
|
hasContent: !!ioEntry.content,
|
||||||
|
contentType: ioEntry.content?.['@_type'] || ioEntry.content?.type,
|
||||||
|
hasMProperties: !!ioEntry.content?.['m:properties'],
|
||||||
|
hasProperties: !!ioEntry.content?.properties
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle both namespace-prefixed and non-prefixed property names
|
||||||
|
const content = ioEntry.content || {};
|
||||||
|
const properties = content['m:properties'] || content.properties || (content['@_type'] === 'application/xml' ? content : null);
|
||||||
|
|
||||||
|
if (properties) {
|
||||||
|
ioOutputData = properties;
|
||||||
|
// Try both namespace-prefixed and non-prefixed field names
|
||||||
|
message = ioOutputData['d:Message'] || ioOutputData.Message || ioOutputData['#text'] || '';
|
||||||
|
|
||||||
|
logger.info(`[SAP] ✅ Found XML structure - lt_io_output data extracted`);
|
||||||
|
logger.info(`[SAP] XML properties keys (${Object.keys(ioOutputData).length} keys):`, Object.keys(ioOutputData));
|
||||||
|
|
||||||
|
// Log the raw properties object to see exact structure
|
||||||
|
logger.info(`[SAP] XML properties full object:`, JSON.stringify(ioOutputData, null, 2));
|
||||||
|
|
||||||
|
// Check ALL keys that might contain "Available" or "Amount"
|
||||||
|
const allKeys = Object.keys(ioOutputData);
|
||||||
|
const availableKeys = allKeys.filter(key =>
|
||||||
|
key.toLowerCase().includes('available') ||
|
||||||
|
key.toLowerCase().includes('amount') ||
|
||||||
|
key.toLowerCase().includes('remaining') ||
|
||||||
|
key.toLowerCase().includes('balance')
|
||||||
|
);
|
||||||
|
logger.info(`[SAP] Keys containing 'available', 'amount', 'remaining', or 'balance':`, availableKeys);
|
||||||
|
|
||||||
|
// Log all possible Available_Amount field variations with their actual values
|
||||||
|
const availableAmountVariations: Record<string, any> = {};
|
||||||
|
const variationsToCheck = [
|
||||||
|
'd:Available_Amount',
|
||||||
|
'Available_Amount',
|
||||||
|
'd:AvailableAmount',
|
||||||
|
'AvailableAmount',
|
||||||
|
'RemainingBalance',
|
||||||
|
'd:RemainingBalance',
|
||||||
|
'Remaining',
|
||||||
|
'RemainingAmount',
|
||||||
|
'AvailableBalance',
|
||||||
|
'Balance',
|
||||||
|
'Available'
|
||||||
|
];
|
||||||
|
|
||||||
|
variationsToCheck.forEach(key => {
|
||||||
|
if (ioOutputData[key] !== undefined) {
|
||||||
|
availableAmountVariations[key] = {
|
||||||
|
value: ioOutputData[key],
|
||||||
|
type: typeof ioOutputData[key],
|
||||||
|
stringValue: String(ioOutputData[key])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[SAP] Available Amount field variations found:`, availableAmountVariations);
|
||||||
|
|
||||||
|
// Also check if the XML parser might have stripped the namespace prefix
|
||||||
|
// Sometimes XML parsers convert 'd:Available_Amount' to just 'Available_Amount'
|
||||||
|
if (allKeys.includes('Available_Amount') && !allKeys.includes('d:Available_Amount')) {
|
||||||
|
logger.info(`[SAP] ℹ️ Found 'Available_Amount' without 'd:' prefix (parser may have stripped namespace)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly check for d:Available_Amount and log its value
|
||||||
|
if (ioOutputData['d:Available_Amount'] !== undefined) {
|
||||||
|
logger.info(`[SAP] ✅ Found d:Available_Amount: ${ioOutputData['d:Available_Amount']} (type: ${typeof ioOutputData['d:Available_Amount']})`);
|
||||||
|
} else if (ioOutputData.Available_Amount !== undefined) {
|
||||||
|
logger.info(`[SAP] ✅ Found Available_Amount (without d: prefix): ${ioOutputData.Available_Amount} (type: ${typeof ioOutputData.Available_Amount})`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[SAP] ⚠️ d:Available_Amount not found in properties. All property keys:`, allKeys);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[SAP] ⚠️ Properties not found in ioEntry.content. Content structure:`, JSON.stringify(content, null, 2));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[SAP] ⚠️ No entry found in ioOutputLink.inline.feed. Structure:`, {
|
||||||
|
hasInline: !!ioOutputLink.inline,
|
||||||
|
hasFeed: !!ioOutputLink.inline?.feed,
|
||||||
|
hasEntry: !!ioOutputLink.inline?.feed?.entry,
|
||||||
|
inlineKeys: ioOutputLink.inline ? Object.keys(ioOutputLink.inline) : []
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[SAP] ⚠️ No lt_io_output link found in entry.links. Available links:`, links.map((l: any) => ({
|
||||||
|
rel: l['@_rel'] || l.rel || 'unknown',
|
||||||
|
title: l['@_title'] || l.title || 'unknown'
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -739,6 +888,16 @@ export class SAPIntegrationService {
|
|||||||
success = message.includes('Successful') || message.includes('Success') || !message.includes('Error');
|
success = message.includes('Successful') || message.includes('Success') || !message.includes('Error');
|
||||||
blockedAmount = amount; // Use the amount we sent (from lt_io_input)
|
blockedAmount = amount; // Use the amount we sent (from lt_io_input)
|
||||||
remainingBalance = extractRemainingBalance(ioOutputData); // Available_Amount from XML
|
remainingBalance = extractRemainingBalance(ioOutputData); // Available_Amount from XML
|
||||||
|
|
||||||
|
// If not found in lt_io_output, try main entry properties
|
||||||
|
if (remainingBalance === 0 && mainEntryProperties) {
|
||||||
|
logger.info(`[SAP] Available_Amount not found in lt_io_output, trying main entry properties`);
|
||||||
|
remainingBalance = extractRemainingBalance(mainEntryProperties);
|
||||||
|
if (remainingBalance > 0) {
|
||||||
|
logger.info(`[SAP] Found Available_Amount in main entry properties: ${remainingBalance}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try both namespace-prefixed and non-prefixed field names
|
// Try both namespace-prefixed and non-prefixed field names
|
||||||
blockId = ioOutputData['d:Sap_Reference_no'] ||
|
blockId = ioOutputData['d:Sap_Reference_no'] ||
|
||||||
ioOutputData.Sap_Reference_no ||
|
ioOutputData.Sap_Reference_no ||
|
||||||
@ -752,7 +911,8 @@ export class SAPIntegrationService {
|
|||||||
message,
|
message,
|
||||||
availableAmount: remainingBalance,
|
availableAmount: remainingBalance,
|
||||||
sapReference: blockId,
|
sapReference: blockId,
|
||||||
allKeys: Object.keys(ioOutputData)
|
allKeys: Object.keys(ioOutputData),
|
||||||
|
foundInMainEntry: remainingBalance > 0 && mainEntryProperties ? true : false
|
||||||
});
|
});
|
||||||
} else if (responseData.ls_response && Array.isArray(responseData.ls_response) && responseData.ls_response.length > 0) {
|
} else if (responseData.ls_response && Array.isArray(responseData.ls_response) && responseData.ls_response.length > 0) {
|
||||||
// Response in ls_response array
|
// Response in ls_response array
|
||||||
@ -789,8 +949,14 @@ export class SAPIntegrationService {
|
|||||||
blockedAmount,
|
blockedAmount,
|
||||||
remainingBalance,
|
remainingBalance,
|
||||||
blockId,
|
blockId,
|
||||||
note: remainingBalance === 0 ? 'Remaining balance will be calculated from availableBalance - blockedAmount' : 'Remaining balance from SAP response'
|
responseStructure: ioOutputData ? 'XML lt_io_output' : responseData.d ? 'JSON d' : responseData.ls_response ? 'ls_response' : responseData.lt_io_output ? 'lt_io_output' : 'unknown',
|
||||||
|
note: remainingBalance === 0 ? '⚠️ Remaining balance is 0 - will be calculated from availableBalance - blockedAmount' : '✅ Remaining balance from SAP response'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If remaining balance is 0, log the full response structure for debugging
|
||||||
|
if (remainingBalance === 0 && response.status === 200 || response.status === 201) {
|
||||||
|
logger.warn(`[SAP] ⚠️ Remaining balance is 0, but request was successful. Full response structure:`, JSON.stringify(responseData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.info(`[SAP] Budget blocked successfully for IO ${ioNumber}. Blocked: ${blockedAmount}, Remaining: ${remainingBalance}`);
|
logger.info(`[SAP] Budget blocked successfully for IO ${ioNumber}. Blocked: ${blockedAmount}, Remaining: ${remainingBalance}`);
|
||||||
|
|||||||
@ -332,6 +332,9 @@ export class WorkflowService {
|
|||||||
|
|
||||||
// Shift existing levels at and after target level
|
// Shift existing levels at and after target level
|
||||||
// IMPORTANT: Shift in REVERSE order to avoid unique constraint violations
|
// IMPORTANT: Shift in REVERSE order to avoid unique constraint violations
|
||||||
|
// IMPORTANT: Preserve original level names when shifting (don't overwrite them)
|
||||||
|
// IMPORTANT: Update status of shifted levels - if they were IN_PROGRESS, set to PENDING
|
||||||
|
// because they're no longer the current active step (new approver is being added before them)
|
||||||
const levelsToShift = allLevels
|
const levelsToShift = allLevels
|
||||||
.filter(l => (l as any).levelNumber >= targetLevel)
|
.filter(l => (l as any).levelNumber >= targetLevel)
|
||||||
.sort((a, b) => (b as any).levelNumber - (a as any).levelNumber); // Sort descending
|
.sort((a, b) => (b as any).levelNumber - (a as any).levelNumber); // Sort descending
|
||||||
@ -339,24 +342,47 @@ export class WorkflowService {
|
|||||||
for (const levelToShift of levelsToShift) {
|
for (const levelToShift of levelsToShift) {
|
||||||
const oldLevelNumber = (levelToShift as any).levelNumber;
|
const oldLevelNumber = (levelToShift as any).levelNumber;
|
||||||
const newLevelNumber = oldLevelNumber + 1;
|
const newLevelNumber = oldLevelNumber + 1;
|
||||||
|
const existingLevelName = (levelToShift as any).levelName;
|
||||||
|
const currentStatus = (levelToShift as any).status;
|
||||||
|
|
||||||
|
// If the level being shifted was IN_PROGRESS or PENDING, set it to PENDING
|
||||||
|
// because it's no longer the current active step (a new approver is being added before it)
|
||||||
|
const newStatus = (currentStatus === ApprovalStatus.IN_PROGRESS || currentStatus === ApprovalStatus.PENDING)
|
||||||
|
? ApprovalStatus.PENDING
|
||||||
|
: currentStatus; // Keep APPROVED, REJECTED, SKIPPED as-is
|
||||||
|
|
||||||
|
// Preserve the original level name - don't overwrite it
|
||||||
await levelToShift.update({
|
await levelToShift.update({
|
||||||
levelNumber: newLevelNumber,
|
levelNumber: newLevelNumber,
|
||||||
levelName: `Level ${newLevelNumber}`
|
// Keep existing levelName if it exists, otherwise use generic
|
||||||
});
|
levelName: existingLevelName || `Level ${newLevelNumber}`,
|
||||||
logger.info(`[Workflow] Shifted level ${oldLevelNumber} → ${newLevelNumber}`);
|
status: newStatus,
|
||||||
|
// Clear levelStartTime and tatStartTime since this is no longer the active step
|
||||||
|
levelStartTime: undefined,
|
||||||
|
tatStartTime: undefined,
|
||||||
|
} as any);
|
||||||
|
logger.info(`[Workflow] Shifted level ${oldLevelNumber} → ${newLevelNumber}, preserved levelName: ${existingLevelName || 'N/A'}, updated status: ${currentStatus} → ${newStatus}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total levels in workflow
|
// Update total levels in workflow
|
||||||
await workflow.update({ totalLevels: allLevels.length + 1 });
|
await workflow.update({ totalLevels: allLevels.length + 1 });
|
||||||
|
|
||||||
// Auto-generate smart level name
|
// Auto-generate smart level name for newly added approver
|
||||||
let levelName = `Level ${targetLevel}`;
|
// Use "Additional Approver" to identify dynamically added approvers
|
||||||
|
let levelName = `Additional Approver`;
|
||||||
if (designation) {
|
if (designation) {
|
||||||
levelName = `${designation} Approval`;
|
levelName = `Additional Approver - ${designation}`;
|
||||||
} else if (department) {
|
} else if (department) {
|
||||||
levelName = `${department} Approval`;
|
levelName = `Additional Approver - ${department}`;
|
||||||
|
} else if (userName) {
|
||||||
|
levelName = `Additional Approver - ${userName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if the new level should be IN_PROGRESS
|
||||||
|
// If we're adding at the current level, the new approver becomes the active approver
|
||||||
|
const workflowCurrentLevel = (workflow as any).currentLevel;
|
||||||
|
const isAddingAtCurrentLevel = targetLevel === workflowCurrentLevel;
|
||||||
|
|
||||||
// Create new approval level at target position
|
// Create new approval level at target position
|
||||||
const newLevel = await ApprovalLevel.create({
|
const newLevel = await ApprovalLevel.create({
|
||||||
requestId,
|
requestId,
|
||||||
@ -367,11 +393,16 @@ export class WorkflowService {
|
|||||||
approverName: userName,
|
approverName: userName,
|
||||||
tatHours,
|
tatHours,
|
||||||
// tatDays is auto-calculated by database as a generated column
|
// tatDays is auto-calculated by database as a generated column
|
||||||
status: targetLevel === (workflow as any).currentLevel ? ApprovalStatus.IN_PROGRESS : ApprovalStatus.PENDING,
|
status: isAddingAtCurrentLevel ? ApprovalStatus.IN_PROGRESS : ApprovalStatus.PENDING,
|
||||||
isFinalApprover: targetLevel === allLevels.length + 1,
|
isFinalApprover: targetLevel === allLevels.length + 1,
|
||||||
levelStartTime: targetLevel === (workflow as any).currentLevel ? new Date() : null,
|
levelStartTime: isAddingAtCurrentLevel ? new Date() : null,
|
||||||
tatStartTime: targetLevel === (workflow as any).currentLevel ? new Date() : null
|
tatStartTime: isAddingAtCurrentLevel ? new Date() : null
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
// IMPORTANT: If we're adding at the current level, the workflow's currentLevel stays the same
|
||||||
|
// (it's still the same level number, just with a new approver)
|
||||||
|
// The status update we did above ensures the shifted level becomes PENDING
|
||||||
|
// No need to update workflow.currentLevel - it's already correct
|
||||||
|
|
||||||
// Update isFinalApprover for previous final approver (now it's not final anymore)
|
// Update isFinalApprover for previous final approver (now it's not final anymore)
|
||||||
if (allLevels.length > 0) {
|
if (allLevels.length > 0) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user