added the new invoice flow where new lables added like hsn and part_ amount for gst invoice generation
This commit is contained in:
parent
fd6032f21b
commit
abba8aefdd
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user