Re_Backend/src/services/dmsWebhook.service.ts

409 lines
13 KiB
TypeScript

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