From d3cc6b3710f23438b37c38b0b48bdb5d0309f306 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Wed, 21 May 2025 11:31:44 +0000 Subject: [PATCH] phonepe (#43) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/43 --- .../payments/phonepe/invoice/directInvoice.ts | 2 - .../payments/phonepe/invoice/getInvoiceUrl.ts | 1 - .../phonepe/invoice/invoiceService.ts | 165 ++++++++++++------ functions/src/payments/phonepe/paymentData.ts | 16 +- 4 files changed, 114 insertions(+), 70 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/directInvoice.ts b/functions/src/payments/phonepe/invoice/directInvoice.ts index 93969f5..5df728d 100644 --- a/functions/src/payments/phonepe/invoice/directInvoice.ts +++ b/functions/src/payments/phonepe/invoice/directInvoice.ts @@ -65,11 +65,9 @@ export const directGenerateInvoice = onRequest({ paymentMethod: paymentMethod || 'Online' }; - // Generate the invoice without updating any payment records const invoicePath = await invoiceService.generateInvoice(invoiceData); const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); - // Send email if requested let emailSent = false; if (sendEmail && email) { emailSent = await invoiceService.sendInvoiceEmail(invoicePath, { diff --git a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts index 038a14c..df433f4 100644 --- a/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts +++ b/functions/src/payments/phonepe/invoice/getInvoiceUrl.ts @@ -35,7 +35,6 @@ export const getInvoiceUrl = onRequest({ return; } - // Get a download URL for the invoice const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath as string); response.json({ diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 8c3d491..bcc78a4 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -43,7 +43,7 @@ export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); - + const hasGst = data.gstNumber && data.gstNumber.length > 0; const baseAmount = hasGst ? data.amount / 1.18 : data.amount; const sgst = hasGst ? baseAmount * 0.09 : 0; @@ -51,66 +51,110 @@ export class InvoiceService { const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); const doc = new jsPDF(); - + doc.setFontSize(20); doc.setFont('helvetica', 'bold'); doc.text(data.businessName, 20, 20); - + doc.setFontSize(12); doc.setFont('helvetica', 'normal'); - doc.text(data.address, 20, 30); - - if (hasGst) { - doc.text(`GSTIN: ${data.gstNumber}`, 20, 40); + + const maxWidth = 170; + const lineHeight = 5; + + 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.setFont('helvetica', 'bold'); doc.text('RECEIPT', 190, 20, { align: 'right' }); - + doc.setFontSize(12); doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); - + doc.line(20, 45, 190, 45); - + 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.text('Receipt To:', 20, 60); - + doc.setFont('helvetica', 'normal'); doc.text(data.customerName, 20, 70); doc.text(`Phone: ${data.phoneNumber}`, 20, 80); doc.text(`Email: ${data.email}`, 20, 90); - - autoTable(doc,{ + + autoTable(doc, { startY: 110, head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], body: [ ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], - headStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: 'bold' }, - styles: { halign: 'center' }, + headStyles: { + 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: { 0: { halign: 'center', cellWidth: 20 }, 1: { halign: 'left' }, 2: { halign: 'center', cellWidth: 40 }, 3: { halign: 'right', cellWidth: 40 } - } + }, + theme: 'grid', + tableLineWidth: 0.5, + tableLineColor: [0, 0, 0], + }); - + const finalY = (doc as any).lastAutoTable.finalY + 20; - + if (hasGst) { doc.text('Taxable Amount:', 150, 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.toFixed(2)} INR`, 190, finalY + 10, { align: 'right' }); - + doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); - + doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, 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(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); } - + 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.text('Payment Information:', 20, paymentY); - + doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 20, paymentY + 10); doc.text(`Payment Method: ${data.paymentMethod}`, 20, paymentY + 20); doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); - + doc.setFontSize(12); doc.setFont('helvetica', 'italic'); doc.text('Thank you for your business!', 105, 270, { align: 'center' }); - + doc.setFontSize(10); doc.setFont('helvetica', 'normal'); 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'))); - + const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { @@ -148,9 +203,9 @@ export class InvoiceService { contentType: 'application/pdf', }, }); - + fs.unlinkSync(tempFilePath); - + return invoicePath; } catch (error: any) { logger.error('Error generating invoice:', error); @@ -162,15 +217,15 @@ export class InvoiceService { try { const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); - - const expirationMs = 7 * 24 * 60 * 60 * 1000; - + + const expirationMs = 7 * 24 * 60 * 60 * 1000; + const [signedUrl] = await file.getSignedUrl({ action: 'read', expires: Date.now() + expirationMs, responseDisposition: `attachment; filename="${path.basename(invoicePath)}"`, }); - + return signedUrl; } catch (error: any) { logger.error('Error getting invoice download URL:', error); @@ -183,37 +238,37 @@ export class InvoiceService { const membershipPaymentsRef = admin.firestore() .collection('membership_payments') .doc(membershipId); - + const docSnapshot = await membershipPaymentsRef.get(); - + if (!docSnapshot.exists) { logger.error(`No membership payments found for membershipId: ${membershipId}`); return false; } - + const data = docSnapshot.data(); const paymentsData = data?.payments || []; - + let found = false; for (let i = 0; i < paymentsData.length; i++) { - if (paymentsData[i].referenceNumber === paymentId || - paymentsData[i].transactionId === paymentId) { + if (paymentsData[i].referenceNumber === paymentId || + paymentsData[i].transactionId === paymentId) { paymentsData[i].invoicePath = invoicePath; found = true; break; } } - + if (!found) { logger.error(`No payment found with ID: ${paymentId} in membership: ${membershipId}`); return false; } - + await membershipPaymentsRef.update({ 'payments': paymentsData, 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), }); - + logger.info(`Successfully updated invoice path for payment: ${paymentId}`); return true; } catch (error: any) { @@ -225,11 +280,11 @@ export class InvoiceService { async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { try { 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().toLocaleDateString('en-GB'); - + const emailHtml = emailOptions.customHtml || ` @@ -251,7 +306,7 @@ export class InvoiceService { `; - + await sendEmailWithAttachmentUtil( emailOptions.recipientEmail, emailOptions.subject || 'Your Fitlien Membership Invoice', @@ -259,7 +314,7 @@ export class InvoiceService { downloadUrl, `Invoice_${path.basename(invoicePath)}` ); - + logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`); return true; } catch (error: any) { @@ -269,7 +324,7 @@ export class InvoiceService { } async processInvoice( - membershipId: string, + membershipId: string, paymentId: string, invoiceData: InvoiceData, emailOptions?: EmailOptions @@ -282,9 +337,9 @@ export class InvoiceService { }> { try { const invoicePath = await this.generateInvoice(invoiceData); - + const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); - + if (!updateSuccess) { return { success: false, @@ -293,14 +348,14 @@ export class InvoiceService { error: 'Failed to update payment with invoice path' }; } - + const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); - + let emailSent = false; if (emailOptions && emailOptions.recipientEmail) { emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); } - + return { success: true, invoicePath, diff --git a/functions/src/payments/phonepe/paymentData.ts b/functions/src/payments/phonepe/paymentData.ts index b6eca48..13d7536 100644 --- a/functions/src/payments/phonepe/paymentData.ts +++ b/functions/src/payments/phonepe/paymentData.ts @@ -3,7 +3,6 @@ import { getAdmin, getLogger } from "../../shared/config"; const admin = getAdmin(); const logger = getLogger(); -// Define an interface for the payment data to avoid type errors interface PaymentData { id: string; date: string; @@ -14,7 +13,7 @@ interface PaymentData { discount: any; transactionId: string; createdAt: Date; - invoicePath?: string; // Make this optional + invoicePath?: string; } export async function updatePaymentDataAfterSuccess( @@ -24,7 +23,6 @@ export async function updatePaymentDataAfterSuccess( invoicePath?: string ): Promise { try { - // Get the payment order from Firestore const orderQuery = await admin.firestore() .collection('payment_orders') .where('merchantOrderId', '==', merchantOrderId) @@ -39,19 +37,17 @@ export async function updatePaymentDataAfterSuccess( const orderDoc = orderQuery.docs[0]; const orderData = orderDoc.data(); - // Extract membership ID from metaInfo const membershipId = orderData.metaInfo?.membershipId; if (!membershipId) { logger.error(`No membershipId found in metaInfo for order: ${merchantOrderId}`); return false; } - const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); // DD-MM-YYYY format + const isoDate = new Date().toLocaleDateString('en-GB').split('/').join('-'); const dateTimestamp = admin.firestore.Timestamp.now(); - // Create payment data object with proper typing const paymentData: PaymentData = { - id: admin.firestore().collection('_').doc().id, // Generate a UUID + id: admin.firestore().collection('_').doc().id, date: isoDate, dateTimestamp: dateTimestamp, amount: orderData.amount, @@ -62,19 +58,16 @@ export async function updatePaymentDataAfterSuccess( createdAt: new Date() }; - // Add invoice path if provided if (invoicePath) { paymentData.invoicePath = invoicePath; } - // Get reference to membership payments document const membershipPaymentsRef = admin.firestore() .collection('membership_payments') .doc(membershipId); const docSnapshot = await membershipPaymentsRef.get(); - // Update or create the membership payments document if (docSnapshot.exists) { await membershipPaymentsRef.update({ 'payments': admin.firestore.FieldValue.arrayUnion(paymentData), @@ -92,7 +85,6 @@ export async function updatePaymentDataAfterSuccess( }); } - // Update membership status await updateMembershipStatus(membershipId, orderData.userId); logger.info(`Successfully updated payment data for membership: ${membershipId}`); @@ -118,7 +110,7 @@ async function updateMembershipStatus(membershipId: string, userId: string): Pro .collection('memberships') .doc(membershipId) .update({ - 'status': 'ACTIVE', // Assuming this matches your InvitationStatus.active + 'status': 'ACTIVE', 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), });