PayPal Credit Card Tokenization in Magento

Chris Nanninga

As a robust eCommerce platform, Magento has long supported integration with a variety of PayPal’s credit card payment solutions. Classy Llama’s newly released PayPal Credit Card Tokenization extension adds a feature that’s been sorely missed by merchants who use PayPal, however: the ability for customers to save a credit card for repeated use.

Building this extension was a new experience for Classy Llama in many ways, particularly because we are primarily an agency rather than a distributable extension developer. This was, in fact, our first go at developing an extension for Magento Connect, the platform’s official marketplace. This article offers a brief peek at how the project evolved and a few highlights of the development process.

A Missing Piece

“But I really need to allow customers to save their credit cards.” We heard this sentiment with some frequency from clients, expressing a major barrier to choosing PayPal over competitor Authorize.Net as their payment gateway or processor.

You see, what we’re talking about isn’t actually stored credit card data at all. The PCI DSS compliance that comes with storing sensitive payment data is unfeasible for many online merchants, and secure token alternatives exist in both PayPal and Authorize.Net API integration. These solutions allow the full experience of saved cards for the customer, while the credit card data itself is stored at the payment provider instead of the merchant site.

The core Magento codebase, however, has yet to implement support for these features on either service. And while several paid third-party extensions exist to enhance Authorize.Net integration with this functionality, there were thus far no off-the-shelf options for PayPal.

Consistently, we found clients unwilling to bear the costs of developing this functionality for their site. When the prospect came up again during planning of a recent client site, we decided to reach out to PayPal: would they be willing to share the cost of developing saved credit card functionality for our new client’s site, if the result was a distributable extension that could add value to PayPal for other merchants as well?

The response was enthusiastic. The lack of such an extension was strongly felt among the company’s integration specialists and sales force, especially when many merchants inaccurately expected PayPal to have direct control over what was and was not included in Magento (both companies are part of the eBay family of brands).

We finally had a viable plan for completing a key piece of functionality that would add a great deal of value for current and future clients. And the best part for merchants: The extension would be completely free, positioning PayPal strongly against the paid Authorize.Net alternatives.

The scope of the work soon took shape, including a few more bells and whistles beyond simple saved credit card functionality. Since we had a specific target client for the initial build, this client’s specific needs partly determined the features that would be included (support for billing agreements with a PayFlow gateway integration is one such feature). This article, however, will focus primarily on the saved credit card functionality.

Development

Environment

A few special challenges come with the development of a distributable extension that are not present with typical agency work. As a stand-alone product, only the module itself will go into your VCS, rather than an entire site codebase. Yet you’ll need an active environment integrating your module files during development, without the hassle of copying those files again and again.

Usually, there is also the need for easily testing the code among different versions of the platform routinely during development. These challenges can be met by simply excluding a great many working files from your VCS, but this is not ideal, particularly in Magento, where your custom code must be scattered to some degree among the core directories.

Fortunately, there is a tool available for Linux/Unix or Mac OSX environments to solve these issues with little fuss. Colin Mollenhour’s lightweight and excellent modman script was inspired specifically by Magento development and reduces the work of maintaining a project across multiple working codebases down to a few simple commands (the tool works by keeping your project files in a central hidden directory and placing symlinks in your actual codebase). We had used modman on occasion in the past, and it was the clear choice for managing our development environments for our PayPal extension.

A Version Problem

With a good system for version testing in place, which versions would we in fact be supporting with our Credit Card Tokenization extension? The current version of Magento Enterprise at the time was 1.12.0.2; this was the priority not only according to common sense, but because the client site that would enjoy the first fruits of the project was a new build on this version. The project scope was quite clear that compatibility with prior versions, initially, would be limited to those requiring little extra development. After a little analysis, we knew the answer to the question with a rare and unfortunate finality. Our initial extension would not be backward compatible at all, not even to the 1.12.0.0 release.

