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 { DealerClaimDetails } from '../models/DealerClaimDetails'; import { User } from '../models/User'; import { ApprovalService } from './approval.service'; import logger from '../utils/logger'; import crypto from 'crypto'; import { activityService } from './activity.service'; import { notificationService } from './notification.service'; /** * 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.logEInvoiceGenerationActivity(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 (optional - credit note can exist without invoice) const invoice = await ClaimInvoice.findOne({ where: { requestId: request.requestId }, }); // 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, hasInvoice: !!invoice, }); creditNote = await ClaimCreditNote.create({ requestId: request.requestId, invoiceId: invoice?.invoiceId || undefined, // Allow undefined if no invoice exists 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, hasInvoice: !!invoice, }); // Log activity and notify initiator await this.logCreditNoteCreationActivity( request.requestId, payload.request_number, payload.document_no, creditNote.creditNoteAmount || payload.total_amount || payload.credit_amount ); } else { // Update existing credit note with DMS response data await creditNote.update({ invoiceId: invoice?.invoiceId || creditNote.invoiceId, // Preserve existing invoiceId if no invoice found 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, hasInvoice: !!invoice, }); // Log activity and notify initiator for updated credit note await this.logCreditNoteCreationActivity( request.requestId, payload.request_number, payload.document_no, creditNote.creditNoteAmount || payload.total_amount || payload.credit_amount ); } 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(' | ') : ''; } /** * Log Credit Note Creation as activity and notify initiator * This is called after credit note is created/updated from DMS webhook */ private async logCreditNoteCreationActivity( requestId: string, requestNumber: string, creditNoteNumber: string, creditNoteAmount: number ): 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 credit note activity logging', { requestId }); return; } const workflowType = (request as any).workflowType; if (workflowType !== 'CLAIM_MANAGEMENT') { logger.info('[DMSWebhook] Not a claim management workflow, skipping credit note activity logging', { requestId, workflowType, }); return; } const initiatorId = (request as any).initiatorId; if (!initiatorId) { logger.warn('[DMSWebhook] Initiator ID not found for credit note notification', { requestId }); return; } // Log activity await activityService.log({ requestId, type: 'status_change', user: undefined, // System event (no user means it's a system event) timestamp: new Date().toISOString(), action: 'Credit Note Generated', details: `Credit note generated from DMS. Credit Note Number: ${creditNoteNumber}. Credit Note Amount: ₹${creditNoteAmount || 0}. Request: ${requestNumber}`, category: 'credit_note', severity: 'INFO', }); logger.info('[DMSWebhook] Credit note activity logged successfully', { requestId, requestNumber, creditNoteNumber, }); // Get dealer information from claim details const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } }); let dealerUserId: string | null = null; if (claimDetails?.dealerEmail) { const dealerUser = await User.findOne({ where: { email: claimDetails.dealerEmail.toLowerCase() }, attributes: ['userId'], }); dealerUserId = dealerUser?.userId || null; if (dealerUserId) { logger.info('[DMSWebhook] Found dealer user for notification', { requestId, dealerEmail: claimDetails.dealerEmail, dealerUserId, }); } else { logger.warn('[DMSWebhook] Dealer email found but user not found in system', { requestId, dealerEmail: claimDetails.dealerEmail, }); } } else { logger.info('[DMSWebhook] No dealer email found in claim details', { requestId }); } // Send notification to initiator await notificationService.sendToUsers([initiatorId], { title: 'Credit Note Generated', body: `Credit note ${creditNoteNumber} has been generated for request ${requestNumber}. Amount: ₹${creditNoteAmount || 0}`, requestId, requestNumber, url: `/request/${requestNumber}`, type: 'status_change', priority: 'MEDIUM', actionRequired: false, metadata: { creditNoteNumber, creditNoteAmount, source: 'dms_webhook', }, }); logger.info('[DMSWebhook] Credit note notification sent to initiator', { requestId, requestNumber, initiatorId, creditNoteNumber, }); // Send notification to dealer if dealer user exists if (dealerUserId) { await notificationService.sendToUsers([dealerUserId], { title: 'Credit Note Generated', body: `Credit note ${creditNoteNumber} has been generated for your claim request ${requestNumber}. Amount: ₹${creditNoteAmount || 0}`, requestId, requestNumber, url: `/request/${requestNumber}`, type: 'status_change', priority: 'MEDIUM', actionRequired: false, metadata: { creditNoteNumber, creditNoteAmount, source: 'dms_webhook', recipient: 'dealer', }, }); logger.info('[DMSWebhook] Credit note notification sent to dealer', { requestId, requestNumber, dealerUserId, dealerEmail: claimDetails?.dealerEmail, creditNoteNumber, }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[DMSWebhook] Error logging credit note activity:', { requestId, requestNumber, error: errorMessage, }); // Don't throw error - webhook processing should continue even if activity/notification fails // The credit note is already created/updated, which is the primary goal } } /** * Log E-Invoice Generation as activity (no longer an approval step) * This is called after invoice is created/updated from DMS webhook */ private async logEInvoiceGenerationActivity(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; } // E-Invoice Generation is now an activity log only, not an approval step // Log the activity using the dealerClaimService const { DealerClaimService } = await import('./dealerClaim.service'); const dealerClaimService = new DealerClaimService(); const invoice = await ClaimInvoice.findOne({ where: { requestId } }); const invoiceNumber = invoice?.invoiceNumber || 'N/A'; await dealerClaimService.logEInvoiceGenerationActivity(requestId, invoiceNumber); logger.info('[DMSWebhook] E-Invoice Generation activity logged successfully', { requestId, requestNumber, invoiceNumber, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[DMSWebhook] Error logging E-Invoice Generation activity:', { requestId, requestNumber, error: errorMessage, }); // Don't throw error - webhook processing should continue even if activity logging fails // The invoice is already created/updated, which is the primary goal } } }