debit CSV and details page fixed

This commit is contained in:
Aaditya Jaiswal 2026-03-13 14:15:55 +05:30
parent b3dcaca697
commit 89beffee2e
8 changed files with 75 additions and 164 deletions

1
.gitignore vendored
View File

@ -136,3 +136,4 @@ uploads/
# GCP Service Account Key # GCP Service Account Key
config/gcp-key.json config/gcp-key.json
Jenkinsfile Jenkinsfile
clear-26as-data.ts

View File

@ -113,12 +113,15 @@ SAP_REQUESTER=REFMS
# WARNING: Only use in development/testing environments # WARNING: Only use in development/testing environments
SAP_DISABLE_SSL_VERIFY=false SAP_DISABLE_SSL_VERIFY=false
# WFM file paths (base path; dealer claims use DLR_INC_CLAIMS, Form 16 uses FORM_16) # WFM file paths (base path; dealer claims use DLR_INC_CLAIMS, Form 16 uses FORM16_CRDT / FORM16_DEBT)
# If unset: Windows defaults to C:\WFM; Linux/Mac defaults to <cwd>/wfm (paths are cross-platform). # If unset: Windows defaults to C:\WFM; Linux/Mac defaults to <cwd>/wfm (paths are cross-platform).
# WFM_BASE_PATH=C:\WFM # WFM_BASE_PATH=C:\WFM
# WFM_INCOMING_CLAIMS_PATH=WFM-QRE\INCOMING\WFM_MAIN\DLR_INC_CLAIMS # WFM_INCOMING_CLAIMS_PATH=WFM-QRE\INCOMING\WFM_MAIN\DLR_INC_CLAIMS
# WFM_OUTGOING_CLAIMS_PATH=WFM-QRE\OUTGOING\WFM_SAP_MAIN\DLR_INC_CLAIMS # WFM_OUTGOING_CLAIMS_PATH=WFM-QRE\OUTGOING\WFM_SAP_MAIN\DLR_INC_CLAIMS
# Form 16 credit/debit note CSV: INCOMING/WFM_MAIN/FORM_16, OUTGOING/WFM_SAP_MAIN/FORM_16 # Form 16 credit note CSV (incoming): INCOMING/WFM_MAIN/FORM16_CRDT
# WFM_FORM16_INCOMING_PATH=WFM-QRE\INCOMING\WFM_MAIN\Form_16 # Form 16 debit note CSV (incoming): INCOMING/WFM_MAIN/FORM16_DEBT
# WFM_FORM16_OUTGOING_PATH=WFM-QRE\OUTGOING\WFM_SAP_MAIN\Form_16 # Form 16 SAP responses (outgoing): OUTGOING/WFM_SAP_MAIN/FORM16_CRDT
# WFM_FORM16_CREDIT_INCOMING_PATH=WFM-QRE\INCOMING\WFM_MAIN\FORM16_CRDT
# WFM_FORM16_DEBIT_INCOMING_PATH=WFM-QRE\INCOMING\WFM_MAIN\FORM16_DEBT
# WFM_FORM16_OUTGOING_PATH=WFM-QRE\OUTGOING\WFM_SAP_MAIN\FORM16_CRDT

View File

@ -26,6 +26,7 @@
"seed:demo-dealers": "ts-node -r tsconfig-paths/register src/scripts/seed-demo-dealers.ts", "seed:demo-dealers": "ts-node -r tsconfig-paths/register src/scripts/seed-demo-dealers.ts",
"cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts", "cleanup:dealer-claims": "ts-node -r tsconfig-paths/register src/scripts/cleanup-dealer-claims.ts",
"clear:form16-and-demo": "ts-node -r tsconfig-paths/register src/scripts/clear-form16-and-demo-data.ts", "clear:form16-and-demo": "ts-node -r tsconfig-paths/register src/scripts/clear-form16-and-demo-data.ts",
"clear:26as": "ts-node -r tsconfig-paths/register src/scripts/clear-26as-data.ts",
"redis:start": "docker run -d --name redis-workflow -p 6379:6379 redis:7-alpine", "redis:start": "docker run -d --name redis-workflow -p 6379:6379 redis:7-alpine",
"redis:stop": "docker rm -f redis-workflow", "redis:stop": "docker rm -f redis-workflow",
"test": "jest --passWithNoTests --forceExit", "test": "jest --passWithNoTests --forceExit",

