added the new invoice flow where new lables added like hsn and part_ amount for gst invoice generation

This commit is contained in:
laxman h 2026-03-24 21:27:46 +05:30
parent fd6032f21b
commit abba8aefdd
4 changed files with 253 additions and 70 deletions

View File

@ -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,7 +1115,8 @@ export class DealerClaimController {
const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST';
// Construct CSV with pipe separator
const headers = [
const headers = isNonGst
? [
'TRNS_UNIQ_NO',
'CLAIM_NUMBER',
'INV_NUMBER',
@ -1124,42 +1126,38 @@ export class DealerClaimController {
'CLAIM_TYPE',
'CLAIM_DATE',
'CLAIM_AMT'
];
if (!isNonGst) {
headers.push('GST_AMT', 'GST_PERCENTAGE');
}
]
: [...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,
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,
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);
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);

View File

@ -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);

View File

@ -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_*_<requestNumber>.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<any[]> {
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;
}
}

View File

@ -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 ?? '',
};
};