phonepe function completed

This commit is contained in:
AllenTJ7 2025-05-05 17:59:23 +05:30
parent 22fb060cb9
commit c28597f3ee
5 changed files with 31 additions and 334 deletions

View File

@ -25,7 +25,7 @@
"port": 5005
},
"firestore": {
"port": 8081
"port": 8085
},
"storage": {
"port": 9199

View File

@ -1,5 +1,3 @@
export { createPhonePeOrder } from './createPhonepeOrder';
export { checkPhonePePaymentStatus } from './checkStatus';
// export { createPhonePePaymentLink } from './paymentLink';
export { phonePeWebhook } from './webhook';
// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus';

View File

@ -1,183 +0,0 @@
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 axios from "axios";
const admin = getAdmin();
const logger = getLogger();
const corsHandler = getCorsHandler();
interface PaymentLinkPayload {
merchantId: string;
merchantOrderId: string;
merchantUserId: string;
amount: number;
mobileNumber?: string;
email?: string;
shortName: string;
expiryDate: number;
redirectUrl: string;
redirectMode: string;
paymentInstrument: {
type: string;
};
notifyCustomer?: boolean;
}
export const createPhonePePaymentLink = 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 {
const decodedToken = await admin.auth().verifyIdToken(idToken);
const uid = decodedToken.uid;
const {
amount,
orderId,
customerName,
customerEmail,
customerPhone,
productInfo,
expiryDays = 7,
callbackUrl,
notifyCustomer = false
} = request.body;
if (!amount || !orderId) {
response.status(400).json({ error: 'Missing required fields' });
return;
}
const clientId = process.env.PHONEPE_CLIENT_ID;
const clientSecret = process.env.PHONEPE_CLIENT_SECRET;
const apiUrl = process.env.PHONEPE_API_URL;
const merchantId = process.env.PHONEPE_MERCHANT_ID;
const clientVersion = process.env.PHONEPE_CLIENT_VERSION || '1';
if (!clientId || !clientSecret || !apiUrl || !merchantId) {
logger.error('PhonePe credentials not configured');
response.status(500).json({ error: 'Payment gateway configuration error' });
return;
}
try {
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 expiryInSeconds = Math.floor(Date.now() / 1000) + (expiryDays * 24 * 60 * 60);
const paymentLinkPayload: PaymentLinkPayload = {
merchantId: merchantId,
merchantOrderId: orderId,
merchantUserId: uid,
amount: parseInt(amount) * 100,
shortName: productInfo || "Payment",
expiryDate: expiryInSeconds,
redirectUrl: callbackUrl,
redirectMode: "REDIRECT",
paymentInstrument: {
type: "PAY_PAGE"
}
};
if (customerPhone) {
paymentLinkPayload.mobileNumber = customerPhone;
}
if (customerEmail) {
paymentLinkPayload.email = customerEmail;
}
if (notifyCustomer && (customerEmail || customerPhone)) {
paymentLinkPayload.notifyCustomer = true;
}
const paymentLinkResponse = await axios.post(
`${apiUrl}/v3/payment-links/create`,
paymentLinkPayload,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
}
);
try {
await admin.firestore().collection('payment_links').doc(orderId).set({
userId: uid,
amount: amount,
customerName: customerName,
customerEmail: customerEmail,
customerPhone: customerPhone,
orderStatus: 'CREATED',
paymentGateway: 'PhonePe',
createdAt: new Date(),
expiryDate: new Date(expiryInSeconds * 1000),
orderId: orderId,
paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId,
paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl,
rawResponse: paymentLinkResponse.data
});
} catch (firestoreError) {
logger.error('Error storing payment link in Firestore:', firestoreError);
}
response.json({
success: true,
orderId: orderId,
paymentLinkId: paymentLinkResponse.data.data?.linkId || paymentLinkResponse.data.linkId,
paymentLinkUrl: paymentLinkResponse.data.data?.linkUrl || paymentLinkResponse.data.linkUrl,
response: paymentLinkResponse.data
});
logger.info(`PhonePe payment link created: ${orderId}`);
} catch (apiError: any) {
logger.error('PhonePe API error:', apiError.response?.data || apiError);
response.status(apiError.response?.status || 500).json({
success: false,
error: 'Payment gateway error',
details: apiError.response?.data || apiError.message,
code: apiError.code
});
}
} catch (authError) {
logger.error('Authentication error:', authError);
response.status(401).json({
success: false,
error: 'Invalid authentication token'
});
}
} catch (error: any) {
logger.error('PhonePe payment link creation error:', error);
response.status(500).json({
success: false,
error: 'Failed to create payment link',
details: error.message
});
}
});
});