View File

@ -442,33 +442,6 @@ export class Form16Controller {
} }
} }
/**
* POST /api/v1/form16/credit-notes/:id/generate-debit-note
* RE only. Generate debit note for a credit note (dealer + credit note number + amount SAP simulation save debit note).
*/
async generateForm16DebitNote(req: Request, res: Response): Promise<void> {
try {
const userId = (req as AuthenticatedRequest).user?.userId;
if (!userId) return ResponseHandler.unauthorized(res, 'Authentication required');
const creditNoteId = parseInt((req.params as { id: string }).id, 10);
if (Number.isNaN(creditNoteId) || creditNoteId <= 0) {
return ResponseHandler.error(res, 'Valid credit note id is required', 400);
}
const body = (req.body || {}) as { amount?: number };
const amount = typeof body.amount === 'number' ? body.amount : parseFloat(String(body.amount || 0));
const result = await form16Service.generateForm16DebitNoteForCreditNote(creditNoteId, userId, amount);
return ResponseHandler.success(
res,
{ debitNote: result.debitNote, creditNote: result.creditNote },
'Debit note generated'
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[Form16Controller] generateForm16DebitNote error:', error);
return ResponseHandler.error(res, errorMessage, 400);
}
}
/** /**
* POST /api/v1/form16/26as/upload * POST /api/v1/form16/26as/upload
* RE only. Upload a single TXT file containing 26AS data (all dealers). Data stored in tds_26as_entries. * RE only. Upload a single TXT file containing 26AS data (all dealers). Data stored in tds_26as_entries.

View File

@ -111,14 +111,6 @@ router.post(
asyncHandler(form16Controller.sapSimulateDebitNote.bind(form16Controller)) asyncHandler(form16Controller.sapSimulateDebitNote.bind(form16Controller))
); );
// RE only: generate debit note for a credit note (hits SAP simulation; replace with real SAP later).
router.post(
'/credit-notes/:id/generate-debit-note',
requireForm16ReOnly,
requireForm16SubmissionAccess,
asyncHandler(form16Controller.generateForm16DebitNote.bind(form16Controller))
);
// Dealer-only: pending submissions and pending quarters (Form 16 Pending Submissions page) // Dealer-only: pending submissions and pending quarters (Form 16 Pending Submissions page)
router.get( router.get(
'/dealer/submissions', '/dealer/submissions',

View File

@ -2,8 +2,8 @@
* Form 16 (Form 16A TDS Credit) service. * Form 16 (Form 16A TDS Credit) service.
* Quarter-based reconciliation: 26AS (aggregated by tan+fy+quarter), Form 16A match, credit/debit, ledger. * Quarter-based reconciliation: 26AS (aggregated by tan+fy+quarter), Form 16A match, credit/debit, ledger.
* *
* Credit note: run26asMatchAndCreditNote only (on Form 16A submit, match 26AS CN-F-16-{certificateNumber}-{dealerCode}-{FY}-{quarter}-V{version}, ledger, CSV to WFM FORM_16). * Credit note: run26asMatchAndCreditNote only (on Form 16A submit, match 26AS CN-F-16-{...}, ledger, CSV to WFM FORM_16).
* Debit: generateForm16DebitNoteForCreditNote (manual) and process26asUploadAggregation (auto when 26AS total drops); DN-F-16-{creditNoteCertificateNumber}-{dc}-{fy}-{q}-V{version} (uses the credit notes certificate number). * Debit: process26asUploadAggregation only (when 26AS total drops for a SETTLED quarter); DN-F-16-{...}, CSV to WFM FORM_16.
*/ */
import crypto from 'crypto'; import crypto from 'crypto';
@ -579,31 +579,27 @@ async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise
validationNotes: null, validationNotes: null,
}); });
// Push Form 16 credit note CSV to WFM INCOMING/WFM_MAIN/FORM_16 (pipe | separator, no double quotes) // Push Form 16 credit note incoming CSV to WFM INCOMING/WFM_MAIN/FORM16_CRDT (SAP credit note generation exact fields only)
try { try {
const dealer = await Dealer.findOne({
where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
attributes: ['dealership', 'dealerPrincipalName'],
});
const dealerName = dealer ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode) : dealerCode;
const trnsUniqNo = `F16-CN-${submission.id}-${creditNote.id}-${Date.now()}`; const trnsUniqNo = `F16-CN-${submission.id}-${creditNote.id}-${Date.now()}`;
const claimDate = now.toISOString().slice(0, 10).replace(/-/g, ''); const docDate = now.toISOString().slice(0, 10).replace(/-/g, '');
const csvRow = { const fyCompact = form16FyCompact(financialYear) || '';
CREDIT_TYPE: 'Form16', const finYearAndQuarter = fyCompact && quarter ? `FY_${fyCompact}_${quarter}` : '';
DEALER_CODE: dealerCode, const csvRow: Record<string, string | number> = {
DEALER_NAME: dealerName,
AMOUNT: tdsAmount,
FINANCIAL_YEAR: financialYear,
QUARTER: quarter,
CREDIT_NOTE_NUMBER: cnNumber,
TRNS_UNIQ_NO: trnsUniqNo, TRNS_UNIQ_NO: trnsUniqNo,
CLAIM_DATE: claimDate, TDS_TRNS_ID: cnNumber,
DEALER_CODE: dealerCode,
TDS_TRNS_DOC_TYP: 'ZTDS',
DLR_TAN_NO: tanNumber,
'FIN_YEAR & QUARTER': finYearAndQuarter,
DOC_DATE: docDate,
TDS_AMT: Number(tdsAmount).toFixed(2),
}; };
const fileName = `${cnNumber}.csv`; const fileName = `${cnNumber}.csv`;
await wfmFileService.generateForm16IncomingCSV([csvRow], fileName); await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'credit');
logger.info(`[Form16] Credit note CSV pushed to WFM FORM_16: ${cnNumber}`); logger.info(`[Form16] Credit note CSV pushed to WFM FORM16_CRDT: ${cnNumber}`);
} catch (csvErr: any) { } catch (csvErr: any) {
logger.error('[Form16] Failed to push credit note CSV to WFM FORM_16:', csvErr?.message || csvErr); logger.error('[Form16] Failed to push credit note CSV to WFM FORM16_CRDT:', csvErr?.message || csvErr);
// Do not fail the flow; credit note and ledger are already created // Do not fail the flow; credit note and ledger are already created
} }
@ -1307,74 +1303,6 @@ export async function getCreditNoteById(creditNoteId: number) {
}; };
} }
/**
* RE only. Generate debit note for a credit note (Form 16). Creates Form16DebitNote with DN-F-16-{creditNoteCertificateNumber}-{dc}-{fy}-{q}-V{v} and pushes CSV to WFM INCOMING/WFM_MAIN/FORM_16 for SAP.
*/
export async function generateForm16DebitNoteForCreditNote(
creditNoteId: number,
userId: string,
amount: number
): Promise<{ debitNote: Form16DebitNote; creditNote: Form16CreditNote }> {
if (!amount || amount <= 0) throw new Error('Valid amount is required to generate debit note.');
const creditNote = await Form16CreditNote.findByPk(creditNoteId, {
attributes: ['id', 'creditNoteNumber', 'amount', 'financialYear', 'quarter', 'issueDate'],
include: [{ model: Form16aSubmission, as: 'submission', attributes: ['id', 'dealerCode', 'version', 'form16aNumber'] }],
});
if (!creditNote || !(creditNote as any).submission) throw new Error('Credit note not found.');
const existing = await Form16DebitNote.findOne({ where: { creditNoteId }, attributes: ['id'] });
if (existing) throw new Error('A debit note already exists for this credit note.');
// Dealer code, version and certificate number from the credit note's submission (DN uses same cert as the CN being reversed)
const dealerCode = ((creditNote as any).submission?.dealerCode || '').toString().trim();
const financialYear = (creditNote as any).financialYear || '';
const quarter = (creditNote as any).quarter || '';
const version = typeof (creditNote as any).submission?.version === 'number' && (creditNote as any).submission.version >= 1 ? (creditNote as any).submission.version : 1;
const creditNoteCertNumber = ((creditNote as any).submission?.form16aNumber || '').toString().trim();
const dnNumber = formatForm16DebitNoteNumber(dealerCode || 'UNKNOWN', financialYear, quarter, version, creditNoteCertNumber);
const now = new Date();
const debitNote = await Form16DebitNote.create({
creditNoteId,
debitNoteNumber: dnNumber,
amount,
issueDate: now,
status: 'issued',
reason: 'Debit note pushed to WFM FORM16 for SAP.',
createdBy: userId,
});
// Push Form 16 debit note CSV to WFM INCOMING/WFM_MAIN/FORM_16
try {
const dealer = await Dealer.findOne({
where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
attributes: ['dealership', 'dealerPrincipalName'],
});
const dealerName = dealer ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode) : dealerCode;
const trnsUniqNo = `F16-DN-${creditNoteId}-${debitNote.id}-${Date.now()}`;
const claimDate = now.toISOString().slice(0, 10).replace(/-/g, '');
const creditNoteIssueDate = (creditNote as any).issueDate
? new Date((creditNote as any).issueDate).toISOString().slice(0, 10).replace(/-/g, '')
: '';
const csvRow = {
CREDIT_NOTE_NUMBER: (creditNote as any).creditNoteNumber,
DEALER_CODE: dealerCode || 'UNKNOWN',
DEALER_NAME: dealerName,
AMOUNT: amount,
FINANCIAL_YEAR: financialYear,
QUARTER: quarter,
DEBIT_NOTE_NUMBER: dnNumber,
TRNS_UNIQ_NO: trnsUniqNo,
CLAIM_DATE: claimDate,
CREDIT_NOTE_DATE: creditNoteIssueDate,
};
const fileName = `${dnNumber}.csv`;
await wfmFileService.generateForm16IncomingCSV([csvRow], fileName);
logger.info(`[Form16] Manual debit note CSV pushed to WFM FORM_16: ${dnNumber}`);
} catch (csvErr: any) {
logger.error('[Form16] Failed to push manual debit note CSV to WFM FORM_16:', csvErr?.message || csvErr);
}
return { debitNote, creditNote };
}
// ---------- Non-submitted dealers (RE only) ---------- // ---------- Non-submitted dealers (RE only) ----------
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'] as const; const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'] as const;
@ -2100,35 +2028,28 @@ export async function process26asUploadAggregation(uploadLogId: number): Promise
await setQuarterStatusDebitIssued(tanNumber, fy, q, debit.id); await setQuarterStatusDebitIssued(tanNumber, fy, q, debit.id);
debitsCreated++; debitsCreated++;
// Push Form 16 debit note CSV to WFM INCOMING/WFM_MAIN/FORM_16 // Push Form 16 debit note CSV to WFM INCOMING/WFM_MAIN/FORM16_DEBT (same column set as credit note / SAP expectation)
try { try {
const dealer = await Dealer.findOne({
where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
attributes: ['dealership', 'dealerPrincipalName'],
});
const dealerName = dealer ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode) : dealerCode;
const trnsUniqNo = `F16-DN-${creditNote.id}-${debit.id}-${Date.now()}`; const trnsUniqNo = `F16-DN-${creditNote.id}-${debit.id}-${Date.now()}`;
const claimDate = now.toISOString().slice(0, 10).replace(/-/g, ''); const docDate = now.toISOString().slice(0, 10).replace(/-/g, '');
const creditNoteIssueDate = (creditNote as any).issueDate const fyCompact = form16FyCompact(cnFy) || '';
? new Date((creditNote as any).issueDate).toISOString().slice(0, 10).replace(/-/g, '') const finYearAndQuarter = fyCompact && cnQuarter ? `FY ${fyCompact}_${cnQuarter}` : '';
: ''; const csvRow: Record<string, string | number> = {
const csvRow = {
CREDIT_NOTE_NUMBER: (creditNote as any).creditNoteNumber,
DEALER_CODE: dealerCode || 'XX',
DEALER_NAME: dealerName,
AMOUNT: amount,
FINANCIAL_YEAR: cnFy,
QUARTER: cnQuarter,
DEBIT_NOTE_NUMBER: debitNum,
TRNS_UNIQ_NO: trnsUniqNo, TRNS_UNIQ_NO: trnsUniqNo,
CLAIM_DATE: claimDate, TDS_TRNS_ID: debitNum,
CREDIT_NOTE_DATE: creditNoteIssueDate, DEALER_CODE: dealerCode || 'XX',
TDS_TRNS_DOC_TYP: 'ZTDS',
'Org.Document Number': debit.id,
DLR_TAN_NO: tanNumber,
'FIN_YEAR & QUARTER': finYearAndQuarter,
DOC_DATE: docDate,
TDS_AMT: Number(amount).toFixed(2),
}; };
const fileName = `${debitNum}.csv`; const fileName = `${debitNum}.csv`;
await wfmFileService.generateForm16IncomingCSV([csvRow], fileName); await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'debit');
logger.info(`[Form16] Debit note CSV pushed to WFM FORM_16: ${debitNum}`); logger.info(`[Form16] Debit note CSV pushed to WFM FORM16_DEBT: ${debitNum}`);
} catch (csvErr: any) { } catch (csvErr: any) {
logger.error('[Form16] Failed to push debit note CSV to WFM FORM_16:', csvErr?.message || csvErr); logger.error('[Form16] Failed to push debit note CSV to WFM FORM16_DEBT:', csvErr?.message || csvErr);
} }
} }
} }

