From b3dcaca697e804b3bbb26e38c01d85c311ff404a Mon Sep 17 00:00:00 2001 From: Aaditya Jaiswal Date: Thu, 12 Mar 2026 18:37:05 +0530 Subject: [PATCH] credit note format change, handeling versions --- src/services/form16.service.ts | 80 +++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/src/services/form16.service.ts b/src/services/form16.service.ts index f3c5a68..ddc436a 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 generation: run26asMatchAndCreditNote only (on Form 16A submit, match 26AS Section 194Q/Booking F/O → CN-F-16-{dealerCode}-{FY}-{quarter}, ledger, CSV to WFM FORM_16). - * Debit: generateForm16DebitNoteForCreditNote (manual) and process26asUploadAggregation (auto when 26AS total drops); DN-F-16-{dc}-{fy}-{q}. + * 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). */ import crypto from 'crypto'; @@ -416,23 +416,49 @@ function form16FyCompact(financialYear: string): string { } /** - * Form 16 credit note number: CN-F-16-DC-FY-Q (CN=credit note, F=form, 16, DC=dealer code, FY=financial year, Q=quarter) + * Sanitize certificate number for use in note numbers (alphanumeric and single hyphens only). */ -export function formatForm16CreditNoteNumber(dealerCode: string, financialYear: string, quarter: string): string { - const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX'; - const fy = form16FyCompact(financialYear) || 'XX'; - const q = normalizeQuarter(quarter) || 'X'; - return `CN-F-16-${dc}-${fy}-${q}`; +function sanitizeCertificateNumber(raw: string): string { + const s = (raw || '').trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9-]/g, '') || ''; + return s || 'XX'; } /** - * Form 16 debit note number: DN-F-16-DC-FY-Q (DN=debit note, F=form, 16, DC=dealer code, FY=financial year, Q=quarter) + * Form 16 credit note number: CN-F-16-{certificateNumber}-{dealerCode}-{FY}-{quarter}-V{version} + * Supports revised 26AS / Form 16 resubmission versioning. */ -export function formatForm16DebitNoteNumber(dealerCode: string, financialYear: string, quarter: string): string { +export function formatForm16CreditNoteNumber( + dealerCode: string, + financialYear: string, + quarter: string, + certificateNumber: string, + version: number = 1 +): string { + const cert = sanitizeCertificateNumber(certificateNumber); const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX'; const fy = form16FyCompact(financialYear) || 'XX'; const q = normalizeQuarter(quarter) || 'X'; - return `DN-F-16-${dc}-${fy}-${q}`; + const v = Math.max(1, Math.floor(version)); + return `CN-F-16-${cert}-${dc}-${fy}-${q}-V${v}`; +} + +/** + * Form 16 debit note number: DN-F-16-{creditNoteCertificateNumber}-{dc}-{fy}-{q}-V{version} + * Uses the certificate number of the credit note being reversed (same Form 16A certificate that led to that credit note). + */ +export function formatForm16DebitNoteNumber( + dealerCode: string, + financialYear: string, + quarter: string, + version: number = 1, + creditNoteCertificateNumber: string = '' +): string { + const cert = sanitizeCertificateNumber(creditNoteCertificateNumber) || 'XX'; + const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX'; + const fy = form16FyCompact(financialYear) || 'XX'; + const q = normalizeQuarter(quarter) || 'X'; + const v = Math.max(1, Math.floor(version)); + return `DN-F-16-${cert}-${dc}-${fy}-${q}-V${v}`; } /** @@ -520,9 +546,11 @@ async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise } } - // Dealer code from submission (set at create from users.employee_number) + // Dealer code, certificate number and version from submission (for revised 26AS / Form 16 versioning) const dealerCode = (sub.dealerCode || '').toString().trim(); - const cnNumber = formatForm16CreditNoteNumber(dealerCode, financialYear, quarter); + const certificateNumber = (sub.form16aNumber || '').toString().trim(); + const version = typeof sub.version === 'number' && sub.version >= 1 ? sub.version : 1; + const cnNumber = formatForm16CreditNoteNumber(dealerCode, financialYear, quarter, certificateNumber, version); const now = new Date(); const creditNote = await Form16CreditNote.create({ submissionId: submission.id, @@ -734,6 +762,14 @@ export async function createSubmission( logger.info( `[Form16] Submission match complete: requestId=${requestId}, status=${validationStatus}${validationNotes ? `, notes=${validationNotes}` : ''}${creditNoteNumber ? `, creditNote=${creditNoteNumber}` : ''}.` ); + // When credit note is issued (completed), set workflow status to CLOSED so the request appears on Closed requests page + if (validationStatus === 'success' && creditNoteNumber) { + const workflow = await WorkflowRequest.findOne({ where: { requestId }, attributes: ['requestId', 'status'] }); + if (workflow && (workflow as any).status !== WorkflowStatus.CLOSED) { + await workflow.update({ status: WorkflowStatus.CLOSED }); + logger.info(`[Form16] Request ${requestId} set to CLOSED (credit note issued).`); + } + } } catch (err: any) { logger.error( `[Form16] 26AS match/credit note error for requestId=${requestId}: Form 16A TAN=${(submission as any).tanNumber}, FY=${(submission as any).financialYear}, Quarter=${(submission as any).quarter}, TDS amount=${(submission as any).tdsAmount}. Error:`, @@ -1272,7 +1308,7 @@ export async function getCreditNoteById(creditNoteId: number) { } /** - * RE only. Generate debit note for a credit note (Form 16). Creates Form16DebitNote with DN-F-16-DC-FY-Q format and pushes CSV to WFM INCOMING/WFM_MAIN/FORM_16 for SAP. + * 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, @@ -1282,16 +1318,18 @@ export async function generateForm16DebitNoteForCreditNote( 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'] }], + 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 from submission (set at Form 16 submit from users.employee_number) + // 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 dnNumber = formatForm16DebitNoteNumber(dealerCode || 'UNKNOWN', financialYear, 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, @@ -2032,12 +2070,14 @@ export async function process26asUploadAggregation(uploadLogId: number): Promise }); if (creditNote) { const amount = parseFloat(String((creditNote as any).amount ?? 0)); - const submission = await Form16aSubmission.findByPk((creditNote as any).submissionId, { attributes: ['dealerCode'] }); - // Dealer code from submission (set at Form 16 submit from users.employee_number) + const submission = await Form16aSubmission.findByPk((creditNote as any).submissionId, { attributes: ['dealerCode', 'version', 'form16aNumber'] }); + // Dealer code, version and certificate number from submission (DN uses same cert as the credit note being reversed) const dealerCode = submission ? ((submission as any).dealerCode || '').toString().trim() : ''; + const version = typeof (submission as any)?.version === 'number' && (submission as any).version >= 1 ? (submission as any).version : 1; + const creditNoteCertNumber = submission ? ((submission as any).form16aNumber || '').toString().trim() : ''; const cnFy = (creditNote as any).financialYear || fy; const cnQuarter = (creditNote as any).quarter || q; - const debitNum = formatForm16DebitNoteNumber(dealerCode || 'XX', cnFy, cnQuarter); + const debitNum = formatForm16DebitNoteNumber(dealerCode || 'XX', cnFy, cnQuarter, version, creditNoteCertNumber); const now = new Date(); const debit = await Form16DebitNote.create({ creditNoteId: creditNote.id,