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 :

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.