phonepe function completed
This commit is contained in:
parent
22fb060cb9
commit
c28597f3ee
@ -25,7 +25,7 @@
|
||||
"port": 5005
|
||||
},
|
||||
"firestore": {
|
||||
"port": 8081
|
||||
"port": 8085
|
||||
},
|
||||
"storage": {
|
||||
"port": 9199
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
export { createPhonePeOrder } from './createPhonepeOrder';
|
||||
export { checkPhonePePaymentStatus } from './checkStatus';
|
||||
// export { createPhonePePaymentLink } from './paymentLink';
|
||||
export { phonePeWebhook } from './webhook';
|
||||
// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus';
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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 });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user