View File

@ -67,7 +67,13 @@ export async function trigger26AsDataAddedNotification(): Promise<void> {
return; return;
} }
const { notificationService } = await import('./notification.service'); const { notificationService } = await import('./notification.service');
const reUserIds = await getReUserIdsFor26As();
// Base RE audience (admins / RE viewers). This helper already tries to exclude dealers,
// but we defensively re-filter below so that 26AS notifications are never sent to dealers.
const baseReUserIds = await getReUserIdsFor26As();
const dealerUserIds = await getDealerUserIds();
const dealerSet = new Set(dealerUserIds);
const reUserIds = baseReUserIds.filter((id) => !dealerSet.has(id));
const title = 'Form 16 26AS data updated'; const title = 'Form 16 26AS data updated';
if (reUserIds.length > 0 && n.templateRe) { if (reUserIds.length > 0 && n.templateRe) {

View File

@ -5,13 +5,14 @@ import logger from '../utils/logger';
/** Default WFM folder names (joined with path.sep for current OS). */ /** 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_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_CLAIMS_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'DLR_INC_CLAIMS');
const DEFAULT_FORM16_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM_16'); const DEFAULT_FORM16_CREDIT_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM16_CRDT');
const DEFAULT_FORM16_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM_16'); const DEFAULT_FORM16_DEBIT_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM16_DEBT');
const DEFAULT_FORM16_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM16_CRDT');
/** /**
* WFM File Service * WFM File Service
* Handles generation and storage of CSV files in the WFM folder structure. * Handles generation and storage of CSV files in the WFM folder structure.
* Dealer claims use DLR_INC_CLAIMS; Form 16 uses FORM_16 under INCOMING/WFM_MAIN and OUTGOING/WFM_SAP_MAIN. * Dealer claims use DLR_INC_CLAIMS; Form 16 uses FORM16_CRDT (credit) and FORM16_DEBT (debit) under INCOMING/WFM_MAIN.
* Paths are cross-platform; set WFM_BASE_PATH (and optional path overrides) in .env for production. * Paths are cross-platform; set WFM_BASE_PATH (and optional path overrides) in .env for production.
*/ */
export class WFMFileService { export class WFMFileService {
@ -20,9 +21,11 @@ export class WFMFileService {
private incomingNonGstClaimsPath: string; private incomingNonGstClaimsPath: string;
private outgoingGstClaimsPath: string; private outgoingGstClaimsPath: string;
private outgoingNonGstClaimsPath: string; private outgoingNonGstClaimsPath: string;
/** Form 16: INCOMING/WFM_MAIN/FORM_16 */ /** Form 16 credit notes: INCOMING/WFM_MAIN/FORM16_CRDT */
private form16IncomingPath: string; private form16IncomingCreditPath: string;
/** Form 16: OUTGOING/WFM_SAP_MAIN/FORM_16 */ /** Form 16 debit notes: INCOMING/WFM_MAIN/FORM16_DEBT */
private form16IncomingDebitPath: string;
/** Form 16: OUTGOING/WFM_SAP_MAIN/FORM16_CRDT (SAP responses) */
private form16OutgoingPath: string; private form16OutgoingPath: string;
constructor() { constructor() {
@ -31,7 +34,14 @@ export class WFMFileService {
this.incomingNonGstClaimsPath = process.env.WFM_INCOMING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_NON_GST'; this.incomingNonGstClaimsPath = process.env.WFM_INCOMING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\INCOMING\\WFM_MAIN\\DLR_INC_CLAIMS_NON_GST';
this.outgoingGstClaimsPath = process.env.WFM_OUTGOING_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_GST'; this.outgoingGstClaimsPath = process.env.WFM_OUTGOING_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_GST';
this.outgoingNonGstClaimsPath = process.env.WFM_OUTGOING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_NON_GST'; this.outgoingNonGstClaimsPath = process.env.WFM_OUTGOING_NON_GST_CLAIMS_PATH || 'WFM-QRE\\OUTGOING\\WFM_SAP_MAIN\\DLR_INC_CLAIMS_NON_GST';
this.form16IncomingPath = process.env.WFM_FORM16_INCOMING_PATH || DEFAULT_FORM16_INCOMING;
// 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.form16IncomingDebitPath =
process.env.WFM_FORM16_DEBIT_INCOMING_PATH || legacyForm16Incoming || DEFAULT_FORM16_DEBIT_INCOMING;
this.form16OutgoingPath = process.env.WFM_FORM16_OUTGOING_PATH || DEFAULT_FORM16_OUTGOING; this.form16OutgoingPath = process.env.WFM_FORM16_OUTGOING_PATH || DEFAULT_FORM16_OUTGOING;
} }
@ -133,18 +143,22 @@ export class WFMFileService {
} }
/** /**
* Generate a CSV file for Form 16 (credit/debit note) and store in INCOMING/WFM_MAIN/FORM_16. * 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). * 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 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 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): Promise<string> { async generateForm16IncomingCSV(data: any[], fileName: string, type: 'credit' | 'debit' = 'credit'): Promise<string> {
const maxRetries = 3; const maxRetries = 3;
let retryCount = 0; let retryCount = 0;
while (retryCount <= maxRetries) { while (retryCount <= maxRetries) {
try { try {
const targetDir = path.join(this.basePath, this.form16IncomingPath); const targetPath = type === 'debit' ? this.form16IncomingDebitPath : this.form16IncomingCreditPath;
const targetDir = path.join(this.basePath, targetPath);
this.ensureDirectoryExists(targetDir); this.ensureDirectoryExists(targetDir);
const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`); const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`);