From abba8aefdd8ea75eacbbcdf977b92d27c6f299f4 Mon Sep 17 00:00:00 2001 From: laxman h Date: Tue, 24 Mar 2026 21:27:46 +0530 Subject: [PATCH] added the new invoice flow where new lables added like hsn and part_ amount for gst invoice generation --- src/controllers/dealerClaim.controller.ts | 122 ++++++++++++++-------- src/services/dealerClaim.service.ts | 38 +++---- src/services/wfmFile.service.ts | 73 +++++++++++-- src/utils/helpers.ts | 90 ++++++++++++++++ 4 files changed, 253 insertions(+), 70 deletions(-) diff --git a/src/controllers/dealerClaim.controller.ts b/src/controllers/dealerClaim.controller.ts index fefb373..7d9e797 100644 --- a/src/controllers/dealerClaim.controller.ts +++ b/src/controllers/dealerClaim.controller.ts @@ -15,10 +15,11 @@ import crypto from 'crypto'; import { DealerClaimDetails } from '../models/DealerClaimDetails'; import { ClaimInvoice } from '../models/ClaimInvoice'; import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem'; +import { ClaimCreditNote } from '../models/ClaimCreditNote'; import { ActivityType } from '../models/ActivityType'; import { Participant } from '../models/Participant'; import { sanitizeObject, sanitizePermissive } from '../utils/sanitizer'; -import { padDealerCode } from '../utils/helpers'; +import { buildWfmClaimCsvRow, padDealerCode, WFM_CLAIM_CSV_HEADERS } from '../utils/helpers'; import { costBreakupSchema, closedExpensesSchema, updateEInvoiceSchema, updateIOSchema } from '../validators/dealerClaim.validator'; export class DealerClaimController { @@ -1114,52 +1115,49 @@ export class DealerClaimController { const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST'; // Construct CSV with pipe separator - const headers = [ - 'TRNS_UNIQ_NO', - 'CLAIM_NUMBER', - 'INV_NUMBER', - 'DEALER_CODE', - 'IO_NUMBER', - 'CLAIM_DOC_TYP', - 'CLAIM_TYPE', - 'CLAIM_DATE', - 'CLAIM_AMT' - ]; - - if (!isNonGst) { - headers.push('GST_AMT', 'GST_PERCENTAGE'); - } + const headers = isNonGst + ? [ + 'TRNS_UNIQ_NO', + 'CLAIM_NUMBER', + 'INV_NUMBER', + 'DEALER_CODE', + 'IO_NUMBER', + 'CLAIM_DOC_TYP', + 'CLAIM_TYPE', + 'CLAIM_DATE', + 'CLAIM_AMT' + ] + : [...WFM_CLAIM_CSV_HEADERS]; const rows = items.map(item => { - // For Non-GST, we hide HSN (often stored in transactionCode) - const trnsUniqNo = item.transactionCode || ''; - const claimNumber = requestNumber; - const invNumber = invoice?.invoiceNumber || ''; - const dealerCode = padDealerCode(claimDetails?.dealerCode || ''); - const ioNumber = internalOrder?.ioNumber || ''; - const claimDocTyp = sapRefNo; - const claimType = claimDetails?.activityType || ''; - const claimDate = invoice?.createdAt ? new Date(invoice.createdAt).toISOString().split('T')[0] : ''; - const claimAmt = item.assAmt; - - const rowItems = [ - trnsUniqNo, - claimNumber, - invNumber, - dealerCode, - ioNumber, - claimDocTyp, - claimType, - claimDate, - claimAmt - ]; - - if (!isNonGst) { - const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0); - rowItems.push(totalTax.toFixed(2), item.gstRt || 0); + if (isNonGst) { + const d = new Date(invoice?.invoiceDate || invoice?.createdAt || new Date()); + const claimDate = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; + return [ + item.transactionCode || '', + requestNumber, + invoice?.invoiceNumber || '', + padDealerCode(claimDetails?.dealerCode || ''), + internalOrder?.ioNumber || '', + sapRefNo, + claimDetails?.activityType || '', + claimDate, + item.assAmt + ].join('|'); } - return rowItems.join('|'); + const row = buildWfmClaimCsvRow({ + item: item as any, + requestNumber, + invoiceNumber: invoice?.invoiceNumber || '', + invoiceDate: (invoice?.invoiceDate as Date) || (invoice?.createdAt as Date) || new Date(), + dealerCode: claimDetails?.dealerCode || '', + ioNumber: internalOrder?.ioNumber || '', + claimDocTyp: sapRefNo, + claimType: claimDetails?.activityType || '', + }); + + return headers.map((key) => String((row as any)[key] ?? '')).join('|'); }); const csvContent = [headers.join('|'), ...rows].join('\n'); @@ -1232,9 +1230,43 @@ export class DealerClaimController { } const { wfmFileService } = await import('../services/wfmFile.service'); - const creditNoteData = await wfmFileService.getCreditNoteDetails(claimDetails.dealerCode, requestNumber, isNonGst); + const existingCreditNote = await ClaimCreditNote.findOne({ where: { requestId } }); + if (existingCreditNote?.sapDocumentNumber || existingCreditNote?.creditNoteNumber) { + const payload = [{ + TRNS_UNIQ_NO: '', + CLAIM_NUMBER: requestNumber, + DOC_NO: existingCreditNote.sapDocumentNumber || existingCreditNote.creditNoteNumber || '', + MSG_TYP: existingCreditNote.status || '', + MESSAGE: existingCreditNote.errorMessage || '' + }]; + return ResponseHandler.success(res, payload, 'Credit note data fetched successfully'); + } - return ResponseHandler.success(res, creditNoteData, 'Credit note data fetched successfully'); + const { filePath, data: creditNoteData } = await wfmFileService.getCreditNoteDetailsWithPath( + claimDetails.dealerCode, + requestNumber, + isNonGst + ); + if (!creditNoteData.length) { + return ResponseHandler.success(res, [], 'Credit note data fetched successfully'); + } + + // Current requirement: process/store a single credit note per request. + const firstRow = creditNoteData[0] || {}; + const existingAmount = existingCreditNote?.creditNoteAmount ?? 0; + await ClaimCreditNote.upsert({ + requestId, + creditNoteNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || existingCreditNote?.creditNoteNumber || undefined, + sapDocumentNumber: firstRow.DOC_NO || firstRow.CREDIT_NOTE_NUMBER || existingCreditNote?.sapDocumentNumber || undefined, + status: firstRow.MSG_TYP || existingCreditNote?.status || undefined, + errorMessage: firstRow.MESSAGE || existingCreditNote?.errorMessage || undefined, + creditNoteFilePath: filePath, + creditNoteAmount: Number(firstRow.CLAIM_AMT || firstRow.CREDIT_AMT || existingAmount || 0), + confirmedAt: new Date() + }); + wfmFileService.deleteCreditNoteOutgoingFileByPath(filePath); + + return ResponseHandler.success(res, [firstRow], 'Credit note data fetched successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[DealerClaimController] Error fetching credit note WFM data:', error); diff --git a/src/services/dealerClaim.service.ts b/src/services/dealerClaim.service.ts index c613d23..2e906d2 100644 --- a/src/services/dealerClaim.service.ts +++ b/src/services/dealerClaim.service.ts @@ -21,7 +21,7 @@ import { Document } from '../models/Document'; import { Dealer } from '../models/Dealer'; import { WorkflowService } from './workflow.service'; import { DealerClaimApprovalService } from './dealerClaimApproval.service'; -import { generateRequestNumber, padDealerCode } from '../utils/helpers'; +import { buildWfmClaimCsvRow, generateRequestNumber, padDealerCode } from '../utils/helpers'; import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types'; import { sapIntegrationService } from './sapIntegration.service'; import { pwcIntegrationService } from './pwcIntegration.service'; @@ -3625,16 +3625,9 @@ export class DealerClaimService { isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST'; } - const formatDate = (date: any) => { - const d = new Date(date); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - return `${year}${month}${day}`; - }; - const csvData = invoiceItems.map((item: any) => { - const row: any = { + if (isNonGst) { + return { TRNS_UNIQ_NO: item.transactionCode || '', CLAIM_NUMBER: requestNumber, INV_NUMBER: invoice.invoiceNumber || '', @@ -3642,17 +3635,24 @@ export class DealerClaimService { IO_NUMBER: internalOrder?.ioNumber || '', CLAIM_DOC_TYP: sapRefNo, CLAIM_TYPE: claimDetails.activityType, - CLAIM_DATE: formatDate(invoice.invoiceDate || new Date()), + CLAIM_DATE: (() => { + const d = new Date(invoice.invoiceDate || new Date()); + return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; + })(), CLAIM_AMT: item.assAmt - }; - - if (!isNonGst) { - const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0); - row.GST_AMT = totalTax.toFixed(2); - row.GST_PERCENTAGE = item.gstRt; + }; } - - return row; + + return buildWfmClaimCsvRow({ + item, + requestNumber, + invoiceNumber: invoice.invoiceNumber || '', + invoiceDate: (invoice.invoiceDate as Date) || new Date(), + dealerCode: claimDetails.dealerCode, + ioNumber: internalOrder?.ioNumber || '', + claimDocTyp: sapRefNo, + claimType: claimDetails.activityType || '', + }); }); await wfmFileService.generateIncomingClaimCSV(csvData, `CN_${padDealerCode(claimDetails.dealerCode)}_${requestNumber}.csv`, isNonGst); diff --git a/src/services/wfmFile.service.ts b/src/services/wfmFile.service.ts index 4fa2194..4a49480 100644 --- a/src/services/wfmFile.service.ts +++ b/src/services/wfmFile.service.ts @@ -154,21 +154,67 @@ export class WFMFileService { return path.join(this.basePath, targetPath, fileName); } + private getOutgoingClaimsDir(isNonGst: boolean = false): string { + const targetPath = isNonGst ? this.outgoingNonGstClaimsPath : this.outgoingGstClaimsPath; + return path.join(this.basePath, targetPath); + } + + /** + * Build outgoing credit note file path for a dealer + request. + */ + getCreditNoteOutgoingFilePath(dealerCode: string, requestNumber: string, isNonGst: boolean = false): { fileName: string; filePath: string } { + const dealer = String(dealerCode || '').trim(); + const paddedDealer = dealer.padStart(6, '0'); + const exactCandidates = [ + `CN_${paddedDealer}_${requestNumber}.csv`, + `CN_${dealer}_${requestNumber}.csv`, + ]; + + for (const candidate of exactCandidates) { + const candidatePath = this.getOutgoingPath(candidate, isNonGst); + if (fs.existsSync(candidatePath)) { + return { fileName: candidate, filePath: candidatePath }; + } + } + + // Last fallback: pick any CN_*_.csv from target outgoing folder. + const outgoingDir = this.getOutgoingClaimsDir(isNonGst); + if (fs.existsSync(outgoingDir)) { + const escapedReq = requestNumber.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const reqFileRegex = new RegExp(`^CN_.*_${escapedReq}\\.csv$`, 'i'); + const matched = fs.readdirSync(outgoingDir).find((name) => reqFileRegex.test(name)); + if (matched) { + return { fileName: matched, filePath: path.join(outgoingDir, matched) }; + } + } + + // Keep deterministic default for callers when no file exists yet. + const fileName = exactCandidates[0]; + return { fileName, filePath: this.getOutgoingPath(fileName, isNonGst) }; + } + /** * 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); + const { data } = await this.getCreditNoteDetailsWithPath(dealerCode, requestNumber, isNonGst); + return data; + } + + /** + * Get credit note details and resolved file path from outgoing CSV. + */ + async getCreditNoteDetailsWithPath(dealerCode: string, requestNumber: string, isNonGst: boolean = false): Promise<{ fileName: string; filePath: string; data: any[] }> { + const { fileName, filePath } = this.getCreditNoteOutgoingFilePath(dealerCode, requestNumber, isNonGst); try { if (!fs.existsSync(filePath)) { - return []; + return { fileName, filePath, data: [] }; } 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 + if (lines.length <= 1) return { fileName, filePath, data: [] }; // Only headers or empty const headers = lines[0].split('|'); const data = lines.slice(1).map(line => { @@ -180,10 +226,25 @@ export class WFMFileService { return row; }); - return data; + return { fileName, filePath, data }; } catch (error) { logger.error(`[WFMFileService] Error reading credit note CSV: ${fileName}`, error); - return []; + return { fileName, filePath, data: [] }; + } + } + + /** + * Delete an outgoing credit note file once it has been persisted. + */ + deleteCreditNoteOutgoingFileByPath(filePath: string): void { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + logger.info(`[WFMFileService] Deleted processed credit note CSV: ${filePath}`); + } + } catch (error) { + logger.error('[WFMFileService] Error deleting processed credit note CSV:', filePath, error); + throw error; } } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 6526e4f..036af2b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -115,3 +115,93 @@ export const padDealerCode = (dealerCode: string | number): string => { if (dealerCode === null || dealerCode === undefined) return ''; return String(dealerCode).padStart(6, '0'); }; + +/** + * WFM incoming claim CSV headers in required fixed order. + */ +export const WFM_CLAIM_CSV_HEADERS = [ + 'TRNS_UNIQ_NO', + 'CLAIM_NUMBER', + 'INV_NUMBER', + 'INV_DATE', + 'DEALER_CODE', + 'IO_NUMBER', + 'CLAIM_DOC_TYP', + 'CLAIM_TYPE', + 'CLAIM_DATE', + 'HSN_CODE', + 'SAC_CODE', + 'PART_AMT', + 'LABOUR_AMT', + 'GST_AMT', + 'GST_PERCENTAGE', +] as const; + +type WfmInvoiceItemLike = { + transactionCode?: string; + hsnCd?: string; + assAmt?: number | string; + gstRt?: number | string; + igstAmt?: number | string; + cgstAmt?: number | string; + sgstAmt?: number | string; + utgstAmt?: number | string; + isServc?: string; +}; + +type BuildWfmRowInput = { + item: WfmInvoiceItemLike; + requestNumber: string; + invoiceNumber: string; + invoiceDate: Date; + dealerCode: string; + ioNumber: string; + claimDocTyp: string; + claimType: string; +}; + +/** + * Build one WFM incoming claim CSV row. + * Business rule: + * - Service line -> SAC_CODE + LABOUR_AMT, keep HSN_CODE + PART_AMT empty + * - Material line -> HSN_CODE + PART_AMT, keep SAC_CODE + LABOUR_AMT empty + */ +export const buildWfmClaimCsvRow = ({ + item, + requestNumber, + invoiceNumber, + invoiceDate, + dealerCode, + ioNumber, + claimDocTyp, + claimType, +}: BuildWfmRowInput): Record<(typeof WFM_CLAIM_CSV_HEADERS)[number], string | number> => { + const d = new Date(invoiceDate); + const invDate = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; + const normalizedIsService = String(item.isServc || '').toUpperCase() === 'Y'; + const hsnOrSac = item.hsnCd || ''; + const assesableAmount = Number(item.assAmt || 0); + const totalTax = + Number(item.igstAmt || 0) + + Number(item.cgstAmt || 0) + + Number(item.sgstAmt || 0) + + Number(item.utgstAmt || 0); + + return { + TRNS_UNIQ_NO: item.transactionCode || '', + CLAIM_NUMBER: requestNumber || '', + INV_NUMBER: invoiceNumber || '', + INV_DATE: invDate, + DEALER_CODE: padDealerCode(dealerCode || ''), + IO_NUMBER: ioNumber || '', + CLAIM_DOC_TYP: claimDocTyp || '', + CLAIM_TYPE: claimType || '', + CLAIM_DATE: invDate, + HSN_CODE: normalizedIsService ? '' : hsnOrSac, + SAC_CODE: normalizedIsService ? hsnOrSac : '', + PART_AMT: normalizedIsService ? '' : assesableAmount.toFixed(2), + LABOUR_AMT: normalizedIsService ? assesableAmount.toFixed(2) : '', + GST_AMT: totalTax.toFixed(2), + GST_PERCENTAGE: item.gstRt ?? '', + }; +};