From c28597f3ee49f351b2f8a4c76b9bd712665ca57a Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 5 May 2025 17:59:23 +0530 Subject: [PATCH] phonepe function completed --- firebase.json | 2 +- functions/src/payments/phonepe/index.ts | 2 - functions/src/payments/phonepe/paymentLink.ts | 183 ------------------ .../src/payments/phonepe/paymentLinkStatus.ts | 122 ------------ functions/src/payments/phonepe/webhook.ts | 56 +++--- 5 files changed, 31 insertions(+), 334 deletions(-) delete mode 100644 functions/src/payments/phonepe/paymentLink.ts delete mode 100644 functions/src/payments/phonepe/paymentLinkStatus.ts diff --git a/firebase.json b/firebase.json index 0eb205c..d8682a0 100644 --- a/firebase.json +++ b/firebase.json @@ -25,7 +25,7 @@ "port": 5005 }, "firestore": { - "port": 8081 + "port": 8085 }, "storage": { "port": 9199 diff --git a/functions/src/payments/phonepe/index.ts b/functions/src/payments/phonepe/index.ts index 2fe0eda..258f121 100644 --- a/functions/src/payments/phonepe/index.ts +++ b/functions/src/payments/phonepe/index.ts @@ -1,5 +1,3 @@ export { createPhonePeOrder } from './createPhonepeOrder'; export { checkPhonePePaymentStatus } from './checkStatus'; -// export { createPhonePePaymentLink } from './paymentLink'; export { phonePeWebhook } from './webhook'; -// export { checkPhonePePaymentLinkStatus } from './paymentLinkStatus'; diff --git a/functions/src/payments/phonepe/paymentLink.ts b/functions/src/payments/phonepe/paymentLink.ts deleted file mode 100644 index a676868..0000000 --- a/functions/src/payments/phonepe/paymentLink.ts +++ /dev/null @@ -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 - }); - } - }); - }); diff --git a/functions/src/payments/phonepe/paymentLinkStatus.ts b/functions/src/payments/phonepe/paymentLinkStatus.ts deleted file mode 100644 index 1364fe4..0000000 --- a/functions/src/payments/phonepe/paymentLinkStatus.ts +++ /dev/null @@ -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 - }); - } - }); -}); diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 5f52274..d8696dc 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -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 });