The reason came down to a recent overhaul of the core Magento configuration options for PayPal integration, which oddly enough had occurred between 1.12.0.0 and 1.12.0.2. In Magento, such settings are defined very easily within an XML file in a particular extension. Ours would naturally need a few of its own integrated with the core PayPal options, and the recent overhaul meant that the XML node structure we must work within was drastically different from any past Magento release.

This was a point where scope creep might have set in quite easily and early. The core code of Magento’s PayPal integration (even the code that actually accessed said configuration options) had varied little in several releases, so there were few other barriers to broader compatibility. However, the additional development of two versions of the extension’s configuration file would likely lead to inflated testing and debugging time as well.

And while version detection would normally be a small feat in application code, configuration XML files are one area in Magento where there’s no real avenue for such a thing; two diverging extension packages, therefore, looked like the likely scenario, adding even more complexity to the prospect.

Faced with all these considerations, much as we would have liked to release with broader compatibility, we stuck to the scope. Our extension would initially support Magento Enterprise 1.12.0.2, the parallel Community 1.7.0.2, and the (at the time) upcoming Enterprise 1.13.0.0. And as a result, we released a functioning extension on time and within budget.

Basic Architecture

Given the simplicity of the scope, there were actually few technical implementation choices to be made with this extension. One of those few involved our approach to user interface and that decision’s implications on code structure. Magento’s core code defines different payment methods and the PHP classes that govern them in one of the system’s many types of config XML files. The PayPal payment methods are no exception:

<payment>
    <paypal_express>
        <model>paypal/express</model>
        <title>PayPal Express Checkout</title>
        ...
    </paypal_express>
    <paypal_direct>
        <model>paypal/direct</model>
        <title>PayPal Payments Pro</title>
        ...
    </paypal_direct>
    ...
    <verisign>
        <model>paypal/payflowpro</model>
        <title>Payflow Pro</title>
        ...
    </verisign>
    ...
    <payflow_link>
        <model>paypal/payflowlink</model>
        ...
        <title>Credit Card</title>
        ...
    </payflow_link>
    <payflow_advanced>
        <model>paypal/payflowadvanced</model>
        ...
        <title>Credit Card</title>
        ...
    </payflow_advanced>
    ...
</payment>

A great deal of XML has been truncated in the above, but what’s left shows how PayPal Express and the four direct credit card solutions supported by the extension are defined. Each node under corresponds with an item in the radio button list of payment options with which a customer is presented at checkout (excluding PayPal Express, only one of the above would ever actually be enabled at a time, depending on the merchant’s chosen PayPal integration). The node under each defines the PHP class responsible for the payment method’s behavior.

Given this structure of one main class in charge of one payment method, we needed to decide whether it made more sense to override the functionality of the existing payment methods – which would allow a great deal of flexibility for how we presented the saved card interface – or extend them with entirely separate payment methods.

In the end, the latter made the most sense. Keeping the logic for using a saved card in its own set of classes, and its own collection of config settings tied to them, was a cleaner structure.

<payment>
    <paypal_direct_customerstored>
        <model>cls_paypal/paypal_stored_customerstored_direct</model>
        ...
        <title>Saved Credit Card</title>
        ...
    </paypal_direct_customerstored>        
    <verisign_customerstored>
        <model>cls_paypal/paypal_stored_customerstored_payflowpro</model>
        ...
        <title>Saved Credit Card</title>
        ...
    </verisign_customerstored>
    <payflow_link_customerstored>
        <model>cls_paypal/paypal_stored_customerstored_payflowlink</model>
        ...
        <title>Saved Credit Card</title>
        ...
    </payflow_link_customerstored>
    <payflow_advanced_customerstored>
        <model>cls_paypal/paypal_stored_customerstored_payflowadvanced</model>
        ...
        <title>Saved Credit Card</title>
        ...
    </payflow_advanced_customerstored>
    ...
</payment>

The result for the user interface was a little more rigid, but perfectly clear to the customer: “Saved Credit Card” would appear as a separate option in the radio button list at checkout if the customer had previously chosen to save a card.

Magento PayPal options

