phonepe #56
| @ -43,7 +43,7 @@ export class InvoiceService { | ||||
|   async generateInvoice(data: InvoiceData): Promise<string> { | ||||
|     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<boolean> { | ||||
|     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 || ` | ||||
|         <html> | ||||
|           <body> | ||||
| @ -251,7 +306,7 @@ export class InvoiceService { | ||||
|           </body> | ||||
|         </html> | ||||
|       `;
 | ||||
|        | ||||
| 
 | ||||
|       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, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user