Drupal 7 : integrate a simple payment workflow with Payment module
Par Mathieu le vendredi 5 décembre 2014, 15:00 - Hacks - Lien permanent
Payment forms are common these days, and Drupal has already many out-of-the-box modules to implement a web shop.
But these modules are often very cumbersome, complicated, and not-so-easy to tweak for your own needs.
So, let’s (re)start from the beginning: let’s implement our own Payment form with Payment, and throw Ubercart, Commerce, and all his friends away.
Note that the use of Payment is compatible with Ubercart and Commerce, but please let me make it simpler.
Install Payment and Currency
Download and install Payment and Currency :
Or using Drush : drush dl payment currency
Don’t forget to enable your new modules Payment and Currency. Note that Payment has several submodules, I don’t need them for this example.
Download the payment method(s) you need
Payment only gives the workflow. Payment comes with several Payment methods available, that are implementing Payment API to hook into the workflow. The topic is integrting Payment, not writting a payment method, so you can choose whatever method(s) you want from the Payment project page.
In this example, I will use Paypal. So if you are still with me, download paypal_payment :
- https://www.drupal.org/project/paypal_payment
- or using drush :
drush dl paypal_payment
PayPal Payment has several submodules, enable all available PayPal Payment submodules.
Create a module to set up the form
Inside the module, let’s make the form. Two solutions: either use the standalone form provided by the module to make a simple payment, or integrate the payment to your own existing form.
Using the standalone form
Using the standalone form is pretty straightforward, make a form builder callback function like this:
function _mycheckoutmodule_form($form, &$form_state, $nid) {
$payment = new Payment(array(
'context_data' => array( // Add whatever you want in this array, this is passed to the payment method. If your are using Paypal Payment method, these informations are also POSTed to Paypal
'nid' => $nid,
),
'currency_code' => 'CHF',
'description' => t('Product description'),
'finish_callback' => '_mycheckoutmodule_payment_complete', // The function providing the page where the user is redirected after the payment
));
$payment->setLineItem(new PaymentLineItem(array(
'amount' => 20,
'name' => t('Product name'),
'description' => t('Product description'),
'tax_rate' => '0.2',
'quantity' => 1,
)));
$form = payment_form_standalone($form, $form_state, $payment);
return $form;
}
Integrate with your existing form
Again, simply change your form builder callback:
function _mycheckoutmodule_form($form, &$form_state, $node) {
// My simple form only has 'email' item
$form['email'] = array(
'#type' => 'textfield',
'#title' => t('Email'),
'#required' => true,
'#element_validate' => array('_mycheckoutmodule_email_element_validate'), // Please, implement this
);
$payment = new Payment(array(
'context_data' => array( // Add whatever you want in this array, this is passed to the payment method. If you are using Paypal Payment method, these informations are also POSTed to Paypal
'nid' => $nid,
),
'currency_code' => 'CHF',
'description' => t('Product description'),
'finish_callback' => '_mycheckoutmodule_payment_complete', // The function giving the page wher ethe user is redirected ater the payment
));
$payment->setLineItem(new PaymentLineItem(array(
'amount' => 20,
'name' => t('Product name'),
'description' => t('Product description'),
'tax_rate' => '0.2',
'quantity' => 1,
)));
$form_info = payment_form_embedded($form_state, $payment);
$form = array_merge($form, $form_info['elements']);
// I have a specific #submit callback, so I have to add it, but if you don't have one, just skip theses lines
$form['#submit'] = array_merge(
array('mycheckoutmodule_form_submit'),
$form_info['submit'],
array('payment_form_standalone_submit')
);
$form['submit'][] = array(
'#type' => 'submit',
'#value' => t('Buy now!'),
);
$form['#validate'][] = 'mycheckoutmodule_form_validate'; // You should know how to implement this.
return $form;
}
Call your form and display it
I added a parameter to the form builder function, didn’t you notice ? Actually I am getting the NID of the product the user wants to purchase, so I pass it to the form builder function.
// Call the form builder
$form_build = drupal_get_form('_mycheckoutmodule_form', $nid);
$checkout_form = render($form_build);
echo $checkout_form; // Or pass it to a theme
The “thank you for your purchase” page
The return page after the payment is handled by the function you specified while creating the Payment object.
If the user correctly gets back to the website (keep in mind it’s not always the case), you have access to the context data you passed to the Payment object, along with the Payment method return codes (given by the payment provider). You can use it to build your “thank you” page :
function _mycheckoutmodule_payment_complete($payment) {
watchdog('mycheckoutmodule', '"Thank you" displayed for payment ' . $payment->pid . '.', array(), WATCHDOG_INFO); // Let's spam the logs
if ($payment->statuses[count($payment->statuses)-1]->status == 'payment_status_success') { // Actually pretty bad, but statuses stacks in the stored Payment object
drupal_set_message('Your payment has been received.', 'success');
drupal_goto('node/' . $payment->context_data['nid']);
} else {
drupal_set_message('Your payment is not yet validated, please wait.', 'warning');
drupal_goto('node/' . $payment->context_data['nid']);
}
}
Check if the Payment succeeded
IPN URL (for Paypal)
If you are using the Paypal payment method, you have to set up your Paypal account with the following URL : http://yoursite.com/paypal_payment_pps/return
The callback function
The IPN processing is abstracted by Payment module. Payment can provide you informations about current payment by using the Payment objects stored in Drupal’s database. So, process the payment when receiving the callback :
function _mycheckoutmodule_payment_status_changed($payment) {
watchdog('mycheckoutmodule', 'Status for payment ' . $payment->pid . ' changed.', array(), WATCHDOG_INFO); // Let's spam the logs
if ($payment->statuses[count($payment->statuses)-1]->status == 'payment_status_success') {
watchdog('mycheckoutmodule', 'Status for payment ' . $payment->pid . ' detected completed.', array(), WATCHDOG_INFO); // spam, spam!
_mycheckoutmodule_process_delivery($payment->context_data['nid']); // You should implement this to do something once the payment is received
};
}
You can have a list of the available statuses in the file sites/all/modules/contrib/payment/payment/payment.payment.inc
The callback Rule
I didn’t find how to get the callback function triggering using an hook to Payment API. But Payment provides Rules conditions to handle the callbacks, let’s use it.
You can programmatically add a rule to your module, but if you are a lazy man like me, just add it from Rules UI : define a condition “payment status changed” and action “execute PHP” to call your function :
_mycheckoutmodule_payment_status_changed($payment);
Known bugs
There are some cutting edge while using this module : I didn’t succeeded at having the price rounded while using the “tax” argument. As a walkaround, you can just skip the “tax” argument of the form, or set it to 0. You need a simple form, so you maybe you don’t bother about dislaying the tax rate or not.