web hooks created for the dms team
This commit is contained in:
parent
8dc7fd3307
commit
1ac169dc7f
574
docs/DMS_WEBHOOK_API.md
Normal file
574
docs/DMS_WEBHOOK_API.md
Normal file
@ -0,0 +1,574 @@
|
|||||||
|
# DMS Webhook API Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the webhook endpoints that DMS (Document Management System) will call to notify the RE Workflow System after processing invoice and credit note generation requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Webhook Overview](#1-webhook-overview)
|
||||||
|
2. [Authentication](#2-authentication)
|
||||||
|
3. [Invoice Webhook](#3-invoice-webhook)
|
||||||
|
4. [Credit Note Webhook](#4-credit-note-webhook)
|
||||||
|
5. [Payload Specifications](#5-payload-specifications)
|
||||||
|
6. [Error Handling](#6-error-handling)
|
||||||
|
7. [Testing](#7-testing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Webhook Overview
|
||||||
|
|
||||||
|
### 1.1 Purpose
|
||||||
|
|
||||||
|
After RE Workflow System pushes invoice/credit note generation requests to DMS, DMS processes them and sends webhook callbacks with the generated document details, tax information, and other metadata.
|
||||||
|
|
||||||
|
### 1.2 Webhook Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ RE Workflow │ │ DMS System │
|
||||||
|
│ System │ │ │
|
||||||
|
└────────┬────────┘ └────────┬────────┘
|
||||||
|
│ │
|
||||||
|
│ POST /api/invoices/generate │
|
||||||
|
│ { request_number, dealer_code, ... }│
|
||||||
|
├─────────────────────────────────────►│
|
||||||
|
│ │
|
||||||
|
│ │ Process Invoice
|
||||||
|
│ │ Generate Document
|
||||||
|
│ │ Calculate GST
|
||||||
|
│ │
|
||||||
|
│ │ POST /api/v1/webhooks/dms/invoice
|
||||||
|
│ │ { document_no, irn_no, ... }
|
||||||
|
│◄─────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Update Invoice Record │
|
||||||
|
│ Store IRN, GST Details, etc. │
|
||||||
|
│ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Authentication
|
||||||
|
|
||||||
|
### 2.1 Webhook Signature
|
||||||
|
|
||||||
|
DMS must include a signature in the request header for security validation:
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
```
|
||||||
|
X-DMS-Signature: <HMAC-SHA256-signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Signature Generation:**
|
||||||
|
1. Create HMAC-SHA256 hash of the request body (JSON string)
|
||||||
|
2. Use the shared secret key (`DMS_WEBHOOK_SECRET`)
|
||||||
|
3. Send the hex-encoded signature in the `X-DMS-Signature` header
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
const signature = crypto
|
||||||
|
.createHmac('sha256', DMS_WEBHOOK_SECRET)
|
||||||
|
.update(body)
|
||||||
|
.digest('hex');
|
||||||
|
// Send in header: X-DMS-Signature: <signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Environment Variable
|
||||||
|
|
||||||
|
Configure the webhook secret in RE Workflow System:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If `DMS_WEBHOOK_SECRET` is not configured, signature validation is skipped (development mode only).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invoice Webhook
|
||||||
|
|
||||||
|
### 3.1 Endpoint
|
||||||
|
|
||||||
|
**URL:** `POST /api/v1/webhooks/dms/invoice`
|
||||||
|
|
||||||
|
**Base URL Examples:**
|
||||||
|
- Development: `http://localhost:5000/api/v1/webhooks/dms/invoice`
|
||||||
|
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice`
|
||||||
|
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice`
|
||||||
|
|
||||||
|
### 3.2 Request Headers
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
X-DMS-Signature: <HMAC-SHA256-signature>
|
||||||
|
User-Agent: DMS-Webhook-Client/1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Request Payload
|
||||||
|
|
||||||
|
```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 Campaign",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Payload Field Descriptions
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `request_number` | string | ✅ Yes | Original request number from RE Workflow System (e.g., "REQ-2025-12-0001") |
|
||||||
|
| `document_no` | string | ✅ Yes | Generated invoice/document number from DMS |
|
||||||
|
| `document_type` | string | ✅ Yes | Type of document: "E-INVOICE" or "INVOICE" |
|
||||||
|
| `document_date` | string (ISO 8601) | ✅ Yes | Date when invoice was generated |
|
||||||
|
| `dealer_code` | string | ✅ Yes | Dealer code (should match original request) |
|
||||||
|
| `dealer_name` | string | ✅ Yes | Dealer name (should match original request) |
|
||||||
|
| `activity_name` | string | ✅ Yes | Activity name from original request |
|
||||||
|
| `activity_description` | string | ✅ Yes | Activity description from original request |
|
||||||
|
| `claim_amount` | number | ✅ Yes | Original claim amount (before tax) |
|
||||||
|
| `io_number` | string | No | Internal Order number (if provided in original request) |
|
||||||
|
| `item_code_no` | string | ✅ Yes | Item code number (provided by DMS team based on activity list) |
|
||||||
|
| `hsn_sac_code` | string | ✅ Yes | HSN/SAC code for the invoice |
|
||||||
|
| `cgst_percentage` | number | ✅ Yes | CGST percentage (e.g., 9.0 for 9%) |
|
||||||
|
| `sgst_percentage` | number | ✅ Yes | SGST percentage (e.g., 9.0 for 9%) |
|
||||||
|
| `igst_percentage` | number | ✅ Yes | IGST percentage (0.0 for intra-state, >0 for inter-state) |
|
||||||
|
| `cgst_amount` | number | ✅ Yes | CGST amount in INR |
|
||||||
|
| `sgst_amount` | number | ✅ Yes | SGST amount in INR |
|
||||||
|
| `igst_amount` | number | ✅ Yes | IGST amount in INR |
|
||||||
|
| `total_amount` | number | ✅ Yes | Total invoice amount (claim_amount + all taxes) |
|
||||||
|
| `irn_no` | string | No | Invoice Reference Number (IRN) from GST portal (if generated) |
|
||||||
|
| `invoice_file_path` | string | ✅ Yes | URL or path to the generated invoice PDF/document file |
|
||||||
|
| `error_message` | string | No | Error message if invoice generation failed |
|
||||||
|
| `timestamp` | string (ISO 8601) | ✅ Yes | Timestamp when webhook is sent |
|
||||||
|
|
||||||
|
### 3.5 Success Response
|
||||||
|
|
||||||
|
**Status Code:** `200 OK`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Invoice webhook processed successfully",
|
||||||
|
"data": {
|
||||||
|
"message": "Invoice webhook processed successfully",
|
||||||
|
"invoiceNumber": "EINV-2025-001234",
|
||||||
|
"requestNumber": "REQ-2025-12-0001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 Error Response
|
||||||
|
|
||||||
|
**Status Code:** `400 Bad Request` or `500 Internal Server Error`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Failed to process invoice webhook",
|
||||||
|
"error": "Request not found: REQ-2025-12-0001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Credit Note Webhook
|
||||||
|
|
||||||
|
### 4.1 Endpoint
|
||||||
|
|
||||||
|
**URL:** `POST /api/v1/webhooks/dms/credit-note`
|
||||||
|
|
||||||
|
**Base URL Examples:**
|
||||||
|
- Development: `http://localhost:5000/api/v1/webhooks/dms/credit-note`
|
||||||
|
- UAT: `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
||||||
|
- Production: `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note`
|
||||||
|
|
||||||
|
### 4.2 Request Headers
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
X-DMS-Signature: <HMAC-SHA256-signature>
|
||||||
|
User-Agent: DMS-Webhook-Client/1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Request Payload
|
||||||
|
|
||||||
|
```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 Campaign",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Payload Field Descriptions
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `request_number` | string | ✅ Yes | Original request number from RE Workflow System |
|
||||||
|
| `document_no` | string | ✅ Yes | Generated credit note number from DMS |
|
||||||
|
| `document_type` | string | ✅ Yes | Type of document: "CREDIT_NOTE" |
|
||||||
|
| `document_date` | string (ISO 8601) | ✅ Yes | Date when credit note was generated |
|
||||||
|
| `dealer_code` | string | ✅ Yes | Dealer code (should match original request) |
|
||||||
|
| `dealer_name` | string | ✅ Yes | Dealer name (should match original request) |
|
||||||
|
| `activity_name` | string | ✅ Yes | Activity name from original request |
|
||||||
|
| `activity_description` | string | ✅ Yes | Activity description from original request |
|
||||||
|
| `claim_amount` | string | ✅ Yes | Original claim amount (before tax) |
|
||||||
|
| `io_number` | string | No | Internal Order number (if provided) |
|
||||||
|
| `item_code_no` | string | ✅ Yes | Item code number (provided by DMS team) |
|
||||||
|
| `hsn_sac_code` | string | ✅ Yes | HSN/SAC code for the credit note |
|
||||||
|
| `cgst_percentage` | number | ✅ Yes | CGST percentage |
|
||||||
|
| `sgst_percentage` | number | ✅ Yes | SGST percentage |
|
||||||
|
| `igst_percentage` | number | ✅ Yes | IGST percentage |
|
||||||
|
| `cgst_amount` | number | ✅ Yes | CGST amount in INR |
|
||||||
|
| `sgst_amount` | number | ✅ Yes | SGST amount in INR |
|
||||||
|
| `igst_amount` | number | ✅ Yes | IGST amount in INR |
|
||||||
|
| `total_amount` | number | ✅ Yes | Total credit note amount (claim_amount + all taxes) |
|
||||||
|
| `credit_type` | string | ✅ Yes | Type of credit: "GST" or "Commercial Credit" |
|
||||||
|
| `irn_no` | string | No | Invoice Reference Number (IRN) for credit note (if generated) |
|
||||||
|
| `sap_credit_note_no` | string | ✅ Yes | SAP Credit Note Number (generated by SAP system) |
|
||||||
|
| `credit_note_file_path` | string | ✅ Yes | URL or path to the generated credit note PDF/document file |
|
||||||
|
| `error_message` | string | No | Error message if credit note generation failed |
|
||||||
|
| `timestamp` | string (ISO 8601) | ✅ Yes | Timestamp when webhook is sent |
|
||||||
|
|
||||||
|
### 4.5 Success Response
|
||||||
|
|
||||||
|
**Status Code:** `200 OK`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Credit note webhook processed successfully",
|
||||||
|
"data": {
|
||||||
|
"message": "Credit note webhook processed successfully",
|
||||||
|
"creditNoteNumber": "CN-2025-001234",
|
||||||
|
"requestNumber": "REQ-2025-12-0001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 Error Response
|
||||||
|
|
||||||
|
**Status Code:** `400 Bad Request` or `500 Internal Server Error`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Failed to process credit note webhook",
|
||||||
|
"error": "Credit note record not found for request: REQ-2025-12-0001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Payload Specifications
|
||||||
|
|
||||||
|
### 5.1 Data Mapping: RE Workflow → DMS
|
||||||
|
|
||||||
|
When RE Workflow System sends data to DMS, it includes:
|
||||||
|
|
||||||
|
| RE Workflow Field | DMS Receives | Notes |
|
||||||
|
|-------------------|--------------|-------|
|
||||||
|
| `requestNumber` | `request_number` | Direct mapping |
|
||||||
|
| `dealerCode` | `dealer_code` | Direct mapping |
|
||||||
|
| `dealerName` | `dealer_name` | Direct mapping |
|
||||||
|
| `activityName` | `activity_name` | From claim details |
|
||||||
|
| `activityDescription` | `activity_description` | From claim details |
|
||||||
|
| `claimAmount` | `claim_amount` | Total claim amount |
|
||||||
|
| `ioNumber` | `io_number` | If available |
|
||||||
|
|
||||||
|
### 5.2 Data Mapping: DMS → RE Workflow
|
||||||
|
|
||||||
|
When DMS sends webhook, RE Workflow System stores:
|
||||||
|
|
||||||
|
| DMS Webhook Field | RE Workflow Database Field | Table |
|
||||||
|
|-------------------|---------------------------|-------|
|
||||||
|
| `document_no` | `invoice_number` / `credit_note_number` | `claim_invoices` / `claim_credit_notes` |
|
||||||
|
| `document_date` | `invoice_date` / `credit_note_date` | `claim_invoices` / `claim_credit_notes` |
|
||||||
|
| `total_amount` | `invoice_amount` / `credit_note_amount` | `claim_invoices` / `claim_credit_notes` |
|
||||||
|
| `invoice_file_path` | `invoice_file_path` | `claim_invoices` |
|
||||||
|
| `credit_note_file_path` | `credit_note_file_path` | `claim_credit_notes` |
|
||||||
|
| `irn_no` | Stored in `description` field | Both tables |
|
||||||
|
| `sap_credit_note_no` | `sap_document_number` | `claim_credit_notes` |
|
||||||
|
| `item_code_no` | Stored in `description` field | Both tables |
|
||||||
|
| `hsn_sac_code` | Stored in `description` field | Both tables |
|
||||||
|
| GST amounts | Stored in `description` field | Both tables |
|
||||||
|
| `credit_type` | Stored in `description` field | `claim_credit_notes` |
|
||||||
|
|
||||||
|
### 5.3 GST Calculation Logic
|
||||||
|
|
||||||
|
**Intra-State (Same State):**
|
||||||
|
- CGST: Applied (e.g., 9%)
|
||||||
|
- SGST: Applied (e.g., 9%)
|
||||||
|
- IGST: 0%
|
||||||
|
|
||||||
|
**Inter-State (Different State):**
|
||||||
|
- CGST: 0%
|
||||||
|
- SGST: 0%
|
||||||
|
- IGST: Applied (e.g., 18%)
|
||||||
|
|
||||||
|
**Total Amount Calculation:**
|
||||||
|
```
|
||||||
|
total_amount = claim_amount + cgst_amount + sgst_amount + igst_amount
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Error Handling
|
||||||
|
|
||||||
|
### 6.1 Common Error Scenarios
|
||||||
|
|
||||||
|
| Error | Status Code | Description | Solution |
|
||||||
|
|-------|-------------|-------------|----------|
|
||||||
|
| Invalid Signature | 401 | Webhook signature validation failed | Check `DMS_WEBHOOK_SECRET` and signature generation |
|
||||||
|
| Missing Required Field | 400 | Required field is missing in payload | Ensure all required fields are included |
|
||||||
|
| Request Not Found | 400 | Request number doesn't exist in system | Verify request number matches original request |
|
||||||
|
| Invoice Not Found | 400 | Invoice record not found for request | Ensure invoice was created before webhook |
|
||||||
|
| Credit Note Not Found | 400 | Credit note record not found for request | Ensure credit note was created before webhook |
|
||||||
|
| Database Error | 500 | Internal database error | Check database connection and logs |
|
||||||
|
|
||||||
|
### 6.2 Retry Logic
|
||||||
|
|
||||||
|
DMS should implement retry logic for failed webhook deliveries:
|
||||||
|
|
||||||
|
- **Initial Retry:** After 1 minute
|
||||||
|
- **Second Retry:** After 5 minutes
|
||||||
|
- **Third Retry:** After 15 minutes
|
||||||
|
- **Final Retry:** After 1 hour
|
||||||
|
|
||||||
|
**Maximum Retries:** 4 attempts
|
||||||
|
|
||||||
|
**Retry Conditions:**
|
||||||
|
- HTTP 5xx errors (server errors)
|
||||||
|
- Network timeouts
|
||||||
|
- Connection failures
|
||||||
|
|
||||||
|
**Do NOT Retry:**
|
||||||
|
- HTTP 400 errors (client errors - invalid payload)
|
||||||
|
- HTTP 401 errors (authentication errors)
|
||||||
|
|
||||||
|
### 6.3 Idempotency
|
||||||
|
|
||||||
|
Webhooks should be idempotent. If DMS sends the same webhook multiple times:
|
||||||
|
- RE Workflow System will update the record with the latest data
|
||||||
|
- No duplicate records will be created
|
||||||
|
- Status will be updated to reflect the latest state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Testing
|
||||||
|
|
||||||
|
### 7.1 Test Invoice Webhook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:5000/api/v1/webhooks/dms/invoice" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-DMS-Signature: <calculated-signature>" \
|
||||||
|
-d '{
|
||||||
|
"request_number": "REQ-2025-12-0001",
|
||||||
|
"document_no": "EINV-TEST-001",
|
||||||
|
"document_type": "E-INVOICE",
|
||||||
|
"document_date": "2025-12-17T10:30:00.000Z",
|
||||||
|
"dealer_code": "DLR001",
|
||||||
|
"dealer_name": "Test Dealer",
|
||||||
|
"activity_name": "Test Activity",
|
||||||
|
"activity_description": "Test Description",
|
||||||
|
"claim_amount": 100000.00,
|
||||||
|
"io_number": "IO-TEST-001",
|
||||||
|
"item_code_no": "ITEM-001",
|
||||||
|
"hsn_sac_code": "998314",
|
||||||
|
"cgst_percentage": 9.0,
|
||||||
|
"sgst_percentage": 9.0,
|
||||||
|
"igst_percentage": 0.0,
|
||||||
|
"cgst_amount": 9000.00,
|
||||||
|
"sgst_amount": 9000.00,
|
||||||
|
"igst_amount": 0.00,
|
||||||
|
"total_amount": 118000.00,
|
||||||
|
"irn_no": "IRN123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
"invoice_file_path": "https://dms.example.com/invoices/EINV-TEST-001.pdf",
|
||||||
|
"timestamp": "2025-12-17T10:30:00.000Z"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Test Credit Note Webhook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:5000/api/v1/webhooks/dms/credit-note" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-DMS-Signature: <calculated-signature>" \
|
||||||
|
-d '{
|
||||||
|
"request_number": "REQ-2025-12-0001",
|
||||||
|
"document_no": "CN-TEST-001",
|
||||||
|
"document_type": "CREDIT_NOTE",
|
||||||
|
"document_date": "2025-12-17T11:00:00.000Z",
|
||||||
|
"dealer_code": "DLR001",
|
||||||
|
"dealer_name": "Test Dealer",
|
||||||
|
"activity_name": "Test Activity",
|
||||||
|
"activity_description": "Test Description",
|
||||||
|
"claim_amount": 100000.00,
|
||||||
|
"io_number": "IO-TEST-001",
|
||||||
|
"item_code_no": "ITEM-001",
|
||||||
|
"hsn_sac_code": "998314",
|
||||||
|
"cgst_percentage": 9.0,
|
||||||
|
"sgst_percentage": 9.0,
|
||||||
|
"igst_percentage": 0.0,
|
||||||
|
"cgst_amount": 9000.00,
|
||||||
|
"sgst_amount": 9000.00,
|
||||||
|
"igst_amount": 0.00,
|
||||||
|
"total_amount": 118000.00,
|
||||||
|
"credit_type": "GST",
|
||||||
|
"irn_no": "IRN987654321098765432109876543210987654321098765432109876543210",
|
||||||
|
"sap_credit_note_no": "SAP-CN-TEST-001",
|
||||||
|
"credit_note_file_path": "https://dms.example.com/credit-notes/CN-TEST-001.pdf",
|
||||||
|
"timestamp": "2025-12-17T11:00:00.000Z"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Signature Calculation (Node.js Example)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function calculateSignature(payload, secret) {
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
return crypto
|
||||||
|
.createHmac('sha256', secret)
|
||||||
|
.update(body)
|
||||||
|
.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = { /* webhook payload */ };
|
||||||
|
const secret = process.env.DMS_WEBHOOK_SECRET;
|
||||||
|
const signature = calculateSignature(payload, secret);
|
||||||
|
|
||||||
|
// Use in header: X-DMS-Signature: <signature>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Integration Checklist
|
||||||
|
|
||||||
|
### 8.1 DMS Team Checklist
|
||||||
|
|
||||||
|
- [ ] Configure webhook URLs in DMS system
|
||||||
|
- [ ] Set up `DMS_WEBHOOK_SECRET` (shared secret)
|
||||||
|
- [ ] Implement signature generation (HMAC-SHA256)
|
||||||
|
- [ ] Test webhook delivery to RE Workflow endpoints
|
||||||
|
- [ ] Implement retry logic for failed deliveries
|
||||||
|
- [ ] Set up monitoring/alerting for webhook failures
|
||||||
|
- [ ] Document webhook payload structure
|
||||||
|
- [ ] Coordinate with RE Workflow team for testing
|
||||||
|
|
||||||
|
### 8.2 RE Workflow Team Checklist
|
||||||
|
|
||||||
|
- [ ] Configure `DMS_WEBHOOK_SECRET` in environment variables
|
||||||
|
- [ ] Deploy webhook endpoints to UAT/Production
|
||||||
|
- [ ] Test webhook endpoints with sample payloads
|
||||||
|
- [ ] Verify database updates after webhook processing
|
||||||
|
- [ ] Set up monitoring/alerting for webhook failures
|
||||||
|
- [ ] Document webhook endpoints for DMS team
|
||||||
|
- [ ] Coordinate with DMS team for integration testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Support & Troubleshooting
|
||||||
|
|
||||||
|
### 9.1 Logs
|
||||||
|
|
||||||
|
RE Workflow System logs webhook processing:
|
||||||
|
|
||||||
|
- **Success:** `[DMSWebhook] Invoice webhook processed successfully`
|
||||||
|
- **Error:** `[DMSWebhook] Error processing invoice webhook: <error>`
|
||||||
|
- **Validation:** `[DMSWebhook] Invalid webhook signature`
|
||||||
|
|
||||||
|
### 9.2 Common Issues
|
||||||
|
|
||||||
|
**Issue: Webhook signature validation fails**
|
||||||
|
- Verify `DMS_WEBHOOK_SECRET` matches in both systems
|
||||||
|
- Check signature calculation method (HMAC-SHA256)
|
||||||
|
- Ensure request body is JSON stringified correctly
|
||||||
|
|
||||||
|
**Issue: Request not found**
|
||||||
|
- Verify `request_number` matches the original request
|
||||||
|
- Check if request exists in RE Workflow database
|
||||||
|
- Ensure request was created before webhook is sent
|
||||||
|
|
||||||
|
**Issue: Invoice/Credit Note record not found**
|
||||||
|
- Verify invoice/credit note was created in RE Workflow
|
||||||
|
- Check if webhook is sent before record creation
|
||||||
|
- Review workflow step sequence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Environment Configuration
|
||||||
|
|
||||||
|
### 10.1 Environment Variables
|
||||||
|
|
||||||
|
Add to RE Workflow System `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# DMS Webhook Configuration
|
||||||
|
DMS_WEBHOOK_SECRET=your_shared_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 Webhook URLs by Environment
|
||||||
|
|
||||||
|
| Environment | Invoice Webhook URL | Credit Note Webhook URL |
|
||||||
|
|-------------|---------------------|-------------------------|
|
||||||
|
| Development | `http://localhost:5000/api/v1/webhooks/dms/invoice` | `http://localhost:5000/api/v1/webhooks/dms/credit-note` |
|
||||||
|
| UAT | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow-uat.royalenfield.com/api/v1/webhooks/dms/credit-note` |
|
||||||
|
| Production | `https://reflow.royalenfield.com/api/v1/webhooks/dms/invoice` | `https://reflow.royalenfield.com/api/v1/webhooks/dms/credit-note` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version:** 1.0
|
||||||
|
**Last Updated:** December 2024
|
||||||
|
**Maintained By:** RE Workflow Development Team
|
||||||
|
|
||||||
669
docs/GCP_PRODUCTION_SETUP.md
Normal file
669
docs/GCP_PRODUCTION_SETUP.md
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
# GCP Cloud Storage - Production Setup Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide provides step-by-step instructions for setting up Google Cloud Storage (GCS) for the **Royal Enfield Workflow System** in **Production** environment. This document focuses specifically on production deployment requirements, folder structure, and environment configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Production Requirements](#1-production-requirements)
|
||||||
|
2. [GCP Bucket Configuration](#2-gcp-bucket-configuration)
|
||||||
|
3. [Service Account Setup](#3-service-account-setup)
|
||||||
|
4. [Environment Variables Configuration](#4-environment-variables-configuration)
|
||||||
|
5. [Folder Structure in GCS](#5-folder-structure-in-gcs)
|
||||||
|
6. [Security & Access Control](#6-security--access-control)
|
||||||
|
7. [CORS Configuration](#7-cors-configuration)
|
||||||
|
8. [Lifecycle Management](#8-lifecycle-management)
|
||||||
|
9. [Monitoring & Alerts](#9-monitoring--alerts)
|
||||||
|
10. [Verification & Testing](#10-verification--testing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Production Requirements
|
||||||
|
|
||||||
|
### 1.1 Application Details
|
||||||
|
|
||||||
|
| Item | Production Value |
|
||||||
|
|------|------------------|
|
||||||
|
| **Application** | Royal Enfield Workflow System |
|
||||||
|
| **Environment** | Production |
|
||||||
|
| **Domain** | `https://reflow.royalenfield.com` |
|
||||||
|
| **Purpose** | Store workflow documents, attachments, invoices, and credit notes |
|
||||||
|
| **Storage Type** | Google Cloud Storage (GCS) |
|
||||||
|
| **Region** | `asia-south1` (Mumbai) |
|
||||||
|
|
||||||
|
### 1.2 Storage Requirements
|
||||||
|
|
||||||
|
The application stores:
|
||||||
|
- **Workflow Documents**: Initial documents uploaded during request creation
|
||||||
|
- **Work Note Attachments**: Files attached during approval workflow
|
||||||
|
- **Invoice Files**: Generated e-invoice PDFs
|
||||||
|
- **Credit Note Files**: Generated credit note PDFs
|
||||||
|
- **Dealer Claim Documents**: Proposal documents, completion documents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. GCP Bucket Configuration
|
||||||
|
|
||||||
|
### 2.1 Production Bucket Settings
|
||||||
|
|
||||||
|
| Setting | Production Value |
|
||||||
|
|---------|------------------|
|
||||||
|
| **Bucket Name** | `reflow-documents-prod` |
|
||||||
|
| **Location Type** | Region |
|
||||||
|
| **Region** | `asia-south1` (Mumbai) |
|
||||||
|
| **Storage Class** | Standard (for active files) |
|
||||||
|
| **Access Control** | Uniform bucket-level access |
|
||||||
|
| **Public Access Prevention** | Enforced (Block all public access) |
|
||||||
|
| **Versioning** | Enabled (for recovery) |
|
||||||
|
| **Lifecycle Rules** | Configured (see section 8) |
|
||||||
|
|
||||||
|
### 2.2 Create Production Bucket
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create production bucket
|
||||||
|
gcloud storage buckets create gs://reflow-documents-prod \
|
||||||
|
--project=re-platform-workflow-dealer \
|
||||||
|
--location=asia-south1 \
|
||||||
|
--uniform-bucket-level-access \
|
||||||
|
--public-access-prevention
|
||||||
|
|
||||||
|
# Enable versioning
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--versioning
|
||||||
|
|
||||||
|
# Verify bucket creation
|
||||||
|
gcloud storage buckets describe gs://reflow-documents-prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Bucket Naming Convention
|
||||||
|
|
||||||
|
| Environment | Bucket Name | Purpose |
|
||||||
|
|-------------|-------------|---------|
|
||||||
|
| Development | `reflow-documents-dev` | Development testing |
|
||||||
|
| UAT | `reflow-documents-uat` | User acceptance testing |
|
||||||
|
| Production | `reflow-documents-prod` | Live production data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Service Account Setup
|
||||||
|
|
||||||
|
### 3.1 Create Production Service Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create service account for production
|
||||||
|
gcloud iam service-accounts create reflow-storage-prod-sa \
|
||||||
|
--display-name="RE Workflow Production Storage Service Account" \
|
||||||
|
--description="Service account for production file storage operations" \
|
||||||
|
--project=re-platform-workflow-dealer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Assign Required Roles
|
||||||
|
|
||||||
|
The service account needs the following IAM roles:
|
||||||
|
|
||||||
|
| Role | Purpose | Required For |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| `roles/storage.objectAdmin` | Full control over objects | Upload, delete, update files |
|
||||||
|
| `roles/storage.objectViewer` | Read objects | Download and preview files |
|
||||||
|
| `roles/storage.legacyBucketReader` | Read bucket metadata | List files and check bucket status |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Grant Storage Object Admin role
|
||||||
|
gcloud projects add-iam-policy-binding re-platform-workflow-dealer \
|
||||||
|
--member="serviceAccount:reflow-storage-prod-sa@re-platform-workflow-dealer.iam.gserviceaccount.com" \
|
||||||
|
--role="roles/storage.objectAdmin"
|
||||||
|
|
||||||
|
# Grant Storage Object Viewer role (for read operations)
|
||||||
|
gcloud projects add-iam-policy-binding re-platform-workflow-dealer \
|
||||||
|
--member="serviceAccount:reflow-storage-prod-sa@re-platform-workflow-dealer.iam.gserviceaccount.com" \
|
||||||
|
--role="roles/storage.objectViewer"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Generate Service Account Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate JSON key file for production
|
||||||
|
gcloud iam service-accounts keys create ./config/gcp-key-prod.json \
|
||||||
|
--iam-account=reflow-storage-prod-sa@re-platform-workflow-dealer.iam.gserviceaccount.com \
|
||||||
|
--project=re-platform-workflow-dealer
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Security Warning:**
|
||||||
|
- Store the key file securely (not in Git)
|
||||||
|
- Use secure file transfer methods
|
||||||
|
- Rotate keys periodically (every 90 days recommended)
|
||||||
|
- Restrict file permissions: `chmod 600 ./config/gcp-key-prod.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Environment Variables Configuration
|
||||||
|
|
||||||
|
### 4.1 Required Environment Variables
|
||||||
|
|
||||||
|
Add the following environment variables to your production `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ============================================
|
||||||
|
# Google Cloud Storage (GCP) Configuration
|
||||||
|
# ============================================
|
||||||
|
# GCP Project ID - Must match the project_id in your service account key file
|
||||||
|
GCP_PROJECT_ID=re-platform-workflow-dealer
|
||||||
|
|
||||||
|
# GCP Bucket Name - Production bucket name
|
||||||
|
GCP_BUCKET_NAME=reflow-documents-prod
|
||||||
|
|
||||||
|
# GCP Service Account Key File Path
|
||||||
|
# Can be relative to project root or absolute path
|
||||||
|
# Example: ./config/gcp-key-prod.json
|
||||||
|
# Example: /etc/reflow/config/gcp-key-prod.json
|
||||||
|
GCP_KEY_FILE=./config/gcp-key-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Environment Variable Details
|
||||||
|
|
||||||
|
| Variable | Description | Example Value | Required |
|
||||||
|
|----------|-------------|---------------|----------|
|
||||||
|
| `GCP_PROJECT_ID` | Your GCP project ID. Must match the `project_id` field in the service account JSON key file. | `re-platform-workflow-dealer` | ✅ Yes |
|
||||||
|
| `GCP_BUCKET_NAME` | Name of the GCS bucket where files will be stored. Must exist in your GCP project. | `reflow-documents-prod` | ✅ Yes |
|
||||||
|
| `GCP_KEY_FILE` | Path to the service account JSON key file. Can be relative (from project root) or absolute path. | `./config/gcp-key-prod.json` | ✅ Yes |
|
||||||
|
|
||||||
|
### 4.3 File Path Configuration
|
||||||
|
|
||||||
|
**Relative Path (Recommended for Development):**
|
||||||
|
```env
|
||||||
|
GCP_KEY_FILE=./config/gcp-key-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Absolute Path (Recommended for Production):**
|
||||||
|
```env
|
||||||
|
GCP_KEY_FILE=/etc/reflow/config/gcp-key-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Verification
|
||||||
|
|
||||||
|
After setting environment variables, verify the configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if variables are set
|
||||||
|
echo $GCP_PROJECT_ID
|
||||||
|
echo $GCP_BUCKET_NAME
|
||||||
|
echo $GCP_KEY_FILE
|
||||||
|
|
||||||
|
# Verify key file exists
|
||||||
|
ls -la $GCP_KEY_FILE
|
||||||
|
|
||||||
|
# Verify key file permissions (should be 600)
|
||||||
|
stat -c "%a %n" $GCP_KEY_FILE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Folder Structure in GCS
|
||||||
|
|
||||||
|
### 5.1 Production Bucket Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
reflow-documents-prod/
|
||||||
|
│
|
||||||
|
├── requests/ # All workflow-related files
|
||||||
|
│ ├── REQ-2025-12-0001/ # Request-specific folder
|
||||||
|
│ │ ├── documents/ # Initial request documents
|
||||||
|
│ │ │ ├── 1701234567890-abc123-proposal.pdf
|
||||||
|
│ │ │ ├── 1701234567891-def456-specification.docx
|
||||||
|
│ │ │ └── 1701234567892-ghi789-budget.xlsx
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── attachments/ # Work note attachments
|
||||||
|
│ │ │ ├── 1701234567893-jkl012-approval_note.pdf
|
||||||
|
│ │ │ ├── 1701234567894-mno345-signature.png
|
||||||
|
│ │ │ └── 1701234567895-pqr678-supporting_doc.pdf
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── invoices/ # Generated invoice files
|
||||||
|
│ │ │ └── 1701234567896-stu901-invoice_REQ-2025-12-0001.pdf
|
||||||
|
│ │ │
|
||||||
|
│ │ └── credit-notes/ # Generated credit note files
|
||||||
|
│ │ └── 1701234567897-vwx234-credit_note_REQ-2025-12-0001.pdf
|
||||||
|
│ │
|
||||||
|
│ ├── REQ-2025-12-0002/
|
||||||
|
│ │ ├── documents/
|
||||||
|
│ │ ├── attachments/
|
||||||
|
│ │ ├── invoices/
|
||||||
|
│ │ └── credit-notes/
|
||||||
|
│ │
|
||||||
|
│ └── REQ-2025-12-0003/
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── temp/ # Temporary uploads (auto-deleted after 24h)
|
||||||
|
└── (temporary files before processing)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 File Path Patterns
|
||||||
|
|
||||||
|
| File Type | Path Pattern | Example |
|
||||||
|
|-----------|--------------|---------|
|
||||||
|
| **Documents** | `requests/{requestNumber}/documents/{timestamp}-{hash}-{filename}` | `requests/REQ-2025-12-0001/documents/1701234567890-abc123-proposal.pdf` |
|
||||||
|
| **Attachments** | `requests/{requestNumber}/attachments/{timestamp}-{hash}-{filename}` | `requests/REQ-2025-12-0001/attachments/1701234567893-jkl012-approval_note.pdf` |
|
||||||
|
| **Invoices** | `requests/{requestNumber}/invoices/{timestamp}-{hash}-{filename}` | `requests/REQ-2025-12-0001/invoices/1701234567896-stu901-invoice_REQ-2025-12-0001.pdf` |
|
||||||
|
| **Credit Notes** | `requests/{requestNumber}/credit-notes/{timestamp}-{hash}-{filename}` | `requests/REQ-2025-12-0001/credit-notes/1701234567897-vwx234-credit_note_REQ-2025-12-0001.pdf` |
|
||||||
|
|
||||||
|
### 5.3 File Naming Convention
|
||||||
|
|
||||||
|
Files are automatically renamed with the following pattern:
|
||||||
|
```
|
||||||
|
{timestamp}-{randomHash}-{sanitizedOriginalName}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
- Original: `My Proposal Document (Final).pdf`
|
||||||
|
- Stored: `1701234567890-abc123-My_Proposal_Document__Final_.pdf`
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Prevents filename conflicts
|
||||||
|
- Maintains original filename for reference
|
||||||
|
- Ensures unique file identifiers
|
||||||
|
- Safe for URL encoding
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Security & Access Control
|
||||||
|
|
||||||
|
### 6.1 Bucket Security Settings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enforce public access prevention
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--public-access-prevention
|
||||||
|
|
||||||
|
# Enable uniform bucket-level access
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--uniform-bucket-level-access
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Access Control Strategy
|
||||||
|
|
||||||
|
**Production Approach:**
|
||||||
|
- **Private Bucket**: All files are private by default
|
||||||
|
- **Signed URLs**: Generate time-limited signed URLs for file access (recommended)
|
||||||
|
- **Service Account**: Only service account has direct access
|
||||||
|
- **IAM Policies**: Restrict access to specific service accounts only
|
||||||
|
|
||||||
|
### 6.3 Signed URL Configuration (Recommended)
|
||||||
|
|
||||||
|
For production, use signed URLs instead of public URLs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Generate signed URL (valid for 1 hour)
|
||||||
|
const [url] = await file.getSignedUrl({
|
||||||
|
action: 'read',
|
||||||
|
expires: Date.now() + 60 * 60 * 1000, // 1 hour
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Security Checklist
|
||||||
|
|
||||||
|
- [ ] Public access prevention enabled
|
||||||
|
- [ ] Uniform bucket-level access enabled
|
||||||
|
- [ ] Service account has minimal required permissions
|
||||||
|
- [ ] JSON key file stored securely (not in Git)
|
||||||
|
- [ ] Key file permissions set to 600
|
||||||
|
- [ ] CORS configured for specific domains only
|
||||||
|
- [ ] Bucket versioning enabled
|
||||||
|
- [ ] Access logging enabled
|
||||||
|
- [ ] Signed URLs used for file access (if applicable)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. CORS Configuration
|
||||||
|
|
||||||
|
### 7.1 Production CORS Policy
|
||||||
|
|
||||||
|
Create `cors-config-prod.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"origin": [
|
||||||
|
"https://reflow.royalenfield.com",
|
||||||
|
"https://www.royalenfield.com"
|
||||||
|
],
|
||||||
|
"method": ["GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS"],
|
||||||
|
"responseHeader": [
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Disposition",
|
||||||
|
"Content-Length",
|
||||||
|
"Cache-Control",
|
||||||
|
"x-goog-meta-*"
|
||||||
|
],
|
||||||
|
"maxAgeSeconds": 3600
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Apply CORS Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--cors-file=cors-config-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Verify CORS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check CORS configuration
|
||||||
|
gcloud storage buckets describe gs://reflow-documents-prod \
|
||||||
|
--format="value(cors)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Lifecycle Management
|
||||||
|
|
||||||
|
### 8.1 Lifecycle Rules Configuration
|
||||||
|
|
||||||
|
Create `lifecycle-config-prod.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lifecycle": {
|
||||||
|
"rule": [
|
||||||
|
{
|
||||||
|
"action": { "type": "Delete" },
|
||||||
|
"condition": {
|
||||||
|
"age": 1,
|
||||||
|
"matchesPrefix": ["temp/"]
|
||||||
|
},
|
||||||
|
"description": "Delete temporary files after 24 hours"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": { "type": "SetStorageClass", "storageClass": "NEARLINE" },
|
||||||
|
"condition": {
|
||||||
|
"age": 90,
|
||||||
|
"matchesPrefix": ["requests/"]
|
||||||
|
},
|
||||||
|
"description": "Move old files to Nearline storage after 90 days"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": { "type": "SetStorageClass", "storageClass": "COLDLINE" },
|
||||||
|
"condition": {
|
||||||
|
"age": 365,
|
||||||
|
"matchesPrefix": ["requests/"]
|
||||||
|
},
|
||||||
|
"description": "Move archived files to Coldline storage after 1 year"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Apply Lifecycle Rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--lifecycle-file=lifecycle-config-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Lifecycle Rule Benefits
|
||||||
|
|
||||||
|
| Rule | Purpose | Cost Savings |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| Delete temp files | Remove temporary uploads after 24h | Prevents storage bloat |
|
||||||
|
| Move to Nearline | Archive files older than 90 days | ~50% cost reduction |
|
||||||
|
| Move to Coldline | Archive files older than 1 year | ~70% cost reduction |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Monitoring & Alerts
|
||||||
|
|
||||||
|
### 9.1 Enable Access Logging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create logging bucket (if not exists)
|
||||||
|
gcloud storage buckets create gs://reflow-logs-prod \
|
||||||
|
--project=re-platform-workflow-dealer \
|
||||||
|
--location=asia-south1
|
||||||
|
|
||||||
|
# Enable access logging
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--log-bucket=gs://reflow-logs-prod \
|
||||||
|
--log-object-prefix=reflow-storage-logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Set Up Monitoring Alerts
|
||||||
|
|
||||||
|
**Recommended Alerts:**
|
||||||
|
|
||||||
|
1. **Storage Quota Alert**
|
||||||
|
- Trigger: Storage exceeds 80% of quota
|
||||||
|
- Action: Notify DevOps team
|
||||||
|
|
||||||
|
2. **Unusual Access Patterns**
|
||||||
|
- Trigger: Unusual download patterns detected
|
||||||
|
- Action: Security team notification
|
||||||
|
|
||||||
|
3. **Failed Access Attempts**
|
||||||
|
- Trigger: Multiple failed authentication attempts
|
||||||
|
- Action: Immediate security alert
|
||||||
|
|
||||||
|
4. **High Upload Volume**
|
||||||
|
- Trigger: Upload volume exceeds normal threshold
|
||||||
|
- Action: Performance team notification
|
||||||
|
|
||||||
|
### 9.3 Cost Monitoring
|
||||||
|
|
||||||
|
Monitor storage costs via:
|
||||||
|
- GCP Console → Billing → Reports
|
||||||
|
- Set up budget alerts at 50%, 75%, 90% of monthly budget
|
||||||
|
- Review storage class usage (Standard vs Nearline vs Coldline)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Verification & Testing
|
||||||
|
|
||||||
|
### 10.1 Pre-Deployment Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Verify bucket exists
|
||||||
|
gcloud storage buckets describe gs://reflow-documents-prod
|
||||||
|
|
||||||
|
# 2. Verify service account has access
|
||||||
|
gcloud storage ls gs://reflow-documents-prod \
|
||||||
|
--impersonate-service-account=reflow-storage-prod-sa@re-platform-workflow-dealer.iam.gserviceaccount.com
|
||||||
|
|
||||||
|
# 3. Test file upload
|
||||||
|
echo "test file" > test-upload.txt
|
||||||
|
gcloud storage cp test-upload.txt gs://reflow-documents-prod/temp/test-upload.txt
|
||||||
|
|
||||||
|
# 4. Test file download
|
||||||
|
gcloud storage cp gs://reflow-documents-prod/temp/test-upload.txt ./test-download.txt
|
||||||
|
|
||||||
|
# 5. Test file delete
|
||||||
|
gcloud storage rm gs://reflow-documents-prod/temp/test-upload.txt
|
||||||
|
|
||||||
|
# 6. Clean up
|
||||||
|
rm test-upload.txt test-download.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 Application-Level Testing
|
||||||
|
|
||||||
|
1. **Upload Test:**
|
||||||
|
- Upload a document via API
|
||||||
|
- Verify file appears in GCS bucket
|
||||||
|
- Check database `storage_url` field contains GCS URL
|
||||||
|
|
||||||
|
2. **Download Test:**
|
||||||
|
- Download file via API
|
||||||
|
- Verify file is accessible
|
||||||
|
- Check response headers
|
||||||
|
|
||||||
|
3. **Delete Test:**
|
||||||
|
- Delete file via API
|
||||||
|
- Verify file is removed from GCS
|
||||||
|
- Check database record is updated
|
||||||
|
|
||||||
|
### 10.3 Production Readiness Checklist
|
||||||
|
|
||||||
|
- [ ] Bucket created and configured
|
||||||
|
- [ ] Service account created with correct permissions
|
||||||
|
- [ ] JSON key file generated and stored securely
|
||||||
|
- [ ] Environment variables configured in `.env`
|
||||||
|
- [ ] CORS policy applied
|
||||||
|
- [ ] Lifecycle rules configured
|
||||||
|
- [ ] Versioning enabled
|
||||||
|
- [ ] Access logging enabled
|
||||||
|
- [ ] Monitoring alerts configured
|
||||||
|
- [ ] Upload/download/delete operations tested
|
||||||
|
- [ ] Backup and recovery procedures documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Troubleshooting
|
||||||
|
|
||||||
|
### 11.1 Common Issues
|
||||||
|
|
||||||
|
**Issue: Files not uploading to GCS**
|
||||||
|
- ✅ Check `.env` configuration matches credentials
|
||||||
|
- ✅ Verify service account has correct permissions
|
||||||
|
- ✅ Check bucket name exists and is accessible
|
||||||
|
- ✅ Review application logs for GCS errors
|
||||||
|
- ✅ Verify key file path is correct
|
||||||
|
|
||||||
|
**Issue: Files uploading but not accessible**
|
||||||
|
- ✅ Verify bucket permissions (private vs public)
|
||||||
|
- ✅ Check CORS configuration if accessing from browser
|
||||||
|
- ✅ Ensure `storage_url` is being saved correctly in database
|
||||||
|
- ✅ Verify signed URL generation (if using private bucket)
|
||||||
|
|
||||||
|
**Issue: Permission denied errors**
|
||||||
|
- ✅ Verify service account has `roles/storage.objectAdmin`
|
||||||
|
- ✅ Check bucket IAM policies
|
||||||
|
- ✅ Verify key file is valid and not expired
|
||||||
|
|
||||||
|
### 11.2 Log Analysis
|
||||||
|
|
||||||
|
Check application logs for GCS-related messages:
|
||||||
|
```bash
|
||||||
|
# Search for GCS initialization
|
||||||
|
grep "GCS.*Initialized" logs/app.log
|
||||||
|
|
||||||
|
# Search for GCS errors
|
||||||
|
grep "GCS.*Error" logs/app.log
|
||||||
|
|
||||||
|
# Search for upload failures
|
||||||
|
grep "GCS.*upload.*failed" logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Production Deployment Steps
|
||||||
|
|
||||||
|
### 12.1 Deployment Checklist
|
||||||
|
|
||||||
|
1. **Pre-Deployment:**
|
||||||
|
- [ ] Create production bucket
|
||||||
|
- [ ] Create production service account
|
||||||
|
- [ ] Generate and secure key file
|
||||||
|
- [ ] Configure environment variables
|
||||||
|
- [ ] Test upload/download operations
|
||||||
|
|
||||||
|
2. **Deployment:**
|
||||||
|
- [ ] Deploy application with new environment variables
|
||||||
|
- [ ] Verify GCS initialization in logs
|
||||||
|
- [ ] Test file upload functionality
|
||||||
|
- [ ] Monitor for errors
|
||||||
|
|
||||||
|
3. **Post-Deployment:**
|
||||||
|
- [ ] Verify files are being stored in GCS
|
||||||
|
- [ ] Check database `storage_url` fields
|
||||||
|
- [ ] Monitor storage costs
|
||||||
|
- [ ] Review access logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Cost Estimation (Production)
|
||||||
|
|
||||||
|
| Item | Monthly Estimate | Notes |
|
||||||
|
|------|------------------|-------|
|
||||||
|
| **Storage (500GB)** | ~$10.00 | Standard storage class |
|
||||||
|
| **Operations (100K)** | ~$0.50 | Upload/download operations |
|
||||||
|
| **Network Egress** | Variable | Depends on download volume |
|
||||||
|
| **Nearline Storage** | ~$5.00 | Files older than 90 days |
|
||||||
|
| **Coldline Storage** | ~$2.00 | Files older than 1 year |
|
||||||
|
|
||||||
|
**Total Estimated Monthly Cost:** ~$17.50 (excluding network egress)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Support & Contacts
|
||||||
|
|
||||||
|
| Role | Responsibility | Contact |
|
||||||
|
|------|----------------|---------|
|
||||||
|
| **DevOps Team** | GCP infrastructure setup | [DevOps Email] |
|
||||||
|
| **Application Team** | Application configuration | [App Team Email] |
|
||||||
|
| **Security Team** | Access control and permissions | [Security Email] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Quick Reference
|
||||||
|
|
||||||
|
### 15.1 Essential Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create bucket
|
||||||
|
gcloud storage buckets create gs://reflow-documents-prod \
|
||||||
|
--project=re-platform-workflow-dealer \
|
||||||
|
--location=asia-south1 \
|
||||||
|
--uniform-bucket-level-access \
|
||||||
|
--public-access-prevention
|
||||||
|
|
||||||
|
# Create service account
|
||||||
|
gcloud iam service-accounts create reflow-storage-prod-sa \
|
||||||
|
--display-name="RE Workflow Production Storage" \
|
||||||
|
--project=re-platform-workflow-dealer
|
||||||
|
|
||||||
|
# Generate key
|
||||||
|
gcloud iam service-accounts keys create ./config/gcp-key-prod.json \
|
||||||
|
--iam-account=reflow-storage-prod-sa@re-platform-workflow-dealer.iam.gserviceaccount.com
|
||||||
|
|
||||||
|
# Set CORS
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--cors-file=cors-config-prod.json
|
||||||
|
|
||||||
|
# Enable versioning
|
||||||
|
gcloud storage buckets update gs://reflow-documents-prod \
|
||||||
|
--versioning
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.2 Environment Variables Template
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Production GCP Configuration
|
||||||
|
GCP_PROJECT_ID=re-platform-workflow-dealer
|
||||||
|
GCP_BUCKET_NAME=reflow-documents-prod
|
||||||
|
GCP_KEY_FILE=./config/gcp-key-prod.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: File Structure Reference
|
||||||
|
|
||||||
|
### Database Storage Fields
|
||||||
|
|
||||||
|
The application stores file information in the database:
|
||||||
|
|
||||||
|
| Table | Field | Description |
|
||||||
|
|-------|-------|-------------|
|
||||||
|
| `documents` | `file_path` | GCS path: `requests/{requestNumber}/documents/{filename}` |
|
||||||
|
| `documents` | `storage_url` | Full GCS URL: `https://storage.googleapis.com/bucket/path` |
|
||||||
|
| `work_note_attachments` | `file_path` | GCS path: `requests/{requestNumber}/attachments/{filename}` |
|
||||||
|
| `work_note_attachments` | `storage_url` | Full GCS URL |
|
||||||
|
| `claim_invoices` | `invoice_file_path` | GCS path: `requests/{requestNumber}/invoices/{filename}` |
|
||||||
|
| `claim_credit_notes` | `credit_note_file_path` | GCS path: `requests/{requestNumber}/credit-notes/{filename}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version:** 1.0
|
||||||
|
**Last Updated:** December 2024
|
||||||
|
**Maintained By:** RE Workflow Development Team
|
||||||
|
|
||||||
@ -813,5 +813,42 @@ export class DealerClaimController {
|
|||||||
return ResponseHandler.error(res, 'Failed to update credit note details', 500, errorMessage);
|
return ResponseHandler.error(res, 'Failed to update credit note details', 500, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send credit note to dealer and auto-approve Step 8
|
||||||
|
* POST /api/v1/dealer-claims/:requestId/credit-note/send
|
||||||
|
* Accepts either UUID or requestNumber
|
||||||
|
*/
|
||||||
|
async sendCreditNoteToDealer(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const identifier = req.params.requestId; // Can be UUID or requestNumber
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return ResponseHandler.error(res, 'Unauthorized', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find workflow to get actual UUID
|
||||||
|
const workflow = await this.findWorkflowByIdentifier(identifier);
|
||||||
|
if (!workflow) {
|
||||||
|
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
||||||
|
if (!requestId) {
|
||||||
|
return ResponseHandler.error(res, 'Invalid workflow request', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dealerClaimService.sendCreditNoteToDealer(requestId, userId);
|
||||||
|
|
||||||
|
return ResponseHandler.success(res, { message: 'Credit note sent to dealer and Step 8 approved successfully' }, 'Credit note sent');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DealerClaimController] Error sending credit note to dealer:', error);
|
||||||
|
return ResponseHandler.error(res, 'Failed to send credit note to dealer', 500, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
117
src/controllers/dmsWebhook.controller.ts
Normal file
117
src/controllers/dmsWebhook.controller.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { DMSWebhookService } from '../services/dmsWebhook.service';
|
||||||
|
import { ResponseHandler } from '../utils/responseHandler';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMS Webhook Controller
|
||||||
|
* Handles webhook callbacks from DMS system for invoice and credit note generation
|
||||||
|
*/
|
||||||
|
export class DMSWebhookController {
|
||||||
|
private webhookService = new DMSWebhookService();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle invoice generation webhook from DMS
|
||||||
|
* POST /api/v1/webhooks/dms/invoice
|
||||||
|
*/
|
||||||
|
async handleInvoiceWebhook(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payload = req.body;
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Invoice webhook received', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
documentNo: payload.document_no,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate webhook signature if configured
|
||||||
|
const isValid = await this.webhookService.validateWebhookSignature(req);
|
||||||
|
if (!isValid) {
|
||||||
|
logger.warn('[DMSWebhook] Invalid webhook signature');
|
||||||
|
return ResponseHandler.error(res, 'Invalid webhook signature', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process invoice webhook
|
||||||
|
const result = await this.webhookService.processInvoiceWebhook(payload);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
logger.error('[DMSWebhook] Invoice webhook processing failed', {
|
||||||
|
error: result.error,
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
});
|
||||||
|
return ResponseHandler.error(res, result.error || 'Failed to process invoice webhook', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Invoice webhook processed successfully', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
invoiceNumber: result.invoiceNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
message: 'Invoice webhook processed successfully',
|
||||||
|
invoiceNumber: result.invoiceNumber,
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
},
|
||||||
|
'Webhook processed'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DMSWebhook] Error processing invoice webhook:', error);
|
||||||
|
return ResponseHandler.error(res, 'Failed to process invoice webhook', 500, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle credit note generation webhook from DMS
|
||||||
|
* POST /api/v1/webhooks/dms/credit-note
|
||||||
|
*/
|
||||||
|
async handleCreditNoteWebhook(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payload = req.body;
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Credit note webhook received', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
documentNo: payload.document_no,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate webhook signature if configured
|
||||||
|
const isValid = await this.webhookService.validateWebhookSignature(req);
|
||||||
|
if (!isValid) {
|
||||||
|
logger.warn('[DMSWebhook] Invalid webhook signature');
|
||||||
|
return ResponseHandler.error(res, 'Invalid webhook signature', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process credit note webhook
|
||||||
|
const result = await this.webhookService.processCreditNoteWebhook(payload);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
logger.error('[DMSWebhook] Credit note webhook processing failed', {
|
||||||
|
error: result.error,
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
});
|
||||||
|
return ResponseHandler.error(res, result.error || 'Failed to process credit note webhook', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Credit note webhook processed successfully', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
creditNoteNumber: result.creditNoteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseHandler.success(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
message: 'Credit note webhook processed successfully',
|
||||||
|
creditNoteNumber: result.creditNoteNumber,
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
},
|
||||||
|
'Webhook processed'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DMSWebhook] Error processing credit note webhook:', error);
|
||||||
|
return ResponseHandler.error(res, 'Failed to process credit note webhook', 500, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -23,18 +23,30 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
type: DataTypes.DATEONLY,
|
type: DataTypes.DATEONLY,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
invoice_amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
dms_number: {
|
dms_number: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
amount: {
|
invoice_file_path: {
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.STRING(500),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
generation_status: {
|
||||||
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, SENT, FAILED, CANCELLED
|
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, SENT, FAILED, CANCELLED
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
error_message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
generated_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -54,6 +66,7 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
await queryInterface.addIndex('claim_invoices', ['request_id'], { name: 'idx_claim_invoices_request_id', unique: true });
|
await queryInterface.addIndex('claim_invoices', ['request_id'], { name: 'idx_claim_invoices_request_id', unique: true });
|
||||||
await queryInterface.addIndex('claim_invoices', ['invoice_number'], { name: 'idx_claim_invoices_invoice_number' });
|
await queryInterface.addIndex('claim_invoices', ['invoice_number'], { name: 'idx_claim_invoices_invoice_number' });
|
||||||
await queryInterface.addIndex('claim_invoices', ['dms_number'], { name: 'idx_claim_invoices_dms_number' });
|
await queryInterface.addIndex('claim_invoices', ['dms_number'], { name: 'idx_claim_invoices_dms_number' });
|
||||||
|
await queryInterface.addIndex('claim_invoices', ['generation_status'], { name: 'idx_claim_invoices_status' });
|
||||||
|
|
||||||
await queryInterface.createTable('claim_credit_notes', {
|
await queryInterface.createTable('claim_credit_notes', {
|
||||||
credit_note_id: {
|
credit_note_id: {
|
||||||
@ -69,6 +82,13 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
},
|
},
|
||||||
|
invoice_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'claim_invoices', key: 'invoice_id' },
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
},
|
||||||
credit_note_number: {
|
credit_note_number: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -77,12 +97,35 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
type: DataTypes.DATEONLY,
|
type: DataTypes.DATEONLY,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
credit_note_amount: {
|
credit_amount: {
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
status: {
|
sap_document_number: {
|
||||||
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, SENT, FAILED, CANCELLED
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
credit_note_file_path: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
confirmation_status: {
|
||||||
|
type: DataTypes.STRING(50), // e.g., PENDING, GENERATED, CONFIRMED, FAILED, CANCELLED
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
error_message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
confirmed_by: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'users', key: 'user_id' },
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
},
|
||||||
|
confirmed_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
@ -106,7 +149,10 @@ export async function up(queryInterface: QueryInterface): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['request_id'], { name: 'idx_claim_credit_notes_request_id', unique: true });
|
await queryInterface.addIndex('claim_credit_notes', ['request_id'], { name: 'idx_claim_credit_notes_request_id', unique: true });
|
||||||
|
await queryInterface.addIndex('claim_credit_notes', ['invoice_id'], { name: 'idx_claim_credit_notes_invoice_id' });
|
||||||
await queryInterface.addIndex('claim_credit_notes', ['credit_note_number'], { name: 'idx_claim_credit_notes_number' });
|
await queryInterface.addIndex('claim_credit_notes', ['credit_note_number'], { name: 'idx_claim_credit_notes_number' });
|
||||||
|
await queryInterface.addIndex('claim_credit_notes', ['sap_document_number'], { name: 'idx_claim_credit_notes_sap_doc' });
|
||||||
|
await queryInterface.addIndex('claim_credit_notes', ['confirmation_status'], { name: 'idx_claim_credit_notes_status' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
|||||||
240
src/migrations/20251218-fix-claim-invoice-credit-note-columns.ts
Normal file
240
src/migrations/20251218-fix-claim-invoice-credit-note-columns.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import { QueryInterface, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if a column exists in a table
|
||||||
|
*/
|
||||||
|
async function columnExists(
|
||||||
|
queryInterface: QueryInterface,
|
||||||
|
tableName: string,
|
||||||
|
columnName: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const tableDescription = await queryInterface.describeTable(tableName);
|
||||||
|
return columnName in tableDescription;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration: Fix column names in claim_invoices and claim_credit_notes tables
|
||||||
|
*
|
||||||
|
* This migration handles the case where tables were created with old column names
|
||||||
|
* and need to be updated to match the new schema.
|
||||||
|
*/
|
||||||
|
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if claim_invoices table exists
|
||||||
|
const [invoiceTables] = await queryInterface.sequelize.query(`
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'claim_invoices';
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (invoiceTables.length > 0) {
|
||||||
|
// Fix claim_invoices table
|
||||||
|
const hasOldAmount = await columnExists(queryInterface, 'claim_invoices', 'amount');
|
||||||
|
const hasNewAmount = await columnExists(queryInterface, 'claim_invoices', 'invoice_amount');
|
||||||
|
|
||||||
|
if (hasOldAmount && !hasNewAmount) {
|
||||||
|
// Rename amount to invoice_amount
|
||||||
|
await queryInterface.renameColumn('claim_invoices', 'amount', 'invoice_amount');
|
||||||
|
console.log('✅ Renamed claim_invoices.amount to invoice_amount');
|
||||||
|
} else if (!hasOldAmount && !hasNewAmount) {
|
||||||
|
// Add invoice_amount if neither exists
|
||||||
|
await queryInterface.addColumn('claim_invoices', 'invoice_amount', {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added invoice_amount column to claim_invoices');
|
||||||
|
} else if (hasNewAmount) {
|
||||||
|
console.log('✅ invoice_amount column already exists in claim_invoices');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for status vs generation_status
|
||||||
|
const hasStatus = await columnExists(queryInterface, 'claim_invoices', 'status');
|
||||||
|
const hasGenerationStatus = await columnExists(queryInterface, 'claim_invoices', 'generation_status');
|
||||||
|
|
||||||
|
if (hasStatus && !hasGenerationStatus) {
|
||||||
|
// Rename status to generation_status
|
||||||
|
await queryInterface.renameColumn('claim_invoices', 'status', 'generation_status');
|
||||||
|
console.log('✅ Renamed claim_invoices.status to generation_status');
|
||||||
|
} else if (!hasStatus && !hasGenerationStatus) {
|
||||||
|
// Add generation_status if neither exists
|
||||||
|
await queryInterface.addColumn('claim_invoices', 'generation_status', {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added generation_status column to claim_invoices');
|
||||||
|
} else if (hasGenerationStatus) {
|
||||||
|
console.log('✅ generation_status column already exists in claim_invoices');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if claim_credit_notes table exists
|
||||||
|
const [creditNoteTables] = await queryInterface.sequelize.query(`
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'claim_credit_notes';
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (creditNoteTables.length > 0) {
|
||||||
|
// Fix claim_credit_notes table
|
||||||
|
const hasOldAmount = await columnExists(queryInterface, 'claim_credit_notes', 'credit_note_amount');
|
||||||
|
const hasNewAmount = await columnExists(queryInterface, 'claim_credit_notes', 'credit_amount');
|
||||||
|
|
||||||
|
if (hasOldAmount && !hasNewAmount) {
|
||||||
|
// Rename credit_note_amount to credit_amount
|
||||||
|
await queryInterface.renameColumn('claim_credit_notes', 'credit_note_amount', 'credit_amount');
|
||||||
|
console.log('✅ Renamed claim_credit_notes.credit_note_amount to credit_amount');
|
||||||
|
} else if (!hasOldAmount && !hasNewAmount) {
|
||||||
|
// Add credit_amount if neither exists
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'credit_amount', {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added credit_amount column to claim_credit_notes');
|
||||||
|
} else if (hasNewAmount) {
|
||||||
|
console.log('✅ credit_amount column already exists in claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for status vs confirmation_status
|
||||||
|
const hasStatus = await columnExists(queryInterface, 'claim_credit_notes', 'status');
|
||||||
|
const hasConfirmationStatus = await columnExists(queryInterface, 'claim_credit_notes', 'confirmation_status');
|
||||||
|
|
||||||
|
if (hasStatus && !hasConfirmationStatus) {
|
||||||
|
// Rename status to confirmation_status
|
||||||
|
await queryInterface.renameColumn('claim_credit_notes', 'status', 'confirmation_status');
|
||||||
|
console.log('✅ Renamed claim_credit_notes.status to confirmation_status');
|
||||||
|
} else if (!hasStatus && !hasConfirmationStatus) {
|
||||||
|
// Add confirmation_status if neither exists
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'confirmation_status', {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added confirmation_status column to claim_credit_notes');
|
||||||
|
} else if (hasConfirmationStatus) {
|
||||||
|
console.log('✅ confirmation_status column already exists in claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure invoice_id column exists
|
||||||
|
const hasInvoiceId = await columnExists(queryInterface, 'claim_credit_notes', 'invoice_id');
|
||||||
|
if (!hasInvoiceId) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'invoice_id', {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'claim_invoices',
|
||||||
|
key: 'invoice_id',
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
});
|
||||||
|
console.log('✅ Added invoice_id column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure sap_document_number column exists
|
||||||
|
const hasSapDoc = await columnExists(queryInterface, 'claim_credit_notes', 'sap_document_number');
|
||||||
|
if (!hasSapDoc) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'sap_document_number', {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added sap_document_number column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure credit_note_file_path column exists
|
||||||
|
const hasFilePath = await columnExists(queryInterface, 'claim_credit_notes', 'credit_note_file_path');
|
||||||
|
if (!hasFilePath) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'credit_note_file_path', {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added credit_note_file_path column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure confirmed_by column exists
|
||||||
|
const hasConfirmedBy = await columnExists(queryInterface, 'claim_credit_notes', 'confirmed_by');
|
||||||
|
if (!hasConfirmedBy) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'confirmed_by', {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'user_id',
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
});
|
||||||
|
console.log('✅ Added confirmed_by column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure confirmed_at column exists
|
||||||
|
const hasConfirmedAt = await columnExists(queryInterface, 'claim_credit_notes', 'confirmed_at');
|
||||||
|
if (!hasConfirmedAt) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'confirmed_at', {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added confirmed_at column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure invoice_file_path exists in claim_invoices
|
||||||
|
if (invoiceTables.length > 0) {
|
||||||
|
const hasFilePath = await columnExists(queryInterface, 'claim_invoices', 'invoice_file_path');
|
||||||
|
if (!hasFilePath) {
|
||||||
|
await queryInterface.addColumn('claim_invoices', 'invoice_file_path', {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added invoice_file_path column to claim_invoices');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure error_message exists
|
||||||
|
const hasErrorMessage = await columnExists(queryInterface, 'claim_invoices', 'error_message');
|
||||||
|
if (!hasErrorMessage) {
|
||||||
|
await queryInterface.addColumn('claim_invoices', 'error_message', {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added error_message column to claim_invoices');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure generated_at exists
|
||||||
|
const hasGeneratedAt = await columnExists(queryInterface, 'claim_invoices', 'generated_at');
|
||||||
|
if (!hasGeneratedAt) {
|
||||||
|
await queryInterface.addColumn('claim_invoices', 'generated_at', {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added generated_at column to claim_invoices');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure error_message exists in claim_credit_notes
|
||||||
|
if (creditNoteTables.length > 0) {
|
||||||
|
const hasErrorMessage = await columnExists(queryInterface, 'claim_credit_notes', 'error_message');
|
||||||
|
if (!hasErrorMessage) {
|
||||||
|
await queryInterface.addColumn('claim_credit_notes', 'error_message', {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
console.log('✅ Added error_message column to claim_credit_notes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Migration error:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
// This migration is idempotent and safe to run multiple times
|
||||||
|
// The down migration would reverse the changes, but it's safer to keep the new schema
|
||||||
|
console.log('Note: Down migration not implemented - keeping new column names');
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,29 +1,42 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
import { DataTypes, Model, Optional } from 'sequelize';
|
||||||
import { sequelize } from '@config/database';
|
import { sequelize } from '@config/database';
|
||||||
import { WorkflowRequest } from './WorkflowRequest';
|
import { WorkflowRequest } from './WorkflowRequest';
|
||||||
|
import { ClaimInvoice } from './ClaimInvoice';
|
||||||
|
|
||||||
interface ClaimCreditNoteAttributes {
|
interface ClaimCreditNoteAttributes {
|
||||||
creditNoteId: string;
|
creditNoteId: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
invoiceId?: string;
|
||||||
creditNoteNumber?: string;
|
creditNoteNumber?: string;
|
||||||
creditNoteDate?: Date;
|
creditNoteDate?: Date;
|
||||||
creditNoteAmount?: number;
|
creditNoteAmount?: number;
|
||||||
|
sapDocumentNumber?: string;
|
||||||
|
creditNoteFilePath?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
confirmedBy?: string;
|
||||||
|
confirmedAt?: Date;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaimCreditNoteCreationAttributes extends Optional<ClaimCreditNoteAttributes, 'creditNoteId' | 'creditNoteNumber' | 'creditNoteDate' | 'creditNoteAmount' | 'status' | 'reason' | 'description' | 'createdAt' | 'updatedAt'> {}
|
interface ClaimCreditNoteCreationAttributes extends Optional<ClaimCreditNoteAttributes, 'creditNoteId' | 'invoiceId' | 'creditNoteNumber' | 'creditNoteDate' | 'creditNoteAmount' | 'sapDocumentNumber' | 'creditNoteFilePath' | 'status' | 'errorMessage' | 'confirmedBy' | 'confirmedAt' | 'reason' | 'description' | 'createdAt' | 'updatedAt'> {}
|
||||||
|
|
||||||
class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCreationAttributes> implements ClaimCreditNoteAttributes {
|
class ClaimCreditNote extends Model<ClaimCreditNoteAttributes, ClaimCreditNoteCreationAttributes> implements ClaimCreditNoteAttributes {
|
||||||
public creditNoteId!: string;
|
public creditNoteId!: string;
|
||||||
public requestId!: string;
|
public requestId!: string;
|
||||||
|
public invoiceId?: string;
|
||||||
public creditNoteNumber?: string;
|
public creditNoteNumber?: string;
|
||||||
public creditNoteDate?: Date;
|
public creditNoteDate?: Date;
|
||||||
public creditNoteAmount?: number;
|
public creditNoteAmount?: number;
|
||||||
|
public sapDocumentNumber?: string;
|
||||||
|
public creditNoteFilePath?: string;
|
||||||
public status?: string;
|
public status?: string;
|
||||||
|
public errorMessage?: string;
|
||||||
|
public confirmedBy?: string;
|
||||||
|
public confirmedAt?: Date;
|
||||||
public reason?: string;
|
public reason?: string;
|
||||||
public description?: string;
|
public description?: string;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
@ -50,6 +63,17 @@ ClaimCreditNote.init(
|
|||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
},
|
},
|
||||||
|
invoiceId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'invoice_id',
|
||||||
|
references: {
|
||||||
|
model: 'claim_invoices',
|
||||||
|
key: 'invoice_id',
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
},
|
||||||
creditNoteNumber: {
|
creditNoteNumber: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -63,12 +87,43 @@ ClaimCreditNote.init(
|
|||||||
creditNoteAmount: {
|
creditNoteAmount: {
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'credit_note_amount',
|
field: 'credit_amount',
|
||||||
|
},
|
||||||
|
sapDocumentNumber: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'sap_document_number',
|
||||||
|
},
|
||||||
|
creditNoteFilePath: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'credit_note_file_path',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING(50),
|
type: DataTypes.STRING(50),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'status',
|
field: 'confirmation_status',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'error_message',
|
||||||
|
},
|
||||||
|
confirmedBy: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'confirmed_by',
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'user_id',
|
||||||
|
},
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
},
|
||||||
|
confirmedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'confirmed_at',
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
@ -102,7 +157,10 @@ ClaimCreditNote.init(
|
|||||||
updatedAt: 'updated_at',
|
updatedAt: 'updated_at',
|
||||||
indexes: [
|
indexes: [
|
||||||
{ unique: true, fields: ['request_id'], name: 'idx_claim_credit_notes_request_id' },
|
{ unique: true, fields: ['request_id'], name: 'idx_claim_credit_notes_request_id' },
|
||||||
|
{ fields: ['invoice_id'], name: 'idx_claim_credit_notes_invoice_id' },
|
||||||
{ fields: ['credit_note_number'], name: 'idx_claim_credit_notes_number' },
|
{ fields: ['credit_note_number'], name: 'idx_claim_credit_notes_number' },
|
||||||
|
{ fields: ['sap_document_number'], name: 'idx_claim_credit_notes_sap_doc' },
|
||||||
|
{ fields: ['confirmation_status'], name: 'idx_claim_credit_notes_status' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -119,5 +177,17 @@ ClaimCreditNote.belongsTo(WorkflowRequest, {
|
|||||||
targetKey: 'requestId',
|
targetKey: 'requestId',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ClaimCreditNote.belongsTo(ClaimInvoice, {
|
||||||
|
as: 'claimInvoice',
|
||||||
|
foreignKey: 'invoiceId',
|
||||||
|
targetKey: 'invoiceId',
|
||||||
|
});
|
||||||
|
|
||||||
|
ClaimInvoice.hasMany(ClaimCreditNote, {
|
||||||
|
as: 'creditNotes',
|
||||||
|
foreignKey: 'invoiceId',
|
||||||
|
sourceKey: 'invoiceId',
|
||||||
|
});
|
||||||
|
|
||||||
export { ClaimCreditNote };
|
export { ClaimCreditNote };
|
||||||
|
|
||||||
|
|||||||
@ -7,24 +7,30 @@ interface ClaimInvoiceAttributes {
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
invoiceNumber?: string;
|
invoiceNumber?: string;
|
||||||
invoiceDate?: Date;
|
invoiceDate?: Date;
|
||||||
dmsNumber?: string;
|
|
||||||
amount?: number;
|
amount?: number;
|
||||||
|
dmsNumber?: string;
|
||||||
|
invoiceFilePath?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
generatedAt?: Date;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'invoiceDate' | 'dmsNumber' | 'amount' | 'status' | 'description' | 'createdAt' | 'updatedAt'> {}
|
interface ClaimInvoiceCreationAttributes extends Optional<ClaimInvoiceAttributes, 'invoiceId' | 'invoiceNumber' | 'invoiceDate' | 'amount' | 'dmsNumber' | 'invoiceFilePath' | 'status' | 'errorMessage' | 'generatedAt' | 'description' | 'createdAt' | 'updatedAt'> {}
|
||||||
|
|
||||||
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
|
class ClaimInvoice extends Model<ClaimInvoiceAttributes, ClaimInvoiceCreationAttributes> implements ClaimInvoiceAttributes {
|
||||||
public invoiceId!: string;
|
public invoiceId!: string;
|
||||||
public requestId!: string;
|
public requestId!: string;
|
||||||
public invoiceNumber?: string;
|
public invoiceNumber?: string;
|
||||||
public invoiceDate?: Date;
|
public invoiceDate?: Date;
|
||||||
public dmsNumber?: string;
|
|
||||||
public amount?: number;
|
public amount?: number;
|
||||||
|
public dmsNumber?: string;
|
||||||
|
public invoiceFilePath?: string;
|
||||||
public status?: string;
|
public status?: string;
|
||||||
|
public errorMessage?: string;
|
||||||
|
public generatedAt?: Date;
|
||||||
public description?: string;
|
public description?: string;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
public updatedAt!: Date;
|
public updatedAt!: Date;
|
||||||
@ -60,20 +66,35 @@ ClaimInvoice.init(
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'invoice_date',
|
field: 'invoice_date',
|
||||||
},
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.DECIMAL(15, 2),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'invoice_amount',
|
||||||
|
},
|
||||||
dmsNumber: {
|
dmsNumber: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'dms_number',
|
field: 'dms_number',
|
||||||
},
|
},
|
||||||
amount: {
|
invoiceFilePath: {
|
||||||
type: DataTypes.DECIMAL(15, 2),
|
type: DataTypes.STRING(500),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'amount',
|
field: 'invoice_file_path',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING(50),
|
type: DataTypes.STRING(50),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'status',
|
field: 'generation_status',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'error_message',
|
||||||
|
},
|
||||||
|
generatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'generated_at',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
@ -104,6 +125,7 @@ ClaimInvoice.init(
|
|||||||
{ unique: true, fields: ['request_id'], name: 'idx_claim_invoices_request_id' },
|
{ unique: true, fields: ['request_id'], name: 'idx_claim_invoices_request_id' },
|
||||||
{ fields: ['invoice_number'], name: 'idx_claim_invoices_invoice_number' },
|
{ fields: ['invoice_number'], name: 'idx_claim_invoices_invoice_number' },
|
||||||
{ fields: ['dms_number'], name: 'idx_claim_invoices_dms_number' },
|
{ fields: ['dms_number'], name: 'idx_claim_invoices_dms_number' },
|
||||||
|
{ fields: ['generation_status'], name: 'idx_claim_invoices_status' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -120,5 +142,8 @@ ClaimInvoice.belongsTo(WorkflowRequest, {
|
|||||||
targetKey: 'requestId',
|
targetKey: 'requestId',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Note: hasMany association with ClaimCreditNote is defined in ClaimCreditNote.ts
|
||||||
|
// to avoid circular dependency issues
|
||||||
|
|
||||||
export { ClaimInvoice };
|
export { ClaimInvoice };
|
||||||
|
|
||||||
|
|||||||
@ -86,5 +86,12 @@ router.put('/:requestId/e-invoice', authenticateToken, asyncHandler(dealerClaimC
|
|||||||
*/
|
*/
|
||||||
router.put('/:requestId/credit-note', authenticateToken, asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
router.put('/:requestId/credit-note', authenticateToken, asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
||||||
|
* @desc Send credit note to dealer and auto-approve Step 8
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.post('/:requestId/credit-note/send', authenticateToken, asyncHandler(dealerClaimController.sendCreditNoteToDealer.bind(dealerClaimController)));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
47
src/routes/dmsWebhook.routes.ts
Normal file
47
src/routes/dmsWebhook.routes.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { DMSWebhookController } from '../controllers/dmsWebhook.controller';
|
||||||
|
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const webhookController = new DMSWebhookController();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/v1/webhooks/dms/health
|
||||||
|
* @desc Health check endpoint for webhook routes (for testing)
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/health', (_req, res) => {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'OK',
|
||||||
|
message: 'DMS Webhook routes are active',
|
||||||
|
endpoints: {
|
||||||
|
invoice: 'POST /api/v1/webhooks/dms/invoice',
|
||||||
|
creditNote: 'POST /api/v1/webhooks/dms/credit-note'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/v1/webhooks/dms/invoice
|
||||||
|
* @desc Webhook endpoint for DMS invoice generation callbacks
|
||||||
|
* @access Public (authenticated via webhook signature)
|
||||||
|
* @note This endpoint is called by DMS system after invoice generation
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/invoice',
|
||||||
|
asyncHandler(webhookController.handleInvoiceWebhook.bind(webhookController))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/v1/webhooks/dms/credit-note
|
||||||
|
* @desc Webhook endpoint for DMS credit note generation callbacks
|
||||||
|
* @access Public (authenticated via webhook signature)
|
||||||
|
* @note This endpoint is called by DMS system after credit note generation
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/credit-note',
|
||||||
|
asyncHandler(webhookController.handleCreditNoteWebhook.bind(webhookController))
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ import aiRoutes from './ai.routes';
|
|||||||
import dealerClaimRoutes from './dealerClaim.routes';
|
import dealerClaimRoutes from './dealerClaim.routes';
|
||||||
import templateRoutes from './template.routes';
|
import templateRoutes from './template.routes';
|
||||||
import dealerRoutes from './dealer.routes';
|
import dealerRoutes from './dealer.routes';
|
||||||
|
import dmsWebhookRoutes from './dmsWebhook.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ router.use('/summaries', summaryRoutes);
|
|||||||
router.use('/dealer-claims', dealerClaimRoutes);
|
router.use('/dealer-claims', dealerClaimRoutes);
|
||||||
router.use('/templates', templateRoutes);
|
router.use('/templates', templateRoutes);
|
||||||
router.use('/dealers', dealerRoutes);
|
router.use('/dealers', dealerRoutes);
|
||||||
|
router.use('/webhooks/dms', dmsWebhookRoutes);
|
||||||
|
|
||||||
// TODO: Add other route modules as they are implemented
|
// TODO: Add other route modules as they are implemented
|
||||||
// router.use('/approvals', approvalRoutes);
|
// router.use('/approvals', approvalRoutes);
|
||||||
|
|||||||
@ -132,6 +132,7 @@ async function runMigrations(): Promise<void> {
|
|||||||
const m37 = require('../migrations/20251213-drop-claim-details-invoice-columns');
|
const m37 = require('../migrations/20251213-drop-claim-details-invoice-columns');
|
||||||
const m38 = require('../migrations/20251213-create-claim-invoice-credit-note-tables');
|
const m38 = require('../migrations/20251213-create-claim-invoice-credit-note-tables');
|
||||||
const m39 = require('../migrations/20251214-create-dealer-completion-expenses');
|
const m39 = require('../migrations/20251214-create-dealer-completion-expenses');
|
||||||
|
const m40 = require('../migrations/20251218-fix-claim-invoice-credit-note-columns');
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
@ -176,6 +177,7 @@ async function runMigrations(): Promise<void> {
|
|||||||
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
||||||
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
||||||
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
||||||
|
{ name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import * as m36 from '../migrations/20251211-create-claim-budget-tracking-table'
|
|||||||
import * as m37 from '../migrations/20251213-drop-claim-details-invoice-columns';
|
import * as m37 from '../migrations/20251213-drop-claim-details-invoice-columns';
|
||||||
import * as m38 from '../migrations/20251213-create-claim-invoice-credit-note-tables';
|
import * as m38 from '../migrations/20251213-create-claim-invoice-credit-note-tables';
|
||||||
import * as m39 from '../migrations/20251214-create-dealer-completion-expenses';
|
import * as m39 from '../migrations/20251214-create-dealer-completion-expenses';
|
||||||
|
import * as m40 from '../migrations/20251218-fix-claim-invoice-credit-note-columns';
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -98,6 +99,7 @@ const migrations: Migration[] = [
|
|||||||
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
{ name: '20251213-drop-claim-details-invoice-columns', module: m37 },
|
||||||
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
{ name: '20251213-create-claim-invoice-credit-note-tables', module: m38 },
|
||||||
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
{ name: '20251214-create-dealer-completion-expenses', module: m39 },
|
||||||
|
{ name: '20251218-fix-claim-invoice-credit-note-columns', module: m40 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -447,19 +447,14 @@ export class ApprovalService {
|
|||||||
// Don't fail the Step 3 approval if Step 4 processing fails - log and continue
|
// Don't fail the Step 3 approval if Step 4 processing fails - log and continue
|
||||||
}
|
}
|
||||||
} else if (isClaimManagement && isStep6Approval && isStep7Next && nextLevel && nextLevel.approverEmail === 'system@royalenfield.com') {
|
} else if (isClaimManagement && isStep6Approval && isStep7Next && nextLevel && nextLevel.approverEmail === 'system@royalenfield.com') {
|
||||||
// Step 7 is an auto-step - process it automatically after Step 6 approval
|
// Step 7 is an auto-step - activate it but don't process invoice generation
|
||||||
logger.info(`[Approval] Step 6 approved for claim management workflow. Auto-processing Step 7: E-Invoice Generation`);
|
// Invoice generation will be handled by DMS webhook when invoice is created
|
||||||
try {
|
logger.info(`[Approval] Step 6 approved for claim management workflow. Step 7 activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
const dealerClaimService = new DealerClaimService();
|
// Step 7 will remain in IN_PROGRESS until webhook creates invoice and auto-approves it
|
||||||
await dealerClaimService.processEInvoiceGeneration(level.requestId);
|
// Continue with normal flow to activate Step 7
|
||||||
logger.info(`[Approval] Step 7 auto-processing completed for request ${level.requestId}`);
|
|
||||||
// Skip notification for system auto-processed step
|
|
||||||
return updatedLevel;
|
|
||||||
} catch (step7Error) {
|
|
||||||
logger.error(`[Approval] Error auto-processing Step 7 for request ${level.requestId}:`, step7Error);
|
|
||||||
// Don't fail the Step 6 approval if Step 7 processing fails - log and continue
|
|
||||||
}
|
}
|
||||||
} else if (wf && nextLevel) {
|
|
||||||
|
if (wf && nextLevel) {
|
||||||
// Normal flow - notify next approver (skip for auto-steps)
|
// Normal flow - notify next approver (skip for auto-steps)
|
||||||
// Check if it's an auto-step by checking approverEmail or levelName
|
// Check if it's an auto-step by checking approverEmail or levelName
|
||||||
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com'
|
const isAutoStep = (nextLevel as any).approverEmail === 'system@royalenfield.com'
|
||||||
|
|||||||
@ -1544,6 +1544,7 @@ export class DealerClaimService {
|
|||||||
dmsNumber: invoiceResult.dmsNumber,
|
dmsNumber: invoiceResult.dmsNumber,
|
||||||
amount: invoiceAmount,
|
amount: invoiceAmount,
|
||||||
status: 'GENERATED',
|
status: 'GENERATED',
|
||||||
|
generatedAt: new Date(),
|
||||||
description: invoiceData?.description || `E-Invoice for claim request ${requestNumber}`,
|
description: invoiceData?.description || `E-Invoice for claim request ${requestNumber}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1560,6 +1561,7 @@ export class DealerClaimService {
|
|||||||
dmsNumber: invoiceData.dmsNumber,
|
dmsNumber: invoiceData.dmsNumber,
|
||||||
amount: invoiceData.amount,
|
amount: invoiceData.amount,
|
||||||
status: 'UPDATED',
|
status: 'UPDATED',
|
||||||
|
generatedAt: new Date(),
|
||||||
description: invoiceData.description,
|
description: invoiceData.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1573,19 +1575,18 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
if (step6Level && step6Level.status !== ApprovalStatus.APPROVED) {
|
if (step6Level && step6Level.status !== ApprovalStatus.APPROVED) {
|
||||||
logger.info(`[DealerClaimService] Step 6 not approved yet. Auto-approving Step 6 for request ${requestId}`);
|
logger.info(`[DealerClaimService] Step 6 not approved yet. Auto-approving Step 6 for request ${requestId}`);
|
||||||
// Auto-approve Step 6 - this will trigger Step 7 auto-processing in approval service
|
// Auto-approve Step 6 - Step 7 will be activated but invoice generation will happen via webhook
|
||||||
await this.approvalService.approveLevel(
|
await this.approvalService.approveLevel(
|
||||||
step6Level.levelId,
|
step6Level.levelId,
|
||||||
{ action: 'APPROVE', comments: 'Auto-approved when pushing to DMS. E-Invoice generated.' },
|
{ 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 auto-processing will be handled by approval service when Step 6 is approved
|
// Note: Step 7 invoice generation will be handled by DMS webhook, not here
|
||||||
// But we also call it directly here to ensure it runs (in case approval service logic doesn't trigger)
|
logger.info(`[DealerClaimService] Step 6 approved. Step 7 activated. Waiting for DMS webhook to generate invoice.`);
|
||||||
await this.processEInvoiceGeneration(requestId);
|
|
||||||
} else {
|
} else {
|
||||||
// Step 6 already approved - directly process Step 7
|
// Step 6 already approved - Step 7 should be active, waiting for webhook
|
||||||
await this.processEInvoiceGeneration(requestId);
|
logger.info(`[DealerClaimService] Step 6 already approved. Step 7 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);
|
||||||
@ -1594,8 +1595,9 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Step 7: E-Invoice Generation (Auto-processed after Step 6 approval or when pushing to DMS)
|
* Process Step 7: E-Invoice Generation (Deprecated - now handled by DMS webhook)
|
||||||
* Generates e-invoice via DMS and auto-approves Step 7
|
* This method is kept for backward compatibility but invoice generation is now triggered only via DMS webhook
|
||||||
|
* @deprecated Use DMS webhook to trigger invoice generation instead
|
||||||
*/
|
*/
|
||||||
async processEInvoiceGeneration(requestId: string): Promise<void> {
|
async processEInvoiceGeneration(requestId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -1707,10 +1709,12 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
await ClaimCreditNote.upsert({
|
await ClaimCreditNote.upsert({
|
||||||
requestId,
|
requestId,
|
||||||
|
invoiceId: claimInvoice.invoiceId,
|
||||||
creditNoteNumber: creditNoteResult.creditNoteNumber,
|
creditNoteNumber: creditNoteResult.creditNoteNumber,
|
||||||
creditNoteDate: creditNoteResult.creditNoteDate || new Date(),
|
creditNoteDate: creditNoteResult.creditNoteDate || new Date(),
|
||||||
creditNoteAmount: creditNoteResult.creditNoteAmount,
|
creditNoteAmount: creditNoteResult.creditNoteAmount,
|
||||||
status: 'GENERATED',
|
status: 'GENERATED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
reason: creditNoteData?.reason || 'Claim settlement',
|
reason: creditNoteData?.reason || 'Claim settlement',
|
||||||
description: creditNoteData?.description || `Credit note for claim request ${requestNumber}`,
|
description: creditNoteData?.description || `Credit note for claim request ${requestNumber}`,
|
||||||
});
|
});
|
||||||
@ -1723,10 +1727,12 @@ export class DealerClaimService {
|
|||||||
// Manual entry - just update the fields
|
// Manual entry - just update the fields
|
||||||
await ClaimCreditNote.upsert({
|
await ClaimCreditNote.upsert({
|
||||||
requestId,
|
requestId,
|
||||||
|
invoiceId: claimInvoice.invoiceId,
|
||||||
creditNoteNumber: creditNoteData.creditNoteNumber,
|
creditNoteNumber: creditNoteData.creditNoteNumber,
|
||||||
creditNoteDate: creditNoteData.creditNoteDate || new Date(),
|
creditNoteDate: creditNoteData.creditNoteDate || new Date(),
|
||||||
creditNoteAmount: creditNoteData.creditNoteAmount,
|
creditNoteAmount: creditNoteData.creditNoteAmount,
|
||||||
status: 'UPDATED',
|
status: 'UPDATED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
reason: creditNoteData?.reason,
|
reason: creditNoteData?.reason,
|
||||||
description: creditNoteData?.description,
|
description: creditNoteData?.description,
|
||||||
});
|
});
|
||||||
@ -1739,6 +1745,107 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send credit note to dealer and auto-approve Step 8
|
||||||
|
* This method sends the credit note to the dealer via email/notification and auto-approves Step 8
|
||||||
|
*/
|
||||||
|
async sendCreditNoteToDealer(requestId: string, userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`[DealerClaimService] Sending credit note to dealer for request ${requestId}`);
|
||||||
|
|
||||||
|
// Get credit note details
|
||||||
|
const creditNote = await ClaimCreditNote.findOne({
|
||||||
|
where: { requestId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!creditNote) {
|
||||||
|
throw new Error('Credit note not found. Please ensure credit note is generated before sending to dealer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get claim details for dealer information
|
||||||
|
const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } });
|
||||||
|
if (!claimDetails) {
|
||||||
|
throw new Error('Claim details not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get workflow request
|
||||||
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
|
if (!request) {
|
||||||
|
throw new Error('Workflow request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowType = (request as any).workflowType;
|
||||||
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
||||||
|
throw new Error('This operation is only available for claim management workflows');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Step 8 approval level
|
||||||
|
const step8Level = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!step8Level) {
|
||||||
|
throw new Error(`Step 8 approval level not found for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Step 8 is already approved
|
||||||
|
if (step8Level.status === 'APPROVED') {
|
||||||
|
logger.info(`[DealerClaimService] Step 8 already approved for request ${requestId}`);
|
||||||
|
// 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
|
||||||
|
} else {
|
||||||
|
// Auto-approve Step 8
|
||||||
|
logger.info(`[DealerClaimService] Auto-approving Step 8 for request ${requestId}`);
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
step8Level.levelId,
|
||||||
|
{
|
||||||
|
action: 'APPROVE',
|
||||||
|
comments: `Credit note sent to dealer. Credit Note Number: ${creditNote.creditNoteNumber || 'N/A'}. Step 8 auto-approved.`,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
ipAddress: null,
|
||||||
|
userAgent: 'System Auto-Process',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[DealerClaimService] Step 8 auto-approved successfully for request ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update credit note status to SENT
|
||||||
|
await creditNote.update({
|
||||||
|
status: 'CONFIRMED', // Or 'SENT' if you have that status
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
confirmedBy: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send notification to dealer (you can implement email service here)
|
||||||
|
// For now, we'll just log it
|
||||||
|
logger.info(`[DealerClaimService] Credit note sent to dealer`, {
|
||||||
|
requestId,
|
||||||
|
creditNoteNumber: creditNote.creditNoteNumber,
|
||||||
|
dealerEmail: claimDetails.dealerEmail,
|
||||||
|
dealerName: claimDetails.dealerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Implement email service to send credit note to dealer
|
||||||
|
// await emailService.sendCreditNoteToDealer({
|
||||||
|
// dealerEmail: claimDetails.dealerEmail,
|
||||||
|
// dealerName: claimDetails.dealerName,
|
||||||
|
// creditNoteNumber: creditNote.creditNoteNumber,
|
||||||
|
// creditNoteAmount: creditNote.creditNoteAmount,
|
||||||
|
// requestNumber: (request as any).requestNumber,
|
||||||
|
// });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[DealerClaimService] Error sending credit note to dealer:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process Step 4: Activity Creation (Auto-processed after Step 3 approval)
|
* Process Step 4: Activity Creation (Auto-processed after Step 3 approval)
|
||||||
* Creates activity confirmation and sends emails to dealer, requestor, and department lead
|
* Creates activity confirmation and sends emails to dealer, requestor, and department lead
|
||||||
|
|||||||
408
src/services/dmsWebhook.service.ts
Normal file
408
src/services/dmsWebhook.service.ts
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import { Request } from 'express';
|
||||||
|
import { ClaimInvoice } from '../models/ClaimInvoice';
|
||||||
|
import { ClaimCreditNote } from '../models/ClaimCreditNote';
|
||||||
|
import { WorkflowRequest } from '../models/WorkflowRequest';
|
||||||
|
import { ApprovalLevel } from '../models/ApprovalLevel';
|
||||||
|
import { ApprovalService } from './approval.service';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMS Webhook Service
|
||||||
|
* Handles processing of webhook callbacks from DMS system
|
||||||
|
*/
|
||||||
|
export class DMSWebhookService {
|
||||||
|
private webhookSecret: string;
|
||||||
|
private approvalService: ApprovalService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.webhookSecret = process.env.DMS_WEBHOOK_SECRET || '';
|
||||||
|
this.approvalService = new ApprovalService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate webhook signature for security
|
||||||
|
* DMS should send a signature in the header that we can verify
|
||||||
|
*/
|
||||||
|
async validateWebhookSignature(req: Request): Promise<boolean> {
|
||||||
|
// If webhook secret is not configured, skip validation (for development)
|
||||||
|
if (!this.webhookSecret) {
|
||||||
|
logger.warn('[DMSWebhook] Webhook secret not configured, skipping signature validation');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signature = req.headers['x-dms-signature'] as string;
|
||||||
|
if (!signature) {
|
||||||
|
logger.warn('[DMSWebhook] Missing webhook signature in header');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HMAC hash of the request body
|
||||||
|
const body = JSON.stringify(req.body);
|
||||||
|
const expectedSignature = crypto
|
||||||
|
.createHmac('sha256', this.webhookSecret)
|
||||||
|
.update(body)
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
// Compare signatures (use constant-time comparison to prevent timing attacks)
|
||||||
|
const isValid = crypto.timingSafeEqual(
|
||||||
|
Buffer.from(signature),
|
||||||
|
Buffer.from(expectedSignature)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
logger.warn('[DMSWebhook] Invalid webhook signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[DMSWebhook] Error validating webhook signature:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process invoice generation webhook from DMS
|
||||||
|
*/
|
||||||
|
async processInvoiceWebhook(payload: any): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
invoiceNumber?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
const requiredFields = ['request_number', 'document_no', 'document_type'];
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (!payload[field]) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Missing required field: ${field}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find workflow request by request number
|
||||||
|
const request = await WorkflowRequest.findOne({
|
||||||
|
where: {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Request not found: ${payload.request_number}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create invoice record
|
||||||
|
let invoice = await ClaimInvoice.findOne({
|
||||||
|
where: { requestId: request.requestId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create invoice if it doesn't exist (new flow: webhook creates invoice)
|
||||||
|
if (!invoice) {
|
||||||
|
logger.info('[DMSWebhook] Invoice record not found, creating new invoice from webhook', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
});
|
||||||
|
|
||||||
|
invoice = await ClaimInvoice.create({
|
||||||
|
requestId: request.requestId,
|
||||||
|
invoiceNumber: payload.document_no,
|
||||||
|
dmsNumber: payload.document_no,
|
||||||
|
invoiceDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
|
amount: payload.total_amount || payload.claim_amount,
|
||||||
|
status: 'GENERATED',
|
||||||
|
generatedAt: new Date(),
|
||||||
|
invoiceFilePath: payload.invoice_file_path || null,
|
||||||
|
errorMessage: payload.error_message || null,
|
||||||
|
description: this.buildInvoiceDescription(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Invoice created successfully from webhook', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
invoiceNumber: payload.document_no,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Update existing invoice with DMS response data
|
||||||
|
await invoice.update({
|
||||||
|
invoiceNumber: payload.document_no,
|
||||||
|
dmsNumber: payload.document_no, // DMS document number
|
||||||
|
invoiceDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
|
amount: payload.total_amount || payload.claim_amount,
|
||||||
|
status: 'GENERATED',
|
||||||
|
generatedAt: new Date(),
|
||||||
|
invoiceFilePath: payload.invoice_file_path || null,
|
||||||
|
errorMessage: payload.error_message || null,
|
||||||
|
// Store additional DMS data in description or separate fields if needed
|
||||||
|
description: this.buildInvoiceDescription(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Invoice updated successfully', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
invoiceNumber: payload.document_no,
|
||||||
|
irnNo: payload.irn_no,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-approve Step 7 and move to Step 8
|
||||||
|
await this.autoApproveStep7(request.requestId, payload.request_number);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
invoiceNumber: payload.document_no,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DMSWebhook] Error processing invoice webhook:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process credit note generation webhook from DMS
|
||||||
|
*/
|
||||||
|
async processCreditNoteWebhook(payload: any): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
creditNoteNumber?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
const requiredFields = ['request_number', 'document_no', 'document_type'];
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (!payload[field]) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Missing required field: ${field}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find workflow request by request number
|
||||||
|
const request = await WorkflowRequest.findOne({
|
||||||
|
where: {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Request not found: ${payload.request_number}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find invoice to link credit note
|
||||||
|
const invoice = await ClaimInvoice.findOne({
|
||||||
|
where: { requestId: request.requestId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invoice) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Invoice not found for request: ${payload.request_number}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create credit note record
|
||||||
|
let creditNote = await ClaimCreditNote.findOne({
|
||||||
|
where: { requestId: request.requestId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create credit note if it doesn't exist (new flow: webhook creates credit note)
|
||||||
|
if (!creditNote) {
|
||||||
|
logger.info('[DMSWebhook] Credit note record not found, creating new credit note from webhook', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
});
|
||||||
|
|
||||||
|
creditNote = await ClaimCreditNote.create({
|
||||||
|
requestId: request.requestId,
|
||||||
|
invoiceId: invoice.invoiceId,
|
||||||
|
creditNoteNumber: payload.document_no,
|
||||||
|
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
|
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
||||||
|
sapDocumentNumber: payload.sap_credit_note_no || null,
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
creditNoteFilePath: payload.credit_note_file_path || null,
|
||||||
|
errorMessage: payload.error_message || null,
|
||||||
|
description: this.buildCreditNoteDescription(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Credit note created successfully from webhook', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
creditNoteNumber: payload.document_no,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Update existing credit note with DMS response data
|
||||||
|
await creditNote.update({
|
||||||
|
invoiceId: invoice.invoiceId,
|
||||||
|
creditNoteNumber: payload.document_no,
|
||||||
|
creditNoteDate: payload.document_date ? new Date(payload.document_date) : new Date(),
|
||||||
|
creditNoteAmount: payload.total_amount || payload.credit_amount,
|
||||||
|
sapDocumentNumber: payload.sap_credit_note_no || null,
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
creditNoteFilePath: payload.credit_note_file_path || null,
|
||||||
|
errorMessage: payload.error_message || null,
|
||||||
|
description: this.buildCreditNoteDescription(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Credit note updated successfully', {
|
||||||
|
requestNumber: payload.request_number,
|
||||||
|
creditNoteNumber: payload.document_no,
|
||||||
|
sapCreditNoteNo: payload.sap_credit_note_no,
|
||||||
|
irnNo: payload.irn_no,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
creditNoteNumber: payload.document_no,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DMSWebhook] Error processing credit note webhook:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build invoice description from DMS payload
|
||||||
|
*/
|
||||||
|
private buildInvoiceDescription(payload: any): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (payload.irn_no) {
|
||||||
|
parts.push(`IRN: ${payload.irn_no}`);
|
||||||
|
}
|
||||||
|
if (payload.item_code_no) {
|
||||||
|
parts.push(`Item Code: ${payload.item_code_no}`);
|
||||||
|
}
|
||||||
|
if (payload.hsn_sac_code) {
|
||||||
|
parts.push(`HSN/SAC: ${payload.hsn_sac_code}`);
|
||||||
|
}
|
||||||
|
if (payload.cgst_amount || payload.sgst_amount || payload.igst_amount) {
|
||||||
|
parts.push(`GST - CGST: ${payload.cgst_amount || 0}, SGST: ${payload.sgst_amount || 0}, IGST: ${payload.igst_amount || 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' | ') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build credit note description from DMS payload
|
||||||
|
*/
|
||||||
|
private buildCreditNoteDescription(payload: any): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (payload.irn_no) {
|
||||||
|
parts.push(`IRN: ${payload.irn_no}`);
|
||||||
|
}
|
||||||
|
if (payload.sap_credit_note_no) {
|
||||||
|
parts.push(`SAP CN: ${payload.sap_credit_note_no}`);
|
||||||
|
}
|
||||||
|
if (payload.credit_type) {
|
||||||
|
parts.push(`Credit Type: ${payload.credit_type}`);
|
||||||
|
}
|
||||||
|
if (payload.item_code_no) {
|
||||||
|
parts.push(`Item Code: ${payload.item_code_no}`);
|
||||||
|
}
|
||||||
|
if (payload.hsn_sac_code) {
|
||||||
|
parts.push(`HSN/SAC: ${payload.hsn_sac_code}`);
|
||||||
|
}
|
||||||
|
if (payload.cgst_amount || payload.sgst_amount || payload.igst_amount) {
|
||||||
|
parts.push(`GST - CGST: ${payload.cgst_amount || 0}, SGST: ${payload.sgst_amount || 0}, IGST: ${payload.igst_amount || 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' | ') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-approve Step 7 (E-Invoice Generation) and move to Step 8
|
||||||
|
* This is called after invoice is created/updated from DMS webhook
|
||||||
|
*/
|
||||||
|
private async autoApproveStep7(requestId: string, requestNumber: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if this is a claim management workflow
|
||||||
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
|
if (!request) {
|
||||||
|
logger.warn('[DMSWebhook] Request not found for Step 7 auto-approval', { requestId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowType = (request as any).workflowType;
|
||||||
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
||||||
|
logger.info('[DMSWebhook] Not a claim management workflow, skipping Step 7 auto-approval', {
|
||||||
|
requestId,
|
||||||
|
workflowType,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Step 7 approval level
|
||||||
|
const step7Level = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
levelNumber: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!step7Level) {
|
||||||
|
logger.warn('[DMSWebhook] Step 7 approval level not found', { requestId, requestNumber });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Step 7 is already approved
|
||||||
|
if (step7Level.status === 'APPROVED') {
|
||||||
|
logger.info('[DMSWebhook] Step 7 already approved, skipping auto-approval', {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-approve Step 7
|
||||||
|
logger.info('[DMSWebhook] Auto-approving Step 7 (E-Invoice Generation)', {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
levelId: step7Level.levelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
step7Level.levelId,
|
||||||
|
{
|
||||||
|
action: 'APPROVE',
|
||||||
|
comments: `E-Invoice generated via DMS webhook. Invoice Number: ${(await ClaimInvoice.findOne({ where: { requestId } }))?.invoiceNumber || 'N/A'}. Step 7 auto-approved.`,
|
||||||
|
},
|
||||||
|
'system', // System user for auto-approval
|
||||||
|
{
|
||||||
|
ipAddress: null,
|
||||||
|
userAgent: 'DMS-Webhook-System',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info('[DMSWebhook] Step 7 auto-approved successfully. Workflow moved to Step 8', {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
logger.error('[DMSWebhook] Error auto-approving Step 7:', {
|
||||||
|
requestId,
|
||||||
|
requestNumber,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
// Don't throw error - webhook processing should continue even if Step 7 approval fails
|
||||||
|
// The invoice is already created/updated, which is the primary goal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user