Some checks failed
Deploy FitLien services to Dev / Deploy to Dev (push) Failing after 55s
Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: #27
258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
import axios from "axios";
|
|
import { onRequest } from "firebase-functions/v2/https";
|
|
import { Request} from "firebase-functions/v2/https";
|
|
import { getCorsHandler } from "../../shared/middleware";
|
|
import { getAdmin, getLogger } from "../../shared/config";
|
|
import { updatePaymentDataAfterSuccess } from "./paymentData";
|
|
import { InvoiceService } from "./invoice/invoiceService";
|
|
|
|
const admin = getAdmin();
|
|
const logger = getLogger();
|
|
const corsHandler = getCorsHandler();
|
|
const invoiceService = new InvoiceService();
|
|
|
|
export const checkPhonePePaymentStatus = onRequest({
|
|
region: '#{SERVICES_RGN}#'
|
|
}, async (request: Request, response) => {
|
|
return corsHandler(request, response, async () => {
|
|
try {
|
|
const authHeader = request.headers.authorization || '';
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
response.status(401).json({ error: 'Unauthorized' });
|
|
return;
|
|
}
|
|
|
|
const idToken = authHeader.split('Bearer ')[1];
|
|
try {
|
|
await admin.auth().verifyIdToken(idToken);
|
|
|
|
const merchantOrderId = request.query.merchantOrderId as string;
|
|
if (!merchantOrderId) {
|
|
response.status(400).json({ error: 'Missing merchant order ID' });
|
|
return;
|
|
}
|
|
|
|
const details = request.query.details === 'true';
|
|
const errorContext = request.query.errorContext === 'true';
|
|
|
|
const clientId = process.env.PHONEPE_CLIENT_ID;
|
|
const clientSecret = process.env.PHONEPE_CLIENT_SECRET;
|
|
const apiUrl = process.env.PHONEPE_API_URL;
|
|
const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1';
|
|
|
|
if (!clientId || !clientSecret || !apiUrl) {
|
|
logger.error('PhonePe credentials not configured');
|
|
response.status(500).json({ error: 'Payment gateway configuration error' });
|
|
return;
|
|
}
|
|
|
|
const tokenResponse = await axios.post(
|
|
`${apiUrl}/v1/oauth/token`,
|
|
{
|
|
client_id: clientId,
|
|
client_version: clientVersion,
|
|
client_secret: clientSecret,
|
|
grant_type: 'client_credentials',
|
|
},
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
}
|
|
);
|
|
|
|
const accessToken = tokenResponse.data.access_token;
|
|
|
|
const queryParams = new URLSearchParams();
|
|
if (details) queryParams.append('details', 'true');
|
|
if (errorContext) queryParams.append('errorContext', 'true');
|
|
const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
|
|
|
|
const statusResponse = await axios.get(
|
|
`${apiUrl}/checkout/v2/order/${merchantOrderId}/status${queryString}`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `O-Bearer ${accessToken}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
const orderQuery = await admin.firestore()
|
|
.collection('payment_orders')
|
|
.where('merchantOrderId', '==', merchantOrderId)
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (orderQuery.empty) {
|
|
logger.error(`No payment order found with PhonePe orderId: ${merchantOrderId}`);
|
|
response.status(404).json({
|
|
success: false,
|
|
error: 'Payment order not found',
|
|
message: `No record found for PhonePe order ID: ${merchantOrderId}`
|
|
});
|
|
return;
|
|
}
|
|
|
|
const orderDoc = orderQuery.docs[0];
|
|
const orderData = orderDoc.data();
|
|
|
|
await orderDoc.ref.update({
|
|
orderStatus: statusResponse.data.state || 'UNKNOWN',
|
|
lastChecked: new Date(),
|
|
statusResponse: statusResponse.data
|
|
});
|
|
|
|
if (statusResponse.data.state === 'COMPLETED') {
|
|
try {
|
|
// Update payment data
|
|
const paymentUpdateSuccess = await updatePaymentDataAfterSuccess(
|
|
merchantOrderId,
|
|
statusResponse.data.orderId,
|
|
statusResponse.data
|
|
);
|
|
|
|
if (paymentUpdateSuccess) {
|
|
// Extract membership ID from metaInfo
|
|
const membershipId = orderData.metaInfo?.membershipId;
|
|
|
|
if (membershipId) {
|
|
try {
|
|
// Get user data for invoice
|
|
const membershipDoc = await admin.firestore()
|
|
.collection('memberships')
|
|
.doc(membershipId)
|
|
.get();
|
|
|
|
if (membershipDoc.exists) {
|
|
const membershipData = membershipDoc.data();
|
|
const userId = membershipData?.userId;
|
|
|
|
// Get user details
|
|
const userDoc = await admin.firestore()
|
|
.collection('users')
|
|
.doc(userId)
|
|
.get();
|
|
|
|
if (userDoc.exists) {
|
|
const userData = userDoc.data();
|
|
|
|
// Get gym details
|
|
const gymId = orderData.metaInfo?.gymId || membershipData?.gymId;
|
|
let gymName = 'Fitlien';
|
|
let gymAddress = '';
|
|
let gstNumber = '';
|
|
|
|
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 || '';
|
|
gstNumber = gymData?.gstNumber || '';
|
|
}
|
|
}
|
|
|
|
// Generate invoice data
|
|
const invoiceData = {
|
|
invoiceNumber: `INV-${merchantOrderId.substring(0, 8)}`,
|
|
businessName: gymName,
|
|
address: gymAddress,
|
|
gstNumber: gstNumber,
|
|
customerName: userData?.displayName || `${membershipData?.['first-name'] || ''} ${membershipData?.['last-name'] || ''}`.trim(),
|
|
phoneNumber: membershipData?.['phone-number'] || orderData.metaInfo?.phoneNumber || '',
|
|
email: membershipData?.['email'] || '',
|
|
planName: orderData.metaInfo?.planName || 'Membership',
|
|
amount: orderData.amount,
|
|
transactionId: statusResponse.data.orderId,
|
|
paymentDate: new Date(),
|
|
paymentMethod: 'Online'
|
|
};
|
|
|
|
const invoicePath = await invoiceService.generateInvoice(invoiceData);
|
|
|
|
// Update payment record with invoice path
|
|
await admin.firestore()
|
|
.collection('membership_payments')
|
|
.doc(membershipId)
|
|
.get()
|
|
.then(async (doc) => {
|
|
if (doc.exists) {
|
|
const paymentsData = doc.data()?.payments || [];
|
|
for (let i = 0; i < paymentsData.length; i++) {
|
|
if (paymentsData[i].referenceNumber === merchantOrderId ||
|
|
paymentsData[i].transactionId === statusResponse.data.orderId) {
|
|
paymentsData[i].invoicePath = invoicePath;
|
|
break;
|
|
}
|
|
}
|
|
|
|
await doc.ref.update({
|
|
'payments': paymentsData,
|
|
'updatedAt': admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
}
|
|
});
|
|
|
|
logger.info(`Generated invoice for payment: ${merchantOrderId}, path: ${invoicePath}`);
|
|
}
|
|
}
|
|
} catch (invoiceError) {
|
|
logger.error('Error generating invoice:', invoiceError);
|
|
// Continue processing - don't fail the response
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info(`Payment data updated for completed payment: ${merchantOrderId}`);
|
|
} catch (paymentUpdateError) {
|
|
logger.error('Error updating payment data:', paymentUpdateError);
|
|
// Continue processing - don't fail the response
|
|
}
|
|
}
|
|
|
|
logger.info('PhonePe status response data:', JSON.stringify(statusResponse.data));
|
|
|
|
response.json({
|
|
success: true,
|
|
state: statusResponse.data.state,
|
|
data: statusResponse.data
|
|
});
|
|
|
|
} catch (authError: any) {
|
|
logger.error('Authentication error:', authError);
|
|
|
|
if (authError.response) {
|
|
logger.error('API error details:', {
|
|
status: authError.response.status,
|
|
data: authError.response.data
|
|
});
|
|
|
|
response.status(authError.response.status).json({
|
|
success: false,
|
|
error: 'API error',
|
|
details: authError.response.data
|
|
});
|
|
} else {
|
|
response.status(401).json({
|
|
success: false,
|
|
error: 'Invalid authentication token or API error',
|
|
message: authError.message
|
|
});
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
logger.error('PhonePe payment status check error:', error);
|
|
response.status(500).json({
|
|
success: false,
|
|
error: 'Failed to check payment status',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
});
|