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