308 lines
15 KiB
TypeScript
308 lines
15 KiB
TypeScript
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();
|