View File

@ -1,122 +0,0 @@
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 axios from "axios";
const admin = getAdmin();
const logger = getLogger();
const corsHandler = getCorsHandler();
export const checkPhonePePaymentLinkStatus = 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 linkId = request.query.linkId as string;
if (!linkId) {
response.status(400).json({ error: 'Missing payment link ID' });
return;
}
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 statusResponse = await axios.get(
`${apiUrl}/v3/payment-links/${linkId}/status`,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
}
);
const linkQuery = await admin.firestore()
.collection('payment_links')
.where('paymentLinkId', '==', linkId)
.limit(1)
.get();
if (!linkQuery.empty) {
const linkDoc = linkQuery.docs[0];
await linkDoc.ref.update({
orderStatus: statusResponse.data.data?.state || statusResponse.data.state || 'UNKNOWN',
lastChecked: new Date(),
statusResponse: statusResponse.data
});
}
response.json({
success: true,
state: statusResponse.data.data?.state || statusResponse.data.state,
data: statusResponse.data
});
} catch (authError: any) {
logger.error('Authentication or API 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 link status check error:', error);
response.status(500).json({
success: false,
error: 'Failed to check payment link status',
details: error.message
});
}
});
});

View File

@ -10,57 +10,61 @@ export const phonePeWebhook = onRequest({
region: '#{SERVICES_RGN}#'
}, async (request: Request, response) => {
try {
const signature = request.headers['x-verify'] as string;
const webhookSecret = process.env.PHONEPE_WEBHOOK_SECRET;
const authHeader = request.headers['authorization'] as string;
const username = process.env.PHONEPE_WEBHOOK_USERNAME;
const password = process.env.PHONEPE_WEBHOOK_PASSWORD;
if (!signature || !webhookSecret) {
logger.error('Missing signature or webhook secret');
if (!authHeader || !username || !password) {
logger.error('Missing authorization header or webhook credentials');
response.status(401).json({ error: 'Unauthorized' });
return;
}
const rawBody = JSON.stringify(request.body);
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
// Calculate expected authorization value
const credentialString = `${username}:${password}`;
const expectedAuth = crypto
.createHash('sha256')
.update(credentialString)
.digest('hex');
if (signature !== expectedSignature) {
logger.error('Invalid webhook signature');
response.status(401).json({ error: 'Invalid signature' });
// PhonePe may send the header with a prefix like "SHA256 " or just the hash
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, data } = request.body;
const { event, payload } = request.body;
if (!event || !data || !data.merchantOrderId || !data.orderId) {
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: data.merchantOrderId,
orderId: data.orderId,
state: data.state
merchantOrderId: payload.merchantOrderId,
orderId: payload.orderId,
state: payload.state
});
const orderQuery = await admin.firestore()
.collection('payment_orders')
.where('orderId', '==', data.orderId)
.where('orderId', '==', payload.orderId)
.limit(1)
.get();
if (orderQuery.empty) {
const merchantOrderQuery = await admin.firestore()
.collection('payment_orders')
.where('merchantOrderId', '==', data.merchantOrderId)
.where('merchantOrderId', '==', payload.merchantOrderId)
.limit(1)
.get();
if (merchantOrderQuery.empty) {
logger.error(`No payment order found for PhonePe orderId: ${data.orderId} or merchantOrderId: ${data.merchantOrderId}`);
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'
@ -70,23 +74,23 @@ export const phonePeWebhook = onRequest({
const orderDoc = merchantOrderQuery.docs[0];
await orderDoc.ref.update({
orderStatus: data.state || 'UNKNOWN',
orderStatus: payload.state || 'UNKNOWN',
lastUpdated: new Date(),
webhookEvent: event,
webhookData: data
webhookData: payload
});
logger.info(`Updated order status via webhook for merchantOrderId: ${data.merchantOrderId} to ${data.state}`);
logger.info(`Updated order status via webhook for merchantOrderId: ${payload.merchantOrderId} to ${payload.state}`);
} else {
const orderDoc = orderQuery.docs[0];
await orderDoc.ref.update({
orderStatus: data.state || 'UNKNOWN',
orderStatus: payload.state || 'UNKNOWN',
lastUpdated: new Date(),
webhookEvent: event,
webhookData: data
webhookData: payload
});
logger.info(`Updated order status via webhook for orderId: ${data.orderId} to ${data.state}`);
logger.info(`Updated order status via webhook for orderId: ${payload.orderId} to ${payload.state}`);
}
response.status(200).json({ success: true });