inplemented the gst and non gst invoice generation flow and cost item table enhanced
This commit is contained in:
parent
896b345e02
commit
9fd9c218df
@ -11,6 +11,11 @@ import { sapIntegrationService } from '../services/sapIntegration.service';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { WorkflowRequest } from '../models/WorkflowRequest';
|
||||
import { DealerClaimDetails } from '../models/DealerClaimDetails';
|
||||
import { ClaimInvoice } from '../models/ClaimInvoice';
|
||||
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
||||
import { ActivityType } from '../models/ActivityType';
|
||||
|
||||
export class DealerClaimController {
|
||||
private dealerClaimService = new DealerClaimService();
|
||||
@ -981,5 +986,91 @@ export class DealerClaimController {
|
||||
return ResponseHandler.error(res, error.message || 'Failed to test SAP budget block', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Download Invoice CSV
|
||||
* GET /api/v1/dealer-claims/:requestId/e-invoice/csv
|
||||
*/
|
||||
async downloadInvoiceCsv(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const identifier = req.params.requestId;
|
||||
|
||||
// Use helper to find workflow
|
||||
const workflow = await this.findWorkflowByIdentifier(identifier);
|
||||
if (!workflow) {
|
||||
return ResponseHandler.error(res, 'Workflow request not found', 404);
|
||||
}
|
||||
|
||||
const requestId = (workflow as any).requestId || (workflow as any).request_id;
|
||||
const requestNumber = (workflow as any).requestNumber || (workflow as any).request_number;
|
||||
|
||||
// Fetch related data
|
||||
logger.info(`[DealerClaimController] Preparing CSV for requestId: ${requestId}`);
|
||||
const [invoice, items, claimDetails, internalOrder] = await Promise.all([
|
||||
ClaimInvoice.findOne({ where: { requestId } }),
|
||||
ClaimInvoiceItem.findAll({ where: { requestId }, order: [['slNo', 'ASC']] }),
|
||||
DealerClaimDetails.findOne({ where: { requestId } }),
|
||||
InternalOrder.findOne({ where: { requestId } })
|
||||
]);
|
||||
|
||||
logger.info(`[DealerClaimController] Found ${items.length} items to export for request ${requestNumber}`);
|
||||
|
||||
let sapRefNo = '';
|
||||
if (claimDetails?.activityType) {
|
||||
const activityType = await ActivityType.findOne({ where: { title: claimDetails.activityType } });
|
||||
sapRefNo = activityType?.sapRefNo || '';
|
||||
}
|
||||
|
||||
// Construct CSV
|
||||
const headers = [
|
||||
'TRNS_UNIQ_NO',
|
||||
'CLAIM_NUMBER',
|
||||
'INV_NUMBER',
|
||||
'DEALER_CODE',
|
||||
'IO_NUMBER',
|
||||
'CLAIM_DOC_TYP',
|
||||
'CLAIM_DATE',
|
||||
'CLAIM_AMT',
|
||||
'GST_AMT',
|
||||
'GST_PERCENTAG'
|
||||
];
|
||||
|
||||
const rows = items.map(item => {
|
||||
const trnsUniqNo = item.transactionCode || '';
|
||||
const claimNumber = requestNumber;
|
||||
const invNumber = invoice?.invoiceNumber || '';
|
||||
const dealerCode = claimDetails?.dealerCode || '';
|
||||
const ioNumber = internalOrder?.ioNumber || '';
|
||||
const claimDocTyp = sapRefNo;
|
||||
const claimDate = invoice?.createdAt ? new Date(invoice.createdAt).toISOString().split('T')[0] : '';
|
||||
const claimAmt = item.assAmt;
|
||||
const totalTax = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0) + Number(item.utgstAmt || 0);
|
||||
const gstPercentag = item.gstRt;
|
||||
|
||||
return [
|
||||
trnsUniqNo,
|
||||
claimNumber,
|
||||
invNumber,
|
||||
dealerCode,
|
||||
ioNumber,
|
||||
claimDocTyp,
|
||||
claimDate,
|
||||
claimAmt,
|
||||
totalTax.toFixed(2),
|
||||
gstPercentag
|
||||
].join(',');
|
||||
});
|
||||
|
||||
const csvContent = [headers.join(','), ...rows].join('\n');
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="Invoice_${requestNumber}.csv"`);
|
||||
|
||||
res.status(200).send(csvContent);
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[DealerClaimController] Error downloading invoice CSV:', error);
|
||||
return ResponseHandler.error(res, 'Failed to download invoice CSV', 500, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,31 @@ module.exports = {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
},
|
||||
utgst_amt: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
cgst_rate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
sgst_rate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
igst_rate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
utgst_rate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
tot_item_val: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
|
||||
@ -18,14 +18,19 @@ interface ClaimInvoiceItemAttributes {
|
||||
igstAmt: number;
|
||||
cgstAmt: number;
|
||||
sgstAmt: number;
|
||||
utgstAmt: number;
|
||||
totItemVal: number;
|
||||
isServc: string;
|
||||
igstRate?: number;
|
||||
cgstRate?: number;
|
||||
sgstRate?: number;
|
||||
utgstRate?: number;
|
||||
expenseIds?: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface ClaimInvoiceItemCreationAttributes extends Optional<ClaimInvoiceItemAttributes, 'itemId' | 'invoiceNumber' | 'transactionCode' | 'expenseIds' | 'createdAt' | 'updatedAt'> { }
|
||||
interface ClaimInvoiceItemCreationAttributes extends Optional<ClaimInvoiceItemAttributes, 'itemId' | 'invoiceNumber' | 'transactionCode' | 'expenseIds' | 'createdAt' | 'updatedAt' | 'utgstAmt' | 'igstRate' | 'cgstRate' | 'sgstRate' | 'utgstRate'> { }
|
||||
|
||||
class ClaimInvoiceItem extends Model<ClaimInvoiceItemAttributes, ClaimInvoiceItemCreationAttributes> implements ClaimInvoiceItemAttributes {
|
||||
public itemId!: string;
|
||||
@ -43,8 +48,13 @@ class ClaimInvoiceItem extends Model<ClaimInvoiceItemAttributes, ClaimInvoiceIte
|
||||
public igstAmt!: number;
|
||||
public cgstAmt!: number;
|
||||
public sgstAmt!: number;
|
||||
public utgstAmt!: number;
|
||||
public totItemVal!: number;
|
||||
public isServc!: string;
|
||||
public igstRate?: number;
|
||||
public cgstRate?: number;
|
||||
public sgstRate?: number;
|
||||
public utgstRate?: number;
|
||||
public expenseIds?: string[];
|
||||
public createdAt!: Date;
|
||||
public updatedAt!: Date;
|
||||
@ -134,6 +144,36 @@ ClaimInvoiceItem.init(
|
||||
allowNull: false,
|
||||
field: 'sgst_amt',
|
||||
},
|
||||
utgstAmt: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
field: 'utgst_amt',
|
||||
},
|
||||
igstRate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
field: 'igst_rate',
|
||||
},
|
||||
cgstRate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
field: 'cgst_rate',
|
||||
},
|
||||
sgstRate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
field: 'sgst_rate',
|
||||
},
|
||||
utgstRate: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true,
|
||||
defaultValue: 0,
|
||||
field: 'utgst_rate',
|
||||
},
|
||||
totItemVal: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
|
||||
@ -94,7 +94,8 @@ router.get('/:requestId/e-invoice/pdf', authenticateToken, asyncHandler(dealerCl
|
||||
* @desc Update credit note details (Step 8)
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/:requestId/credit-note', authenticateToken, asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
||||
router.get('/:requestId/e-invoice/csv', authenticateToken, asyncHandler(dealerClaimController.downloadInvoiceCsv.bind(dealerClaimController)));
|
||||
router.post('/:requestId/credit-note', authenticateToken, upload.single('creditNoteFile'), asyncHandler(dealerClaimController.updateCreditNote.bind(dealerClaimController)));
|
||||
|
||||
/**
|
||||
* @route POST /api/v1/dealer-claims/:requestId/credit-note/send
|
||||
|
||||
@ -17,6 +17,7 @@ import { User } from '../models/User';
|
||||
import { DealerClaimHistory, SnapshotType } from '../models/DealerClaimHistory';
|
||||
import { ActivityType } from '../models/ActivityType';
|
||||
import { Document } from '../models/Document';
|
||||
import { Dealer } from '../models/Dealer';
|
||||
import { WorkflowService } from './workflow.service';
|
||||
import { DealerClaimApprovalService } from './dealerClaimApproval.service';
|
||||
import { generateRequestNumber } from '../utils/helpers';
|
||||
@ -1090,6 +1091,20 @@ export class DealerClaimService {
|
||||
} else {
|
||||
serializedClaimDetails.defaultGstRate = 18; // Fallback
|
||||
}
|
||||
|
||||
// Fetch dealer GSTIN from dealers table
|
||||
try {
|
||||
const dealer = await Dealer.findOne({
|
||||
where: { dlrcode: claimDetails.dealerCode }
|
||||
});
|
||||
if (dealer) {
|
||||
serializedClaimDetails.dealerGstin = dealer.gst || null;
|
||||
// Also add for backward compatibility if needed
|
||||
serializedClaimDetails.dealerGSTIN = dealer.gst || null;
|
||||
}
|
||||
} catch (dealerError) {
|
||||
logger.warn(`[DealerClaimService] Error fetching dealer GSTIN for ${claimDetails.dealerCode}:`, dealerError);
|
||||
}
|
||||
}
|
||||
|
||||
// Transform proposal details to include cost items as array
|
||||
@ -1484,14 +1499,18 @@ export class DealerClaimService {
|
||||
gstRate = 18; // Default fallback
|
||||
}
|
||||
|
||||
const igstRate = isIGST ? gstRate : 0;
|
||||
const cgstRate = !isIGST ? gstRate / 2 : 0;
|
||||
const sgstRate = !isIGST ? gstRate / 2 : 0;
|
||||
const hasUtgst = (Number(item.utgstRate) > 0 || Number(item.utgstAmt) > 0);
|
||||
|
||||
const igstAmt = isIGST ? (baseTotal * gstRate) / 100 : 0;
|
||||
const cgstAmt = !isIGST ? (baseTotal * gstRate) / 200 : 0;
|
||||
const sgstAmt = !isIGST ? (baseTotal * gstRate) / 200 : 0;
|
||||
const gstAmt = igstAmt + cgstAmt + sgstAmt;
|
||||
const finalIgstRate = isIGST ? (Number(item.igstRate) || gstRate) : 0;
|
||||
const finalCgstRate = !isIGST ? (Number(item.cgstRate) || gstRate / 2) : 0;
|
||||
const finalSgstRate = (!isIGST && !hasUtgst) ? (Number(item.sgstRate) || gstRate / 2) : 0;
|
||||
const finalUtgstRate = (!isIGST && hasUtgst) ? (Number(item.utgstRate) || gstRate / 2) : 0;
|
||||
|
||||
const finalIgstAmt = isIGST ? (Number(item.igstAmt) || (baseTotal * finalIgstRate) / 100) : 0;
|
||||
const finalCgstAmt = !isIGST ? (Number(item.cgstAmt) || (baseTotal * finalCgstRate) / 100) : 0;
|
||||
const finalSgstAmt = (!isIGST && !hasUtgst) ? (Number(item.sgstAmt) || (baseTotal * finalSgstRate) / 100) : 0;
|
||||
const finalUtgstAmt = (!isIGST && hasUtgst) ? (Number(item.utgstAmt) || (baseTotal * finalUtgstRate) / 100) : 0;
|
||||
const totalTaxAmt = finalIgstAmt + finalCgstAmt + finalSgstAmt + finalUtgstAmt;
|
||||
|
||||
return {
|
||||
requestId,
|
||||
@ -1501,18 +1520,18 @@ export class DealerClaimService {
|
||||
quantity,
|
||||
hsnCode: item.hsnCode || '',
|
||||
gstRate,
|
||||
gstAmt: Number(item.gstAmt) || gstAmt,
|
||||
cgstRate: Number(item.cgstRate) || cgstRate,
|
||||
cgstAmt: Number(item.cgstAmt) || cgstAmt,
|
||||
sgstRate: Number(item.sgstRate) || sgstRate,
|
||||
sgstAmt: Number(item.sgstAmt) || sgstAmt,
|
||||
igstRate: Number(item.igstRate) || igstRate,
|
||||
igstAmt: Number(item.igstAmt) || igstAmt,
|
||||
utgstRate: Number(item.utgstRate) || 0,
|
||||
utgstAmt: Number(item.utgstAmt) || 0,
|
||||
gstAmt: totalTaxAmt,
|
||||
cgstRate: finalCgstRate,
|
||||
cgstAmt: finalCgstAmt,
|
||||
sgstRate: finalSgstRate,
|
||||
sgstAmt: finalSgstAmt,
|
||||
igstRate: finalIgstRate,
|
||||
igstAmt: finalIgstAmt,
|
||||
utgstRate: finalUtgstRate,
|
||||
utgstAmt: finalUtgstAmt,
|
||||
cessRate: Number(item.cessRate) || 0,
|
||||
cessAmt: Number(item.cessAmt) || 0,
|
||||
totalAmt: Number(item.totalAmt) || (baseTotal + gstAmt),
|
||||
totalAmt: Number(item.totalAmt) || (baseTotal + totalTaxAmt),
|
||||
isService: !!item.isService,
|
||||
expenseDate: item.date instanceof Date ? item.date : (item.date ? new Date(item.date) : (completionData.activityCompletionDate || new Date())),
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ import { DealerClaimDetails } from '../models/DealerClaimDetails';
|
||||
import { DealerCompletionExpense } from '../models/DealerCompletionExpense';
|
||||
import { DealerCompletionDetails } from '../models/DealerCompletionDetails';
|
||||
import { ClaimInvoiceItem } from '../models/ClaimInvoiceItem';
|
||||
import { findDealerLocally, DealerInfo } from './dealer.service';
|
||||
|
||||
/**
|
||||
* PWC E-Invoice Integration Service
|
||||
@ -91,10 +92,11 @@ export class PWCIntegrationService {
|
||||
if (!request) return { success: false, error: 'Request not found' };
|
||||
|
||||
const claimDetails = (request as any).claimDetails;
|
||||
const dealer = await Dealer.findOne({ where: { dlrcode: claimDetails?.dealerCode } });
|
||||
const dealer: DealerInfo | null = await findDealerLocally(claimDetails?.dealerCode, claimDetails?.dealerEmail);
|
||||
const activity = await ActivityType.findOne({ where: { title: claimDetails?.activityType } });
|
||||
|
||||
if (!dealer || !activity) {
|
||||
logger.warn(`[PWCIntegration] Dealer or Activity missing for request ${requestId}. Dealer lookup: ${claimDetails?.dealerCode} / ${claimDetails?.dealerEmail}`);
|
||||
return { success: false, error: 'Dealer or Activity details missing' };
|
||||
}
|
||||
|
||||
@ -109,98 +111,8 @@ export class PWCIntegrationService {
|
||||
const formatQty = (val: number) => Number(val.toFixed(3));
|
||||
const formatRate = (val: number) => Number(val.toFixed(2));
|
||||
|
||||
// NEW LOGIC: Check for Non-GST Activity
|
||||
const isNonGST = activity.taxationType === 'Non GST';
|
||||
|
||||
if (isNonGST) {
|
||||
logger.info(`[PWC] Activity ${activity.title} is Non-GST. Skipping IRN generation and grossing up values.`);
|
||||
|
||||
let totalNonGstVal = 0;
|
||||
let nonGstSlNo = 1;
|
||||
|
||||
// Clear existing items
|
||||
await ClaimInvoiceItem.destroy({ where: { requestId } });
|
||||
|
||||
if (expenses && expenses.length > 0) {
|
||||
for (const expense of expenses) {
|
||||
// Gross Up Logic: Total Amount (Base + Tax) becomes the new Base Amount
|
||||
// stored in 'totalAmt' or calculated from components
|
||||
const grossAmount = Number(expense.totalAmt) || (Number(expense.amount) + Number(expense.gstAmt || 0) + Number(expense.cessAmt || 0));
|
||||
|
||||
// Transaction Code
|
||||
const transactionCode = `${customInvoiceNumber}-${String(nonGstSlNo).padStart(2, '0')}`;
|
||||
|
||||
await ClaimInvoiceItem.create({
|
||||
requestId,
|
||||
invoiceNumber: customInvoiceNumber,
|
||||
transactionCode: transactionCode,
|
||||
slNo: nonGstSlNo,
|
||||
description: expense.description || activity.title,
|
||||
hsnCd: expense.hsnCode || activity.hsnCode || "998311",
|
||||
qty: 1,
|
||||
unit: "NOS",
|
||||
unitPrice: formatAmount(grossAmount),
|
||||
assAmt: formatAmount(grossAmount), // Taxable value is the gross amount
|
||||
gstRt: 0, // 0% Tax
|
||||
igstAmt: 0,
|
||||
cgstAmt: 0,
|
||||
sgstAmt: 0,
|
||||
totItemVal: formatAmount(grossAmount),
|
||||
isServc: "Y", // Non-GST reimbursements are generally treated as service/settlement
|
||||
expenseIds: [expense.expenseId]
|
||||
});
|
||||
|
||||
totalNonGstVal += grossAmount;
|
||||
nonGstSlNo++;
|
||||
}
|
||||
} else {
|
||||
// Fallback if no specific expenses (rare)
|
||||
const fallbackAmount = finalAmount;
|
||||
const transactionCode = `${customInvoiceNumber}-${String(nonGstSlNo).padStart(2, '0')}`;
|
||||
|
||||
await ClaimInvoiceItem.create({
|
||||
requestId,
|
||||
invoiceNumber: customInvoiceNumber,
|
||||
transactionCode: transactionCode,
|
||||
slNo: nonGstSlNo,
|
||||
description: activity.title,
|
||||
hsnCd: activity.hsnCode || "998311",
|
||||
qty: 1,
|
||||
unit: "NOS",
|
||||
unitPrice: formatAmount(fallbackAmount),
|
||||
assAmt: formatAmount(fallbackAmount),
|
||||
gstRt: 0,
|
||||
igstAmt: 0,
|
||||
cgstAmt: 0,
|
||||
sgstAmt: 0,
|
||||
totItemVal: formatAmount(fallbackAmount),
|
||||
isServc: "Y",
|
||||
expenseIds: []
|
||||
});
|
||||
totalNonGstVal += fallbackAmount;
|
||||
}
|
||||
|
||||
// Return internal success response (No IRN)
|
||||
return {
|
||||
success: true,
|
||||
irn: undefined,
|
||||
ackNo: undefined,
|
||||
ackDate: new Date(),
|
||||
signedInvoice: undefined,
|
||||
qrCode: undefined, // No QR for Non-GST
|
||||
qrImage: undefined,
|
||||
totalIgstAmt: 0,
|
||||
totalCgstAmt: 0,
|
||||
totalSgstAmt: 0,
|
||||
totalAssAmt: formatAmount(totalNonGstVal)
|
||||
};
|
||||
}
|
||||
|
||||
// --- END Non-GST Logic ---
|
||||
|
||||
// Existing GST Logic starts here...
|
||||
// Extract State Code from Dealer GSTIN
|
||||
let dealerGst = (dealer as any).gst;
|
||||
let dealerGst = dealer?.gstin;
|
||||
|
||||
// HOTFIX: For PWC QA Environment, use a known valid GSTIN if dealer has the invalid test one
|
||||
// The test GSTIN 29AAACE3882D1ZZ is not registered in PWC QA Master, causing Error 701
|
||||
@ -220,14 +132,13 @@ export class PWCIntegrationService {
|
||||
// Try to extract from GSTIN (first 2 chars)
|
||||
if (dealerGst && dealerGst.length >= 2 && !isNaN(Number(dealerGst.substring(0, 2)))) {
|
||||
dealerStateCode = dealerGst.substring(0, 2);
|
||||
} else if ((dealer as any).stateCode) {
|
||||
dealerStateCode = (dealer as any).stateCode;
|
||||
} else if (dealer?.state) {
|
||||
// Approximate state code from state name or use 33 as default if it's RE state
|
||||
dealerStateCode = dealer.state.toLowerCase().includes('tamil') ? "33" : "24";
|
||||
}
|
||||
|
||||
// Fetch expenses if available (Moved to top)
|
||||
// const expenses = await DealerCompletionExpense.findAll({ where: { requestId } });
|
||||
|
||||
let itemList: any[] = [];
|
||||
let claimInvoiceItemsToCreate: any[] = [];
|
||||
let totalAssAmt = 0;
|
||||
let totalIgstAmt = 0;
|
||||
let totalCgstAmt = 0;
|
||||
@ -235,6 +146,7 @@ export class PWCIntegrationService {
|
||||
let totalInvVal = 0;
|
||||
|
||||
const isIGST = dealerStateCode !== "33"; // If dealer state != Buyer state (33), it's IGST
|
||||
const isNonGSTActivity = activity.taxationType === 'Non GST';
|
||||
|
||||
if (expenses && expenses.length > 0) {
|
||||
// Group expenses by HSN/SAC and GST Rate
|
||||
@ -252,11 +164,6 @@ export class PWCIntegrationService {
|
||||
const amount = Number(expense.amount) || 0;
|
||||
const baseAmt = amount * qty;
|
||||
|
||||
const igst = isIGST ? (baseAmt * (gstRate / 100)) : 0;
|
||||
const cgst = !isIGST ? (baseAmt * (gstRate / 200)) : 0;
|
||||
const sgst = !isIGST ? (baseAmt * (gstRate / 200)) : 0;
|
||||
const itemTotal = baseAmt + igst + cgst + sgst;
|
||||
|
||||
if (!groupedExpenses[groupKey]) {
|
||||
groupedExpenses[groupKey] = {
|
||||
hsnCd,
|
||||
@ -266,9 +173,11 @@ export class PWCIntegrationService {
|
||||
igst: 0,
|
||||
cgst: 0,
|
||||
sgst: 0,
|
||||
utgst: 0,
|
||||
itemTotal: 0,
|
||||
description: expense.description || activity.title,
|
||||
expenseIds: [expense.expenseId]
|
||||
expenseIds: [expense.expenseId],
|
||||
hasUtgst: Number(expense.utgstRate || 0) > 0 || Number(expense.utgstAmt || 0) > 0
|
||||
};
|
||||
} else {
|
||||
const nextDesc = expense.description || activity.title;
|
||||
@ -280,68 +189,98 @@ export class PWCIntegrationService {
|
||||
: updatedDesc;
|
||||
}
|
||||
groupedExpenses[groupKey].expenseIds.push(expense.expenseId);
|
||||
if (Number(expense.utgstRate || 0) > 0 || Number(expense.utgstAmt || 0) > 0) {
|
||||
groupedExpenses[groupKey].hasUtgst = true;
|
||||
}
|
||||
}
|
||||
|
||||
groupedExpenses[groupKey].baseAmt += baseAmt;
|
||||
groupedExpenses[groupKey].igst += igst;
|
||||
groupedExpenses[groupKey].cgst += cgst;
|
||||
groupedExpenses[groupKey].sgst += sgst;
|
||||
groupedExpenses[groupKey].itemTotal += itemTotal;
|
||||
});
|
||||
|
||||
// Persistence: Delete old items and insert new ones for this request
|
||||
await ClaimInvoiceItem.destroy({ where: { requestId } });
|
||||
|
||||
itemList = Object.values(groupedExpenses).map((group: any, index: number) => {
|
||||
// STRICT CALCULATION: Recalculate tax based on grouped baseAmt to satisfy PWC validation (Tax = Base * Rate)
|
||||
const groupGstRate = Number(group.gstRate || 0);
|
||||
const groupBaseAmt = Number(group.baseAmt || 0);
|
||||
|
||||
let calcIgst = 0, calcCgst = 0, calcSgst = 0, calcUtgst = 0;
|
||||
let calcIgstRate = 0, calcCgstRate = 0, calcSgstRate = 0, calcUtgstRate = 0;
|
||||
|
||||
if (isIGST) {
|
||||
calcIgst = Number((groupBaseAmt * groupGstRate / 100).toFixed(2));
|
||||
calcIgstRate = groupGstRate;
|
||||
} else {
|
||||
const halfRate = groupGstRate / 2;
|
||||
const halfTax = Number((groupBaseAmt * halfRate / 100).toFixed(2));
|
||||
calcCgst = halfTax;
|
||||
calcCgstRate = halfRate;
|
||||
// Use UTGST if detected in any expense of this group, otherwise SGST
|
||||
if (group.hasUtgst) {
|
||||
calcUtgst = halfTax;
|
||||
calcUtgstRate = halfRate;
|
||||
} else {
|
||||
calcSgst = halfTax;
|
||||
calcSgstRate = halfRate;
|
||||
}
|
||||
}
|
||||
|
||||
const calcTotalInvVal = Number((groupBaseAmt + calcIgst + calcCgst + calcSgst + calcUtgst).toFixed(2));
|
||||
|
||||
// Accumulate overall totals
|
||||
totalAssAmt += group.baseAmt;
|
||||
totalIgstAmt += group.igst;
|
||||
totalCgstAmt += group.cgst;
|
||||
totalSgstAmt += group.sgst;
|
||||
totalInvVal += group.itemTotal;
|
||||
totalAssAmt += groupBaseAmt;
|
||||
totalIgstAmt += calcIgst;
|
||||
totalCgstAmt += calcCgst;
|
||||
totalSgstAmt += calcSgst + calcUtgst; // Sum of SGST and UTGST for summary
|
||||
totalInvVal += calcTotalInvVal;
|
||||
|
||||
const slNo = index + 1;
|
||||
|
||||
// Save to DB
|
||||
const transactionCode = `${customInvoiceNumber}-${String(slNo).padStart(2, '0')}`;
|
||||
|
||||
ClaimInvoiceItem.create({
|
||||
claimInvoiceItemsToCreate.push({
|
||||
requestId,
|
||||
invoiceNumber: customInvoiceNumber,
|
||||
transactionCode: transactionCode,
|
||||
slNo: slNo,
|
||||
description: group.description,
|
||||
hsnCd: group.hsnCd, // Use actual HSN from group
|
||||
hsnCd: group.hsnCd,
|
||||
qty: 1,
|
||||
unit: "NOS",
|
||||
unitPrice: formatAmount(group.baseAmt),
|
||||
assAmt: formatAmount(group.baseAmt),
|
||||
gstRt: formatRate(Number(group.gstRate)),
|
||||
igstAmt: formatAmount(group.igst),
|
||||
cgstAmt: formatAmount(group.cgst),
|
||||
sgstAmt: formatAmount(group.sgst),
|
||||
totItemVal: formatAmount(group.itemTotal),
|
||||
isServc: group.isService ? "Y" : "N", // Use actual isService from group
|
||||
unitPrice: formatAmount(groupBaseAmt),
|
||||
assAmt: formatAmount(groupBaseAmt),
|
||||
gstRt: formatRate(groupGstRate),
|
||||
igstAmt: formatAmount(calcIgst),
|
||||
cgstAmt: formatAmount(calcCgst),
|
||||
sgstAmt: formatAmount(calcSgst),
|
||||
utgstAmt: formatAmount(calcUtgst),
|
||||
igstRate: formatRate(calcIgstRate),
|
||||
cgstRate: formatRate(calcCgstRate),
|
||||
sgstRate: formatRate(calcSgstRate),
|
||||
utgstRate: formatRate(calcUtgstRate),
|
||||
totItemVal: formatAmount(calcTotalInvVal),
|
||||
isServc: group.isService ? "Y" : "N",
|
||||
expenseIds: group.expenseIds
|
||||
}).catch((err: any) => logger.error(`[PWCIntegrationService] Error saving ClaimInvoiceItem:`, err));
|
||||
});
|
||||
|
||||
// PWC Payload Item format
|
||||
const hsnForPwc = isNonGSTActivity ? group.hsnCd : "87141090"; // Force valid HSN for PWC Payload if GST
|
||||
const isServcForPwc = isNonGSTActivity ? (group.isService ? "Y" : "N") : "N"; // 87141090 is Goods
|
||||
|
||||
return {
|
||||
SlNo: String(slNo),
|
||||
PrdNm: group.description,
|
||||
PrdDesc: group.description,
|
||||
HsnCd: "87141090", // Known working HSN from old invoice
|
||||
IsServc: "N", // 87141090 is Goods
|
||||
HsnCd: hsnForPwc,
|
||||
IsServc: isServcForPwc,
|
||||
Qty: formatQty(1),
|
||||
Unit: "NOS",
|
||||
UnitPrice: formatAmount(group.baseAmt),
|
||||
TotAmt: formatAmount(group.baseAmt),
|
||||
UnitPrice: formatAmount(groupBaseAmt),
|
||||
TotAmt: formatAmount(groupBaseAmt),
|
||||
Discount: 0,
|
||||
PreTaxVal: formatAmount(group.baseAmt),
|
||||
AssAmt: formatAmount(group.baseAmt),
|
||||
GstRt: formatRate(Number(group.gstRate)),
|
||||
IgstAmt: formatAmount(group.igst),
|
||||
CgstAmt: formatAmount(group.cgst),
|
||||
SgstAmt: formatAmount(group.sgst),
|
||||
PreTaxVal: formatAmount(groupBaseAmt),
|
||||
AssAmt: formatAmount(groupBaseAmt),
|
||||
GstRt: formatRate(groupGstRate),
|
||||
IgstAmt: formatAmount(calcIgst),
|
||||
CgstAmt: formatAmount(calcCgst),
|
||||
SgstAmt: formatAmount(calcSgst + calcUtgst),
|
||||
CesRt: 0,
|
||||
CesAmt: 0,
|
||||
CesNonAdValAmt: 0,
|
||||
@ -349,17 +288,22 @@ export class PWCIntegrationService {
|
||||
StateCesAmt: 0,
|
||||
StateCesNonAdValAmt: 0,
|
||||
OthChrg: 0,
|
||||
TotItemVal: formatAmount(group.itemTotal)
|
||||
TotItemVal: formatAmount(calcTotalInvVal)
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Fallback to single line item if no expenses found
|
||||
const gstRate = Number(activity.gstRate || 18);
|
||||
const gstRate = isNonGSTActivity ? 0 : Number(activity.gstRate || 18);
|
||||
const assAmt = finalAmount;
|
||||
const igstAmt = isIGST ? (finalAmount * (gstRate / 100)) : 0;
|
||||
const cgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
|
||||
const sgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
|
||||
const totalTax = igstAmt + cgstAmt + sgstAmt;
|
||||
let igstAmt = 0, cgstAmt = 0, sgstAmt = 0;
|
||||
|
||||
if (!isNonGSTActivity) {
|
||||
igstAmt = isIGST ? (finalAmount * (gstRate / 100)) : 0;
|
||||
cgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
|
||||
sgstAmt = !isIGST ? (finalAmount * (gstRate / 200)) : 0;
|
||||
}
|
||||
const utgstAmt = 0; // Fallback assumes SGST for simplicity unless we detect state
|
||||
const totalTax = igstAmt + cgstAmt + sgstAmt + utgstAmt;
|
||||
const totalItemVal = finalAmount + totalTax;
|
||||
|
||||
totalAssAmt = assAmt;
|
||||
@ -370,14 +314,11 @@ export class PWCIntegrationService {
|
||||
|
||||
const slNo = 1;
|
||||
|
||||
// Persistence: Delete old items and insert single fallback item
|
||||
await ClaimInvoiceItem.destroy({ where: { requestId } });
|
||||
|
||||
const fallbackHsn = activity.hsnCode || activity.sacCode || "998311";
|
||||
const fallbackIsService = this.isServiceHSN(fallbackHsn) === "Y";
|
||||
const transactionCode = `${customInvoiceNumber}-${String(slNo).padStart(2, '0')}`;
|
||||
|
||||
ClaimInvoiceItem.create({
|
||||
claimInvoiceItemsToCreate.push({
|
||||
requestId,
|
||||
invoiceNumber: customInvoiceNumber,
|
||||
transactionCode: transactionCode,
|
||||
@ -392,19 +333,22 @@ export class PWCIntegrationService {
|
||||
igstAmt: formatAmount(igstAmt),
|
||||
cgstAmt: formatAmount(cgstAmt),
|
||||
sgstAmt: formatAmount(sgstAmt),
|
||||
utgstAmt: 0,
|
||||
totItemVal: formatAmount(totalItemVal),
|
||||
isServc: fallbackIsService ? "Y" : "N",
|
||||
expenseIds: []
|
||||
}).catch((err: any) => logger.error(`[PWCIntegrationService] Error saving ClaimInvoiceItem (fallback):`, err));
|
||||
});
|
||||
|
||||
const hsnCd = "87141090"; // Force valid HSN for PWC Payload
|
||||
// PWC Payload Item format
|
||||
const hsnForPwc = isNonGSTActivity ? fallbackHsn : "87141090"; // Force valid HSN for PWC Payload if GST
|
||||
const isServcForPwc = isNonGSTActivity ? (fallbackIsService ? "Y" : "N") : "N"; // 87141090 is Goods
|
||||
|
||||
itemList = [{
|
||||
SlNo: String(slNo),
|
||||
PrdNm: activity.title,
|
||||
PrdDesc: activity.title,
|
||||
HsnCd: hsnCd,
|
||||
IsServc: "N",
|
||||
HsnCd: hsnForPwc,
|
||||
IsServc: isServcForPwc,
|
||||
Qty: formatQty(1),
|
||||
Unit: "NOS",
|
||||
UnitPrice: formatAmount(assAmt),
|
||||
@ -427,6 +371,33 @@ export class PWCIntegrationService {
|
||||
}];
|
||||
}
|
||||
|
||||
// NEW LOGIC: Check for Non-GST Activity
|
||||
if (isNonGSTActivity) {
|
||||
logger.info(`[PWC] Activity ${activity.title} is Non-GST. Skipping IRN generation.`);
|
||||
|
||||
// Persistence for Non-GST (Awaited)
|
||||
await ClaimInvoiceItem.destroy({ where: { requestId } });
|
||||
if (claimInvoiceItemsToCreate.length > 0) {
|
||||
await ClaimInvoiceItem.bulkCreate(claimInvoiceItemsToCreate);
|
||||
logger.info(`[PWCIntegration] Persisted ${claimInvoiceItemsToCreate.length} line items for Non-GST request ${requestId}`);
|
||||
}
|
||||
|
||||
// Return internal success response (No IRN)
|
||||
return {
|
||||
success: true,
|
||||
irn: undefined,
|
||||
ackNo: undefined,
|
||||
ackDate: new Date(),
|
||||
signedInvoice: undefined,
|
||||
qrCode: undefined, // No QR for Non-GST
|
||||
qrImage: undefined,
|
||||
totalIgstAmt: formatAmount(totalIgstAmt),
|
||||
totalCgstAmt: formatAmount(totalCgstAmt),
|
||||
totalSgstAmt: formatAmount(totalSgstAmt),
|
||||
totalAssAmt: formatAmount(totalAssAmt)
|
||||
};
|
||||
}
|
||||
|
||||
// Construct PWC Payload - Aligned with sample format provided by user
|
||||
const payload = [
|
||||
{
|
||||
@ -456,16 +427,16 @@ export class PWCIntegrationService {
|
||||
},
|
||||
SellerDtls: {
|
||||
Gstin: dealerGst,
|
||||
LglNm: (dealer as any).dealership || 'Dealer',
|
||||
TrdNm: (dealer as any).dealership || 'Dealer',
|
||||
Addr1: (dealer as any).showroomAddress || "Address Line 1",
|
||||
Loc: (dealer as any).location || "Location",
|
||||
Pin: (dealerGst === validQaGst && String((dealer as any).showroomPincode || '').substring(0, 1) !== '3')
|
||||
LglNm: dealer?.dealerName || 'Dealer',
|
||||
TrdNm: dealer?.dealerName || 'Dealer',
|
||||
Addr1: dealer?.city || "Address Line 1",
|
||||
Loc: dealer?.city || "Location",
|
||||
Pin: (dealerGst === validQaGst)
|
||||
? 380001
|
||||
: Number((dealer as any).showroomPincode) || 600001,
|
||||
: 600001,
|
||||
Stcd: dealerStateCode,
|
||||
Ph: (dealer as any).dpContactNumber || "9998887776",
|
||||
Em: (dealer as any).dealerPrincipalEmailId || "Supplier@inv.com"
|
||||
Ph: dealer?.phone || "9998887776",
|
||||
Em: dealer?.email || "Supplier@inv.com"
|
||||
},
|
||||
BuyerDtls: {
|
||||
Gstin: "33AAACE3882D1ZZ", // Royal Enfield GST (Tamil Nadu)
|
||||
@ -482,7 +453,7 @@ export class PWCIntegrationService {
|
||||
AssVal: formatAmount(totalAssAmt),
|
||||
IgstVal: formatAmount(totalIgstAmt),
|
||||
CgstVal: formatAmount(totalCgstAmt),
|
||||
SgstVal: formatAmount(totalSgstAmt),
|
||||
SgstVal: formatAmount(totalSgstAmt), // Sum of SGST and UTGST for summary
|
||||
TotInvVal: formatAmount(totalInvVal)
|
||||
}
|
||||
}
|
||||
@ -544,6 +515,19 @@ export class PWCIntegrationService {
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
// Persistence for GST (Awaited) - Only if IRN generation was successful
|
||||
await ClaimInvoiceItem.destroy({ where: { requestId } });
|
||||
if (claimInvoiceItemsToCreate.length > 0) {
|
||||
await ClaimInvoiceItem.bulkCreate(claimInvoiceItemsToCreate);
|
||||
logger.info(`[PWCIntegration] Persisted ${claimInvoiceItemsToCreate.length} line items for GST request ${requestId}`);
|
||||
}
|
||||
|
||||
// Update invoice with dealer GSTIN if available
|
||||
if (dealer?.gstin) {
|
||||
await ClaimInvoice.update({ consignorGsin: dealer.gstin }, { where: { requestId } })
|
||||
.catch(err => logger.error(`[PWCIntegration] Error updating invoice consignorGsin:`, err));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
irn,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user