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 { 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 { 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 { 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 { 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 { 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();