Of course, some overriding of the basic credit card method classes was still necessary. Customers had to be offered the ability to save a card in the first place, after all. But the actual differing payment processing logic would be cleanly separated, without intruding on the core integration’s classes with conditional branching at every turn.

Minor Bumps

One issue with the class structure defined just above was a typical inheritance problem. It so happened that the primary classes responsible for three of our four supported solutions – PayFlow Pro, PayFlow Link, and Payments Advanced – already formed an inheritance chain with only slight variations. Our “saved card” versions of all three, naturally, would be nearly identical as well, and we would have liked nothing more than to leverage inheritance for our own logic as well.

Alas, each of our classes needed to extend its counterpart in the core code. The solution we ended up with was something of the best of both worlds, with a core class containing the main processing logic, referenced as a property rather than extended by the payment method classes. Methods on the latter almost universally simply called through to the core class. A truncated example of one of the payment classes should make the gist clear:

class CLS_Paypal_Model_Paypal_Stored_Customerstored_Payflowpro extends CLS_Paypal_Model_Paypal_Payflowpro
{
    //...

    protected $_commonPayflowMethod;

    public function __construct()
    {
        parent::__construct();

        // Initialize common method
        $this->_commonPayflowMethod = Mage::getModel(
            'cls_paypal/paypal_stored_customerstored_payflow',
            array(
                'caller_method' => $this,
                //... various other params
            )
        );
    }

    //...

    public function authorize(Varien_Object $payment, $amount)
    {
        return $this->_commonPayflowMethod->authorize($payment, $amount);
    }

    public function capture(Varien_Object $payment, $amount)
    {
        return $this->_commonPayflowMethod->capture($payment, $amount);
    }
    
    //...
}

Other quandaries surfaced and were conquered here and there, but none of much consequence. In fact, we couldn’t have been happier with how smoothly the extension’s development proceeded. One drawback of the finished extension, however, was the rigidity made necessary by the way information about the various PayPal payment methods was hard-coded in the core module.

You’ve seen above how easy it is to define new payment methods via XML, and were our new methods truly self-contained, they’d be up and running with little intrusion into core code. Relying as they do on the existing PayPal integration code, however, they are subject to numerous instances of class constants referencing payment methods directly, case statements dictating how config information is to be retrieved, and even hard-coded logic for which countries a payment method supports.

Below are a few snippets of our own rewrite of the ubiquitous PayPal Config class, demonstrating a few of the places we were obliged to interject our new payment options:

class CLS_Paypal_Model_Paypal_Config extends Mage_Paypal_Model_Config
{
    //...

    // "Customer stored" payment methods
    const METHOD_PAYPAL_DIRECT_CUSTOMERSTORED          = 'paypal_direct_customerstored';
    const METHOD_PAYPAL_PAYFLOWADVANCED_CUSTOMERSTORED = 'payflow_advanced_customerstored';
    const METHOD_PAYPAL_PAYFLOWPRO_CUSTOMERSTORED      = 'verisign_customerstored';
    const METHOD_PAYPAL_PAYFLOWLINK_CUSTOMERSTORED     = 'payflow_link_customerstored';

    //...

    public function getCountryMethods($countryCode = null)
    {
        //Countries where this method is available
        //...

        // PayPal Direct 'Stored card' methods
        $countriesByMethod[self::METHOD_PAYPAL_DIRECT_CUSTOMERSTORED] = array(
            'US',
            'CA',
            'GB'
        );

        // PayPal Payflow-based 'Stored card' methods (Payflow Pro defines the list of supported countries)
        $countriesByMethod[self::METHOD_PAYPAL_PAYFLOWADVANCED_CUSTOMERSTORED] =
        $countriesByMethod[self::METHOD_PAYPAL_PAYFLOWLINK_CUSTOMERSTORED] =
        $countriesByMethod[self::METHOD_PAYPAL_PAYFLOWPRO_CUSTOMERSTORED] = array(
            'US',
            'CA',
            'AU',
            'NZ'
        );

        $countryMethods = parent::getCountryMethods($countryCode);

        foreach ($countriesByMethod as $methodCode => $countries) {
            //Add this method to the list of available methods in appropriate countries
            if (is_null($countryCode)) {
                foreach ($countries as $country) {
                    array_push($countryMethods[$country], $methodCode);
                }
            } elseif (in_array($countryCode, $countries)) {
                array_push($countryMethods, $methodCode);
            }
        }

        return $countryMethods;
    }

