import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; import { getAdmin, getLogger } from "../../shared/config"; import crypto from "crypto"; import { updatePaymentDataAfterSuccess } from "./paymentData"; import { InvoiceService } from "./invoice/invoiceService"; import * as path from 'path'; import { sendEmailWithAttachmentUtil } from "../../utils/emailService"; import { format } from 'date-fns'; const admin = getAdmin(); const logger = getLogger(); const invoiceService = new InvoiceService(); export const phonePeWebhook = onRequest({ region: '#{SERVICES_RGN}#' }, async (request: Request, response) => { try { logger.info('Received webhook request', { headers: request.headers, body: request.body, method: request.method }); const authHeader = request.headers['authorization'] as string; const username = process.env.PHONEPE_WEBHOOK_USERNAME; const password = process.env.PHONEPE_WEBHOOK_PASSWORD; if (!authHeader || !username || !password) { logger.error('Missing authorization header or webhook credentials'); response.status(401).json({ error: 'Unauthorized' }); return; } const credentialString = `${username}:${password}`; const expectedAuth = crypto .createHash('sha256') .update(credentialString) .digest('hex'); const receivedAuth = authHeader.replace(/^SHA256\s+/i, ''); if (receivedAuth.toLowerCase() !== expectedAuth.toLowerCase()) { logger.error('Invalid webhook authorization'); response.status(401).json({ error: 'Invalid authorization' }); return; } const { event, payload } = request.body; if (!event || !payload || !payload.merchantOrderId || !payload.orderId) { logger.error('Invalid webhook payload', request.body); response.status(400).json({ error: 'Invalid payload' }); return; } logger.info(`Received PhonePe webhook: ${event}`, { merchantOrderId: payload.merchantOrderId, orderId: payload.orderId, state: payload.state }); const orderQuery = await admin.firestore() .collection('payment_orders') .where('orderId', '==', payload.orderId) .limit(1) .get(); let orderDoc; if (orderQuery.empty) { const merchantOrderQuery = await admin.firestore() .collection('payment_orders') .where('merchantOrderId', '==', payload.merchantOrderId) .limit(1) .get(); if (merchantOrderQuery.empty) { logger.error(`No payment order found for PhonePe orderId: ${payload.orderId} or merchantOrderId: ${payload.merchantOrderId}`); response.status(404).json({ success: false, error: 'Payment order not found' }); return; } orderDoc = merchantOrderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`); } else { orderDoc = orderQuery.docs[0]; await orderDoc.ref.update({ orderStatus: payload.state || 'UNKNOWN', lastUpdated: new Date(), webhookEvent: event, webhookData: payload }); logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`); } 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}`); const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( payload.merchantOrderId, payload.orderId, payload ); logger.info(`Payment update result for merchantOrderId: ${payload.merchantOrderId}`, { success: paymentUpdateSuccess, orderId: payload.orderId }); 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; 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(), phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', email: membershipData?.fields?.['email'] || '', 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 (membershipData?.fields?.['email']) { 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 = `
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:
If you have any questions, please contact us.
Regards,
Fitlien Team
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:
Booking Details:
Please find the invoice attached.
Regards,
Fitlien Team
Dear ${trainerData.fullName || 'Trainer'},
A new client has signed up for personal training with you at ${gymName}.
Client Details:
Booking Details:
Please find the invoice attached.
Regards,
Fitlien Team