import fs from 'fs'; import { wfmFileService } from './wfmFile.service'; import { WorkflowRequest } from '../models/WorkflowRequest'; import { ClaimCreditNote } from '../models/ClaimCreditNote'; import { ClaimCreditNoteItem } from '../models/ClaimCreditNoteItem'; import { sequelize } from '@config/database'; import logger from '../utils/logger'; export class CreditNoteSyncService { /** * Main sync function to process all outgoing files */ async syncCreditNotes(): Promise { try { const gstFiles = wfmFileService.listOutgoingFiles(false); const nonGstFiles = wfmFileService.listOutgoingFiles(true); const allFiles = [ ...gstFiles.map(f => ({ path: f, isNonGst: false })), ...nonGstFiles.map(f => ({ path: f, isNonGst: true })) ]; if (allFiles.length === 0) return; logger.info(`[CreditNoteSyncService] Found ${allFiles.length} files to process`); for (const fileInfo of allFiles) { await this.processFile(fileInfo.path); } } catch (error) { logger.error('[CreditNoteSyncService] Error during sync:', error); } } /** * Process a single CSV file */ async processFile(filePath: string): Promise { try { if (!fs.existsSync(filePath)) return false; const fileContent = fs.readFileSync(filePath, 'utf-8'); const lines = fileContent.split(/\r?\n/).filter(l => l.trim() !== ''); if (lines.length <= 1) { // Empty or only headers - delete it fs.unlinkSync(filePath); logger.info(`[CreditNoteSyncService] Deleted empty/header-only file: ${filePath}`); return true; } const headers = lines[0].split('|').map(h => h.trim().toUpperCase()); const rows = lines.slice(1).map(line => { const values = line.split('|'); const row: any = {}; headers.forEach((h, i) => { row[h] = values[i]?.trim() || ''; }); return row; }); // Group rows by CLAIM_NUMBER const groups: Record = {}; rows.forEach(row => { const claimNum = row.CLAIM_NUMBER; if (!claimNum) return; if (!groups[claimNum]) groups[claimNum] = []; groups[claimNum].push(row); }); // Process each group let allProcessed = true; for (const [claimNumber, rows] of Object.entries(groups)) { const success = await this.processClaimGroup(claimNumber, rows, filePath); if (!success) { allProcessed = false; logger.warn(`[CreditNoteSyncService] Failed to process claim group ${claimNumber} in file ${filePath}`); } } if (allProcessed && rows.length > 0) { fs.unlinkSync(filePath); logger.info(`[CreditNoteSyncService] Successfully processed and deleted file: ${filePath}`); return true; } return false; } catch (error) { logger.error(`[CreditNoteSyncService] Error processing file ${filePath}:`, error); return false; } } private async processClaimGroup(claimNumber: string, rows: any[], filePath: string): Promise { const t = await sequelize.transaction(); try { // 1. Find the request by requestNumber (which is the CLAIM_NUMBER in CSV) const request = await WorkflowRequest.findOne({ where: { requestNumber: claimNumber }, transaction: t }); if (!request) { logger.warn(`[CreditNoteSyncService] WorkflowRequest not found for claim number: ${claimNumber}`); await t.rollback(); // We return true here because we might still want to delete the file if other claims are processed // or if this is a filtered/old claim we don't care about. return true; } const requestId = request.requestId; // 2. Calculate totals let totalAmount = 0; let totalTds = 0; let totalCredit = 0; rows.forEach(row => { totalAmount += Number(row.CREDITED_TOTAL_AMT || row.CLAIM_AMT || row.CREDIT_AMT || 0); totalTds += Number(row.TDS_AMT || 0); totalCredit += Number(row.CREDITED_TOTAL_AMT || row.CREDIT_AMT || row.FINAL_AMT || 0); }); const firstRow = rows[0]; // 3. Upsert Header const [cnHeader] = await ClaimCreditNote.upsert({ requestId, creditNoteNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || undefined, sapDocumentNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || undefined, status: firstRow.MSG_TYP || 'CONFIRMED', errorMessage: firstRow.MESSAGE || undefined, creditNoteFilePath: filePath, creditNoteAmount: totalAmount, transactionNo: firstRow.TRNS_UNIQ_NO || undefined, tdsAmount: totalTds, creditAmount: totalCredit, confirmedAt: new Date() }, { transaction: t, returning: true }); // 4. Update Line Items // Clear existing items await ClaimCreditNoteItem.destroy({ where: { creditNoteId: cnHeader.creditNoteId }, transaction: t }); // Bulk create new items const itemsToCreate = rows.map((row, index) => ({ creditNoteId: cnHeader.creditNoteId, slNo: index + 1, transactionNo: row.TRNS_UNIQ_NO, description: row.DESCRIPTION || row.MESSAGE || '', hsnCd: row.HSN_CODE || row.SAC_CODE || '', amount: Number(row.CREDITED_TOTAL_AMT || row.FINAL_AMT || row.CREDIT_AMT || 0), claimAmount: Number(row.CREDITED_TOTAL_AMT || row.CLAIM_AMT || 0), tdsAmount: Number(row.TDS_AMT || 0), creditAmount: Number(row.CREDITED_TOTAL_AMT || row.FINAL_AMT || row.CREDIT_AMT || 0) })); await ClaimCreditNoteItem.bulkCreate(itemsToCreate, { transaction: t }); await t.commit(); return true; } catch (error) { if (t) await t.rollback(); logger.error(`[CreditNoteSyncService] Error processing claim ${claimNumber}:`, error); return false; } } } export const creditNoteSyncService = new CreditNoteSyncService();