409 lines
13 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|
|
|