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.