samoulla-backend / utils /paymobService.js
Samoulla Sync Bot
Auto-deploy Samoulla Backend: 8574a71f0fc617aeb1ce9b5e35dac24c5319a12a
59c49c1
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,
};