    protected function _getSpecificConfigPath($fieldName)
    {
        $path = parent::_getSpecificConfigPath($fieldName);

        if (is_null($path)) {
            switch ($this->_methodCode) {
                //...
                case self::METHOD_PAYPAL_DIRECT_CUSTOMERSTORED:
                    $path = $this->_mapStoredFieldset($fieldName);
                    break;
                default:
            }
        }

        return $path;
    }

    public function isMethodAvailable($methodCode = null)
    {
        $result = parent::isMethodAvailable($methodCode);

        if (!$result) {
            return false;
        }

        //...

        switch ($methodCode) {
            case self::METHOD_PAYPAL_DIRECT_CUSTOMERSTORED:
                if (!$this->isMethodActive(self::METHOD_WPP_DIRECT)) {
                    $result = false;
                }
                break;
            case self::METHOD_PAYPAL_PAYFLOWADVANCED_CUSTOMERSTORED:
                if (!$this->isMethodActive(self::METHOD_PAYFLOWADVANCED)) {
                    $result = false;
                }
                break;
            case self::METHOD_PAYPAL_PAYFLOWLINK_CUSTOMERSTORED:
                if (!$this->isMethodActive(self::METHOD_PAYFLOWLINK)) {
                    $result = false;
                }
                break;
            case self::METHOD_PAYPAL_PAYFLOWPRO_CUSTOMERSTORED:
                if (!$this->isMethodActive(self::METHOD_PAYFLOWPRO)) {
                    $result = false;
                }
                break;
        }

        return $result;
    }

    //...
}

In the end, I can’t say for certain that I could have architected such a complex integration in any more flexible a manner. However that may fall, though, the end result of hard-coded elements like these makes it more likely that minor re-tooling will be required to support future Magento releases.

No API Worries

Being, as it is, primarily a web service integration, this extension relies on API calls for its core functionality. In this area, however, our task was made extremely easy by the fact that we were merely enhancing an existing integration. Magento’s core had already defined classes for issuing the appropriate requests, and in fact we had no need whatsoever for defining low level code to construct API calls.

Our extension was concerned with PayPal’s two name-value-pair (NVP) APIs – the direct API and the PayFlow gateway API. Magento’s Mage_Paypal_Model_Api_Nvp class defines the logic for the former, while the latter is handled directly in the PayFlow Pro payment method class and its descendants. A very brief snapshot of some of the direct API class:

class Mage_Paypal_Model_Api_Nvp extends Mage_Paypal_Model_Api_Abstract
{
    /**
     * Paypal methods definition
     */
    const DO_DIRECT_PAYMENT = 'DoDirectPayment';
    const DO_CAPTURE = 'DoCapture';
    const DO_AUTHORIZATION = 'DoAuthorization';
    //... (other constants for API method names)

    protected $_globalMap = array(
        // each call
        'VERSION'      => 'version',
        'USER'         => 'api_username',
        'PWD'          => 'api_password',
        'SIGNATURE'    => 'api_signature',
        //... (other API parameters and the properties they map to on this class
    )

    //...

    protected $_doAuthorizationRequest = array('TRANSACTIONID', 'AMT', 'CURRENCYCODE');
    protected $_doAuthorizationResponse = array('TRANSACTIONID', 'AMT');

    //...

    public function callDoAuthorization()
    {
        $request = $this->_exportToRequest($this->_doAuthorizationRequest);
        $response = $this->call(self::DO_AUTHORIZATION, $request);
        $this->_importFromResponse($this->_paymentInformationResponse, $response);
        $this->_importFromResponse($this->_doAuthorizationResponse, $response);

        return $this;
    }
}

