import { getAdmin, getLogger } from "../../../shared/config"; import PDFDocument from 'pdfkit'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; const admin = getAdmin(); const logger = getLogger(); export interface InvoiceData { invoiceNumber: string; businessName: string; address: string; gstNumber?: string; customerName: string; phoneNumber: string; email: string; planName: string; amount: number; transactionId: string; paymentDate: Date; paymentMethod: string; } export interface EmailOptions { recipientEmail: string; recipientName?: string; subject?: string; customHtml?: string; additionalData?: { gymName?: string; planName?: string; amount?: number; transactionId?: string; paymentDate?: Date; paymentMethod?: string; }; } export class InvoiceService { async generateInvoice(data: InvoiceData): Promise { try { const tempFilePath = path.join(os.tmpdir(), `invoice_${data.invoiceNumber}.pdf`); const doc = new PDFDocument({ margin: 50 }); // Create a write stream to the temporary file const writeStream = fs.createWriteStream(tempFilePath); doc.pipe(writeStream); // Check if GST is applicable const hasGst = data.gstNumber && data.gstNumber.length > 0; const baseAmount = hasGst ? data.amount / 1.18 : data.amount; const sgst = hasGst ? baseAmount * 0.09 : 0; const cgst = hasGst ? baseAmount * 0.09 : 0; // Add business details doc.fontSize(20).font('Helvetica-Bold').text(data.businessName, 50, 50); doc.fontSize(12).font('Helvetica').text(data.address, 50, 75, { width: 250 }); if (hasGst) { doc.text(`GSTIN: ${data.gstNumber}`, 50, 115); } // Add invoice title and details doc.fontSize(24).font('Helvetica-Bold').text('RECEIPT', 400, 50, { align: 'right' }); doc.fontSize(12).font('Helvetica').text(`Receipt #: ${data.invoiceNumber}`, 400, 80, { align: 'right' }); doc.text(`Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 400, 95, { align: 'right' }); // Add customer details doc.rect(50, 150, 500, 80).stroke(); doc.fontSize(12).font('Helvetica-Bold').text('Receipt To:', 60, 160); doc.fontSize(12).font('Helvetica').text(data.customerName, 60, 175); doc.text(`Phone: ${data.phoneNumber}`, 60, 190); doc.text(`Email: ${data.email}`, 60, 205); // Add table header const tableTop = 260; doc.rect(50, tableTop, 500, 30).stroke(); doc.fontSize(12).font('Helvetica-Bold').text('No.', 60, tableTop + 10, { width: 30, align: 'center' }); doc.text('Description', 100, tableTop + 10, { width: 250 }); doc.text('HSN/SAC', 350, tableTop + 10, { width: 80, align: 'center' }); doc.text('Amount (INR)', 430, tableTop + 10, { width: 100, align: 'right' }); // Add table row const rowTop = tableTop + 30; doc.rect(50, rowTop, 500, 30).stroke(); doc.fontSize(12).font('Helvetica').text('1', 60, rowTop + 10, { width: 30, align: 'center' }); doc.text(`${data.planName} Subscription`, 100, rowTop + 10, { width: 250 }); doc.text('999723', 350, rowTop + 10, { width: 80, align: 'center' }); doc.text(baseAmount.toFixed(2), 430, rowTop + 10, { width: 100, align: 'right' }); // Add totals let currentY = rowTop + 50; if (hasGst) { doc.fontSize(12).font('Helvetica').text('Taxable Amount:', 350, currentY, { width: 100, align: 'right' }); doc.text(`${baseAmount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); currentY += 20; doc.text('SGST (9%):', 350, currentY, { width: 100, align: 'right' }); doc.text(`${sgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); currentY += 20; doc.text('CGST (9%):', 350, currentY, { width: 100, align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); currentY += 20; } doc.moveTo(350, currentY).lineTo(550, currentY).stroke(); currentY += 10; doc.fontSize(12).font('Helvetica-Bold').text('Total Amount:', 350, currentY, { width: 100, align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 450, currentY, { width: 100, align: 'right' }); // Add payment information currentY += 40; doc.rect(50, currentY, 500, 80).stroke(); doc.fontSize(12).font('Helvetica-Bold').text('Payment Information:', 60, currentY + 10); doc.fontSize(12).font('Helvetica').text(`Transaction ID: ${data.transactionId}`, 60, currentY + 30); doc.text(`Payment Method: ${data.paymentMethod}`, 60, currentY + 45); doc.text(`Payment Date: ${format(data.paymentDate, 'dd/MM/yyyy')}`, 60, currentY + 60); // Add footer currentY += 100; doc.fontSize(12).font('Helvetica-Oblique').text('Thank you for your business!', 50, currentY, { align: 'center' }); doc.fontSize(10).font('Helvetica').text('This is a computer-generated receipt and does not require a signature.', 50, currentY + 20, { align: 'center' }); // Finalize the PDF doc.end(); // Wait for the file to be written await new Promise((resolve, reject) => { writeStream.on('finish', () => resolve()); writeStream.on('error', reject); }); // Upload to Firebase Storage using admin SDK const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { destination: invoicePath, metadata: { contentType: 'application/pdf', }, }); // Clean up the temporary file fs.unlinkSync(tempFilePath); // Return the storage path return invoicePath; } catch (error: any) { logger.error('Error generating invoice:', error); throw new Error(`Failed to generate invoice: ${error.message}`); } } async getInvoiceDownloadUrl(invoicePath: string): Promise { try { // Using admin SDK to generate a signed URL const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); const expirationMs = 7 * 24 * 60 * 60 * 1000; // 7 days 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); throw new Error(`Failed to get invoice download URL: ${error.message}`); } } async updateInvoicePath(membershipId: string, paymentId: string, invoicePath: string): Promise { try { 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 || []; // Find the payment by referenceNumber or transactionId let found = false; for (let i = 0; i < paymentsData.length; i++) { 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) { logger.error('Error updating payment with invoice path:', error); return false; } } async sendInvoiceEmail(invoicePath: string, emailOptions: EmailOptions): Promise { try { // Get the download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); // Format the date const formattedDate = emailOptions.additionalData?.paymentDate ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') : new Date().toLocaleDateString('en-GB'); // Create email HTML content if not provided const emailHtml = emailOptions.customHtml || `

Thank you for your payment

Dear ${emailOptions.recipientName || 'Valued Customer'},

Thank you for your payment. Your membership has been successfully activated.

Please find attached your invoice for the payment.

Membership Details:

  • Gym: ${emailOptions.additionalData?.gymName || 'Fitlien'}
  • Plan: ${emailOptions.additionalData?.planName || 'Membership'}
  • Amount: ₹${emailOptions.additionalData?.amount || '0'}
  • Transaction ID: ${emailOptions.additionalData?.transactionId || 'N/A'}
  • Date: ${formattedDate}
  • Payment Method: ${emailOptions.additionalData?.paymentMethod || 'Online'}

If you have any questions, please contact us.

Regards,
Fitlien Team

`; // Send the email with attachment await sendEmailWithAttachmentUtil( emailOptions.recipientEmail, emailOptions.subject || 'Your Fitlien Membership Invoice', emailHtml, downloadUrl, `Invoice_${path.basename(invoicePath)}` ); logger.info(`Invoice email sent to ${emailOptions.recipientEmail}`); return true; } catch (error: any) { logger.error('Error sending invoice email:', error); return false; } } async processInvoice( membershipId: string, paymentId: string, invoiceData: InvoiceData, emailOptions?: EmailOptions ): Promise<{ success: boolean; invoicePath?: string; downloadUrl?: string; emailSent: boolean; error?: string; }> { try { // Generate the invoice const invoicePath = await this.generateInvoice(invoiceData); // Update the payment record with the invoice path const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); if (!updateSuccess) { return { success: false, invoicePath, emailSent: false, error: 'Failed to update payment with invoice path' }; } // Get a download URL for the invoice const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); // Send email if email options are provided let emailSent = false; if (emailOptions && emailOptions.recipientEmail) { emailSent = await this.sendInvoiceEmail(invoicePath, emailOptions); } return { success: true, invoicePath, downloadUrl, emailSent }; } catch (error: any) { logger.error('Error processing invoice:', error); return { success: false, emailSent: false, error: error.message }; } } }