PayPal integration is such a common requirement for e-commerce sites, irrespective of the platform the site is built with, right. So recently a client approached me for integrating PayPal payments into their Drupal site, and my immediate question to them was why are they asking me to invent the wheel again. There sure are many PayPal integration modules available on drupal.org, and why don’t they use one of them.
But they had a custom workflow involving phased payments and all they needed for PayPal integration was ability to receive payments through PayPal and then receiving and logging the PayPal IPN callback information in the Drupal database where it could be further integrated with Drupal node and user infrastructure. Payments were mostly initiated through PayPal buttons and hence also required dynamic generation of PayPal buttons with html variables based on user selections on the Drupal site.
Writing the actual code for handling IPN callbacks was such a simple task that I thought did it really make sense to use heavy-duty modules available off d.o. for such trivial thing when writing custom code can enable much better integration with your workflows (without needing to continuously maintain integration with the api of custom module you choose on d.o). And thus I wanted to share the code for handling PayPal IPN callbacks and record the information in the database.
So much for the introduction, now let’s concentrate on code. First, I will show you the sql for the custom table that will record the IPN information (its for MySql but you can easily adapt it for any db server):
CREATE TABLE `paypal_ipn` ( `txn_id` varchar(50) NOT NULL, `receiver_email` varchar(50) NOT NULL, `receiver_id` varchar(50) NOT NULL, `residence_country` varchar(50) NOT NULL, `test_ipn` varchar(50) NOT NULL, `transaction_subject` varchar(1000) NOT NULL, `txn_type` varchar(50) NOT NULL, `payer_email` varchar(50) NOT NULL, `payer_id` varchar(50) NOT NULL, `payer_status` varchar(50) NOT NULL, `first_name` varchar(50) NOT NULL, `last_name` varchar(50) NOT NULL, `address_city` varchar(50) NOT NULL, `address_country` varchar(50) NOT NULL, `address_country_code` varchar(50) NOT NULL, `address_name` varchar(50) NOT NULL, `address_state` varchar(50) NOT NULL, `address_status` varchar(50) NOT NULL, `address_street` varchar(50) NOT NULL, `address_zip` varchar(50) NOT NULL, `handling_amount` double NOT NULL, `item_name` varchar(1000) NOT NULL, `item_number` varchar(50) NOT NULL, `mc_currency` varchar(50) NOT NULL, `mc_fee` double NOT NULL, `mc_gross` double NOT NULL, `payment_date` varchar(50) NOT NULL, `payment_fee` double NOT NULL, `payment_gross` double NOT NULL, `payment_status` varchar(50) NOT NULL, `payment_type` varchar(50) NOT NULL, `protection_eligibility` varchar(50) NOT NULL, `quantity` int(11) NOT NULL, `shipping` double NOT NULL, `tax` double NOT NULL, `notify_version` varchar(50) NOT NULL, `charset` varchar(50) NOT NULL, `verify_sign` varchar(50) NOT NULL, `normalized_payment_date` varchar(50) NOT NULL, PRIMARY KEY (`txn_id`) ) ENGINE=MyISAM
Well the fields have been taken directly from PayPal’s IPN reference. You might want to add/remove fields depending upon which variables you expect in your IPN callback. You can find IPN variable information on this and this page and you might also want to check official PayPal docs for more information on how IPN works and which variables to expect for which types of transactions.
Now on Drupal side, you first need to provide a path where IPN callbacks would be handled through hook_menu:
{syntaxhighlighter brush: php;fontsize: 100; first-line: 1; }function paypal_menu() {
$items = array();
$items[‘paypal/payment/ipn’] = array(
‘title’ => t(‘Paypal Payment IPN’),
‘description’ => t(‘Paypal Payment IPN’),
‘type’ => MENU_CALLBACK,
‘page callback’ => ‘paypal_payment_paypal_ipn_callback’,
‘access callback’ => TRUE,
);
return $items;
}{/syntaxhighlighter}
Two very important things to remember while handling IPN callbacks are: 1) IPN callbacks are asynchronous, so they don’t happen synchronously as your user is making the payment, and 2) PayPal’s server would invoke IPN call back to your server, so it would be an unauthenticated user and thus you need to make sure that your IPN callback has no access restriction defined. The second point also means you need to be extra vigilant in processing the IPN callback to guard against malicious calls not originating from PayPal but this is implicitly handled when you post the received information back to PayPal for validation in your callback handler (you will see this code now).
So after you have defined the above menu entry, the following method would receive the IPN callback, validate it and then add the information to your ‘paypal_ipn” db table:
{syntaxhighlighter brush: php;fontsize: 100; first-line: 1; }function paypal_payment_paypal_ipn_callback () {
header(“Content-type: text/html”);
header(“Expires: Wed, 29 Jan 1975 04:15:00 GMT”);
header(“Last-Modified: ” . gmdate(“D, d M Y H:i:s”) . ” GMT”);
header(“Cache-Control: no-cache, must-revalidate”);
header(“Pragma: no-cache”);
// read the post from PayPal system and add ‘cmd’
$req = ‘cmd=_notify-validate’;
foreach ($_POST as $key => $value) {
$value = urlencode(stripslashes($value));
$req .= “&$key=$value”;
}
// post back to PayPal system to validate
$header .= “POST /cgi-bin/webscr HTTP/1.0\r\n”;
$header .= “Content-Type: application/x-www-form-urlencoded\r\n”;
$header .= “Content-Length: ” . strlen($req) . “\r\n\r\n”;
$fp = fsockopen (‘ssl://www.paypal.com’, 443, $errno, $errstr, 30);
if (!$fp) {
watchdog(‘paypal’, ‘HTTP error’);
} else {
fputs ($fp, $header . $req);
watchdog(‘paypal’, $header . $req);
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp ($res, “VERIFIED”) == 0) {
// assign posted variables to local variables
$txn_id = $_POST[‘txn_id’];
//Information about you:
$receiver_email = $_POST[‘receiver_email’];
$receiver_id = $_POST[‘receiver_id’];
$residence_country = $_POST[‘residence_country’];
//Information about the transaction:
$test_ipn = $_POST[‘test_ipn’];
$transaction_subject = $_POST[‘transaction_subject’];
$txn_type = $_POST[‘txn_type’];
//Information about your buyer:
$payer_email = $_POST[‘payer_email’];
$payer_id = $_POST[‘payer_id ‘];
$payer_status = $_POST[‘payer_status’];
$first_name = $_POST[‘first_name’];
$last_name = $_POST[‘last_name’];
$address_city = $_POST[‘address_city’];
$address_country = $_POST[‘address_country’];
$address_country_code = $_POST[‘address_country_code’];
$address_name = $_POST[‘address_name’];
$address_state = $_POST[‘address_state’];
$address_status = $_POST[‘address_status’];
$address_street = $_POST[‘address_street’];
$address_zip = $_POST[‘address_zip’];
//Information about the payment:
$handling_amount = $_POST[‘handling_amount’];
$item_name = $_POST[‘item_name’];
$item_number = $_POST[‘item_number’];
$mc_currency = $_POST[‘mc_currency’];
$mc_fee = $_POST[‘mc_fee’];
$mc_gross = $_POST[‘mc_gross’];
$payment_date = $_POST[‘payment_date’];
$payment_fee = $_POST[‘payment_fee’];
$payment_gross = $_POST[‘payment_gross’];
$payment_status = $_POST[‘payment_status’];
$payment_type = $_POST[‘payment_type’];
$protection_eligibility = $_POST[‘protection_eligibility’];
$quantity = $_POST[‘quantity’];
$shipping = $_POST[‘shipping’];
$tax = $_POST[‘tax’];
//Other information about the transaction:
$notify_version = $_POST[‘notify_version’];
$charset = $_POST[‘charset’];
$verify_sign = $_POST[‘verify_sign’];
$normalized_payment_date = strptime($payment_date, ‘%H:%M:%S %b %e, %Y %Z’);
$normalized_payment_date = (1900 + $normalized_payment_date[‘tm_year’]) . ‘/’ .
(1 + str_pad($normalized_payment_date[‘tm_mon’], 2, ‘0’, STR_PAD_LEFT)) . ‘/’ .
(str_pad($normalized_payment_date[‘tm_mday’], 2, ‘0’, STR_PAD_LEFT));
// check the payment_status is Completed
// check that txn_id has not been previously processed
// check that receiver_email is your Primary PayPal email
// check that payment_amount/payment_currency are correct
// process payment
$results = db_query(‘SELECT * FROM {paypal_ipn} WHERE txn_id = “%s”‘, $txn_id);
if (!db_fetch_object($results)) {
db_query(‘INSERT INTO {paypal_ipn} (‘ .
‘txn_id, ‘ .
‘receiver_email, receiver_id, residence_country, ‘ .
‘test_ipn, transaction_subject, txn_type, ‘ .
‘payer_email, payer_id, payer_status, first_name, last_name, address_city, address_country, address_country_code, address_name, address_state, address_status, address_street, address_zip, ‘ .
‘handling_amount, item_name, item_number, mc_currency, mc_fee, mc_gross, payment_date, payment_fee, payment_gross, payment_status, payment_type, protection_eligibility, quantity, shipping, tax, ‘ .
‘notify_version, charset, verify_sign, ‘ .
‘normalized_payment_date’ .
‘) VALUES (‘.
‘”%s”, “%s”, ‘ .
‘”%s”, “%s”, “%s”, ‘ .
‘”%s”, “%s”, “%s”, ‘ .
‘”%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, ‘ .
‘”%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, “%s”, ‘ .
‘”%s”, “%s”, “%s”, ‘ .
‘”%s”‘ .
‘);’,
$txn_id,
$receiver_email, $receiver_id, $residence_country,
$test_ipn, $transaction_subject, $txn_type,
$payer_email, $payer_id, $payer_status, $first_name, $last_name, $address_city, $address_country, $address_country_code, $address_name, $address_state, $address_status, $address_street, $address_zip,
$handling_amount, $item_name, $item_number, $mc_currency, $mc_fee, $mc_gross, $payment_date, $payment_fee, $payment_gross, $payment_status, $payment_type, $protection_eligibility, $quantity, $shipping, $tax,
$notify_version, $charset, $verify_sign,
$normalized_payment_date
);
watchdog(‘paypal’, ‘Paypal payment received with transaction id @txn_id.’, array(‘@txn_id’ => $txn_id), WATCHDOG_NOTICE);
global $user;
$payer_subject = ‘Payment Confirmation’;
$payer_body = <<<BODY
Dear $address_name,
This is a payment receipt for transaction $txn_id on $payment_date for “$item_name”;
Note: This email will serve as an official receipt for this payment.
Billing & Support Area
Your Company Name
BODY;
drupal_mail(‘paypal’, ‘payer_confirmation’, $payer_email, user_preferred_language($user), array());
$admin_subject = ‘Payment Confirmation: $txn_id’;
$payer_body = <<<BODY
A payment has been made for $mc_currency $payment_gross for Item Number: $item_number ($item_name).
The PayPal transaction id is: $txn_id;
Payer Details:
Email: $payer_email
Name: $address_name
Street: $address_street
City: $address_city
Zip: $address_zip
Country: $address_country
Payment Details:
Date: $payment_date
Fee: $payment_fee
Gross: $payment_gross
Quantity: $quantity
Billing & Support Area
Your Company Name
BODY;
drupal_mail(‘paypal’, ‘admin_confirmation’, $receiver_email, user_preferred_language($user), array());
watchdog(‘paypal’, ‘Mails sent @txn_id.’, array(‘@txn_id’ => $txn_id), WATCHDOG_NOTICE);
}
}
else if (strcmp ($res, “INVALID”) == 0) {
watchdog(‘paypal’, ‘INVALID’);
// log for manual investigation
}
}
fclose ($fp);
}
die();
}{/syntaxhighlighter}
This is all boilerplate code mostly taken from PayPal’s code sample to handle IPN callback with appropriate modifications to extract desired variables, inserting them into the database and then dispatching separate mails to the payer and receiver acknowledging the receipt of payment.
The code is for Drupal 6, but should need only minimal changes (if any) for porting to D7.
Again you might need to modify the variables depending upon which ones are you expecting in your IPN callback depending upon your transaction type, but other than that, you should be able to use this code as is for handling IPN callbacks (off-course stuffing it with missing pieces from your workflow if any).
In our case, we had embedded user and node information in the item_number and item_name variables (recall I said we generated on the fly PayPal buttons depending upon user selections) which we extracted and passed on to Accounting and Billing systems to take over from there and also executed some other custom actions on Drupal side associated to the receipt of payment from the user.
One important thing to take care of is to specify the correct ‘notify_url’ or ‘ipn_notification_url’ (depending upon your transaction type) in the html variables for the PayPal button or whichever other way you are accepting the payment from the user, or you won’t receive IPN callbacks at all. It should be a fully qualified url including your domain name, and if in doubt, you can use the return value of the following call from Drupal’s url method:
url('paypal/payment/ipn', array('absolute' => TRUE))
(Please change the menu entry appropriately depending upon what you specified in hook_menu).
Attached below are 2 files, a paypal.module file that contains the code for registering menu item and handling IPN callback (you would need to write a paypal.info file for Drupal to recognize your paypal.module file) and the sql file for generating the db table to store IPN information.
Great post, thanks. I’m considering a custom paypal module myself and would love to take a look at your sample but keep getting a ‘forbidden’ error when attempting to view your source files. If you’re still willing to share your work, I would appreciate you looking into it. Thanks in advance.
Thanks again for the post, Rahul. I love the detailed explanation and links to IPN docs. Posting the schema was brilliant. I might very well end up using some ideas from it as I’m about to redo PayPal integration for an ecommerce site. This one is not Drupal based but this post was still very informative for general PayPal Instant Payment Notification.
Thanks Rahul for a great article on how to process the IPNs from PayPal to close the loop on transactions. I had a similar scenario helping out a local organization where all the overhead and trying to retro fit the commerce modules into an existing site was not worth the effort. They just needed to accept payments online and I wanted to process the IPNs so they can close that loop and offer simple reporting of payments on the site (instead of through PayPal).
This article was extremely helpful and I was able to process the IPNs successfully after converting the DB calls to D7. Also, I had changed some of the paypal_ipn table columns to allow NULLs for my own processing which may be helpful to others and found that using the PayPal IPN simulator helped debug much of any processing errors.
Thanks again.
Hello Rahul,
I came across your post searching for custom implementation of paypal into the Drupal & found it useful. I need to understand it more so that I can apply it my project.
I have the scenario like this page – http://www.ctaep.org/newMembership.html
My user will register by selecting the role which he want by paying. Once he submit the form, he will be forwarded to paypal & pay. If payments confirms, he will be assigned the role which he has choosen.
I tried finding some solution but couldn’t reach anything exactly & so that made a roadmap.
I have altered the registration form and put this select list. On submission of the form I will call a function which will get all the values of the form and put in switch according to selected membership type. In the case…I will save the user first as authenticated type..and will forward to paypal..then will check ACK in if else…if yes then I will assign the role he wanted….if NO then will remove him from authenticated role & give him the message…but this all things will happen inside the case of switch…
Now how this paypal IPN code can help me? I urgently need it…
I will wait for your reply…
Thanks