diff --git a/docs/DMS_INTEGRATION_API.md b/docs/DMS_INTEGRATION_API.md new file mode 100644 index 0000000..862d389 --- /dev/null +++ b/docs/DMS_INTEGRATION_API.md @@ -0,0 +1,423 @@ +# DMS Integration API Documentation + +## Overview + +This document describes the data exchange between the Royal Enfield Workflow System and the DMS (Document Management System) for: +1. **E-Invoice Generation** - Submitting claim data to DMS for e-invoice creation +2. **Credit Note Generation** - Fetching/Generating credit note from DMS + +--- + +## 1. E-Invoice Generation (DMS Push) + +### When It's Called + +This API is called when: +- **Step 6** of the claim management workflow is approved (Requestor approves the claim) +- User manually pushes claim data to DMS via the "Push to DMS" action +- System auto-generates e-invoice after claim approval + +### Request Details + +**Endpoint:** `POST {DMS_BASE_URL}/api/invoices/generate` + +**Headers:** +```http +Authorization: Bearer {DMS_API_KEY} +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "request_number": "REQ-2025-001234", + "dealer_code": "DLR001", + "dealer_name": "ABC Motors", + "amount": 150000.00, + "description": "E-Invoice for claim request REQ-2025-001234", + "io_number": "IO-2025-001", + "tax_details": { + "cgst": 0, + "sgst": 0, + "igst": 0, + "total_tax": 0 + } +} +``` + +### Request Field Descriptions + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `request_number` | string | Yes | Unique request number from RE Workflow System (e.g., "REQ-2025-001234") | +| `dealer_code` | string | Yes | Dealer's unique code/identifier | +| `dealer_name` | string | Yes | Dealer's business name | +| `amount` | number | Yes | Total invoice amount (in INR, decimal format) | +| `description` | string | Yes | Description of the invoice/claim | +| `io_number` | string | No | Internal Order (IO) number if available | +| `tax_details` | object | No | Tax breakdown (CGST, SGST, IGST, Total Tax) | + +### Expected Response + +**Success Response (200 OK):** +```json +{ + "success": true, + "e_invoice_number": "EINV-2025-001234", + "dms_number": "DMS-2025-001234", + "invoice_date": "2025-12-17T10:30:00.000Z", + "invoice_url": "https://dms.example.com/invoices/EINV-2025-001234" +} +``` + +### Response Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Indicates if the request was successful | +| `e_invoice_number` | string | Generated e-invoice number (unique identifier) | +| `dms_number` | string | DMS internal tracking number | +| `invoice_date` | string (ISO 8601) | Date when the invoice was generated | +| `invoice_url` | string | URL to view/download the invoice document | + +### Error Response + +**Error Response (400/500):** +```json +{ + "success": false, + "error": "Error message describing what went wrong", + "error_code": "INVALID_DEALER_CODE" +} +``` + +### Error Scenarios + +| Error Code | Description | Possible Causes | +|------------|-------------|-----------------| +| `INVALID_DEALER_CODE` | Dealer code not found in DMS | Dealer not registered in DMS | +| `INVALID_AMOUNT` | Amount validation failed | Negative amount or invalid format | +| `IO_NOT_FOUND` | IO number not found | Invalid or non-existent IO number | +| `DMS_SERVICE_ERROR` | DMS internal error | DMS system unavailable or processing error | + +### Example cURL Request + +```bash +curl -X POST "https://dms.example.com/api/invoices/generate" \ + -H "Authorization: Bearer YOUR_DMS_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "request_number": "REQ-2025-001234", + "dealer_code": "DLR001", + "dealer_name": "ABC Motors", + "amount": 150000.00, + "description": "E-Invoice for claim request REQ-2025-001234", + "io_number": "IO-2025-001" + }' +``` + +--- + +## 2. Credit Note Generation (DMS Fetch) + +### When It's Called + +This API is called when: +- **Step 8** of the claim management workflow is initiated (Credit Note Confirmation) +- User requests to generate/fetch credit note from DMS +- System auto-generates credit note after e-invoice is confirmed + +### Request Details + +**Endpoint:** `POST {DMS_BASE_URL}/api/credit-notes/generate` + +**Headers:** +```http +Authorization: Bearer {DMS_API_KEY} +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "request_number": "REQ-2025-001234", + "e_invoice_number": "EINV-2025-001234", + "dealer_code": "DLR001", + "dealer_name": "ABC Motors", + "amount": 150000.00, + "reason": "Claim settlement", + "description": "Credit note for claim request REQ-2025-001234" +} +``` + +### Request Field Descriptions + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `request_number` | string | Yes | Original request number from RE Workflow System | +| `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_name` | string | Yes | Dealer's business name | +| `amount` | number | Yes | Credit note amount (in INR, decimal format) - typically matches invoice amount | +| `reason` | string | Yes | Reason for credit note (e.g., "Claim settlement", "Return", "Adjustment") | +| `description` | string | No | Additional description/details about the credit note | + +### Expected Response + +**Success Response (200 OK):** +```json +{ + "success": true, + "credit_note_number": "CN-2025-001234", + "credit_note_date": "2025-12-17T10:30:00.000Z", + "credit_note_amount": 150000.00, + "credit_note_url": "https://dms.example.com/credit-notes/CN-2025-001234" +} +``` + +### Response Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Indicates if the request was successful | +| `credit_note_number` | string | Generated credit note number (unique identifier) | +| `credit_note_date` | string (ISO 8601) | Date when the credit note was generated | +| `credit_note_amount` | number | Credit note amount (should match request amount) | +| `credit_note_url` | string | URL to view/download the credit note document | + +### Error Response + +**Error Response (400/500):** +```json +{ + "success": false, + "error": "Error message describing what went wrong", + "error_code": "INVOICE_NOT_FOUND" +} +``` + +### Error Scenarios + +| Error Code | Description | Possible Causes | +|------------|-------------|-----------------| +| `INVOICE_NOT_FOUND` | E-invoice number not found in DMS | Invoice was not generated or invalid invoice number | +| `INVALID_AMOUNT` | Amount validation failed | Amount mismatch with invoice or invalid format | +| `DEALER_MISMATCH` | Dealer code/name doesn't match invoice | Different dealer code than original invoice | +| `CREDIT_NOTE_EXISTS` | Credit note already generated for this invoice | Duplicate request for same invoice | +| `DMS_SERVICE_ERROR` | DMS internal error | DMS system unavailable or processing error | + +### Example cURL Request + +```bash +curl -X POST "https://dms.example.com/api/credit-notes/generate" \ + -H "Authorization: Bearer YOUR_DMS_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "request_number": "REQ-2025-001234", + "e_invoice_number": "EINV-2025-001234", + "dealer_code": "DLR001", + "dealer_name": "ABC Motors", + "amount": 150000.00, + "reason": "Claim settlement", + "description": "Credit note for claim request REQ-2025-001234" + }' +``` + +--- + +## Configuration + +### Environment Variables + +The following environment variables need to be configured in the RE Workflow System: + +```env +# DMS Integration Configuration +DMS_BASE_URL=https://dms.example.com +DMS_API_KEY=your_dms_api_key_here + +# Alternative: Username/Password Authentication +DMS_USERNAME=your_dms_username +DMS_PASSWORD=your_dms_password +``` + +### Authentication Methods + +DMS supports two authentication methods: + +1. **API Key Authentication** (Recommended) + - Set `DMS_API_KEY` in environment variables + - Header: `Authorization: Bearer {DMS_API_KEY}` + +2. **Username/Password Authentication** + - Set `DMS_USERNAME` and `DMS_PASSWORD` in environment variables + - Use Basic Auth or custom authentication as per DMS requirements + +--- + +## Integration Flow + +### E-Invoice Generation Flow + +``` +┌─────────────────┐ +│ RE Workflow │ +│ System │ +│ (Step 6) │ +└────────┬────────┘ + │ + │ POST /api/invoices/generate + │ { request_number, dealer_code, amount, ... } + │ + ▼ +┌─────────────────┐ +│ DMS System │ +│ │ +│ - Validates │ +│ - Generates │ +│ E-Invoice │ +└────────┬────────┘ + │ + │ Response: { e_invoice_number, dms_number, ... } + │ + ▼ +┌─────────────────┐ +│ RE Workflow │ +│ System │ +│ │ +│ - Stores │ +│ invoice data │ +│ - Updates │ +│ workflow │ +└─────────────────┘ +``` + +### Credit Note Generation Flow + +``` +┌─────────────────┐ +│ RE Workflow │ +│ System │ +│ (Step 8) │ +└────────┬────────┘ + │ + │ POST /api/credit-notes/generate + │ { e_invoice_number, request_number, amount, ... } + │ + ▼ +┌─────────────────┐ +│ DMS System │ +│ │ +│ - Validates │ +│ invoice │ +│ - Generates │ +│ Credit Note │ +└────────┬────────┘ + │ + │ Response: { credit_note_number, credit_note_date, ... } + │ + ▼ +┌─────────────────┐ +│ RE Workflow │ +│ System │ +│ │ +│ - Stores │ +│ credit note │ +│ - Updates │ +│ workflow │ +└─────────────────┘ +``` + +--- + +## Data Mapping + +### RE Workflow System → DMS + +| RE Workflow Field | DMS Request Field | Notes | +|-------------------|-------------------|-------| +| `request.requestNumber` | `request_number` | Direct mapping | +| `claimDetails.dealerCode` | `dealer_code` | Direct mapping | +| `claimDetails.dealerName` | `dealer_name` | Direct mapping | +| `budgetTracking.closedExpenses` | `amount` | Total claim amount | +| `internalOrder.ioNumber` | `io_number` | Optional, if available | +| `claimInvoice.invoiceNumber` | `e_invoice_number` | For credit note only | + +### DMS → RE Workflow System + +| DMS Response Field | RE Workflow Field | Notes | +|-------------------|-------------------|-------| +| `e_invoice_number` | `ClaimInvoice.invoiceNumber` | Stored in database | +| `dms_number` | `ClaimInvoice.dmsNumber` | Stored in database | +| `invoice_date` | `ClaimInvoice.invoiceDate` | Converted to Date object | +| `credit_note_number` | `ClaimCreditNote.creditNoteNumber` | Stored in database | +| `credit_note_date` | `ClaimCreditNote.creditNoteDate` | Converted to Date object | +| `credit_note_amount` | `ClaimCreditNote.creditNoteAmount` | Stored in database | + +--- + +## Testing + +### Mock Mode + +When DMS is not configured, the system operates in **mock mode**: +- Returns mock invoice/credit note numbers +- Logs warnings instead of making actual API calls +- Useful for development and testing + +### Test Data + +**E-Invoice Test Request:** +```json +{ + "request_number": "REQ-TEST-001", + "dealer_code": "TEST-DLR-001", + "dealer_name": "Test Dealer", + "amount": 10000.00, + "description": "Test invoice generation" +} +``` + +**Credit Note Test Request:** +```json +{ + "request_number": "REQ-TEST-001", + "e_invoice_number": "EINV-TEST-001", + "dealer_code": "TEST-DLR-001", + "dealer_name": "Test Dealer", + "amount": 10000.00, + "reason": "Test credit note", + "description": "Test credit note generation" +} +``` + +--- + +## Notes + +1. **Idempotency**: Both APIs should be idempotent - calling them multiple times with the same data should not create duplicates. + +2. **Amount Validation**: DMS should validate that credit note amount matches or is less than the original invoice amount. + +3. **Invoice Dependency**: Credit note generation requires a valid e-invoice to exist in DMS first. + +4. **Error Handling**: RE Workflow System handles DMS errors gracefully and allows manual entry if DMS is unavailable. + +5. **Retry Logic**: Consider implementing retry logic for transient DMS failures. + +6. **Webhooks** (Optional): DMS can send webhooks to notify RE Workflow System when invoice/credit note status changes. + +--- + +## Support + +For issues or questions regarding DMS integration: +- **Backend Team**: Check logs in `Re_Backend/src/services/dmsIntegration.service.ts` +- **DMS Team**: Contact DMS support for API-related issues +- **Documentation**: Refer to DMS API documentation for latest updates + +--- + +**Last Updated:** December 17, 2025 +**Version:** 1.0 + diff --git a/src/services/dealerClaim.service.ts b/src/services/dealerClaim.service.ts index a7c43a3..d0b2ffa 100644 --- a/src/services/dealerClaim.service.ts +++ b/src/services/dealerClaim.service.ts @@ -89,6 +89,7 @@ export class DealerClaimService { // Now create workflow request (manager is validated) // For claim management, requests are submitted immediately (not drafts) // Step 1 will be active for dealer to submit proposal + const now = new Date(); const workflowRequest = await WorkflowRequest.create({ initiatorId: userId, requestNumber, @@ -103,6 +104,7 @@ export class DealerClaimService { totalTatHours: 0, // Will be calculated from approval levels isDraft: false, // Not a draft - submitted and ready for workflow isDeleted: false, + submissionDate: now, // Set submission date for SLA tracking (required for overall SLA calculation) }); // Create claim details @@ -133,6 +135,34 @@ export class DealerClaimService { // Pass the already-resolved departmentLead to avoid re-searching await this.createClaimApprovalLevels(workflowRequest.requestId, userId, claimData.dealerEmail, claimData.selectedManagerEmail, departmentLead); + // Schedule TAT jobs for Step 1 (Dealer Proposal Submission) - first active step + // This ensures SLA tracking starts immediately from request creation + const { tatSchedulerService } = await import('./tatScheduler.service'); + const dealerLevel = await ApprovalLevel.findOne({ + where: { + requestId: workflowRequest.requestId, + levelNumber: 1 // Step 1: Dealer Proposal Submission + } + }); + + if (dealerLevel && dealerLevel.approverId && dealerLevel.levelStartTime) { + try { + const workflowPriority = (workflowRequest as any)?.priority || 'STANDARD'; + await tatSchedulerService.scheduleTatJobs( + workflowRequest.requestId, + (dealerLevel as any).levelId, + dealerLevel.approverId, + Number(dealerLevel.tatHours || 0), + dealerLevel.levelStartTime, + workflowPriority + ); + logger.info(`[DealerClaimService] TAT jobs scheduled for Step 1 (Dealer Proposal Submission) - Priority: ${workflowPriority}`); + } catch (tatError) { + logger.error(`[DealerClaimService] Failed to schedule TAT jobs for Step 1:`, tatError); + // Don't fail request creation if TAT scheduling fails + } + } + // Create participants (initiator, dealer, department lead, finance - exclude system) await this.createClaimParticipants(workflowRequest.requestId, userId, claimData.dealerEmail); @@ -164,13 +194,7 @@ export class DealerClaimService { // Step 1: Dealer Proposal Submission (first active step - log assignment at creation) // Subsequent steps will have assignment logged when they become active (via approval service) - // Notify Step 1 (Dealer) - this is the first active step, so log assignment now - const dealerLevel = await ApprovalLevel.findOne({ - where: { - requestId: workflowRequest.requestId, - levelNumber: 1 // Step 1: Dealer Proposal Submission - } - }); + // Notify Step 1 (Dealer) - dealerLevel was already fetched above for TAT scheduling if (dealerLevel && dealerLevel.approverId) { // Skip notifications for system processes @@ -400,6 +424,11 @@ export class DealerClaimService { approverName = approverName || 'Unknown Approver'; } + // Step 1 is the first active step - set levelStartTime immediately so SLA tracking starts + // For other steps, levelStartTime will be set when they become active + const now = new Date(); + const isStep1 = step.level === 1; + await ApprovalLevel.create({ requestId, levelNumber: step.level, @@ -418,6 +447,10 @@ export class DealerClaimService { elapsedHours: 0, remainingHours: step.tatHours, 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, + tatStartTime: isStep1 ? now : undefined, }); } } diff --git a/src/services/workflow.service.ts b/src/services/workflow.service.ts index f1f2d33..05402fa 100644 --- a/src/services/workflow.service.ts +++ b/src/services/workflow.service.ts @@ -2754,10 +2754,16 @@ export class WorkflowService { const status = (approval.status || '').toString().toUpperCase(); const approvalData = approval.toJSON(); const isPausedLevel = status === 'PAUSED' || approval.isPaused; + const approvalLevelNumber = approval.levelNumber || 0; + const workflowCurrentLevelNumber = currentLevel ? (currentLevel as any).levelNumber : ((workflow as any).currentLevel || 1); - // Calculate SLA for active approvals (pending/in-progress/paused) + // Calculate SLA ONLY for the CURRENT active level (matching currentLevel) + // This ensures that when in step 1, only step 1 has elapsed time, others have 0 // Include PAUSED so we show SLA for the paused approver, not the next one - if (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') { + const isCurrentLevel = approvalLevelNumber === workflowCurrentLevelNumber; + const shouldCalculateSLA = isCurrentLevel && (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED'); + + if (shouldCalculateSLA) { const levelStartTime = approval.levelStartTime || approval.tatStartTime || approval.createdAt; const tatHours = Number(approval.tatHours || 0); @@ -2814,23 +2820,39 @@ export class WorkflowService { } } + // For waiting levels (future levels that haven't started), set elapsedHours to 0 + // This ensures that when in step 1, steps 2-8 show elapsedHours = 0 + if (approvalLevelNumber > workflowCurrentLevelNumber && status !== 'APPROVED' && status !== 'REJECTED') { + return { + ...approvalData, + elapsedHours: 0, + remainingHours: Number(approval.tatHours || 0), + tatPercentageUsed: 0, + }; + } + // For completed/rejected levels, return as-is (already has final values from database) return approvalData; })); // Calculate overall request SLA based on cumulative elapsed hours from all levels // This correctly accounts for pause periods since each level's elapsedHours is pause-adjusted - const submissionDate = (workflow as any).submissionDate; + // Use submissionDate if available, otherwise fallback to createdAt for SLA calculation + const submissionDate = (workflow as any).submissionDate || (workflow as any).createdAt; const totalTatHours = updatedApprovals.reduce((sum, a) => sum + Number(a.tatHours || 0), 0); let overallSLA = null; if (submissionDate && totalTatHours > 0) { // Calculate total elapsed hours by summing elapsed hours from all levels - // This ensures pause periods are correctly excluded from the overall calculation + // CRITICAL: Only count elapsed hours from completed levels + current active level + // Waiting levels (future steps) should contribute 0 elapsed hours + // This ensures that when in step 1, only step 1's elapsed hours are counted let totalElapsedHours = 0; + const workflowCurrentLevelNumber = currentLevel ? (currentLevel as any).levelNumber : ((workflow as any).currentLevel || 1); for (const approval of updatedApprovals) { const status = (approval.status || '').toString().toUpperCase(); + const approvalLevelNumber = approval.levelNumber || 0; if (status === 'APPROVED' || status === 'REJECTED') { // For completed levels, use the stored elapsedHours (already pause-adjusted from when level was completed) @@ -2839,12 +2861,19 @@ export class WorkflowService { // Skipped levels don't contribute to elapsed time continue; } else if (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED') { - // For active/paused levels, use the SLA-calculated elapsedHours (pause-adjusted) - if (approval.sla?.elapsedHours !== undefined) { - totalElapsedHours += Number(approval.sla.elapsedHours); - } else { - totalElapsedHours += Number(approval.elapsedHours || 0); + // CRITICAL: Only count elapsed hours for the CURRENT active level + // Waiting levels (future steps) should NOT contribute elapsed hours + // This ensures request-level elapsed time matches the current step's elapsed time + const isCurrentLevel = approvalLevelNumber === workflowCurrentLevelNumber; + if (isCurrentLevel) { + // For active/paused levels, use the SLA-calculated elapsedHours (pause-adjusted) + if (approval.sla?.elapsedHours !== undefined) { + totalElapsedHours += Number(approval.sla.elapsedHours); + } else { + totalElapsedHours += Number(approval.elapsedHours || 0); + } } + // Waiting levels (approvalLevelNumber > workflowCurrentLevelNumber) contribute 0 elapsed hours } // WAITING levels haven't started yet, so no elapsed time } @@ -2865,7 +2894,7 @@ export class WorkflowService { overallStatus = 'approaching'; } - // Format time display + // Format time display (simple format - frontend will handle detailed formatting) const formatTime = (hours: number) => { if (hours < 1) return `${Math.round(hours * 60)}m`; const wholeHours = Math.floor(hours);