From 56f3f1e0bd5b4b9837c35579c87462404d9ce378 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 24 Jun 2025 08:40:11 +0000 Subject: [PATCH] phonepe (#61) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/61 --- .../phonepe/invoice/invoiceService.ts | 4 +- functions/src/payments/phonepe/webhook.ts | 208 +++++++++++++++++- 2 files changed, 209 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/invoice/invoiceService.ts b/functions/src/payments/phonepe/invoice/invoiceService.ts index df4fbe4..d8eb460 100644 --- a/functions/src/payments/phonepe/invoice/invoiceService.ts +++ b/functions/src/payments/phonepe/invoice/invoiceService.ts @@ -111,8 +111,8 @@ export class InvoiceService { doc.setFont('helvetica', 'normal'); doc.text(data.customerName, 18, 70); - doc.text(`Phone: ${data.phoneNumber}`, 18, 75); - doc.text(`Email: ${data.email}`, 18, 80); + doc.text(`${data.phoneNumber}`, 18, 75); + doc.text(`${data.email}`, 18, 80); autoTable(doc, { startY: 110, diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 1cb7cff..86bb323 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -120,6 +120,7 @@ export const phonePeWebhook = onRequest({ const orderData = orderDoc.data(); const membershipId = orderData.metaInfo?.membershipId; const bookingId = orderData.metaInfo?.bookingId; + const serviceId = orderData.metaInfo?.serviceId; if (bookingId) { await processDayPassBooking(payload, orderData, bookingId); @@ -138,8 +139,10 @@ export const phonePeWebhook = onRequest({ if (paymentUpdateSuccess) { await processMembershipPayment(payload, orderData, membershipId); } + } else if (serviceId) { + await processServicePayment(payload, orderData, serviceId); } else { - logger.error(`No membershipId or bookingId found in metaInfo for order: ${payload.merchantOrderId}`); + logger.error(`No membershipId, bookingId, or serviceId found in metaInfo for order: ${payload.merchantOrderId}`); } logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`); @@ -623,3 +626,206 @@ async function processMembershipPayment(payload: any, orderData: any, membership } } } + +async function processServicePayment(payload: any, orderData: any, serviceId: string) { + try { + logger.info(`Processing service payment for serviceId: ${serviceId}`); + + const serviceRef = admin.firestore().collection('service_payments').doc(serviceId); + const serviceDoc = await serviceRef.get(); + + if (!serviceDoc.exists) { + logger.error(`Service booking not found for serviceId: ${serviceId}`); + return; + } + + await serviceRef.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 service booking status to 'CONFIRMED' for serviceId: ${serviceId}`); + + const serviceData = serviceDoc.data(); + const gymId = orderData.metaInfo?.gymId || serviceData?.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 = `SRV-${payload.merchantOrderId.substring(0, 8)}`; + logger.info(`Generated invoice number for service: ${invoiceNumber}`); + + const discountPercentage = orderData.metaInfo?.discount || 0; + const hasDiscount = discountPercentage > 0; + const isFreeService = discountPercentage === 100; + const originalAmount = hasDiscount ? + orderData.amount / (1 - discountPercentage / 100) : + orderData.amount; + const discountText = isFreeService ? + " (Free Service)" : + hasDiscount ? ` (${discountPercentage.toFixed(0)}% discount applied)` : + ''; + const amountSaved = hasDiscount ? + originalAmount - orderData.amount : + 0; + + const invoiceData = { + invoiceNumber, + businessName: gymName, + address: gymAddress, + gstNumber: orderData.metaInfo?.gstNumber, + customerName: orderData.metaInfo?.customerName || serviceData?.customerName || '', + phoneNumber: orderData.metaInfo?.customerPhone || serviceData?.phoneNumber || '', + email: orderData.metaInfo?.customerEmail || serviceData?.email || '', + planName: orderData.metaInfo?.serviceName || serviceData?.serviceName || 'Service', + amount: orderData.amount, + transactionId: payload.orderId, + paymentDate: new Date(), + paymentMethod: 'Online' + }; + + const invoicePath = await invoiceService.generateInvoice(invoiceData); + logger.info(`Service invoice generated successfully at path: ${invoicePath}`); + + await serviceRef.update({ + invoicePath: invoicePath, + invoiceNumber: invoiceNumber + }); + + logger.info(`Updated service booking with invoice path: ${invoicePath}`); + + const downloadUrl = await invoiceService.getInvoiceDownloadUrl(invoicePath); + const formattedDate = format(new Date(), 'dd/MM/yyyy'); + + if (invoiceData.email) { + logger.info(`Preparing to send service invoice email to customer: ${invoiceData.email}`); + try { + const emailSubject = isFreeService + ? `Free Service Confirmed - ${gymName}` + : `Service Booking Confirmed - ${gymName}`; + + const customerEmailHtml = ` + + +

${isFreeService ? 'Free Service Confirmed' : 'Service Booking Confirmed'}

+

Dear ${invoiceData.customerName},

+

${isFreeService ? 'Your free service has been successfully confirmed.' : 'Thank you for your payment. Your service booking has been confirmed.'}

+

Please find attached your invoice for the service.

+

Service Details:

+ +

If you have any questions, please contact us.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + invoiceData.email, + emailSubject, + customerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Service invoice email sent to customer (${invoiceData.email}) for payment: ${payload.merchantOrderId}`); + } catch (emailError) { + logger.error('Error sending customer service invoice email:', emailError); + } + } + + if (gymOwnerEmail) { + logger.info(`Preparing to send service invoice email to gym owner: ${gymOwnerEmail}`); + try { + const ownerEmailSubject = isFreeService + ? `Free Service Assigned - ${gymName}` + : `New Service Booking - ${gymName}`; + + const gymOwnerEmailHtml = ` + + +

${isFreeService ? 'Free Service Assigned' : 'New Service Booking Received'}

+

Dear Gym Owner,

+

${isFreeService ? 'A free service has been assigned' : 'A new service booking has been received'} for your gym.

+

Customer Details:

+ +

Service Details:

+ +

Please find the invoice attached.

+

Regards,
Fitlien Team

+ + + `; + + await sendEmailWithAttachmentUtil( + gymOwnerEmail, + ownerEmailSubject, + gymOwnerEmailHtml, + downloadUrl, + `Invoice_${path.basename(invoicePath)}` + ); + + logger.info(`Service invoice email sent to gym owner (${gymOwnerEmail}) for payment: ${payload.merchantOrderId}`); + } catch (ownerEmailError) { + logger.error('Error sending gym owner service invoice email:', ownerEmailError); + } + } + + } catch (invoiceError) { + logger.error('Error generating service invoice:', invoiceError); + } + } + + } catch (error) { + logger.error('Error processing service payment:', error); + } +}