Re_Backend/src/services/creditNoteSync.service.ts

162 lines
5.9 KiB
TypeScript

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<void> {
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<boolean> {
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<string, any[]> = {};
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<boolean> {
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();