162 lines
5.9 KiB
TypeScript
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();
|