Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m47s
Deploy FitLien services / Deploy (push) Successful in 4m30s

This commit is contained in:
Benoy Bose 2025-05-30 12:01:13 +05:30
commit ecbe9d184b
4 changed files with 550 additions and 335 deletions

View File

@ -1,4 +1,4 @@
name: Deploy FitLien services to Dev name: Deploy FitLien services to QA
on: on:
push: push:

View File

@ -0,0 +1,64 @@
name: Deploy FitLien services
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env
- name: Replace variables in .env
run: |
sed -i "s/#{TWILIO_ACCOUNT_SID}#/${{ secrets.TWILIO_ACCOUNT_SID }}/" functions/.env
sed -i "s/#{TWILIO_AUTH_TOKEN}#/${{ secrets.TWILIO_AUTH_TOKEN }}/" functions/.env
sed -i "s/#{TWILIO_PHONE_NUMBER}#/${{ secrets.TWILIO_PHONE_NUMBER }}/" functions/.env
sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/" functions/.env
sed -i "s/#{GOOGLE_MAPS_API_KEY}#/${{ secrets.GOOGLE_MAPS_API_KEY }}/" functions/.env
sed -i "s/#{SES_FROM_EMAIL}#/${{ vars.SES_FROM_EMAIL }}/" functions/.env
sed -i "s/#{SES_REPLY_TO_EMAIL}#/${{ vars.SES_REPLY_TO_EMAIL }}/" functions/.env
sed -i "s/#{AWS_ACCESS_KEY_ID}#/${{ secrets.AWS_ACCESS_KEY_ID }}/" functions/.env
sed -i "s/#{AWS_SECRET_ACCESS_KEY}#/${{ secrets.AWS_SECRET_ACCESS_KEY }}/" functions/.env
sed -i "s/#{AWS_REGION}#/${{ secrets.AWS_REGION }}/" functions/.env
sed -i "s/#{PHONEPE_CLIENT_ID}#/${{ secrets.PHONEPE_CLIENT_ID }}/" functions/.env
sed -i "s/#{PHONEPE_CLIENT_SECRET}#/${{ secrets.PHONEPE_CLIENT_SECRET }}/" functions/.env
sed -i "s/#{PHONEPE_API_URL}#/${{ secrets.PHONEPE_API_URL }}/" functions/.env
sed -i "s/#{PHONEPE_WEBHOOK_USERNAME}#/${{ secrets.PHONEPE_WEBHOOK_USERNAME }}/" functions/.env
sed -i "s/#{PHONEPE_WEBHOOK_PASSWORD}#/${{ secrets.PHONEPE_WEBHOOK_PASSWORD }}/" functions/.env
cat functions/.env
- name: "Replace #{SERVICES_RGN}# in all .ts files"
run: |
find . -type f -name "*.ts" -exec sed -i "s/#{SERVICES_RGN}#/${{ vars.SERVICES_RGN }}/g" {} +
- name: Build
run: |
npm install -g typescript
cd functions
npm install
npx tsc
cd ..
ls -la
- name: Deploy
run: |
curl -sL firebase.tools | upgrade=true bash
firebase use --token ${{ secrets.FIREBASE_TOKEN }} release
firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}" --force --non-interactive

View File

@ -1,7 +1,8 @@
{ {
"firestore": { "firestore": {
"rules": "firestore.rules", "rules": "firestore.rules",
"indexes": "firestore.indexes.json" "indexes": "firestore.indexes.json",
"database": "(default)"
}, },
"functions": [ "functions": [
{ {

View File

@ -117,22 +117,190 @@ export const phonePeWebhook = onRequest({
try { try {
logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`); logger.info(`Starting payment update process for merchantOrderId: ${payload.merchantOrderId}`);
const orderData = orderDoc.data();
const membershipId = orderData.metaInfo?.membershipId;
const bookingId = orderData.metaInfo?.bookingId;
if (bookingId) {
await processDayPassBooking(payload, orderData, bookingId);
} else if (membershipId) {
const paymentUpdateSuccess = await updatePaymentDataAfterSuccess( const paymentUpdateSuccess = await updatePaymentDataAfterSuccess(
payload.merchantOrderId, payload.merchantOrderId,
payload.orderId, payload.orderId,
payload payload
); );
logger.info(`Payment update result for merchantOrderId: ${payload.merchantOrderId}`, { logger.info(`Payment update result for membershipId: ${membershipId}`, {
success: paymentUpdateSuccess, success: paymentUpdateSuccess,
orderId: payload.orderId orderId: payload.orderId
}); });
if (paymentUpdateSuccess) { if (paymentUpdateSuccess) {
const orderData = orderDoc.data(); await processMembershipPayment(payload, orderData, membershipId);
const membershipId = orderData.metaInfo?.membershipId; }
} else {
logger.error(`No membershipId or bookingId found in metaInfo for order: ${payload.merchantOrderId}`);
}
logger.info(`Processing invoice for completed payment`, { 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
});
}
});
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 = `
<html>
<body>
<h2>New Day Pass Payment Received</h2>
<p>Dear Gym Owner,</p>
<p>A new day pass payment has been received for your gym.</p>
<p>Customer Details:</p>
<ul>
<li>Name: ${invoiceData.customerName}</li>
<li>Phone: ${invoiceData.phoneNumber}</li>
</ul>
<p>Day Pass Details:</p>
<ul>
<li>Service: Day Pass</li>
<li>Amount: ${orderData.amount.toFixed(2)}</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(`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, merchantOrderId: payload.merchantOrderId,
orderId: payload.orderId, orderId: payload.orderId,
membershipId: membershipId || 'not-provided' membershipId: membershipId || 'not-provided'
@ -455,21 +623,3 @@ export const phonePeWebhook = onRequest({
} }
} }
} }
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
});
}
});