Our chief concern was with what are known as reference transactions, as this is the key tokenization feature of the PayPal API that makes our version of “saved credit cards” work. As it turned out, we were quite fortunate in regard to the direct API at least, because reference transactions were a required component of a feature the core integration already supported: billing agreements.

Delighted to find a callDoReferenceTransaction method defined on the API class already, we merely needed to set the right properties and call it from within our payment method class:

class CLS_Paypal_Model_Paypal_Stored_Customerstored_Direct extends CLS_Paypal_Model_Paypal_Direct
{
	//...

    protected function _placeOrder(Mage_Sales_Model_Order_Payment $payment, $amount)
    {
        // Get Reference ID
        $paymentStoredCardId = $this->getInfoInstance()->getData('stored_card_id');
        $referenceId = null;

        if ($paymentStoredCardId) {
            $storedCardModel = Mage::getModel('cls_paypal/customerstored')->load($paymentStoredCardId);
            if ($storedCardModel->getId()) {
                $referenceId = $storedCardModel->getData('transaction_id');
            }
        }

        //...

        $order = $payment->getOrder();

        $api = $this->_pro->getApi()
            ->setReferenceId($referenceId)
            ->setPaymentAction($this->getConfigData('payment_action'))
            ->setIpAddress(Mage::app()->getRequest()->getClientIp(false))

            ->setAmount($amount)
            ->setCurrencyCode($order->getBaseCurrencyCode())
            ->setInvNum($order->getIncrementId())
            ->setEmail($order->getCustomerEmail());

        //...

        // call api and import transaction and other payment information
        $api->callDoReferenceTransaction();
        $this->_importResultToPayment($api, $payment);

        //...

        return $this;
    }

}

We anticipated more work for the PayFlow based solutions, but even here things remained fairly simple. The PayFlow API does not, in fact, expect an explicit method name among request parameters. A reference transaction is performed merely by including an ORIGID value in addition to the transaction type (authorization or capture, for instance).

By fetching and setting the appropriate token, we were in business. The chief class involved was not much longer than this:

class CLS_Paypal_Model_Paypal_Stored_Customerstored_Payflow extends CLS_Paypal_Model_Paypal_Stored_Payflow
{

    //...

    protected function _placeOrder(Mage_Sales_Model_Order_Payment $payment, $amount, $transactionType = self::TRXTYPE_AUTH_ONLY)
    {
        // Get Reference ID
        $paymentStoredCardId = $this->_commonMethod->getInfoInstance()->getData('stored_card_id');
        $referenceId = null;

        if ($paymentStoredCardId) {
            $storedCardModel = Mage::getModel('cls_paypal/customerstored')->load($paymentStoredCardId);
            if ($storedCardModel->getId()) {
                $referenceId = $storedCardModel->getData('transaction_id');
            }
        }

        //...

        // Prepare and run 'Reference Transaction' request
        $request = $this->_buildBasicRequest($payment);
        $request->setTrxtype($transactionType);
        $request->setAmt(round($amount, 2));
        $request->setOrigid($referenceId);

        $response = $this->_postRequest($request);
        $this->_processErrors($response);

        //...
        
        return $this;
    }

}

In point of fact, we had more API mapping work to do for our PayFlow Pro billing agreement payment method (not the focus of this article) than for any of our saved credit card functionality.

The Road Ahead

Right now, the most glaring limitation of the PayPal Credit Card Tokenization extension is its stringent compatibility requirements. I’m happy to report that we’re already on track to broaden compatibility with previous Magento versions in the near future. (Keep an eye on our blog, if you like, for updates.)

There are also a slew of other features we’d like to see added in the future. Support for Magento’s recurring payment profiles, for example, would be a natural fit for saved credit cards. We’ll be paying careful attention to the most requested functionality as we plan for the future of the extension.

The development of our first extension for Magento Connect has been a major learning experience for Classy Llama, and an extremely positive one. We’re excited to see the benefit PayPal’s tokenization features can provide for Magento merchants.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.