phonepe #51
@ -43,7 +43,7 @@ export class InvoiceService {
|
|||||||
async generateInvoice(data: InvoiceData): Promise<string> {
|
async generateInvoice(data: InvoiceData): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`);
|
const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`);
|
||||||
|
|
||||||
const hasGst = data.gstNumber && data.gstNumber.length > 0;
|
const hasGst = data.gstNumber && data.gstNumber.length > 0;
|
||||||
const baseAmount = hasGst ? data.amount / 1.18 : data.amount;
|
const baseAmount = hasGst ? data.amount / 1.18 : data.amount;
|
||||||
const sgst = hasGst ? baseAmount * 0.09 : 0;
|
const sgst = hasGst ? baseAmount * 0.09 : 0;
|
||||||
@ -51,66 +51,110 @@ export class InvoiceService {
|
|||||||
|
|
||||||
const formattedDate = format(data.paymentDate, 'dd/MM/yyyy');
|
const formattedDate = format(data.paymentDate, 'dd/MM/yyyy');
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
|
|
||||||
doc.setFontSize(20);
|
doc.setFontSize(20);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text(data.businessName, 20, 20);
|
doc.text(data.businessName, 20, 20);
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text(data.address, 20, 30);
|
|
||||||
|
const maxWidth = 170;
|
||||||
if (hasGst) {
|
const lineHeight = 5;
|
||||||
doc.text(`GSTIN: ${data.gstNumber}`, 20, 40);
|
|
||||||
|
const addressLines = doc.splitTextToSize(data.address, maxWidth);
|
||||||
|
|
||||||
|
if (addressLines.length <= 2) {
|
||||||
|
for (let i = 0; i < addressLines.length; i++) {
|
||||||
|
doc.text(addressLines[i], 20, 30 + (i * lineHeight));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc.text(addressLines[0], 20, 30);
|
||||||
|
|
||||||
|
let secondLine = addressLines[1];
|
||||||
|
if (secondLine.length > 3) {
|
||||||
|
secondLine = secondLine.substring(0, secondLine.length - 3) + '...';
|
||||||
|
} else {
|
||||||
|
secondLine += '...';
|
||||||
|
}
|
||||||
|
doc.text(secondLine, 20, 35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gstYPosition = 40;
|
||||||
|
|
||||||
|
if (hasGst) {
|
||||||
|
doc.text(`GSTIN: ${data.gstNumber}`, 20, gstYPosition);
|
||||||
|
}
|
||||||
|
|
||||||
doc.setFontSize(24);
|
doc.setFontSize(24);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('RECEIPT', 190, 20, { align: 'right' });
|
doc.text('RECEIPT', 190, 20, { align: 'right' });
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' });
|
doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' });
|
||||||
doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' });
|
doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' });
|
||||||
|
|
||||||
doc.line(20, 45, 190, 45);
|
doc.line(20, 45, 190, 45);
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
|
const receiptToBoxX = 15;
|
||||||
|
const receiptToBoxY = 55;
|
||||||
|
const receiptToBoxWidth = 175;
|
||||||
|
const receiptToBoxHeight = 45;
|
||||||
|
|
||||||
|
doc.setDrawColor(0, 0, 0);
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.rect(receiptToBoxX, receiptToBoxY, receiptToBoxWidth, receiptToBoxHeight);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('Receipt To:', 20, 60);
|
doc.text('Receipt To:', 20, 60);
|
||||||
|
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text(data.customerName, 20, 70);
|
doc.text(data.customerName, 20, 70);
|
||||||
doc.text(`Phone: ${data.phoneNumber}`, 20, 80);
|
doc.text(`Phone: ${data.phoneNumber}`, 20, 80);
|
||||||
doc.text(`Email: ${data.email}`, 20, 90);
|
doc.text(`Email: ${data.email}`, 20, 90);
|
||||||
|
|
||||||
autoTable(doc,{
|
autoTable(doc, {
|
||||||
startY: 110,
|
startY: 110,
|
||||||
head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']],
|
head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']],
|
||||||
body: [
|
body: [
|
||||||
['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)]
|
['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)]
|
||||||
],
|
],
|
||||||
headStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: 'bold' },
|
headStyles: {
|
||||||
styles: { halign: 'center' },
|
fillColor: [220, 220, 220],
|
||||||
|
textColor: [0, 0, 0],
|
||||||
|
fontStyle: 'bold',
|
||||||
|
lineWidth: 0.5,
|
||||||
|
lineColor: [0, 0, 0]
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
halign: 'center',
|
||||||
|
lineWidth: 0.5,
|
||||||
|
lineColor: [0, 0, 0]
|
||||||
|
},
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
0: { halign: 'center', cellWidth: 20 },
|
0: { halign: 'center', cellWidth: 20 },
|
||||||
1: { halign: 'left' },
|
1: { halign: 'left' },
|
||||||
2: { halign: 'center', cellWidth: 40 },
|
2: { halign: 'center', cellWidth: 40 },
|
||||||
3: { halign: 'right', cellWidth: 40 }
|
3: { halign: 'right', cellWidth: 40 }
|
||||||
}
|
},
|
||||||
|
theme: 'grid',
|
||||||
|
tableLineWidth: 0.5,
|
||||||
|
tableLineColor: [0, 0, 0],
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalY = (doc as any).lastAutoTable.finalY + 20;
|
const finalY = (doc as any).lastAutoTable.finalY + 20;
|
||||||
|
|
||||||
if (hasGst) {
|
if (hasGst) {
|
||||||
doc.text('Taxable Amount:', 150, finalY, { align: 'right' });
|
doc.text('Taxable Amount:', 150, finalY, { align: 'right' });
|
||||||
doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' });
|
doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' });
|
||||||
|
|
||||||
doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' });
|
doc.text('SGST (9%):', 150, finalY + 10, { align: 'right' });
|
||||||
doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' });
|
doc.text(`${sgst.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' });
|
||||||
|
|
||||||
doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' });
|
doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' });
|
||||||
doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' });
|
doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' });
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('Total Amount:', 150, finalY + 30, { align: 'right' });
|
doc.text('Total Amount:', 150, finalY + 30, { align: 'right' });
|
||||||
doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' });
|
doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY + 30, { align: 'right' });
|
||||||
@ -119,27 +163,38 @@ export class InvoiceService {
|
|||||||
doc.text('Total Amount:', 150, finalY, { align: 'right' });
|
doc.text('Total Amount:', 150, finalY, { align: 'right' });
|
||||||
doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' });
|
doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentY = hasGst ? finalY + 50 : finalY + 20;
|
const paymentY = hasGst ? finalY + 50 : finalY + 20;
|
||||||
|
|
||||||
|
doc.line(20, 45, 190, 45);
|
||||||
|
|
||||||
|
const boxX = 15;
|
||||||
|
const boxY = paymentY - 5;
|
||||||
|
const boxWidth = 175;
|
||||||
|
const boxHeight = 45;
|
||||||
|
|
||||||
|
doc.setDrawColor(0, 0, 0);
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.rect(boxX, boxY, boxWidth, boxHeight);
|
||||||
|
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text('Payment Information:', 20, paymentY);
|
doc.text('Payment Information:', 20, paymentY);
|
||||||
|
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10);
|
doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10);
|
||||||
doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20);
|
doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20);
|
||||||
doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30);
|
doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30);
|
||||||
|
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
doc.setFont('helvetica', 'italic');
|
doc.setFont('helvetica', 'italic');
|
||||||
doc.text('Thank you for your business!', 105, 270, { align: 'center' });
|
doc.text('Thank you for your business!', 105, 270, { align: 'center' });
|
||||||
|
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' });
|
doc.text('This is a computer-generated receipt and does not require a signature.', 105, 280, { align: 'center' });
|
||||||
|
|
||||||
fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer')));
|
fs.writeFileSync(tempFilePath, Buffer.from(doc.output('arraybuffer')));
|
||||||
|
|
||||||
const invoicePath = `invoices/${data.invoiceNumber}.pdf`;
|
const invoicePath = `invoices/${data.invoiceNumber}.pdf`;
|
||||||
const bucket = admin.storage().bucket();
|
const bucket = admin.storage().bucket();
|
||||||
await bucket.upload(tempFilePath, {
|
await bucket.upload(tempFilePath, {
|
||||||
@ -148,9 +203,9 @@ export class InvoiceService {
|
|||||||
contentType: 'application/pdf',
|
contentType: 'application/pdf',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.unlinkSync(tempFilePath);
|
fs.unlinkSync(tempFilePath);
|
||||||
|
|
||||||
return invoicePath;
|
return invoicePath;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Error generating invoice:', error);
|
logger.error('Error generating invoice:', error);
|
||||||
@ -162,15 +217,15 @@ export class InvoiceService {
|
|||||||
try {
|
try {
|
||||||
const bucket = admin.storage().bucket();
|
const bucket = admin.storage().bucket();
|
||||||
const file = bucket.file(invoicePath);
|
const file = bucket.file(invoicePath);
|
||||||
|
|
||||||
const expirationMs = 7 * 24 * 60 * 60 * 1000;
|
const expirationMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const [signedUrl] = await file.getSignedUrl({
|
const [signedUrl] = await file.getSignedUrl({
|
||||||
action: 'read',
|
action: 'read',
|
||||||
expires: Date.now() + expirationMs,
|
expires: Date.now() + expirationMs,
|
||||||
responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`,
|
responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return signedUrl;
|
return signedUrl;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Error getting invoice download URL:', error);
|
logger.error('Error getting invoice download URL:', error);
|
||||||
@ -183,37 +238,37 @@ export class InvoiceService {
|
|||||||
const membershipPaymentsRef = admin.firestore()
|
const membershipPaymentsRef = admin.firestore()
|
||||||
.collection('membership_payments')
|
.collection('membership_payments')
|
||||||
.doc(membershipId);
|
.doc(membershipId);
|
||||||
|
|
||||||
const docSnapshot = await membershipPaymentsRef.get();
|
const docSnapshot = await membershipPaymentsRef.get();
|
||||||
|
|
||||||
if (!docSnapshot.exists) {
|
if (!docSnapshot.exists) {
|
||||||
logger.error(`No membership payments found for membershipId: ${membershipId}`);
|
logger.error(`No membership payments found for membershipId: ${membershipId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = docSnapshot.data();
|
const data = docSnapshot.data();
|
||||||
const paymentsData = data?.payments || [];
|
const paymentsData = data?.payments || [];
|
||||||
|
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let i = 0; i < paymentsData.length; i++) {
|
for (let i = 0; i < paymentsData.length; i++) {
|
||||||
if (paymentsData[i].referenceNumber === paymentId ||
|
if (paymentsData[i].referenceNumber === paymentId ||
|
||||||
paymentsData[i].transactionId === paymentId) {
|
paymentsData[i].transactionId === paymentId) {
|
||||||
paymentsData[i].invoicePath = invoicePath;
|
paymentsData[i].invoicePath = invoicePath;
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`);
|
logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await membershipPaymentsRef.update({
|
await membershipPaymentsRef.update({
|
||||||
'payments': paymentsData,
|
'payments': paymentsData,
|
||||||
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
|
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Successfully updated invoice path for payment: ${paymentId}`);
|
logger.info(`Successfully updated invoice path for payment: ${paymentId}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -225,11 +280,11 @@ export class InvoiceService {
|
|||||||
async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise<boolean> {
|
async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
|
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
|
||||||
|
|
||||||
const formattedDate = emailOptions.additionalData?.paymentDate
|
const formattedDate = emailOptions.additionalData?.paymentDate
|
||||||
? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB')
|
? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB')
|
||||||
: new Date().toLocaleDateString('en-GB');
|
: new Date().toLocaleDateString('en-GB');
|
||||||
|
|
||||||
const emailHtml = emailOptions.customHtml || `
|
const emailHtml = emailOptions.customHtml || `
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
@ -251,7 +306,7 @@ export class InvoiceService {
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await sendEmailWithAttachmentUtil(
|
await sendEmailWithAttachmentUtil(
|
||||||
emailOptions.recipientEmail,
|
emailOptions.recipientEmail,
|
||||||
emailOptions.subject || 'Your Fitlien Membership Invoice',
|
emailOptions.subject || 'Your Fitlien Membership Invoice',
|
||||||
@ -259,7 +314,7 @@ export class InvoiceService {
|
|||||||
downloadUrl,
|
downloadUrl,
|
||||||
`Invoice_${path.basename(invoicePath)}`
|
`Invoice_${path.basename(invoicePath)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`);
|
logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -269,7 +324,7 @@ export class InvoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processInvoice(
|
async processInvoice(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
paymentId: string,
|
paymentId: string,
|
||||||
invoiceData: InvoiceData,
|
invoiceData: InvoiceData,
|
||||||
emailOptions?: EmailOptions
|
emailOptions?: EmailOptions
|
||||||
@ -282,9 +337,9 @@ export class InvoiceService {
|
|||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const invoicePath = await this.generateInvoice(invoiceData);
|
const invoicePath = await this.generateInvoice(invoiceData);
|
||||||
|
|
||||||
const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath);
|
const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath);
|
||||||
|
|
||||||
if (!updateSuccess) {
|
if (!updateSuccess) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -293,14 +348,14 @@ export class InvoiceService {
|
|||||||
error: 'Failed to update payment with invoice path'
|
error: 'Failed to update payment with invoice path'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
|
const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath);
|
||||||
|
|
||||||
let emailSent = false;
|
let emailSent = false;
|
||||||
if (emailOptions && emailOptions.recipientEmail) {
|
if (emailOptions && emailOptions.recipientEmail) {
|
||||||
emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions);
|
emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
invoicePath,
|
invoicePath,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user