inplemented the gst and non gst invoice generation flow and cost item table enhanced

This commit is contained in:
laxmanhalaki 2026-02-20 20:39:36 +05:30
parent 896b345e02
commit 9fd9c218df
6 changed files with 341 additions and 181 deletions

View File

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

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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())),
};

View File

@ -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,