Spaces:
Running
Running
| const axios = require('axios'); | |
| const PAYMOB_API_KEY = process.env.PAYMOB_API_KEY; | |
| const PAYMOB_BASE_URL = 'https://accept.paymob.com/api'; | |
| /** | |
| * Step 1: Authenticate with Paymob to get authentication token | |
| */ | |
| const getAuthToken = async () => { | |
| try { | |
| const response = await axios.post(`${PAYMOB_BASE_URL}/auth/tokens`, { | |
| api_key: PAYMOB_API_KEY, | |
| }); | |
| return response.data.token; | |
| } catch (error) { | |
| console.error( | |
| 'Paymob authentication error:', | |
| error.response && error.response.data | |
| ? error.response.data | |
| : error.message, | |
| ); | |
| throw new Error('Failed to authenticate with Paymob'); | |
| } | |
| }; | |
| /** | |
| * Step 2: Create an order on Paymob | |
| */ | |
| const createPaymobOrder = async (authToken, orderData) => { | |
| try { | |
| const response = await axios.post(`${PAYMOB_BASE_URL}/ecommerce/orders`, { | |
| auth_token: authToken, | |
| delivery_needed: 'true', | |
| amount_cents: Math.round(orderData.amount * 100), // Convert to cents | |
| currency: 'EGP', | |
| items: orderData.items.map((item) => ({ | |
| name: item.name, | |
| amount_cents: Math.round(item.unitPrice * 100), | |
| description: item.description || item.name, | |
| quantity: item.quantity, | |
| })), | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| console.error( | |
| 'Paymob order creation error:', | |
| error.response && error.response.data | |
| ? error.response.data | |
| : error.message, | |
| ); | |
| throw new Error('Failed to create Paymob order'); | |
| } | |
| }; | |
| /** | |
| * Step 3: Generate payment key for the payment iframe | |
| */ | |
| const getPaymentKey = async (authToken, orderData, paymobOrderId) => { | |
| try { | |
| const response = await axios.post( | |
| `${PAYMOB_BASE_URL}/acceptance/payment_keys`, | |
| { | |
| auth_token: authToken, | |
| amount_cents: Math.round(orderData.amount * 100), | |
| expiration: 3600, // 1 hour | |
| order_id: paymobOrderId, | |
| billing_data: { | |
| apartment: orderData.billingData.apartment || 'NA', | |
| email: orderData.billingData.email || 'customer@example.com', | |
| floor: orderData.billingData.floor || 'NA', | |
| first_name: orderData.billingData.firstName, | |
| street: orderData.billingData.street, | |
| building: orderData.billingData.building || 'NA', | |
| phone_number: orderData.billingData.phone, | |
| shipping_method: 'PKG', | |
| postal_code: orderData.billingData.postalCode || 'NA', | |
| city: orderData.billingData.city, | |
| country: orderData.billingData.country || 'EG', | |
| last_name: | |
| orderData.billingData.lastName || orderData.billingData.firstName, | |
| state: orderData.billingData.state || orderData.billingData.city, | |
| }, | |
| currency: 'EGP', | |
| integration_id: process.env.PAYMOB_INTEGRATION_ID, // You'll need to add this from Paymob dashboard | |
| }, | |
| ); | |
| return response.data.token; | |
| } catch (error) { | |
| console.error( | |
| 'Paymob payment key error:', | |
| error.response && error.response.data | |
| ? error.response.data | |
| : error.message, | |
| ); | |
| throw new Error('Failed to generate payment key'); | |
| } | |
| }; | |
| /** | |
| * Main function to initiate Paymob payment | |
| * Returns the payment URL/token that frontend will use | |
| */ | |
| const initiatePayment = async (orderData) => { | |
| try { | |
| // Step 1: Get auth token | |
| const authToken = await getAuthToken(); | |
| // Step 2: Create Paymob order | |
| const paymobOrder = await createPaymobOrder(authToken, orderData); | |
| // Step 3: Get payment key | |
| const paymentKey = await getPaymentKey( | |
| authToken, | |
| orderData, | |
| paymobOrder.id, | |
| ); | |
| return { | |
| success: true, | |
| paymentKey, | |
| paymobOrderId: paymobOrder.id, | |
| iframeUrl: `https://accept.paymob.com/api/acceptance/iframes/${process.env.PAYMOB_IFRAME_ID}?payment_token=${paymentKey}`, | |
| }; | |
| } catch (error) { | |
| console.error('Paymob payment initiation error:', error.message); | |
| return { | |
| success: false, | |
| error: error.message, | |
| }; | |
| } | |
| }; | |
| /** | |
| * Verify payment callback from Paymob | |
| */ | |
| const verifyPayment = async (transactionData) => { | |
| try { | |
| const crypto = require('crypto'); | |
| const hmacSecret = process.env.PAYMOB_HMAC_SECRET; | |
| // The data we care about is in .obj for POST callbacks, or the root for GET callbacks | |
| const isPostCallback = !!transactionData.obj; | |
| const data = isPostCallback ? transactionData.obj : transactionData; | |
| console.log('🔍 HMAC Verification Debug:'); | |
| console.log( | |
| 'Callback Type:', | |
| isPostCallback ? 'POST (Processed)' : 'GET (Response)', | |
| ); | |
| console.log('HMAC Secret set:', !!hmacSecret); | |
| // Get the HMAC from the transaction data | |
| // In GET: it's at root level. In POST: it should be at root level too. | |
| const receivedHmac = transactionData.hmac || data.hmac; | |
| if (!receivedHmac) { | |
| console.error('❌ No HMAC found in callback data'); | |
| return { | |
| success: false, | |
| error: 'No HMAC signature provided', | |
| }; | |
| } | |
| // Extract fields carefully with destructuring | |
| const { | |
| amount_cents: amount, | |
| created_at: createdAt, | |
| currency, | |
| id, | |
| integration_id: integrationId, | |
| owner, | |
| } = data; | |
| const errorOccured = String(data.error_occured); | |
| const hasParentTransaction = String(data.has_parent_transaction); | |
| const is3dSecure = String(data.is_3d_secure); | |
| const isAuth = String(data.is_auth); | |
| const isCapture = String(data.is_capture); | |
| const isRefunded = String(data.is_refunded); | |
| const isStandalonePayment = String(data.is_standalone_payment); | |
| const isVoided = String(data.is_voided); | |
| const pending = String(data.pending); | |
| const success = String(data.success); | |
| // orderId extraction | |
| const orderId = | |
| data.order && typeof data.order === 'object' ? data.order.id : data.order; | |
| // source_data fields - handled differently for POST vs GET | |
| const sourcePan = | |
| data.source_data_pan || | |
| (data.source_data && data.source_data.pan ? data.source_data.pan : ''); | |
| const sourceSubType = | |
| data.source_data_sub_type || | |
| (data.source_data && data.source_data.sub_type | |
| ? data.source_data.sub_type | |
| : ''); | |
| const sourceType = | |
| data.source_data_type || | |
| (data.source_data && data.source_data.type ? data.source_data.type : ''); | |
| // VARIATION 1: Standard V1 HMAC (used in both callbacks usually) | |
| const var1String = `${amount}${createdAt}${currency}${errorOccured}${hasParentTransaction}${id}${integrationId}${is3dSecure}${isAuth}${isCapture}${isRefunded}${isStandalonePayment}${isVoided}${orderId}${owner}${pending}${sourcePan}${sourceSubType}${sourceType}${success}`; | |
| // VARIATION 2: Without source_data (often omitted in Response callback) | |
| const var2String = `${amount}${createdAt}${currency}${errorOccured}${hasParentTransaction}${id}${integrationId}${is3dSecure}${isAuth}${isCapture}${isRefunded}${isStandalonePayment}${isVoided}${orderId}${owner}${pending}${success}`; | |
| // VARIATION 3: Alphabetical Sort (Common for Redirect callbacks) | |
| // Paymob documentation for V2 (Redirect) often requires alphabetical sorting of all query params. | |
| const sortedKeys = Object.keys(data) | |
| .filter((key) => key !== 'hmac') | |
| .sort(); | |
| // Use only primitive values for concatenation to avoid [object Object] | |
| const alphabeticalString = sortedKeys | |
| .map((key) => { | |
| const val = data[key]; | |
| if (val === null || val === undefined) return ''; | |
| if (typeof val === 'object') return ''; // Usually objects aren't in the redirect HMAC | |
| return String(val); | |
| }) | |
| .join(''); | |
| const variations = [ | |
| { name: 'Standard V1 (with source_data)', string: var1String }, | |
| { name: 'Standard V1 (WITHOUT source_data)', string: var2String }, | |
| { | |
| name: 'Alphabetical Sort (Redirect Style)', | |
| string: alphabeticalString, | |
| }, | |
| ]; | |
| let matchedVariation = null; | |
| variations.forEach((variation) => { | |
| if (matchedVariation) return; | |
| const hash = crypto | |
| .createHmac('sha512', hmacSecret) | |
| .update(variation.string) | |
| .digest('hex'); | |
| console.log(`Testing ${variation.name}...`); | |
| console.log(`String snippet: ${variation.string.substring(0, 50)}...`); | |
| console.log(`Hash: ${hash}`); | |
| if (hash === receivedHmac) { | |
| matchedVariation = variation.name; | |
| } | |
| }); | |
| if (!matchedVariation) { | |
| console.error('❌ HMAC verification failed for all variations!'); | |
| console.error('Received HMAC:', receivedHmac); | |
| return { | |
| success: false, | |
| hmacVerified: false, | |
| error: 'Invalid HMAC signature', | |
| }; | |
| } | |
| console.log(`✅ HMAC verification successful using: ${matchedVariation}`); | |
| return { | |
| success: String(data.success) === 'true', | |
| hmacVerified: true, | |
| transactionId: data.id, | |
| orderId: orderId, | |
| amount: parseFloat(data.amount_cents) / 100, | |
| currency: data.currency, | |
| }; | |
| } catch (error) { | |
| console.error('Paymob verification error:', error.message); | |
| return { | |
| success: false, | |
| error: error.message, | |
| }; | |
| } | |
| }; | |
| /** | |
| * Refund a transaction | |
| */ | |
| const refundTransaction = async (transactionId, amount) => { | |
| try { | |
| const authToken = await getAuthToken(); | |
| const response = await axios.post( | |
| `${PAYMOB_BASE_URL}/acceptance/void_refund/refund`, | |
| { | |
| auth_token: authToken, | |
| transaction_id: transactionId, | |
| amount_cents: Math.round(amount * 100), | |
| }, | |
| ); | |
| return { | |
| success: true, | |
| data: response.data, | |
| }; | |
| } catch (error) { | |
| console.error( | |
| 'Paymob refund error:', | |
| error.response && error.response.data | |
| ? error.response.data | |
| : error.message, | |
| ); | |
| return { | |
| success: false, | |
| error: error.message, | |
| }; | |
| } | |
| }; | |
| module.exports = { | |
| initiatePayment, | |
| verifyPayment, | |
| refundTransaction, | |
| getAuthToken, | |
| }; | |