From 89beffee2e9c281244c41cafd7149334100861c8 Mon Sep 17 00:00:00 2001 From: Aaditya Jaiswal Date: Fri, 13 Mar 2026 14:15:55 +0530 Subject: [PATCH] debit CSV and details page fixed --- .gitignore | 3 +- env.example | 11 +- package.json | 1 + src/controllers/form16.controller.ts | 27 ---- src/routes/form16.routes.ts | 8 -- src/services/form16.service.ts | 147 +++++---------------- src/services/form16Notification.service.ts | 8 +- src/services/wfmFile.service.ts | 34 +++-- 8 files changed, 75 insertions(+), 164 deletions(-) diff --git a/.gitignore b/.gitignore index bfda407..7c04960 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,5 @@ uploads/ # GCP Service Account Key config/gcp-key.json -Jenkinsfile \ No newline at end of file +Jenkinsfile +clear-26as-data.ts \ No newline at end of file diff --git a/env.example b/env.example index e0db438..5a5306d 100644 --- a/env.example +++ b/env.example @@ -113,12 +113,15 @@ SAP_REQUESTER=REFMS # WARNING: Only use in development/testing environments 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 /wfm (paths are cross-platform). # WFM_BASE_PATH=C:\WFM # 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 -# Form 16 credit/debit note CSV: INCOMING/WFM_MAIN/FORM_16, OUTGOING/WFM_SAP_MAIN/FORM_16 -# WFM_FORM16_INCOMING_PATH=WFM-QRE\INCOMING\WFM_MAIN\Form_16 -# WFM_FORM16_OUTGOING_PATH=WFM-QRE\OUTGOING\WFM_SAP_MAIN\Form_16 +# Form 16 credit note CSV (incoming): INCOMING/WFM_MAIN/FORM16_CRDT +# Form 16 debit note CSV (incoming): INCOMING/WFM_MAIN/FORM16_DEBT +# 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 diff --git a/package.json b/package.json index fc24bfb..6a77614 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "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", "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:stop": "docker rm -f redis-workflow", "test": "jest --passWithNoTests --forceExit", diff --git a/src/controllers/form16.controller.ts b/src/controllers/form16.controller.ts index 50b2b62..4199e0a 100644 --- a/src/controllers/form16.controller.ts +++ b/src/controllers/form16.controller.ts @@ -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 { - 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 * RE only. Upload a single TXT file containing 26AS data (all dealers). Data stored in tds_26as_entries. diff --git a/src/routes/form16.routes.ts b/src/routes/form16.routes.ts index d9b3c83..df8f3ad 100644 --- a/src/routes/form16.routes.ts +++ b/src/routes/form16.routes.ts @@ -111,14 +111,6 @@ router.post( 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) router.get( '/dealer/submissions', diff --git a/src/services/form16.service.ts b/src/services/form16.service.ts index ddc436a..869b5f8 100644 --- a/src/services/form16.service.ts +++ b/src/services/form16.service.ts @@ -2,8 +2,8 @@ * Form 16 (Form 16A TDS Credit) service. * 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). - * Debit: generateForm16DebitNoteForCreditNote (manual) and process26asUploadAggregation (auto when 26AS total drops); DN-F-16-{creditNoteCertificateNumber}-{dc}-{fy}-{q}-V{version} (uses the credit note’s certificate number). + * Credit note: run26asMatchAndCreditNote only (on Form 16A submit, match 26AS → CN-F-16-{...}, ledger, CSV to WFM FORM_16). + * Debit: process26asUploadAggregation only (when 26AS total drops for a SETTLED quarter); DN-F-16-{...}, CSV to WFM FORM_16. */ import crypto from 'crypto'; @@ -579,31 +579,27 @@ async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise 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 { - 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 claimDate = now.toISOString().slice(0, 10).replace(/-/g, ''); - const csvRow = { - CREDIT_TYPE: 'Form16', - DEALER_CODE: dealerCode, - DEALER_NAME: dealerName, - AMOUNT: tdsAmount, - FINANCIAL_YEAR: financialYear, - QUARTER: quarter, - CREDIT_NOTE_NUMBER: cnNumber, + const docDate = now.toISOString().slice(0, 10).replace(/-/g, ''); + const fyCompact = form16FyCompact(financialYear) || ''; + const finYearAndQuarter = fyCompact && quarter ? `FY_${fyCompact}_${quarter}` : ''; + const csvRow: Record = { 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`; - await wfmFileService.generateForm16IncomingCSV([csvRow], fileName); - logger.info(`[Form16] Credit note CSV pushed to WFM FORM_16: ${cnNumber}`); + await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'credit'); + logger.info(`[Form16] Credit note CSV pushed to WFM FORM16_CRDT: ${cnNumber}`); } 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 } @@ -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) ---------- 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); 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 { - 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 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 || 'XX', - DEALER_NAME: dealerName, - AMOUNT: amount, - FINANCIAL_YEAR: cnFy, - QUARTER: cnQuarter, - DEBIT_NOTE_NUMBER: debitNum, + const docDate = now.toISOString().slice(0, 10).replace(/-/g, ''); + const fyCompact = form16FyCompact(cnFy) || ''; + const finYearAndQuarter = fyCompact && cnQuarter ? `FY ${fyCompact}_${cnQuarter}` : ''; + const csvRow: Record = { TRNS_UNIQ_NO: trnsUniqNo, - CLAIM_DATE: claimDate, - CREDIT_NOTE_DATE: creditNoteIssueDate, + TDS_TRNS_ID: debitNum, + 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`; - await wfmFileService.generateForm16IncomingCSV([csvRow], fileName); - logger.info(`[Form16] Debit note CSV pushed to WFM FORM_16: ${debitNum}`); + const fileName = `${debitNum}.csv`; + await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'debit'); + logger.info(`[Form16] Debit note CSV pushed to WFM FORM16_DEBT: ${debitNum}`); } 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); } } } diff --git a/src/services/form16Notification.service.ts b/src/services/form16Notification.service.ts index 3075434..4f61d38 100644 --- a/src/services/form16Notification.service.ts +++ b/src/services/form16Notification.service.ts @@ -67,7 +67,13 @@ export async function trigger26AsDataAddedNotification(): Promise { return; } 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'; if (reUserIds.length > 0 && n.templateRe) { diff --git a/src/services/wfmFile.service.ts b/src/services/wfmFile.service.ts index cc63052..8d9fd37 100644 --- a/src/services/wfmFile.service.ts +++ b/src/services/wfmFile.service.ts @@ -5,13 +5,14 @@ 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_INCOMING = path.join('WFM-QRE', 'INCOMING', 'WFM_MAIN', 'FORM_16'); -const DEFAULT_FORM16_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM_16'); +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_OUTGOING = path.join('WFM-QRE', 'OUTGOING', 'WFM_SAP_MAIN', 'FORM16_CRDT'); /** * WFM File Service * 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. */ export class WFMFileService { @@ -20,9 +21,11 @@ export class WFMFileService { private incomingNonGstClaimsPath: string; private outgoingGstClaimsPath: string; private outgoingNonGstClaimsPath: string; - /** Form 16: INCOMING/WFM_MAIN/FORM_16 */ - private form16IncomingPath: string; - /** Form 16: OUTGOING/WFM_SAP_MAIN/FORM_16 */ + /** Form 16 credit notes: INCOMING/WFM_MAIN/FORM16_CRDT */ + private form16IncomingCreditPath: string; + /** 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; 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.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.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; } @@ -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). * @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): Promise { + async generateForm16IncomingCSV(data: any[], fileName: string, type: 'credit' | 'debit' = 'credit'): Promise { const maxRetries = 3; let retryCount = 0; while (retryCount <= maxRetries) { 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); const filePath = path.join(targetDir, fileName.endsWith('.csv') ? fileName : `${fileName}.csv`);