import puppeteer from 'puppeteer'; import { WorkflowRequest } from '@models/WorkflowRequest'; import { ClaimInvoice } from '@models/ClaimInvoice'; import { DealerClaimDetails } from '@models/DealerClaimDetails'; import { DealerProposalDetails } from '@models/DealerProposalDetails'; import { DealerCompletionExpense } from '@models/DealerCompletionExpense'; import { ClaimInvoiceItem } from '@models/ClaimInvoiceItem'; import { ActivityType } from '@models/ActivityType'; import { amountToWords } from '@utils/currencyUtils'; import { findDealerLocally } from './dealer.service'; import path from 'path'; import fs from 'fs'; import logger from '@utils/logger'; import dayjs from 'dayjs'; export class PdfService { constructor() { } async generateInvoicePdf(requestId: string): Promise { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); try { const page = await browser.newPage(); const request = await WorkflowRequest.findByPk(requestId); const invoice = await ClaimInvoice.findOne({ where: { requestId } }); const claimDetails = await DealerClaimDetails.findOne({ where: { requestId } }); const proposalDetails = await DealerProposalDetails.findOne({ where: { requestId } }); const completionExpenses = await DealerCompletionExpense.findAll({ where: { requestId }, order: [['createdAt', 'ASC']] }); const invoiceItems = await ClaimInvoiceItem.findAll({ where: { requestId }, order: [['slNo', 'ASC']] }); const dealer = await findDealerLocally(claimDetails?.dealerCode, claimDetails?.dealerEmail); // Resolve taxationType let taxationType = 'GST'; if (claimDetails?.activityType) { const activity = await ActivityType.findOne({ where: { title: claimDetails?.activityType } }); taxationType = activity?.taxationType || (claimDetails.activityType.toLowerCase().includes('non') ? 'Non GST' : 'GST'); } if (!request || !invoice) { throw new Error('Request or Invoice not found'); } const htmlContent = this.getInvoiceHtmlTemplate({ request, invoice, claimDetails, proposalDetails, completionExpenses, invoiceItems, dealer, taxationType }); await page.setContent(htmlContent, { waitUntil: 'networkidle0' }); const pdfBuffer = Buffer.from(await page.pdf({ format: 'A4', printBackground: true, margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' } })); return pdfBuffer; } catch (error) { logger.error(`[PdfService] Error generating PDF for request ${requestId}:`, error); throw error; } finally { await browser.close(); } } private getInvoiceHtmlTemplate(data: any): string { const { request, invoice, dealer, claimDetails, completionExpenses = [], invoiceItems = [], taxationType } = data; const qrImage = invoice.qrImage ? `data:image/png;base64,${invoice.qrImage}` : ''; const logoUrl = `{{LOGO_URL}}`; const isNonGst = taxationType === 'Non GST' || taxationType === 'Non-GST'; let tableRows = ''; if (invoiceItems && invoiceItems.length > 0) { // Use persisted invoice items (matches PWC payload exactly) tableRows = invoiceItems.map((item: any) => ` ${item.slNo} ${item.description} ${!isNonGst ? `${item.hsnCd}` : ''} ${Number(item.assAmt).toFixed(2)} 0.00 ${item.unit} ${!isNonGst ? `${Number(item.assAmt).toFixed(2)}` : ''} ${!isNonGst ? ` ${Number(item.igstAmt) > 0 ? Number(item.gstRt).toFixed(2) : '0.00'} ${Number(item.igstAmt).toFixed(2)} ${Number(item.cgstAmt) > 0 ? (Number(item.gstRt) / 2).toFixed(2) : '0.00'} ${Number(item.cgstAmt).toFixed(2)} ${Number(item.sgstAmt) > 0 ? (Number(item.gstRt) / 2).toFixed(2) : '0.00'} ${Number(item.sgstAmt).toFixed(2)} ` : ''} ${Number(isNonGst ? item.assAmt : item.totItemVal).toFixed(2)} `).join(''); } else if (completionExpenses.length > 0) { // Group expenses by HSN/SAC and GST Rate for clubbed display const grouped: Record = {}; completionExpenses.forEach((item: any) => { const hsn = item.hsnCode || 'N/A'; const rate = Number(item.gstRate || 0); const key = isNonGst ? 'NON_GST' : `${hsn}_${rate}`; if (!grouped[key]) { grouped[key] = { hsn, rate, description: item.description || 'Expense', amount: 0, igstRate: item.igstRate || 0, igstAmt: 0, cgstRate: item.cgstRate || 0, cgstAmt: 0, sgstRate: item.sgstRate || 0, sgstAmt: 0, totalAmt: 0 }; } const qty = Number(item.quantity || 1); grouped[key].amount += Number(item.amount || 0) * qty; grouped[key].igstAmt += Number(item.igstAmt || 0); grouped[key].cgstAmt += Number(item.cgstAmt || 0); grouped[key].sgstAmt += Number(item.sgstAmt || 0); grouped[key].totalAmt += Number(item.totalAmt || 0); }); tableRows = Object.values(grouped).map((item: any) => ` EXPENSE ${item.description} ${!isNonGst ? `${item.hsn}` : ''} ${Number(item.amount).toFixed(2)} 0.00 EA ${!isNonGst ? `${Number(item.amount).toFixed(2)}` : ''} ${!isNonGst ? ` ${Number(item.igstRate || 0).toFixed(2)} ${Number(item.igstAmt || 0).toFixed(2)} ${Number(item.cgstRate || 0).toFixed(2)} ${Number(item.cgstAmt || 0).toFixed(2)} ${Number(item.sgstRate || 0).toFixed(2)} ${Number(item.sgstAmt || 0).toFixed(2)} ` : ''} ${Number(isNonGst ? item.amount : item.totalAmt).toFixed(2)} `).join(''); } else { tableRows = ` CLAIM ${request.title || 'Warranty Claim'} ${!isNonGst ? '998881' : ''} ${Number(invoice.taxableValue || invoice.amount || 0).toFixed(2)} 0.00 EA ${!isNonGst ? `${Number(invoice.taxableValue || invoice.amount || 0).toFixed(2)}` : ''} ${!isNonGst ? ` ${invoice.igstTotal > 0 ? (Number(invoice.igstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'} ${Number(invoice.igstTotal || 0).toFixed(2)} ${invoice.cgstTotal > 0 ? (Number(invoice.cgstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'} ${Number(invoice.cgstTotal || 0).toFixed(2)} ${invoice.sgstTotal > 0 ? (Number(invoice.sgstTotal) / Number(invoice.taxableValue || invoice.amount || 1) * 100).toFixed(2) : '0.00'} ${Number(invoice.sgstTotal || 0).toFixed(2)} ` : ''} ${Number(isNonGst ? (invoice.taxableValue || invoice.amount) : invoice.amount || 0).toFixed(2)} `; } let totalTaxable = 0; let totalTax = 0; let grandTotal = 0; if (invoiceItems && invoiceItems.length > 0) { invoiceItems.forEach((item: any) => { totalTaxable += Number(item.assAmt || 0); if (!isNonGst) { totalTax += Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0); grandTotal += Number(item.totItemVal || 0); } else { grandTotal += Number(item.assAmt || 0); } }); } else if (completionExpenses.length > 0) { completionExpenses.forEach((item: any) => { const qty = Number(item.quantity || 1); const baseAmt = Number(item.amount || 0) * qty; totalTaxable += baseAmt; if (!isNonGst) { const taxAmt = Number(item.igstAmt || 0) + Number(item.cgstAmt || 0) + Number(item.sgstAmt || 0); totalTax += taxAmt; grandTotal += (baseAmt + taxAmt); } else { grandTotal += baseAmt; } }); } else { totalTaxable = Number(invoice.taxableValue || invoice.amount || 0); if (!isNonGst) { totalTax = Number(invoice.igstTotal || 0) + Number(invoice.cgstTotal || 0) + Number(invoice.sgstTotal || 0); grandTotal = Number(invoice.amount || 0); } else { grandTotal = totalTaxable; } } const totalValueInWords = amountToWords(grandTotal); const totalTaxInWords = amountToWords(totalTax); return `
${invoice.irn ? `
IRN No : ${invoice.irn}
` : ''} ${invoice.ackNo ? `
Ack No : ${invoice.ackNo}
` : ''} ${invoice.ackDate ? `
Ack Date & Time : ${dayjs(invoice.ackDate).format('YYYY-MM-DD HH:mm:ss')}
` : ''}
${qrImage ? `` : ''}
${isNonGst ? 'CLAIM INVOICE' : 'CLAIM TAX INVOICE'}
Customer Name
Royal Enfield
Customer GSTIN
33AAACE3882D1ZZ
Customer Address
State Highway 48, Vallam Industrial Corridor, Vallakottai Chennai, Tamil Nadu - 631604
Dealer
${dealer?.dealerName || claimDetails?.dealerName || 'N/A'}
Store
${dealer?.dealerName || claimDetails?.dealerName || 'N/A'}
Supplier GSTIN
${dealer?.gstin || 'N/A'}

POS
${invoice.placeOfSupply || dealer?.state || 'Test State'}
Claim Request Number
${request.requestNumber || 'N/A'}
Claim Invoice No.
${invoice.invoiceNumber || 'N/A'}
Claim Invoice Date
${invoice.invoiceDate ? dayjs(invoice.invoiceDate).format('DD-MM-YYYY') : dayjs().format('DD-MM-YYYY')}
Last Approval Date
${invoice.generatedAt ? dayjs(invoice.generatedAt).format('DD-MM-YYYY') : dayjs().format('DD-MM-YYYY')}
${!isNonGst ? '' : ''} ${!isNonGst ? '' : ''} ${!isNonGst ? ` ` : ''} ${tableRows}
Part DescriptionHSN/SAC${isNonGst ? 'Amount' : 'Base Amount'} Disc UOMTaxable ValueIGST % IGST CGST % CGST SGST % SGSTTotal
${isNonGst ? 'TOTAL AMOUNT' : 'TAXABLE TOTAL'}:${totalTaxable.toFixed(2)}
${!isNonGst ? `
TOTAL TAX:${totalTax.toFixed(2)}
` : ''}
GRAND TOTAL:${grandTotal.toFixed(2)}
TOTAL VALUE IN WORDS: Rupees ${totalValueInWords}
${!isNonGst ? `
TOTAL TAX IN WORDS: Rupees ${totalTaxInWords}
` : ''}
`; } } export const pdfService = new PdfService();