From de9ea73dcdc6ff37d08a2c8d35a6184ba133bff6 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 15:45:20 +0530 Subject: [PATCH 01/26] Update firebase.json --- firebase.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase.json b/firebase.json index ecc3d3c..6920866 100644 --- a/firebase.json +++ b/firebase.json @@ -7,7 +7,7 @@ { "source": "functions", "codebase": "default", - "timeoutSeconds": 300, + "timeoutSeconds": 360, "ignore": [ "node_modules", ".git", -- 2.43.0 From 95492d304fc8faadf993758ac30816f85ac55121 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 16:08:33 +0530 Subject: [PATCH 02/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 1694817..90a0a43 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -105,7 +105,15 @@ export const phonePeWebhook = onRequest({ logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } - if (payload.state === 'COMPLETED') { + logger.info(`Checking payment state`, { + state: payload.state, + stateType: typeof payload.state, + stateLength: payload.state ? payload.state.length : 0, + stateUpperCase: payload.state ? payload.state.toUpperCase() : null, + stateComparison: payload.state === 'COMPLETED' + }); + + if (payload.state && payload.state.trim().toUpperCase() === 'COMPLETED') { try { logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`); -- 2.43.0 From 4ce107c4aff6b1c0bd81fd813055ac370350b9e9 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 16:16:18 +0530 Subject: [PATCH 03/26] Update index.ts --- functions/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/functions/src/index.ts b/functions/src/index.ts index 19fa4e0..8bc48e9 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,3 +6,6 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; +import { setGlobalOptions } from "firebase-functions/v2"; + +setGlobalOptions({ timeoutSeconds: 300 }); \ No newline at end of file -- 2.43.0 From 66a65aed386efe885680bf7a0269196b6fee0743 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 16:16:21 +0530 Subject: [PATCH 04/26] Update firebase.json --- firebase.json | 1 - 1 file changed, 1 deletion(-) diff --git a/firebase.json b/firebase.json index 6920866..dfc4226 100644 --- a/firebase.json +++ b/firebase.json @@ -7,7 +7,6 @@ { "source": "functions", "codebase": "default", - "timeoutSeconds": 360, "ignore": [ "node_modules", ".git", -- 2.43.0 From 5d138e5937160633fd5702f18445f10367ec595e Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 16:21:38 +0530 Subject: [PATCH 05/26] Update index.ts --- functions/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 8bc48e9..57a12f6 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,4 +8,4 @@ export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; import { setGlobalOptions } from "firebase-functions/v2"; -setGlobalOptions({ timeoutSeconds: 300 }); \ No newline at end of file +setGlobalOptions({ timeoutSeconds: 400 }); \ No newline at end of file -- 2.43.0 From c252ba8620cc20e50d0b9a8db179d5de0a5f76d1 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 16:27:05 +0530 Subject: [PATCH 06/26] timeout changes --- firebase.json | 1 + functions/src/index.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/firebase.json b/firebase.json index dfc4226..608886c 100644 --- a/firebase.json +++ b/firebase.json @@ -7,6 +7,7 @@ { "source": "functions", "codebase": "default", + "timeoutSeconds": 540, "ignore": [ "node_modules", ".git", diff --git a/functions/src/index.ts b/functions/src/index.ts index 1b8cb5b..19fa4e0 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,6 +6,3 @@ export { processNotificationOnCreate } from './notifications'; export * from './payments'; export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { registerClient } from './clientRegistration'; -import { setGlobalOptions } from "firebase-functions/v2"; - -setGlobalOptions({ timeoutSeconds: 400 }); -- 2.43.0 From 3d64e11a056ad2343b51824b3052a6f17805194c Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 20 May 2025 17:05:30 +0530 Subject: [PATCH 07/26] changes for deploying --- firebase.json | 1 + functions/src/index.ts | 11 +++++++++++ package.json | 6 +----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/firebase.json b/firebase.json index 608886c..21d0f5f 100644 --- a/firebase.json +++ b/firebase.json @@ -8,6 +8,7 @@ "source": "functions", "codebase": "default", "timeoutSeconds": 540, + "memory": "1GiB", "ignore": [ "node_modules", ".git", diff --git a/functions/src/index.ts b/functions/src/index.ts index 19fa4e0..660ddc7 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,3 +1,14 @@ +import { setGlobalOptions } from "firebase-functions/v2"; + +setGlobalOptions({ + region: "#{SERVICES_RGN}#", + memory: "1GiB", + timeoutSeconds: 540, + minInstances: 0, + maxInstances: 10, + concurrency: 80 +}); + export * from './shared/config'; export { sendEmailMessage, sendEmailWithAttachment, sendEmailSES } from './email'; export { sendSMSMessage } from './sms'; diff --git a/package.json b/package.json index 073e12e..da0eb6c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,7 @@ { "dependencies": { "@types/busboy": "^1.5.4", - "@types/nodemailer": "^6.4.17", - "@types/pdfkit": "^0.13.9", "busboy": "^1.6.0", - "date-fns": "^4.1.0", - "nodemailer": "^7.0.3", - "pdfkit": "^0.17.1" + "date-fns": "^4.1.0" } } -- 2.43.0 From 623ebc84dffd69bbd8c5ef33db4c1d2c3cd7bc73 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 21 May 2025 14:12:33 +0530 Subject: [PATCH 08/26] added dependency --- functions/package-lock.json | 10 ++++++++++ functions/package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/functions/package-lock.json b/functions/package-lock.json index e5b23b4..149bfc8 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -13,6 +13,7 @@ "aws-sdk": "^2.1692.0", "axios": "^1.9.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", @@ -3805,6 +3806,15 @@ "utrie": "^1.0.2" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", diff --git a/functions/package.json b/functions/package.json index b9f1c89..0e77a8f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -20,6 +20,7 @@ "aws-sdk": "^2.1692.0", "axios": "^1.9.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "firebase-admin": "^12.6.0", "firebase-functions": "^6.0.1", "form-data": "^4.0.1", -- 2.43.0 From dd060fb963855bd09f24ea58f68039db2ea678b3 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 21 May 2025 15:37:57 +0530 Subject: [PATCH 09/26] changed import and collection name --- functions/src/payments/phonepe/invoice/invoiceService.ts | 4 ++-- functions/src/payments/phonepe/webhook.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 95ffa7d..8c3d491 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { format } from 'date-fns'; import { sendEmailWithAttachmentUtil } from "../../../utils/emailService"; import { jsPDF } from "jspdf"; -import 'jspdf-autotable'; +import autoTable from 'jspdf-autotable'; const admin = getAdmin(); const logger = getLogger(); @@ -83,7 +83,7 @@ export class InvoiceService { doc.text(`Phone: ${data.phoneNumber}`, 20, 80); doc.text(`Email: ${data.email}`, 20, 90); - (doc as any).autoTable({ + autoTable(doc,{ startY: 110, head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], body: [ diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 90a0a43..42aa29f 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -154,7 +154,7 @@ export const phonePeWebhook = onRequest({ logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() - .collection('users') + .collection('client_profiles') .doc(userId) .get(); if (userDoc.exists) { -- 2.43.0 From d73f02726ee2d9d09487e7d358fa33c179a7a7ec Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 21 May 2025 16:55:54 +0530 Subject: [PATCH 10/26] changed pdf layout --- .../phonepe/invoice/invoiceService.ts | 165 ++++++++++++------ 1 file changed, 110 insertions(+), 55 deletions(-) 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, -- 2.43.0 From 18afada52ae6fac65652d564717e0a000d529b5d Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 21 May 2025 17:00:19 +0530 Subject: [PATCH 11/26] changes --- .../payments/phonepe/invoice/directInvoice.ts | 2 -- .../payments/phonepe/invoice/getInvoiceUrl.ts | 1 - functions/src/payments/phonepe/paymentData.ts | 16 ++++------------ 3 files changed, 4 insertions(+), 15 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/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(), }); -- 2.43.0 From 89b9ba897a938bdd5673bfda022441949d3bede6 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 21 May 2025 23:23:22 +0530 Subject: [PATCH 12/26] changes to invoice layout --- .../phonepe/invoice/invoiceService.ts | 26 +++++++++---------- functions/src/payments/phonepe/webhook.ts | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index bcc78a4..741b900 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -54,7 +54,7 @@ export class InvoiceService { doc.setFontSize(20); doc.setFont('helvetica', 'bold'); - doc.text(data.businessName, 20, 20); + doc.text(data.businessName, 13, 20); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); @@ -66,10 +66,10 @@ export class InvoiceService { if (addressLines.length <= 2) { for (let i = 0; i < addressLines.length; i++) { - doc.text(addressLines[i], 20, 30 + (i * lineHeight)); + doc.text(addressLines[i], 13, 30 + (i * lineHeight)); } } else { - doc.text(addressLines[0], 20, 30); + doc.text(addressLines[0], 13, 30); let secondLine = addressLines[1]; if (secondLine.length > 3) { @@ -77,7 +77,7 @@ export class InvoiceService { } else { secondLine += '...'; } - doc.text(secondLine, 20, 35); + doc.text(secondLine, 13, 35); } const gstYPosition = 40; @@ -97,10 +97,10 @@ export class InvoiceService { doc.line(20, 45, 190, 45); doc.setFontSize(12); - const receiptToBoxX = 15; + const receiptToBoxX = 13; const receiptToBoxY = 55; - const receiptToBoxWidth = 175; - const receiptToBoxHeight = 45; + const receiptToBoxWidth = 140; + const receiptToBoxHeight = 40; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -166,12 +166,12 @@ export class InvoiceService { const paymentY = hasGst ? finalY + 50 : finalY + 20; - doc.line(20, 45, 190, 45); + doc.line(13, 45, 190, 45); - const boxX = 15; + const boxX = 13; const boxY = paymentY - 5; - const boxWidth = 175; - const boxHeight = 45; + const boxWidth = 140; + const boxHeight = 40; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -181,8 +181,8 @@ export class InvoiceService { 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(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); + doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); doc.setFontSize(12); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 42aa29f..9c1f6cc 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -154,7 +154,7 @@ export const phonePeWebhook = onRequest({ logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() - .collection('client_profiles') + .collection('users') .doc(userId) .get(); if (userDoc.exists) { @@ -245,7 +245,7 @@ export const phonePeWebhook = onRequest({ customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', email: membershipData?.fields?.['email'] || '', - planName: orderData.metaInfo?.planName || subscriptionName || paymentType, + planName: orderData.metaInfo?.planName || subscriptionName, amount: orderData.amount, transactionId: payload.orderId, paymentDate: new Date(), -- 2.43.0 From df404e405ccdafc06cc19dc03b52b5c7271c35ba Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 13:04:45 +0530 Subject: [PATCH 13/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 9c1f6cc..537d431 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -154,11 +154,11 @@ export const phonePeWebhook = onRequest({ logger.info(`Fetching user data for userId: ${userId}`); const userDoc = await admin.firestore() - .collection('users') + .collection('client_profiles') .doc(userId) .get(); if (userDoc.exists) { - logger.info(`User data retrieved successfully for userId: ${userId}`); + logger.info(`User data retrieved successfully for userId(Client): ${userId}`); logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); -- 2.43.0 From 354881d5bf82f7f1fbec859685fc4eeffe94bad2 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 13:19:15 +0530 Subject: [PATCH 14/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 537d431..f12eb58 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -150,15 +150,15 @@ export const phonePeWebhook = onRequest({ logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`); const membershipData = membershipDoc.data(); - const userId = membershipData?.userId; + const uid = membershipData?.userId; - logger.info(`Fetching user data for userId: ${userId}`); + logger.info(`Fetching user data for uid(Client): ${uid}`); const userDoc = await admin.firestore() .collection('client_profiles') - .doc(userId) + .doc(uid) .get(); if (userDoc.exists) { - logger.info(`User data retrieved successfully for userId(Client): ${userId}`); + logger.info(`User data retrieved successfully for uid(Client): ${uid}`); logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); -- 2.43.0 From ed33fe3c46800b598a4e81dadafc126686a5ef7c Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 13:43:08 +0530 Subject: [PATCH 15/26] changed layout --- .../phonepe/invoice/invoiceService.ts | 21 ++++++++++--------- functions/src/payments/phonepe/webhook.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 741b900..df7afcf 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -99,19 +99,19 @@ export class InvoiceService { doc.setFontSize(12); const receiptToBoxX = 13; const receiptToBoxY = 55; - const receiptToBoxWidth = 140; + const receiptToBoxWidth = 100; const receiptToBoxHeight = 40; 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.text('Receipt To:', 18, 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); + doc.text(data.customerName, 18, 70); + doc.text(`Phone: ${data.phoneNumber}`, 18, 80); + doc.text(`Email: ${data.email}`, 18, 90); autoTable(doc, { startY: 110, @@ -155,10 +155,13 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 190, finalY + 20, { align: 'right' }); + doc.line(120, finalY + 25, 190, finalY + 25); + 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' }); } else { + doc.line(120, finalY - 5, 190, finalY - 5); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); @@ -166,11 +169,9 @@ export class InvoiceService { const paymentY = hasGst ? finalY + 50 : finalY + 20; - doc.line(13, 45, 190, 45); - const boxX = 13; const boxY = paymentY - 5; - const boxWidth = 140; + const boxWidth = 100; const boxHeight = 40; doc.setDrawColor(0, 0, 0); @@ -178,12 +179,12 @@ export class InvoiceService { doc.rect(boxX, boxY, boxWidth, boxHeight); doc.setFont('helvetica', 'bold'); - doc.text('Payment Information:', 20, paymentY); + doc.text('Payment Information:', 18, paymentY); doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); - doc.text(`Payment Date: ${formattedDate}`, 20, paymentY + 30); + doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 30); doc.setFontSize(12); doc.setFont('helvetica', 'italic'); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index f12eb58..2ba5136 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -197,7 +197,7 @@ export const phonePeWebhook = onRequest({ const gymData = gymDoc.data(); gymName = gymData?.name || 'Fitlien'; gymAddress = gymData?.address || ''; - subscriptionName = gymData?.subscriptions?.name || ''; + subscriptionName = membershipData?.subscription?.normalizedName || ''; if (gymData?.userId) { const gymOwnerDoc = await admin.firestore() -- 2.43.0 From 1a40513cc4d3ff351d28270492428c3f34ae92c2 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 14:27:02 +0530 Subject: [PATCH 16/26] Update invoiceService.ts --- .../phonepe/invoice/invoiceService.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index df7afcf..fcf9cec 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -54,7 +54,7 @@ export class InvoiceService { doc.setFontSize(20); doc.setFont('helvetica', 'bold'); - doc.text(data.businessName, 13, 20); + doc.text(data.businessName, 15, 20); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); @@ -66,10 +66,10 @@ export class InvoiceService { if (addressLines.length <= 2) { for (let i = 0; i < addressLines.length; i++) { - doc.text(addressLines[i], 13, 30 + (i * lineHeight)); + doc.text(addressLines[i], 15, 30 + (i * lineHeight)); } } else { - doc.text(addressLines[0], 13, 30); + doc.text(addressLines[0], 15, 30); let secondLine = addressLines[1]; if (secondLine.length > 3) { @@ -77,7 +77,7 @@ export class InvoiceService { } else { secondLine += '...'; } - doc.text(secondLine, 13, 35); + doc.text(secondLine, 15, 35); } const gstYPosition = 40; @@ -88,17 +88,17 @@ export class InvoiceService { doc.setFontSize(24); doc.setFont('helvetica', 'bold'); - doc.text('RECEIPT', 190, 20, { align: 'right' }); + doc.text('RECEIPT', 195, 20, { align: 'right' }); doc.setFontSize(12); - doc.text(`Receipt #: ${data.invoiceNumber}`, 190, 30, { align: 'right' }); - doc.text(`Date: ${formattedDate}`, 190, 40, { align: 'right' }); + doc.text(`Receipt #: ${data.invoiceNumber}`, 195, 30, { align: 'right' }); + doc.text(`Date: ${formattedDate}`, 195, 40, { align: 'right' }); - doc.line(20, 45, 190, 45); + doc.line(15, 45, 195, 45); doc.setFontSize(12); - const receiptToBoxX = 13; - const receiptToBoxY = 55; + const receiptToBoxX = 15; + const receiptToBoxY = 50; const receiptToBoxWidth = 100; const receiptToBoxHeight = 40; @@ -110,12 +110,20 @@ export class InvoiceService { doc.setFont('helvetica', 'normal'); doc.text(data.customerName, 18, 70); - doc.text(`Phone: ${data.phoneNumber}`, 18, 80); - doc.text(`Email: ${data.email}`, 18, 90); + doc.text(`Phone: ${data.phoneNumber}`, 18, 75); + doc.text(`Email: ${data.email}`, 18, 80); autoTable(doc, { startY: 110, - head: [['No.', 'Description', 'HSN/SAC', 'Amount (INR)']], + margin: {left: 15, right: 15}, + head: [ + [ + {content: 'No.', styles: {halign: 'center'}}, + {content: 'Description', styles: {halign: 'left'}}, + {content: 'HSN/SAC', styles: {halign: 'center'}}, + {content: 'Amount (INR)', styles: {halign: 'right'}} + ] + ], body: [ ['1', `${data.planName} Subscription`, '999723', baseAmount.toFixed(2)] ], @@ -147,30 +155,30 @@ export class InvoiceService { if (hasGst) { doc.text('Taxable Amount:', 150, finalY, { align: 'right' }); - doc.text(`${baseAmount.toFixed(2)} INR`, 190, finalY, { align: 'right' }); + doc.text(`${baseAmount.toFixed(2)} INR`, 195, 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(`${sgst.toFixed(2)} INR`, 195, 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.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(120, finalY + 25, 190, finalY + 25); + doc.line(120, finalY + 25, 195, finalY + 25); 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' }); + doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY + 30, { align: 'right' }); } else { - doc.line(120, finalY - 5, 190, finalY - 5); + doc.line(120, finalY - 5, 195, finalY - 5); doc.setFont('helvetica', 'bold'); 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`, 195, finalY, { align: 'right' }); } const paymentY = hasGst ? finalY + 50 : finalY + 20; - const boxX = 13; - const boxY = paymentY - 5; + const boxX = 15; + const boxY = paymentY - 10; const boxWidth = 100; const boxHeight = 40; @@ -183,8 +191,8 @@ export class InvoiceService { doc.setFont('helvetica', 'normal'); doc.text(`Transaction ID: ${data.transactionId}`, 18, paymentY + 10); - doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 20); - doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 30); + doc.text(`Payment Method: ${data.paymentMethod}`, 18, paymentY + 15); + doc.text(`Payment Date: ${formattedDate}`, 18, paymentY + 20); doc.setFontSize(12); doc.setFont('helvetica', 'italic'); -- 2.43.0 From 083485d7de58d2e7b1884ef41a0b86c617d20e43 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 14:37:01 +0530 Subject: [PATCH 17/26] Update invoiceService.ts --- functions/src/payments/phonepe/invoice/invoiceService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index fcf9cec..9e1530f 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -100,7 +100,7 @@ export class InvoiceService { const receiptToBoxX = 15; const receiptToBoxY = 50; const receiptToBoxWidth = 100; - const receiptToBoxHeight = 40; + const receiptToBoxHeight = 36; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); @@ -180,7 +180,7 @@ export class InvoiceService { const boxX = 15; const boxY = paymentY - 10; const boxWidth = 100; - const boxHeight = 40; + const boxHeight = 36; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5); -- 2.43.0 From 8ad307f3a984a45d99bbb48b8a1dc90d9be635f1 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 14:40:04 +0530 Subject: [PATCH 18/26] Update invoiceService.ts --- functions/src/payments/phonepe/invoice/invoiceService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 9e1530f..60b88a0 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -163,7 +163,7 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(120, finalY + 25, 195, finalY + 25); + doc.line(15, finalY + 25, 15, finalY + 25); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); -- 2.43.0 From 5fe4d305026765961531f265eca7eef40b04f1b7 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Fri, 23 May 2025 15:00:25 +0530 Subject: [PATCH 19/26] Update invoiceService.ts --- functions/src/payments/phonepe/invoice/invoiceService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index 60b88a0..df4fbe4 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -94,6 +94,7 @@ export class InvoiceService { doc.text(`Receipt #: ${data.invoiceNumber}`, 195, 30, { align: 'right' }); doc.text(`Date: ${formattedDate}`, 195, 40, { align: 'right' }); + doc.setLineWidth(0.5); doc.line(15, 45, 195, 45); doc.setFontSize(12); @@ -163,13 +164,15 @@ export class InvoiceService { doc.text('CGST (9%):', 150, finalY + 20, { align: 'right' }); doc.text(`${cgst.toFixed(2)} INR`, 195, finalY + 20, { align: 'right' }); - doc.line(15, finalY + 25, 15, finalY + 25); + doc.setLineWidth(0.5); + doc.line(15, finalY + 25, 195, finalY + 25); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY + 30, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY + 30, { align: 'right' }); } else { - doc.line(120, finalY - 5, 195, finalY - 5); + doc.setLineWidth(0.5); + doc.line(15, finalY - 5, 195, finalY - 5); doc.setFont('helvetica', 'bold'); doc.text('Total Amount:', 150, finalY, { align: 'right' }); doc.text(`${data.amount.toFixed(2)} INR`, 195, finalY, { align: 'right' }); -- 2.43.0 From 2fb0280e877b65841f6adaf4f5295140cf88bba6 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 27 May 2025 23:36:48 +0530 Subject: [PATCH 20/26] Update emailService.ts --- functions/src/utils/emailService.ts | 259 ++++++++++++++++++++++------ 1 file changed, 206 insertions(+), 53 deletions(-) diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts index b7f70a8..787669a 100644 --- a/functions/src/utils/emailService.ts +++ b/functions/src/utils/emailService.ts @@ -1,15 +1,135 @@ -import * as os from 'os'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as https from 'https'; -import { getLogger } from "../shared/config"; -import formData from 'form-data'; -import Mailgun from 'mailgun.js'; -const { convert } = require('html-to-text'); -const mailgun = new Mailgun(formData); +import { getLogger } from "../shared/config"; +import { SESClient } from "@aws-sdk/client-ses"; +import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; +import * as mime from 'mime-types'; +import axios from 'axios'; + const logger = getLogger(); +interface EmailRequest { + to: string | string[]; + subject: string; + html: string; + text?: string; + from: string; + replyTo?: string; + attachments?: Attachment[]; + fileUrl?: string; + fileName?: string; +} + +interface Attachment { + filename: string; + content: string | Buffer; // Base64 encoded string or Buffer + contentType?: string; +} + +const stripHtml = (html: string): string => { + if (!html) return ''; + return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); +} + +async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { + const ses = new SESClient({ + region: 'ap-south-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' + } + }); + + const command = new SendEmailCommand({ + Source: data.from, + Destination: { ToAddresses: recipients }, + Message: { + Subject: { Data: data.subject }, + Body: { + Html: { Data: data.html }, + Text: { Data: data.text || stripHtml(data.html) } + } + }, + ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined, + }); + + const result = await ses.send(command); + return { messageId: result.MessageId }; +} + +async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { + const ses = new SESClient({ + region: 'ap-south-1', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' + } + }); + + const boundary = `boundary_${Math.random().toString(16).substr(2)}`; + let rawMessage = `From: ${data.from}\n`; + rawMessage += `To: ${recipients.join(', ')}\n`; + rawMessage += `Subject: ${data.subject}\n`; + rawMessage += `MIME-Version: 1.0\n`; + rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; + + // Add email body (multipart/alternative) + rawMessage += `--${boundary}\n`; + rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; + + // Text part + if (data.text) { + rawMessage += `--alt_${boundary}\n`; + rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; + rawMessage += `${data.text}\n\n`; + } + + // HTML part + rawMessage += `--alt_${boundary}\n`; + rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; + rawMessage += `${data.html}\n\n`; + + // Close alternative part + rawMessage += `--alt_${boundary}--\n\n`; + + // Add attachments + for (const attachment of data.attachments || []) { + const contentType = attachment.contentType || + mime.lookup(attachment.filename) || + 'application/octet-stream'; + + rawMessage += `--${boundary}\n`; + rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`; + rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; + rawMessage += `Content-Transfer-Encoding: base64\n\n`; + + const contentBuffer = typeof attachment.content === 'string' + ? Buffer.from(attachment.content, 'base64') + : attachment.content; + + rawMessage += contentBuffer.toString('base64') + '\n\n'; + } + + // Close message + rawMessage += `--${boundary}--`; + + const command = new SendRawEmailCommand({ + RawMessage: { Data: Buffer.from(rawMessage) } + }); + + const result = await ses.send(command); + return { messageId: result.MessageId }; +} + +async function downloadFileFromUrl(url: string): Promise { + try { + const response = await axios.get(url, { responseType: 'arraybuffer' }); + return Buffer.from(response.data); + } catch (error) { + logger.error(`Error downloading file from URL: ${error}`); + throw new Error(`Failed to download file: ${error}`); + } +} + export async function sendEmailWithAttachmentUtil( toAddress: string, subject: string, @@ -18,53 +138,86 @@ export async function sendEmailWithAttachmentUtil( fileName?: string ): Promise { try { - const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(tempFilePath); - https.get(fileUrl, (res) => { - res.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); + logger.info(`Sending email with attachment to: ${toAddress}`); + + // Initialize data with basic fields + const data: EmailRequest = { + to: toAddress, + html: message, + subject: subject, + text: stripHtml(message), + from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', + replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', + attachments: [] + }; + + // Handle file URL if provided + if (fileUrl && fileName) { + logger.info(`Downloading attachment from URL: ${fileUrl}`); + try { + const fileContent = await downloadFileFromUrl(fileUrl); + + // Add the downloaded file as an attachment + data.attachments!.push({ + filename: fileName, + content: fileContent, + contentType: mime.lookup(fileName) || 'application/octet-stream' }); - }).on('error', (err) => { - fs.unlink(tempFilePath, () => {}); - reject(err); - }); - }); - - try { - const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); - const options = { - wordwrap: 130, - }; - const textMessage = convert(message, options); - const fileBuffer = fs.readFileSync(tempFilePath); - const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); - - const data = { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message, - attachment: { - data: fileBuffer, - filename: attachmentFilename, - contentType: 'application/pdf', - } - }; - - const result = await client.messages.create(process.env.MAILGUN_SERVER!, data); - fs.unlinkSync(tempFilePath); - logger.info('Email with attachment from URL sent successfully'); - return { success: true, result }; - } catch (e) { - logger.error(`Error while sending E-mail. Error: ${e}`); - throw e; + + logger.info(`Successfully downloaded attachment: ${fileName}`); + } catch (downloadError) { + logger.error(`Failed to download attachment: ${downloadError}`); + throw new Error(`Failed to process attachment: ${downloadError}`); + } } + + if (!data.to || !data.subject || !data.html || !data.from) { + throw new Error('Missing required email fields'); + } + + logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); + const recipients = Array.isArray(data.to) ? data.to : [data.to]; + + let result; + if (data.attachments && data.attachments.length > 0) { + result = await sendEmailWithAttachments(data, recipients); + } else { + result = await sendSimpleEmail(data, recipients); + } + + logger.info('Email sent successfully via SES'); + return { success: true, result }; + } catch (error) { - logger.error('Error sending email with attachment from URL:', error); + logger.error('Error sending email with attachment via SES:', error); + throw error; + } +} + +// Additional utility function for sending simple emails without attachments +export async function sendSimpleEmailUtil( + toAddress: string, + subject: string, + message: string +): Promise { + try { + const data: EmailRequest = { + to: toAddress, + html: message, + subject: subject, + text: stripHtml(message), + from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', + replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com' + }; + + const recipients = Array.isArray(data.to) ? data.to : [data.to]; + const result = await sendSimpleEmail(data, recipients); + + logger.info('Simple email sent successfully via SES'); + return { success: true, result }; + + } catch (error) { + logger.error('Error sending simple email via SES:', error); throw error; } } -- 2.43.0 From 4817424c710640fdb52147edf70b0241793a71b2 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 27 May 2025 23:47:57 +0530 Subject: [PATCH 21/26] Update emailService.ts --- functions/src/utils/emailService.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts index 787669a..1bb4e06 100644 --- a/functions/src/utils/emailService.ts +++ b/functions/src/utils/emailService.ts @@ -193,31 +193,3 @@ export async function sendEmailWithAttachmentUtil( throw error; } } - -// Additional utility function for sending simple emails without attachments -export async function sendSimpleEmailUtil( - toAddress: string, - subject: string, - message: string -): Promise { - try { - const data: EmailRequest = { - to: toAddress, - html: message, - subject: subject, - text: stripHtml(message), - from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', - replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com' - }; - - const recipients = Array.isArray(data.to) ? data.to : [data.to]; - const result = await sendSimpleEmail(data, recipients); - - logger.info('Simple email sent successfully via SES'); - return { success: true, result }; - - } catch (error) { - logger.error('Error sending simple email via SES:', error); - throw error; - } -} -- 2.43.0 From f0f2cd3411a78c9e0c779b30807be51d5ecf6206 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 28 May 2025 00:14:17 +0530 Subject: [PATCH 22/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 2ba5136..8f797d7 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -172,6 +172,7 @@ export const phonePeWebhook = onRequest({ let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; let trainerId = orderData.metaInfo?.trainerId; let trainerData = null; + let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address']; const discountPercentage = orderData.metaInfo?.discount || 0; const hasDiscount = discountPercentage > 0; @@ -308,7 +309,7 @@ export const phonePeWebhook = onRequest({ const formattedDate = format(new Date(), 'dd/MM/yyyy'); - if (membershipData?.fields?.['email']) { + if (emailCustomer) { logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`); try { const emailSubject = isFreeplan -- 2.43.0 From bb720e6f78b9bde4dec4a13ba10b9be3399f0de4 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 28 May 2025 00:29:28 +0530 Subject: [PATCH 23/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 8f797d7..f9c3591 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -343,7 +343,7 @@ export const phonePeWebhook = onRequest({ `; await sendEmailWithAttachmentUtil( - membershipData?.fields?.['email'], + emailCustomer, emailSubject, customerEmailHtml, downloadUrl, -- 2.43.0 From 7f4ca23cb8b119c2ee187ccce8d71b894f97d9cb Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 28 May 2025 00:43:18 +0530 Subject: [PATCH 24/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index f9c3591..bf09946 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -243,9 +243,9 @@ export const phonePeWebhook = onRequest({ businessName: gymName, address: gymAddress, gstNumber: userData?.gstNumber, - customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), - phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', - email: membershipData?.fields?.['email'] || '', + customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '', + phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '', planName: orderData.metaInfo?.planName || subscriptionName, amount: orderData.amount, transactionId: payload.orderId, -- 2.43.0 From 62f1130d3e80f2516d4c3b66ffceb655145dc5f1 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Wed, 28 May 2025 16:43:40 +0530 Subject: [PATCH 25/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 791 +++++++++++++--------- 1 file changed, 469 insertions(+), 322 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index bf09946..3cad0c5 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -131,328 +131,11 @@ export const phonePeWebhook = onRequest({ if (paymentUpdateSuccess) { const orderData = orderDoc.data(); const membershipId = orderData.metaInfo?.membershipId; - - logger.info(`Processing invoice for completed payment`, { - merchantOrderId: payload.merchantOrderId, - orderId: payload.orderId, - membershipId: membershipId || 'not-provided' - }); - - if (membershipId) { - try { - logger.info(`Fetching membership data for membershipId: ${membershipId}`); - const membershipDoc = await admin.firestore() - .collection('memberships') - .doc(membershipId) - .get(); - - if (membershipDoc.exists) { - logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`); - - const membershipData = membershipDoc.data(); - const uid = membershipData?.userId; - - logger.info(`Fetching user data for uid(Client): ${uid}`); - const userDoc = await admin.firestore() - .collection('client_profiles') - .doc(uid) - .get(); - if (userDoc.exists) { - logger.info(`User data retrieved successfully for uid(Client): ${uid}`); - - logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); - - const userData = userDoc.data(); - - const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; - let gymName = 'Fitlien'; - let gymAddress = ''; - let subscriptionName = ''; - let gymOwnerEmail = ''; - let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; - let trainerId = orderData.metaInfo?.trainerId; - let trainerData = null; - let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address']; - - const discountPercentage = orderData.metaInfo?.discount || 0; - const hasDiscount = discountPercentage > 0; - const isFreeplan = discountPercentage === 100; - const originalAmount = hasDiscount ? - orderData.amount / (1 - discountPercentage / 100) : - orderData.amount; - const discountText = isFreeplan ? - " (Free Plan)" : - hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` : - ''; - const amountSaved = hasDiscount ? - originalAmount - orderData.amount : - 0; - - if (gymId) { - const gymDoc = await admin.firestore() - .collection('gyms') - .doc(gymId) - .get(); - - if (gymDoc.exists) { - const gymData = gymDoc.data(); - gymName = gymData?.name || 'Fitlien'; - gymAddress = gymData?.address || ''; - subscriptionName = membershipData?.subscription?.normalizedName || ''; - - if (gymData?.userId) { - const gymOwnerDoc = await admin.firestore() - .collection('users') - .doc(gymData.userId) - .get(); - - if (gymOwnerDoc.exists) { - const gymOwnerData = gymOwnerDoc.data(); - gymOwnerEmail = gymOwnerData?.email || ''; - } - } - } - } - - if (paymentType === 'Gym Membership with Personal Training' && trainerId) { - try { - const trainerDoc = await admin.firestore() - .collection('trainer_profiles') - .doc(trainerId) - .get(); - - if (trainerDoc.exists) { - trainerData = trainerDoc.data(); - } - } catch (trainerError) { - logger.error('Error fetching trainer data:', trainerError); - } - } - - const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; - - logger.info(`Generated invoice number: ${invoiceNumber}`); - - logger.info(`Preparing invoice data for generation`, { - invoiceNumber, - merchantOrderId: payload.merchantOrderId, - gymName: gymName - }); - const invoiceData = { - invoiceNumber, - businessName: gymName, - address: gymAddress, - gstNumber: userData?.gstNumber, - customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '', - phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '', - email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '', - planName: orderData.metaInfo?.planName || subscriptionName, - amount: orderData.amount, - transactionId: payload.orderId, - paymentDate: new Date(), - paymentMethod: 'Online' - }; - - const invoicePath = await invoiceService.generateInvoice(invoiceData); - logger.info(`Invoice generated successfully at path: ${invoicePath}`); - - logger.info(`Updating membership payment with invoice path`, { - membershipId, - invoicePath - }); - - await admin.firestore() - .collection('membership_payments') - .doc(membershipId) - .get() - .then(async (doc) => { - if (doc.exists) { - logger.info(`Found membership payment document for membershipId: ${membershipId}`); - - const paymentsData = doc.data()?.payments || []; - let paymentFound = false; - - for (let i = 0; i < paymentsData.length; i++) { - if (paymentsData[i].referenceNumber === payload.merchantOrderId || - paymentsData[i].transactionId === payload.orderId) { - paymentsData[i].invoicePath = invoicePath; - paymentFound = true; - break; - } - } - - logger.info(`Payment record ${paymentFound ? 'found' : 'not found'} in membership payments`, { - membershipId, - merchantOrderId: payload.merchantOrderId, - orderId: payload.orderId - }); - - await doc.ref.update({ - 'payments': paymentsData, - 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), - }); - - logger.info(`Successfully updated membership payment with invoice path`, { - membershipId, - invoicePath - }); - } else { - logger.warn(`No membership payment document found for membershipId: ${membershipId}`); - } - }); - - logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`); - - logger.info(`Getting download URL for invoice: ${invoicePath}`); - const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); - logger.info(`Generated download URL for invoice: ${invoicePath}`); - - const formattedDate = format(new Date(), 'dd/MM/yyyy'); - - if (emailCustomer) { - logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`); - try { - const emailSubject = isFreeplan - ? `Free Plan Assigned - ${gymName}` - : `New Membership - ${gymName}`; - - const customerEmailHtml = ` - - -

${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}

-

Dear ${invoiceData.customerName},

-

${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}

-

Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.

-

Membership Details:

-
    -
  • Gym: ${gymName}
  • - ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Your Personal Trainer'}
  • ` : ''} -
  • Plan: ${invoiceData.planName}
  • - ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} - ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} - ${hasDiscount ? `
  • You Save: ₹${amountSaved.toFixed(2)}
  • ` : ''} -
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • -
  • Transaction ID: ${payload.merchantOrderId}
  • -
  • Date: ${formattedDate}
  • - ${isFreeplan ? '
  • Payment Method: Online}
  • ' : ''} -
-

If you have any questions, please contact us.

-

Regards,
Fitlien Team

- - - `; - - await sendEmailWithAttachmentUtil( - emailCustomer, - emailSubject, - customerEmailHtml, - downloadUrl, - `Invoice_${path.basename(invoicePath)}` - ); - - logger.info(`Invoice email sent to ${membershipData?.fields?.['email']} for payment: ${payload.merchantOrderId}`); - } catch (emailError) { - logger.error('Error sending customer invoice email:', emailError); - } - } - - if (gymOwnerEmail) { - logger.info(`Preparing to send invoice email to gym owner: ${gymOwnerEmail}`); - try { - const ownerEmailSubject = isFreeplan - ? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}` - : `New Membership${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`; - - const gymOwnerEmailHtml = ` - - -

${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}

-

Dear Gym Owner,

-

${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.

-

Customer Details:

-
    -
  • Name: ${invoiceData.customerName}
  • -
  • Email: ${invoiceData.email}
  • -
  • Phone: ${invoiceData.phoneNumber}
  • -
-

Booking Details:

-
    -
  • Type: ${invoiceData.planName}
  • - ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Personal Trainer'}
  • ` : ''} - ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} - ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} - ${hasDiscount ? `
  • Amount Saved by Customer: ₹${amountSaved.toFixed(2)}
  • ` : ''} -
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • -
  • Transaction ID: ${payload.merchantOrderId}
  • -
  • Date: ${formattedDate}
  • -
-

Please find the invoice attached.

-

Regards,
Fitlien Team

- - - `; - - await sendEmailWithAttachmentUtil( - gymOwnerEmail, - ownerEmailSubject, - gymOwnerEmailHtml, - downloadUrl, - `Invoice_${path.basename(invoicePath)}` - ); - - logger.info(`Invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); - } catch (ownerEmailError) { - logger.error('Error sending gym owner invoice email:', ownerEmailError); - } - } - - if (paymentType === 'Gym Membership with Personal Training' && trainerData && trainerData.email) { - try { - const trainerEmailHtml = ` - - -

New Personal Training Client

-

Dear ${trainerData.fullName || 'Trainer'},

-

A new client has signed up for personal training with you at ${gymName}.

-

Client Details:

-
    -
  • Name: ${invoiceData.customerName}
  • -
  • Email: ${invoiceData.email}
  • -
  • Phone: ${invoiceData.phoneNumber}
  • -
-

Booking Details:

-
    -
  • Type: Personal Training Membership
  • - ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} - ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} -
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • -
  • Transaction ID: ${payload.merchantOrderId}
  • -
  • Date: ${formattedDate}
  • -
-

Please find the invoice attached.

-

Regards,
Fitlien Team

- - - `; - - await sendEmailWithAttachmentUtil( - trainerData.email, - `New Personal Training Client - ${gymName}`, - trainerEmailHtml, - downloadUrl, - `Invoice_${path.basename(invoicePath)}` - ); - - logger.info(`Invoice email sent to trainer (${trainerData.email}) for payment: ${payload.merchantOrderId}`); - } catch (trainerEmailError) { - logger.error('Error sending trainer invoice email:', trainerEmailError); - } - } - } - } - } catch (invoiceError) { - logger.error('Error generating invoice:', invoiceError); - } + const bookingId = orderData.metaInfo?.bookingId; + if (bookingId) { + await processDayPassBooking(payload, orderData, bookingId); + } else if (membershipId) { + await processMembershipPayment(payload, orderData, membershipId); } } @@ -473,3 +156,467 @@ export const phonePeWebhook = onRequest({ }); } }); + +async function processDayPassBooking(payload: any, orderData: any, bookingId: string) { + try { + logger.info(`Processing day pass booking for bookingId: ${bookingId}`); + + const bookingRef = admin.firestore().collection('day_pass_bookings').doc(bookingId); + const bookingDoc = await bookingRef.get(); + + if (!bookingDoc.exists) { + logger.error(`Day pass booking not found for bookingId: ${bookingId}`); + return; + } + + await bookingRef.update({ + status: 'ACCEPTED', + paymentDetails: { + transactionId: payload.orderId, + merchantOrderId: payload.merchantOrderId, + amount: orderData.amount, + paymentDate: new Date(), + paymentMethod: 'PhonePe' + }, + updatedAt: admin.firestore.FieldValue.serverTimestamp() + }); + + logger.info(`Updated day pass booking status to 'Accepted' for bookingId: ${bookingId}`); + + const bookingData = bookingDoc.data(); + const gymId = orderData.metaInfo?.gymId || bookingData?.gymId; + + if (gymId) { + try { + const gymDoc = await admin.firestore().collection('gyms').doc(gymId).get(); + let gymName = 'Fitlien'; + let gymAddress = ''; + let gymOwnerEmail = ''; + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + + if (gymData?.userId) { + const gymOwnerDoc = await admin.firestore() + .collection('users') + .doc(gymData.userId) + .get(); + + if (gymOwnerDoc.exists) { + const gymOwnerData = gymOwnerDoc.data(); + gymOwnerEmail = gymOwnerData?.email || ''; + } + } + } + + const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; + + logger.info(`Generated invoice number for day pass: ${invoiceNumber}`); + + const invoiceData = { + invoiceNumber, + businessName: gymName, + address: gymAddress, + gstNumber: orderData.metaInfo?.gstNumber, + customerName: orderData.metaInfo?.customerName || bookingData?.customerName || '', + phoneNumber: orderData.metaInfo?.customerPhone || bookingData?.phoneNumber || '', + email: orderData.metaInfo?.customerEmail || bookingData?.email || '', + planName: 'Day Pass', + amount: orderData.amount, + transactionId: payload.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + logger.info(`Day pass invoice generated successfully at path: ${invoicePath}`); + + await bookingRef.update({ + invoicePath: invoicePath, + invoiceNumber: invoiceNumber + }); + + logger.info(`Updated day pass booking with invoice path: ${invoicePath}`); + + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + const formattedDate = format(new Date(), 'dd/MM/yyyy'); + + if (gymOwnerEmail) { + logger.info(`Preparing to send day pass invoice email to gym owner: ${gymOwnerEmail}`); + try { + const ownerEmailSubject = `New Day Pass Payment - ${gymName}`; + + const gymOwnerEmailHtml = ` + + +

New Day Pass Payment Received

+

Dear Gym Owner,

+

A new day pass payment has been received for your gym.

+

Customer Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Day Pass Details:

+
    +
  • Service: Day Pass
  • +
  • Amount: ₹${orderData.amount.toFixed(2)}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + gymOwnerEmail, + ownerEmailSubject, + gymOwnerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Day pass invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); + } catch (ownerEmailError) { + logger.error('Error sending gym owner day pass invoice email:', ownerEmailError); + } + } + + } catch (invoiceError) { + logger.error('Error generating day pass invoice:', invoiceError); + } + } + + } catch (error) { + logger.error('Error processing day pass booking:', error); + } +} + +async function processMembershipPayment(payload: any, orderData: any, membershipId: string) { + logger.info(`Processing membership for completed payment`, { + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId, + membershipId: membershipId || 'not-provided' + }); + + if (membershipId) { + try { + logger.info(`Fetching membership data for membershipId: ${membershipId}`); + const membershipDoc = await admin.firestore() + .collection('memberships') + .doc(membershipId) + .get(); + + if (membershipDoc.exists) { + logger.info(`Membership data retrieved successfully for membershipId: ${membershipId}`); + + const membershipData = membershipDoc.data(); + const uid = membershipData?.userId; + + logger.info(`Fetching user data for uid(Client): ${uid}`); + const userDoc = await admin.firestore() + .collection('client_profiles') + .doc(uid) + .get(); + if (userDoc.exists) { + logger.info(`User data retrieved successfully for uid(Client): ${uid}`); + + logger.info(`Starting invoice generation process for payment: ${payload.merchantOrderId}`); + + const userData = userDoc.data(); + + const gymId = orderData.metaInfo?.gymId || membershipData?.gymId; + let gymName = 'Fitlien'; + let gymAddress = ''; + let subscriptionName = ''; + let gymOwnerEmail = ''; + let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; + let trainerId = orderData.metaInfo?.trainerId; + let trainerData = null; + let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address']; + + const discountPercentage = orderData.metaInfo?.discount || 0; + const hasDiscount = discountPercentage > 0; + const isFreeplan = discountPercentage === 100; + const originalAmount = hasDiscount ? + orderData.amount / (1 - discountPercentage / 100) : + orderData.amount; + const discountText = isFreeplan ? + " (Free Plan)" : + hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` : + ''; + const amountSaved = hasDiscount ? + originalAmount - orderData.amount : + 0; + + if (gymId) { + const gymDoc = await admin.firestore() + .collection('gyms') + .doc(gymId) + .get(); + + if (gymDoc.exists) { + const gymData = gymDoc.data(); + gymName = gymData?.name || 'Fitlien'; + gymAddress = gymData?.address || ''; + subscriptionName = membershipData?.subscription?.normalizedName || ''; + + if (gymData?.userId) { + const gymOwnerDoc = await admin.firestore() + .collection('users') + .doc(gymData.userId) + .get(); + + if (gymOwnerDoc.exists) { + const gymOwnerData = gymOwnerDoc.data(); + gymOwnerEmail = gymOwnerData?.email || ''; + } + } + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerId) { + try { + const trainerDoc = await admin.firestore() + .collection('trainer_profiles') + .doc(trainerId) + .get(); + + if (trainerDoc.exists) { + trainerData = trainerDoc.data(); + } + } catch (trainerError) { + logger.error('Error fetching trainer data:', trainerError); + } + } + + const invoiceNumber = `INV-${payload.merchantOrderId.substring(0, 8)}`; + + logger.info(`Generated invoice number: ${invoiceNumber}`); + + logger.info(`Preparing invoice data for generation`, { + invoiceNumber, + merchantOrderId: payload.merchantOrderId, + gymName: gymName + }); + const invoiceData = { + invoiceNumber, + businessName: gymName, + address: gymAddress, + gstNumber: userData?.gstNumber, + customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '', + phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '', + planName: orderData.metaInfo?.planName || subscriptionName, + amount: orderData.amount, + transactionId: payload.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + logger.info(`Invoice generated successfully at path: ${invoicePath}`); + + logger.info(`Updating membership payment with invoice path`, { + membershipId, + invoicePath + }); + + await admin.firestore() + .collection('membership_payments') + .doc(membershipId) + .get() + .then(async (doc) => { + if (doc.exists) { + logger.info(`Found membership payment document for membershipId: ${membershipId}`); + + const paymentsData = doc.data()?.payments || []; + let paymentFound = false; + + for (let i = 0; i < paymentsData.length; i++) { + if (paymentsData[i].referenceNumber === payload.merchantOrderId || + paymentsData[i].transactionId === payload.orderId) { + paymentsData[i].invoicePath = invoicePath; + paymentFound = true; + break; + } + } + + logger.info(`Payment record ${paymentFound ? 'found' : 'not found'} in membership payments`, { + membershipId, + merchantOrderId: payload.merchantOrderId, + orderId: payload.orderId + }); + + await doc.ref.update({ + 'payments': paymentsData, + 'updatedAt': admin.firestore.FieldValue.serverTimestamp(), + }); + + logger.info(`Successfully updated membership payment with invoice path`, { + membershipId, + invoicePath + }); + } else { + logger.warn(`No membership payment document found for membershipId: ${membershipId}`); + } + }); + + logger.info(`Generated invoice for payment: ${payload.merchantOrderId}, path: ${invoicePath}`); + + logger.info(`Getting download URL for invoice: ${invoicePath}`); + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + logger.info(`Generated download URL for invoice: ${invoicePath}`); + + const formattedDate = format(new Date(), 'dd/MM/yyyy'); + + if (emailCustomer) { + logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`); + try { + const emailSubject = isFreeplan + ? `Free Plan Assigned - ${gymName}` + : `New Membership - ${gymName}`; + + const customerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}

+

Dear ${invoiceData.customerName},

+

${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}

+

Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.

+

Membership Details:

+
    +
  • Gym: ${gymName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Your Personal Trainer'}
  • ` : ''} +
  • Plan: ${invoiceData.planName}
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • You Save: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • + ${isFreeplan ? '
  • Payment Method: Online}
  • ' : ''} +
+

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + emailCustomer, + emailSubject, + customerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to ${membershipData?.fields?.['email']} for payment: ${payload.merchantOrderId}`); + } catch (emailError) { + logger.error('Error sending customer invoice email:', emailError); + } + } + + if (gymOwnerEmail) { + logger.info(`Preparing to send invoice email to gym owner: ${gymOwnerEmail}`); + try { + const ownerEmailSubject = isFreeplan + ? `Free Plan Assigned${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}` + : `New Membership${paymentType === 'Gym Membership with Personal Training' ? ' with Personal Training' : ''} - ${gymName}`; + + const gymOwnerEmailHtml = ` + + +

${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}

+

Dear Gym Owner,

+

${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.

+

Customer Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: ${invoiceData.planName}
  • + ${trainerData ? `
  • Trainer: ${trainerData.fullName || 'Personal Trainer'}
  • ` : ''} + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} + ${hasDiscount ? `
  • Amount Saved by Customer: ₹${amountSaved.toFixed(2)}
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + gymOwnerEmail, + ownerEmailSubject, + gymOwnerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); + } catch (ownerEmailError) { + logger.error('Error sending gym owner invoice email:', ownerEmailError); + } + } + + if (paymentType === 'Gym Membership with Personal Training' && trainerData && trainerData.email) { + try { + const trainerEmailHtml = ` + + +

New Personal Training Client

+

Dear ${trainerData.fullName || 'Trainer'},

+

A new client has signed up for personal training with you at ${gymName}.

+

Client Details:

+
    +
  • Name: ${invoiceData.customerName}
  • +
  • Email: ${invoiceData.email}
  • +
  • Phone: ${invoiceData.phoneNumber}
  • +
+

Booking Details:

+
    +
  • Type: Personal Training Membership
  • + ${hasDiscount ? `
  • Original Price: ₹${originalAmount.toFixed(2)}
  • ` : ''} + ${hasDiscount ? `
  • Discount: ${discountPercentage.toFixed(1)}%
  • ` : ''} +
  • Amount: ₹${orderData.amount.toFixed(2)}${discountText}
  • +
  • Transaction ID: ${payload.merchantOrderId}
  • +
  • Date: ${formattedDate}
  • +
+

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + trainerData.email, + `New Personal Training Client - ${gymName}`, + trainerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Invoice email sent to trainer (${trainerData.email}) for payment: ${payload.merchantOrderId}`); + } catch (trainerEmailError) { + logger.error('Error sending trainer invoice email:', trainerEmailError); + } + } + } + } + } catch (invoiceError) { + logger.error('Error generating invoice:', invoiceError); + } + } +} -- 2.43.0 From fb68d8202426ae7274d6fc7bb1eaa4910925639b Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Thu, 29 May 2025 14:51:37 +0530 Subject: [PATCH 26/26] Update webhook.ts --- functions/src/payments/phonepe/webhook.ts | 35 ++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 3cad0c5..1cb7cff 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -117,26 +117,29 @@ export const phonePeWebhook = onRequest({ try { logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`); - const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( - payload.merchantOrderId, - payload.orderId, - payload - ); + const orderData = orderDoc.data(); + const membershipId = orderData.metaInfo?.membershipId; + const bookingId = orderData.metaInfo?.bookingId; - logger.info(`Payment update result for merchantOrderId: ${payload.merchantOrderId}`, { - success: paymentUpdateSuccess, - orderId: payload.orderId - }); + if (bookingId) { + await processDayPassBooking(payload, orderData, bookingId); + } else if (membershipId) { + const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( + payload.merchantOrderId, + payload.orderId, + payload + ); - if (paymentUpdateSuccess) { - const orderData = orderDoc.data(); - const membershipId = orderData.metaInfo?.membershipId; - const bookingId = orderData.metaInfo?.bookingId; - if (bookingId) { - await processDayPassBooking(payload, orderData, bookingId); - } else if (membershipId) { + logger.info(`Payment update result for membershipId: ${membershipId}`, { + success: paymentUpdateSuccess, + orderId: payload.orderId + }); + + if (paymentUpdateSuccess) { await processMembershipPayment(payload, orderData, membershipId); } + } else { + logger.error(`No membershipId or bookingId found in metaInfo for order: ${payload.merchantOrderId}`); } logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`); -- 2.43.0