fitlien-services/functions/src/payments/phonepe/webhook.ts
Allen T J a146315536
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m40s
phonepe (#47)
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com>
Reviewed-on: #47
2025-05-23 08:14:11 +00:00

475 lines
21 KiB
TypeScript

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 = `
<html>
<body>
<h2>${isFreeplan ? 'Free Plan Assigned' : 'Thank you for your payment'}</h2>
<p>Dear ${invoiceData.customerName},</p>
<p>${isFreeplan ? 'Your free membership has been successfully activated.' : 'Thank you for your payment. Your membership has been successfully activated.'}</p>
<p>Please find attached your invoice for the ${isFreeplan ? 'membership' : 'payment'}.</p>
<p>Membership Details:</p>
<ul>
<li>Gym: ${gymName}</li>
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Your Personal Trainer'}</li>` : ''}
<li>Plan: ${invoiceData.planName}</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>You Save: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
${isFreeplan ? '<li>Payment Method: Online}</li>' : ''}
</ul>
<p>If you have any questions, please contact us.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
await sendEmailWithAttachmentUtil(
membershipData?.fields?.['email'],
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 = `
<html>
<body>
<h2>${isFreeplan ? 'Free Plan Assigned' : `New ${paymentType} Booking Received`}</h2>
<p>Dear Gym Owner,</p>
<p>${isFreeplan ? 'A free membership' : 'A new membership'}${paymentType === 'Gym Membership with Personal Training' ? ' with personal training' : ''} has been ${isFreeplan ? 'assigned' : 'received'} for your gym.</p>
<p>Customer Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Email: ${invoiceData.email}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Booking Details:</p>
<ul>
<li>Type: ${invoiceData.planName}</li>
${trainerData ? `<li>Trainer: ${trainerData.fullName || 'Personal Trainer'}</li>` : ''}
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
${hasDiscount ? `<li>Amount Saved by Customer: ₹${amountSaved.toFixed(2)}</li>` : ''}
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
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 = `
<html>
<body>
<h2>New Personal Training Client</h2>
<p>Dear ${trainerData.fullName || 'Trainer'},</p>
<p>A new client has signed up for personal training with you at ${gymName}.</p>
<p>Client Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Email: ${invoiceData.email}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Booking Details:</p>
<ul>
<li>Type: Personal Training Membership</li>
${hasDiscount ? `<li>Original Price: ₹${originalAmount.toFixed(2)}</li>` : ''}
${hasDiscount ? `<li>Discount: ${discountPercentage.toFixed(1)}%</li>` : ''}
<li>Amount: ₹${orderData.amount.toFixed(2)}${discountText}</li>
<li>Transaction ID: ${payload.merchantOrderId}</li>
<li>Date: ${formattedDate}</li>
</ul>
<p>Please find the invoice attached.</p>
<p>Regards,<br>Fitlien Team</p>
</body>
</html>
`;
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);
}
}
}
logger.info(`Payment data updated for completed payment: ${payload.merchantOrderId}`);
} catch (paymentUpdateError) {
logger.error('Error updating payment data:', paymentUpdateError);
}
}
response.status(200).json({ success: true });
} catch (error: any) {
logger.error('PhonePe webhook processing error:', error);
response.status(500).json({
success: false,
error: 'Failed to process webhook',
details: error.message
});
}
});