credit note format change, handeling versions

This commit is contained in:
Aaditya Jaiswal 2026-03-12 18:37:05 +05:30
parent 9f3327ce38
commit b3dcaca697

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 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). * 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-{dc}-{fy}-{q}. * 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).
*/ */
import crypto from 'crypto'; 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 { function sanitizeCertificateNumber(raw: string): string {
const dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX'; const s = (raw || '').trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9-]/g, '') || '';
const fy = form16FyCompact(financialYear) || 'XX'; return s || 'XX';
const q = normalizeQuarter(quarter) || 'X';
return `CN-F-16-${dc}-${fy}-${q}`;
} }
/** /**
* 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 dc = (dealerCode || '').trim().replace(/\s+/g, '-') || 'XX';
const fy = form16FyCompact(financialYear) || 'XX'; const fy = form16FyCompact(financialYear) || 'XX';
const q = normalizeQuarter(quarter) || 'X'; 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 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 now = new Date();
const creditNote = await Form16CreditNote.create({ const creditNote = await Form16CreditNote.create({
submissionId: submission.id, submissionId: submission.id,
@ -734,6 +762,14 @@ export async function createSubmission(
logger.info( logger.info(
`[Form16] Submission match complete: requestId=${requestId}, status=${validationStatus}${validationNotes ? `, notes=${validationNotes}` : ''}${creditNoteNumber ? `, creditNote=${creditNoteNumber}` : ''}.` `[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) { } catch (err: any) {
logger.error( 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:`, `[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( export async function generateForm16DebitNoteForCreditNote(
creditNoteId: number, 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.'); if (!amount || amount <= 0) throw new Error('Valid amount is required to generate debit note.');
const creditNote = await Form16CreditNote.findByPk(creditNoteId, { const creditNote = await Form16CreditNote.findByPk(creditNoteId, {
attributes: ['id', 'creditNoteNumber', 'amount', 'financialYear', 'quarter', 'issueDate'], 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.'); if (!creditNote || !(creditNote as any).submission) throw new Error('Credit note not found.');
const existing = await Form16DebitNote.findOne({ where: { creditNoteId }, attributes: ['id'] }); const existing = await Form16DebitNote.findOne({ where: { creditNoteId }, attributes: ['id'] });
if (existing) throw new Error('A debit note already exists for this credit note.'); 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 dealerCode = ((creditNote as any).submission?.dealerCode || '').toString().trim();
const financialYear = (creditNote as any).financialYear || ''; const financialYear = (creditNote as any).financialYear || '';
const quarter = (creditNote as any).quarter || ''; 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 now = new Date();
const debitNote = await Form16DebitNote.create({ const debitNote = await Form16DebitNote.create({
creditNoteId, creditNoteId,
@ -2032,12 +2070,14 @@ export async function process26asUploadAggregation(uploadLogId: number): Promise
}); });
if (creditNote) { if (creditNote) {
const amount = parseFloat(String((creditNote as any).amount ?? 0)); const amount = parseFloat(String((creditNote as any).amount ?? 0));
const submission = await Form16aSubmission.findByPk((creditNote as any).submissionId, { attributes: ['dealerCode'] }); const submission = await Form16aSubmission.findByPk((creditNote as any).submissionId, { attributes: ['dealerCode', 'version', 'form16aNumber'] });
// Dealer code from submission (set at Form 16 submit from users.employee_number) // 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 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 cnFy = (creditNote as any).financialYear || fy;
const cnQuarter = (creditNote as any).quarter || q; 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 now = new Date();
const debit = await Form16DebitNote.create({ const debit = await Form16DebitNote.create({
creditNoteId: creditNote.id, creditNoteId: creditNote.id,