Re_Backend/src/services/wfmFile.service.ts
2026-03-18 12:59:20 +05:30

308 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs';
import path from 'path';
import logger from '../utils/logger';
/** Default WFM folder names (joined with path.sep for current OS). */
const DEFAULT_CLAIMS_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'DLR_INC_CLAIMS');
const DEFAULT_CLAIMS_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'DLR_INC_CLAIMS');
const DEFAULT_FORM16_CREDIT_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM16_CRDT');
const DEFAULT_FORM16_DEBIT_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM16_DEBT');
const DEFAULT_FORM16_CREDIT_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM16_CRDT');
const DEFAULT_FORM16_DEBIT_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM16_DBT');
/**
* WFM File Service
* Handles generation and storage of CSV files in the WFM folder structure.
* Dealer claims use DLR_INC_CLAIMS; Form 16 uses:
* - FORM16_CRDT (credit) and FORM16_DEBT (debit) under INCOMING/WFM_MAIN
* - FORM16_CRDT (credit) and FORM16_DBT (debit) under OUTGOING/WFM_SAP_MAIN
* Paths are cross-platform; set WFM_BASE_PATH (and optional path overrides) in .env for production.
*/
export class WFMFileService {
private basePath: string;
// --- INCOMING PATHS (WFM_MAIN / WFM_ARACHIVE) ---
private incomingGstClaimsPath: string;
private incomingArchiveGstClaimsPath: string;
private incomingNonGstClaimsPath: string;
private incomingArchiveNonGstClaimsPath: string;
private form16IncomingCreditPath: string;
private incomingArchiveForm16CreditPath: string;
private form16IncomingDebitPath: string;
private incomingArchiveForm16DebitPath: string;
// --- OUTGOING PATHS (WFM_SAP_MAIN) ---
private outgoingGstClaimsPath: string;
private outgoingNonGstClaimsPath: string;
/** Form 16 credit responses: OUTGOING/WFM_SAP_MAIN/FORM16_CRDT */
private form16OutgoingCreditPath: string;
/** Form 16 debit responses: OUTGOING/WFM_SAP_MAIN/FORM16_DBT */
private form16OutgoingDebitPath: string;
constructor() {
this.basePath = process.env.WFM_BASE_PATH || 'C:\\WFM';
// Initialize Incoming Paths from .env or defaults
this.incomingGstClaimsPath = process.env.WFM_INCOMING_GST_CLAIMS_PATH || DEFAULT_CLAIMS_INCOMING + '_GST';
this.incomingArchiveGstClaimsPath = process.env.WFM_ARCHIVE_GST_CLAIMS_PATH || path.join('WFM-QRE', 'INCOMING', 'WFM_ARACHIVE', 'DLR_INC_CLAIMS_GST');
this.incomingNonGstClaimsPath = process.env.WFM_INCOMING_NON_GST_CLAIMS_PATH || DEFAULT_CLAIMS_INCOMING + '_NON_GST';
this.incomingArchiveNonGstClaimsPath = process.env.WFM_ARCHIVE_NON_GST_CLAIMS_PATH || path.join('WFM-QRE', 'INCOMING', 'WFM_ARACHIVE', 'DLR_INC_CLAIMS_NON_GST');
// Backwards-compatible: support legacy WFM_FORM16_INCOMING_PATH if specific credit/debit paths are not set
const legacyForm16Incoming = process.env.WFM_FORM16_INCOMING_PATH;
this.form16IncomingCreditPath =
process.env.WFM_FORM16_CREDIT_INCOMING_PATH || legacyForm16Incoming || DEFAULT_FORM16_CREDIT_INCOMING;
this.incomingArchiveForm16CreditPath = process.env.WFM_FORM16_CREDIT_ARCHIVE_PATH || path.join('WFM-QRE', 'INCOMING', 'WFM_ARACHIVE', 'FORM16_CRDT');
this.form16IncomingDebitPath =
process.env.WFM_FORM16_DEBIT_INCOMING_PATH || legacyForm16Incoming || DEFAULT_FORM16_DEBIT_INCOMING;
this.incomingArchiveForm16DebitPath = process.env.WFM_FORM16_DEBIT_ARCHIVE_PATH || path.join('WFM-QRE', 'INCOMING', 'WFM_ARACHIVE', 'FORM16_DBT');
// Initialize Outgoing Paths from .env or defaults
this.outgoingGstClaimsPath = process.env.WFM_OUTGOING_GST_CLAIMS_PATH || DEFAULT_CLAIMS_OUTGOING + '_GST';
this.outgoingNonGstClaimsPath = process.env.WFM_OUTGOING_NON_GST_CLAIMS_PATH || DEFAULT_CLAIMS_OUTGOING + '_NON_GST';
// Outgoing: allow specific credit/debit overrides; fall back to legacy single path for credit
const legacyForm16Outgoing = process.env.WFM_FORM16_OUTGOING_PATH;
this.form16OutgoingCreditPath =
process.env.WFM_FORM16_CREDIT_OUTGOING_PATH || legacyForm16Outgoing || DEFAULT_FORM16_CREDIT_OUTGOING;
this.form16OutgoingDebitPath =
process.env.WFM_FORM16_DEBIT_OUTGOING_PATH || DEFAULT_FORM16_DEBIT_OUTGOING;
}
/**
* Ensure the target directory exists
*/
private ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
logger.info(`[WFMFileService] Created directory: ${dirPath}`);
}
}
/**
* Generate a CSV file for a credit note/claim and store it in the INCOMING folder
* @param data The data to be written to the CSV
* @param fileName The name of the file (e.g., CLAIM_12345.csv)
*/
async generateIncomingClaimCSV(data: any[], fileName: string, isNonGst: boolean = false): Promise<string> {
const maxRetries = 3;
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const targetPath = isNonGst ? this.incomingNonGstClaimsPath : this.incomingGstClaimsPath;
const targetDir = path.join(this.basePath, targetPath);
this.ensureDirectoryExists(targetDir);
const fileNameWithExt = fileName.endsWith('.csv') ? fileName : `${fileName}.csv`;
const filePath = path.join(targetDir, fileNameWithExt);
// Simple CSV generation logic with pipe separator and no quotes
const headers = Object.keys(data[0] || {}).join('|');
const rows = data.map(item => Object.values(item).map(val => val === null || val === undefined ? '' : String(val)).join('|')).join('\n');
const csvContent = `${headers}\n${rows}`;
fs.writeFileSync(filePath, csvContent);
logger.info(`[WFMFileService] Generated CSV at: ${filePath}`);
// Archive copy
try {
const archivePathPrefix = isNonGst ? this.incomingArchiveNonGstClaimsPath : this.incomingArchiveGstClaimsPath;
const archiveDir = path.join(this.basePath, archivePathPrefix);
this.ensureDirectoryExists(archiveDir);
const archivePath = path.join(archiveDir, fileNameWithExt);
fs.writeFileSync(archivePath, csvContent);
logger.info(`[WFMFileService] Archived CSV copy at: ${archivePath}`);
} catch (archiveError) {
logger.error('[WFMFileService] Error saving archive copy:', archiveError);
// Don't throw archive error to avoid failing the main process
}
return filePath;
} catch (error: any) {
if (error.code === 'EBUSY' && retryCount < maxRetries) {
retryCount++;
const delay = retryCount * 1000;
logger.warn(`[WFMFileService] File busy/locked, retrying in ${delay}ms (Attempt ${retryCount}/${maxRetries}): ${fileName}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (error.code === 'EBUSY') {
throw new Error(`File is locked or open in another program (e.g., Excel). Please close '${fileName}' and try again.`);
}
logger.error('[WFMFileService] Error generating incoming claim CSV:', error);
throw error;
}
}
throw new Error(`Failed to generate CSV after ${maxRetries} retries. Please ensure the file '${fileName}' is not open in any other application.`);
}
/**
* Get the absolute path for an outgoing claim file
*/
getOutgoingPath(fileName: string, isNonGst: boolean = false): string {
const targetPath = isNonGst ? this.outgoingNonGstClaimsPath : this.outgoingGstClaimsPath;
return path.join(this.basePath, targetPath, fileName);
}
/**
* Get credit note details from outgoing CSV
*/
async getCreditNoteDetails(dealerCode: string, requestNumber: string, isNonGst: boolean = false): Promise<any[]> {
const fileName = `CN_${String(dealerCode).padStart(6, '0')}_${requestNumber}.csv`;
const filePath = this.getOutgoingPath(fileName, isNonGst);
try {
if (!fs.existsSync(filePath)) {
return [];
}
const fileContent = fs.readFileSync(filePath, 'utf-8');
const lines = fileContent.split('\n').filter(line => line.trim() !== '');
if (lines.length <= 1) return []; // Only headers or empty
const headers = lines[0].split('|');
const data = lines.slice(1).map(line => {
const values = line.split('|');
const row: any = {};
headers.forEach((header, index) => {
row[header.trim()] = values[index]?.trim() || '';
});
return row;
});
return data;
} catch (error) {
logger.error(`[WFMFileService] Error reading credit note CSV: ${fileName}`, error);
return [];
}
}
/**
* Generate a CSV file for Form 16 (credit/debit note) and store in the appropriate INCOMING/WFM_MAIN folder.
* - Credit: FORM16_CRDT
* - Debit: FORM16_DEBT
* Format: pipe (|) as column separator, no double quotes around values (SAP/WFM requirement).
* @param data Array of one or more row objects (keys become header; use UPPER_SNAKE_CASE for column names)
* @param fileName File name (e.g. CN-F-16-6282-24-25-Q1.csv or DN-F-16-6282-24-25-Q1.csv)
* @param type 'credit' (default) or 'debit' selects FORM16_CRDT vs FORM16_DEBT
*/
async generateForm16IncomingCSV(data: any[], fileName: string, type: 'credit' | 'debit' = 'credit'): Promise<string> {
const maxRetries = 3;
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const targetPath = type === 'debit' ? this.form16IncomingDebitPath : this.form16IncomingCreditPath;
const targetDir = path.join(this.basePath, targetPath);
this.ensureDirectoryExists(targetDir);
const fileNameWithExt = fileName.endsWith('.csv') ? fileName : `${fileName}.csv`;
const filePath = path.join(targetDir, fileNameWithExt);
// Pipe separator, no double quotes (values as plain strings)
const keys = Object.keys(data[0] || {});
const headers = keys.join('|');
const rows = data.map(item =>
keys.map(key => {
const val = item[key];
return val === null || val === undefined ? '' : String(val);
}).join('|')
).join('\n');
const csvContent = `${headers}\n${rows}`;
fs.writeFileSync(filePath, csvContent);
logger.info(`[WFMFileService] Form 16 CSV generated at: ${filePath}`);
// Archive copy
try {
const archivePathPrefix = type === 'debit' ? this.incomingArchiveForm16DebitPath : this.incomingArchiveForm16CreditPath;
const archiveDir = path.join(this.basePath, archivePathPrefix);
this.ensureDirectoryExists(archiveDir);
const archivePath = path.join(archiveDir, fileNameWithExt);
fs.writeFileSync(archivePath, csvContent);
logger.info(`[WFMFileService] Form 16 archived copy at: ${archivePath}`);
} catch (archiveError) {
logger.error('[WFMFileService] Error saving Form 16 archive copy:', archiveError);
// Don't throw archive error to avoid failing the main process
}
return filePath;
} catch (error: any) {
if (error.code === 'EBUSY' && retryCount < maxRetries) {
retryCount++;
const delay = retryCount * 1000;
logger.warn(`[WFMFileService] Form 16 file busy, retrying in ${delay}ms (${retryCount}/${maxRetries}): ${fileName}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (error.code === 'EBUSY') {
throw new Error(`Form 16 file is locked. Please close '${fileName}' and try again.`);
}
logger.error('[WFMFileService] Error generating Form 16 incoming CSV:', error);
throw error;
}
}
throw new Error(`Failed to generate Form 16 CSV after ${maxRetries} retries.`);
}
/**
* Get the absolute path for a Form 16 outgoing (response) file.
* - Credit: WFM/WFM-QRE/OUTGOING/WFM_SAP_MAIN/FORM16_CRDT
* - Debit: WFM/WFM-QRE/OUTGOING/WFM_SAP_MAIN/FORM16_DBT
*/
getForm16OutgoingPath(fileName: string, type: 'credit' | 'debit' = 'credit'): string {
const targetPath = type === 'debit' ? this.form16OutgoingDebitPath : this.form16OutgoingCreditPath;
return path.join(this.basePath, targetPath, fileName);
}
/**
* Read a Form 16 outgoing (SAP) response CSV and return rows as objects keyed by header.
* Uses getForm16OutgoingPath(fileName, type) to resolve the file path.
*/
async readForm16OutgoingResponse(fileName: string, type: 'credit' | 'debit' = 'credit'): Promise<any[]> {
const filePath = this.getForm16OutgoingPath(fileName, type);
return this.readForm16OutgoingResponseByPath(filePath);
}
/**
* Read a Form 16 outgoing (SAP) response CSV from an absolute path.
* Expected columns (both credit and debit): DMS_UNIQ_NO, CLAIM_NUMBER, DOC_NO, MSG_TYP, MESSAGE.
* Delimiter: pipe (|).
*/
async readForm16OutgoingResponseByPath(filePath: string): Promise<any[]> {
try {
if (!fs.existsSync(filePath)) {
return [];
}
const fileContent = fs.readFileSync(filePath, 'utf-8');
const lines = fileContent.split(/\r?\n/).filter(line => line.trim() !== '');
if (lines.length <= 1) return [];
const headers = lines[0].split('|').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split('|');
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index]?.trim() || '';
});
return row;
});
return data;
} catch (error) {
logger.error('[WFMFileService] Error reading Form 16 response CSV:', filePath, error);
return [];
}
}
}
export const wfmFileService = new WFMFileService();