import { getAdmin, getLogger } from "../../../shared/config"; 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"; import * as pdfMake from 'pdfmake/build/pdfmake'; import * as pdfFonts from 'pdfmake/build/vfs_fonts'; (pdfMake as any).vfs = (pdfFonts as any).pdfMake ? (pdfFonts as any).pdfMake.vfs : pdfFonts.vfs; 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 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; const formattedDate = format(data.paymentDate, 'dd/MM/yyyy'); const docDefinition: any = { content: [ { columns: [ [ { text: data.businessName, style: 'businessName' }, { text: data.address, style: 'businessAddress' }, hasGst ? { text: `GSTIN: ${data.gstNumber}`, style: 'businessDetails' } : {} ], [ { text: 'RECEIPT', style: 'invoiceTitle', alignment: 'right' }, { text: `Receipt #: ${data.invoiceNumber}`, style: 'invoiceDetails', alignment: 'right' }, { text: `Date: ${formattedDate}`, style: 'invoiceDetails', alignment: 'right' } ] ] }, { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 0.5 }] }, { text: '', margin: [0, 10] }, { style: 'customerBox', table: { widths: ['*'], body: [ [ { stack: [ { text: 'Receipt To:', style: 'customerTitle' }, { text: data.customerName, style: 'customerDetails' }, { text: `Phone: ${data.phoneNumber}`, style: 'customerDetails' }, { text: `Email: ${data.email}`, style: 'customerDetails' } ], margin: [10, 10] } ] ] }, layout: 'lightHorizontalLines' }, { text: '', margin: [0, 10] }, { table: { headerRows: 1, widths: [30, '*', 80, 100], body: [ [ { text: 'No.', style: 'tableHeader', alignment: 'center' }, { text: 'Description', style: 'tableHeader' }, { text: 'HSN/SAC', style: 'tableHeader', alignment: 'center' }, { text: 'Amount (INR)', style: 'tableHeader', alignment: 'right' } ], [ { text: '1', alignment: 'center' }, { text: `${data.planName} Subscription` }, { text: '999723', alignment: 'center' }, { text: baseAmount.toFixed(2), alignment: 'right' } ] ] } }, { text: '', margin: [0, 10] }, { columns: [ { width: '*', text: '' }, { width: 'auto', table: { widths: [100, 100], body: hasGst ? [ [ { text: 'Taxable Amount:', alignment: 'right' }, { text: `${baseAmount.toFixed(2)} INR`, alignment: 'right' } ], [ { text: 'SGST (9%):', alignment: 'right' }, { text: `${sgst.toFixed(2)} INR`, alignment: 'right' } ], [ { text: 'CGST (9%):', alignment: 'right' }, { text: `${cgst.toFixed(2)} INR`, alignment: 'right' } ], [ { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } ] ] : [ [ { text: 'Total Amount:', style: 'totalAmount', alignment: 'right' }, { text: `${data.amount.toFixed(2)} INR`, style: 'totalAmount', alignment: 'right' } ] ] }, layout: { hLineWidth: function(i: number, node: any) { return (i === node.table.body.length - 1) ? 0.5 : 0; }, vLineWidth: function() { return 0; } } } ] }, { text: '', margin: [0, 20] }, { style: 'paymentBox', table: { widths: ['*'], body: [ [ { stack: [ { text: 'Payment Information:', style: 'paymentTitle' }, { text: `Transaction ID: ${data.transactionId}`, style: 'paymentDetails' }, { text: `Payment Method: ${data.paymentMethod}`, style: 'paymentDetails' }, { text: `Payment Date: ${formattedDate}`, style: 'paymentDetails' } ], margin: [10, 10] } ] ] }, layout: 'lightHorizontalLines' }, { text: '', margin: [0, 20] }, { text: 'Thank you for your business!', style: 'footer', alignment: 'center' }, { text: 'This is a computer-generated receipt and does not require a signature.', style: 'disclaimer', alignment: 'center' } ], styles: { businessName: { fontSize: 20, bold: true, margin: [0, 0, 0, 5] }, businessAddress: { fontSize: 12, margin: [0, 0, 0, 5] }, businessDetails: { fontSize: 12 }, invoiceTitle: { fontSize: 24, bold: true }, invoiceDetails: { fontSize: 12, margin: [0, 5, 0, 0] }, customerBox: { margin: [0, 10, 0, 10] }, customerTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 5] }, customerDetails: { fontSize: 12, margin: [0, 2, 0, 0] }, tableHeader: { fontSize: 12, bold: true, margin: [0, 5, 0, 5] }, totalAmount: { fontSize: 12, bold: true }, paymentBox: { margin: [0, 10, 0, 10] }, paymentTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 5] }, paymentDetails: { fontSize: 12, margin: [0, 2, 0, 0] }, footer: { fontSize: 12, italics: true, margin: [0, 0, 0, 5] }, disclaimer: { fontSize: 10 } }, defaultStyle: { font: 'Helvetica' } }; const pdfDoc = pdfMake.createPdf(docDefinition); await new Promise((resolve, reject) => { pdfDoc.getBuffer((buffer) => { fs.writeFile(tempFilePath, buffer, (err) => { if (err) reject(err); else resolve(); }); }); }); const invoicePath = `invoices/${data.invoiceNumber}.pdf`; const bucket = admin.storage().bucket(); await bucket.upload(tempFilePath, { destination: invoicePath, metadata: { contentType: 'application/pdf', }, }); fs.unlinkSync(tempFilePath); 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 { const bucket = admin.storage().bucket(); const file = bucket.file(invoicePath); 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); 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 || []; 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 { const downloadUrl = await this.getInvoiceDownloadUrl(invoicePath); const formattedDate = emailOptions.additionalData?.paymentDate ? new Date(emailOptions.additionalData.paymentDate).toLocaleDateString('en-GB') : new Date().toLocaleDateString('en-GB'); 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

`; 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 { const invoicePath = await this.generateInvoice(invoiceData); const updateSuccess = await this.updateInvoicePath(membershipId, paymentId, invoicePath); if (!updateSuccess) { return { success: false, invoicePath, emailSent: false, 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, downloadUrl, emailSent }; } catch (error: any) { logger.error('Error processing invoice:', error); return { success: false, emailSent: false, error: error.message }; } } }