Thursday, December 30, 2010

Multi Payment Gateway module for Zend

To be not completely addicted to JEE I'm participating other projects as well. Between the others I'm currently involved in some portal based on PHP Zend Framework. This is my first approach to Zend and I must admit that this is very nice framework, in which development can be really pleasant. Good work - Zend guys!

For those generally fluent in web technologies but not knowing Zend I can tell that it needs some initial commitment. You'll be probably sitting at the docs and examples few days, but you can start writing sensible code almost instantly, and be effective after no more than five days of playing around it (even faster if you don't want to know what's under the hood). Additionally, because this time I have a second project in almost "classic" JEE techs, I started to think that this JEE, which I liked very much once, and which forces me to write 10 classes to just take data from HTML form and put it into the database, it's just a technology created by programmers for programmers. Thanks to them for that, because we need to have a way to make money, but if your'e not a banking customer, and you just need to have a done-and-forget project, PHP with some sensible framework wouldn't be a choice to despise.

OK, but I'm diverging from the main subject. In my project I needed to implement few payment methods, using some publicly available payment gateways. I mean such providers as for example PayPal, AlertPay of PerfectMoney (and many, many more). It seems to be a common case nowadays that you need to have a few of such gateways on your website or store, not just single one. Of course each of them gives you manuals and examples of how to implement their gateway communication only, not providing any common framework for that.

The first spurt for me is always to look for solution, because I don't like to write the same code second time. I thought that this is common task but, surprisingly, there was no regarding code in Zend. Here I've found some proposal for Zend_Payment object, but either I'm blind or these classes do really nothing. And because I like to write reusable pieces of code, I decided to write one this time as well.

This time there's a bit more classes than usually, so I packed everything in one archive and gave links below. The archive content is library-like, and can be used as standalone Zend component. Here I just want to explain what's this all about.

The gateway

I reviewed 5 gateway API-s before started to write any code. All of them works in a similar way, and I believe that the others work likewise. Whole concept is simple and can be read on particular provider's webpage. I don't want to promote anything here, but I have to say that from these ones I worked with, the PerfectMoney has very nice API, code examples, and appropriate testing methods, so it should be easy to start with it at first.

By the transaction and, in general, gateway integration I don't mean here the usage of whole payment API, what can significantly vary depending on the provider, but just the simple and common flow, where user chooses the payment gateway, then is redirected to payment provider's page, does the job and then returns to our webpage having some nice success or error message. The main idea of doing such payment transactions from the web flow point of view is following:
  1. You're posting a form with transaction data to provider's server.
  2. When transaction is done provider's server is sending transaction status data to your server status URL.
  3. When transaction is finished successfully, on the end on whole process, provider's server is redirecting the user to your server success URL.
  4. If transaction is finished unsuccessfully, provider's server is redirecting the user to your server error URL.
The only important thing is that your URL-s are accessed on status URL from payment gateway server and on success/error URL from the user's server. Thus the only reliable information about transaction status can be received to status URL, and the other two are just the endpoints.

The desire

My goal was to have whole flow implemented in following way (in the further part let's consider we are doing integration with PerfectMoney gateway for example):

class MyController {

  public function startpaymentAction() {
    // get payment method from registry (we have many payment gateways implemented)
    $payment = Zend_Payment_Registry::getInstance()->getByType("perfectmoney");

    // begin payment session of, for example, 12.34 for funny face
    // last two params points to success and error url in our app
    $pid = $payment->beginSession(12.34, "For funny face", 
      'my/successpayment', 'my/errorpayment');

    // finally, after start payment session, 
    // we have payment id and we can redirect further
    $this->_redirect("/payment/begin?pid=".$pid); 
  }

  public function successpaymentAction() {
    echo "Thanks for your money!";
  }

  public function errorpaymentAction() {
    echo "Sorry, but if you'd like to feed our pocket you need a little more effort.";
  }

}

Untill now everything should be clear and simple. We are getting a payment method from the registry, beggining the payment session with appropriate data, also giving internal success and error URL-s (pointing to consecutive controller methods). Then we need to redirect to our payment controller, which handles the whole flow and interactions with payment gateway server.

If you are not a daltonian, you've probably noticed those two highlighted parts of code. They are hook points between user code and my payment gateway module and will be explained soon. First one is a payment registry call, getting the payment method by name, and the second one is a call to begin action of payment controller, which handles overall payment process. We start from explaining the controller role, because it's an easier part.

The controller

As we showed in the code above, we expect to have payment controller registered in the application. This requires of making of a new class and putting it into controllers directory:

class PaymentController extends Zend_Payment_Controller {

}


This is really all you need to do to have working payment controller in your application.

The registry

Now a bit more about Zend_Payment_Registry class and its resposibilites. This class holds overall module configuration and stores all registered payment method objects. The registry is a singleton instance and it needs to be feed by Zend_Config before usage. The best place to do it is probably in bootstrap class.

The most reasonable way to hold the payment module config is the main application.ini file. The basic payment module config looks as follows:
payments.default.serverpath = "example.com"
payments.default.controller = "payment"
payments.default.loader = "Zend_Payment_Session_DbLoader"
Where:

  1. serverpath should point to server domain (or virtual host) you working on. The important thing is that your app now talks with the payment gateway server, so it can't be locahost for example. If your application is currently accessible from outside under http://example.com URL, you need put here example.com.
  2. controller is the payment controller name. We named our exemplary payment controller as PaymentController, so we need to put here payment value. Finally, the controller should be accessible under http://example.com/payment.
  3. loader is a name of the class handling with building, saving and restoring payment session. More about this is in next chapter. For most instances the default Zend_Payment_Session_DbLoader will be good.
Now we can feed the registry with payments namespace of the config:

Zend_Payment_Registry::getInstance()->configure($config->payments);


The payment session and loader

Payment session is an object holding all information about particular payment session, as for example which payment method was used, what was the payed amount or what was the payment process result. It also holds all communication and processing events regarding the payment process in log-like style (you can for example restore given session and check out how we were talking with payment server during the process).

Payment session is managed by payment session loader. Loader is able to build, save and restore payment session, and all interactions between payment module classes and payment session are done using loader. 

In general, you don't have to bother about payment session and loader. They are internally represented by Zend_Payment_Session_Interface and Zend_Payment_Session_LoaderInterface interfaces and you can look into a code to see them. 

For real application we need of course some concrete loader and session implementation, not just the interface. In the module there's included default Zend_Payment_Session_Db and Zend_Payment_Session_DbLoader implementation using Zend_Table. To use it, provided you have some db connection configured elsewhere, you just need to create appropriate table in database and configure Zend_Payment_Session_DbLoader as default loader for the registry (what has been just shown in a chapter above). The table creation SQL for SQLite looks following:

CREATE TABLE payment_session (
   id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
   type VARCHAR(64) NOT NULL,
   amount DECIMAL(10,2) NOT NULL,
   description TEXT,
   success_url VARCHAR(255) NOT NULL,
   error_url VARCHAR(255) NOT NULL,
   status INTEGER NOT NULL DEFAULT 0,
   status_data TEXT,
   response_data TEXT
);


If you need more information, just check the related code. You can of course create also your own session and loader implementation, to be used with you application, and then configure it in the registry.

The payment method

The last thing we need now is the concrete payment method implementation. We are still assuming integration with PerfectMoney, so we first need to configure in our application.ini general gateway properties:

payments.perfectmoney.name = "Perfect Money"
payments.perfectmoney.clazz = "Zend_Payment_Gateway_PerfectMoney"
payments.perfectmoney.action = "https://perfectmoney.com/api/step1.asp"
payments.perfectmoney.pass = "alternative pass md5 here"
payments.perfectmoney.form.PAYEE_ACCOUNT = "U1234567"
payments.perfectmoney.form.PAYEE_NAME = "my payee name"
payments.perfectmoney.form.PAYMENT_UNITS = "USD"
payments.perfectmoney.form.PAYMENT_URL_METHOD = "POST"
payments.perfectmoney.form.NOPAYMENT_URL_METHOD = "POST"
payments.perfectmoney.form.BAGGAGE_FIELDS = ""
payments.perfectmoney.form.PAYMENT_METHOD = "Pay Now!"
payments.perfectmoney.form.AVAILABLE_PAYMENT_METHODS = "all" 

These properties make up our overall PerfectMoney payment gateway configuration, but in the similar way you're configuring all other gateways you'd like to use in your app. Here are these parameters explained:
  1. payments.perfectmoney.* is the namespace of the perfectmoney gateway method config. As you remember, we used whole payments config namespace to configure the payments registry previously.
  2. payments.perfectmoney.name is the display name of payment method. Usefull, when you're listing available payment methods in, for example, HTML select.
  3. payments.perfectmoney.clazz is the name of the class implementing given payment method and will be explained further.
  4. payments.perfectmoney.action is the entry action of payment gateway server processing request. They usually give it as proposed <form action=""> element.
  5. payments.perfectmoney.??? is optional. You can pass any parameters you need to payment method object, and they will be accessible during processing. For example PerfectMoney additionally protects their requests by password, thus we can pass it as pass parameter to our method, and it then will be used to verify status requests.
  6. payments.perfectmoney.form.??? parameters are optional too, but they make up all static request data. Payment providers show usually a HTML form, where you have a lot of various fields, and the most of them will be invariant for your app. You can put all these fields here, in configuration.
As it's been told, in the same way you're configuring any other gateway you want. The name, class and action parameters are required always, but the others are up to the payment gateway provider's API.

We've almost finished setting up our little payment gateway framework, but savvy reader probably noticed the lack of one part here. This part is the most important actor in our play, and it is the object implementing payment method. This is the first and the last code you need to write yourself.

The payment method inherits from Zend_Payment_Gateway_Base class, and here is the important class interface, needed to be overridden:

abstract class Zend_Payment_Gateway_Base {

  public function getFormData() {}
  protected abstract function validate(Zend_Controller_Request_Http $request);
  public function findPaymentId(Zend_Controller_Request_Http $request) {}
  protected abstract function getStatusUrlParam();
  protected abstract function getSuccessUrlParam();
  protected abstract function getFailedUrlParam();

}


I hope it's self explanatory, but anyway I'm describing these methods here:
  1. getFormData() can be used to add additional POST data to first request to payment gateway server, because not all passing fields are constant and could be put into method configuration (for example amount value is a variable).
  2. validate() method needs to validate if payment is done properly, and this is the method receiving status data from payment gateway server. Thus this is the most important method from the security point of view. We have a raw request object here to  probe in any way we want. If the payment isn't done properly (someone is trying to hack us?) this method should just throw Zend_Payment_Exception with appropriate message.
  3. findPaymentId() method is helping us to find payment id in the request, when payment server is answering on our status URL. In this case payment gateway has to pass our internal payment id in its POST or GET (and it does). The payment controller looks for this id using some default lookup method, but some payment servers don't send this information in our desired way. In such instances we need to find the payment id on payment server way and this is the responsibility of this method. You have here full request object accessible to lookup.
  4. getXXXUrlParam() methods should return POST parameter names to carry status, success and error URL-s to payment gateway server. They depend on the given payment gateway API, and sometimes are unused, for example if they are configured in payment website account. In such case you just need to return null here.
In our previous configuration we've set Zend_Payment_Gateway_PerfectMoney as out payment method class name. So let's see how such exemplary class could look like.

class Zend_Payment_Gateway_PerfectMoney extends Zend_Payment_Gateway_Base {
        
  public function getFormData() {
    $data = parent::getFormData();

    $data["PAYMENT_ID"] = $this->session->getId();
    $data["PAYMENT_AMOUNT"] = $this->session->getAmount();
    $data["SUGGESTED_MEMO"] = $this->session->getDescription();
    
    return $data;
  }

  public function findPaymentId(Zend_Controller_Request_Http $request) {
    return $request->getParam("PAYMENT_ID");
  }

  protected function validate(Zend_Controller_Request_Http $request) {
    // check if payment id is correct
    if ($request->getParam('PAYMENT_ID') != $this->session->getId())
      throw new Zend_Payment_Exception(sprintf(
        "Gateway transaction id (%s) is not equal with internal transaction id",
        $request->getParam('PAYMENT_ID')));

    // check if amount is correct
    if ($request->getParam('PAYMENT_AMOUNT') != $this->session->getAmount())
      throw new Zend_Payment_Exception(sprintf(
        "Gateway transaction amount (%s) is not equal with internal amount",
        $request->getParam('PAYMENT_AMOUNT')));

    // build verification string and calculate hash
    $string =
      $request->getParam('PAYMENT_ID').':'.
      $request->getParam('PAYEE_ACCOUNT').':'.
      $request->getParam('PAYMENT_AMOUNT').':'.
      $request->getParam('PAYMENT_UNITS').':'.
      $request->getParam('PAYMENT_BATCH_NUM').':'.
      $request->getParam('PAYER_ACCOUNT').':'.
      $this->config->pass.':'.
      $request->getParam('TIMESTAMPGMT');
        
    $v2hash = $request->getParam('V2_HASH');
    $myhash = $hash=strtoupper(md5($string));

    if ($v2hash!=$myhash)
      throw new Zend_Payment_Exception(sprintf(
        "Invalid transaction hash (expected: %s, given: %s); possible hack attempt!",
        $myhash, $v2hash));
  }

  protected function getStatusUrlParam() {
    return "STATUS_URL";
  }

  protected function getSuccessUrlParam() {
    return "PAYMENT_URL";
  }

  protected function getFailedUrlParam() {
    return "NOPAYMENT_URL";
  }
        
}


I hope that this is clear and requires no more description, but in few words:
  1. In getFormData() we are complementing the request with variable fields, as for example payed amount. We have accessible here $this->session object which is the Zend_Payment_Session_Interface instance representing current payment session.
  2. In findPaymentId() we are looking for payment id in gateway returning request, stored in the parameter compliant to PerfectMoney API. Also in getXXXUrlParam() methods we are conformed to their API with parameter names.
  3. In validate() method we are checking if the status request is correct, and we are throwing Zend_Payment_Exception if something is wrong. You have here current payment session in $this->session accessible as well, and also in $this->config you can access other payment method parameters, such as a pass parameter we have previously configured. The verification method varies depending on the payment gateway provider and this one implemented here is rewritten from the original code given by PerfectMoney guys.
In result, after setting up this whole stuff, you can easily add and use any payment gateway in your application, just by providing a gateway method and config. In method object you just need to focus mainly on verification method, and don't bother about the flow. On the end of the overall process you'll be redirected to one of your own controller success or error method, where you can be sure that transaction succeeded or failed (of course if you have good verification methods). Furthermore your payment sessions are persistent and should store all transaction log in the db or other storage you chosen.

The trap

You can freely use my module in your application (I'm giving it under the MIT licence), but you can just use it as payment gateway implementation example, and write your own snippet. In this case be aware of a trap you can fall in doing this. The most natural expectation of successful payment process is following:
  1. Our application sends request to payment server, where transaction is being accomplished.
  2. The payment server is sending us the transaction status.
  3. The payment server is redirecting the user to our success URL.
But in practice it looks that they queue the status requests and you can experience a different flow:
  1. Our application sends request to payment server, where transaction is being accomplished.
  2. The payment server is redirecting the user to our success URL.
  3. After few second the payment server is sending us the transaction status.
If you followed my advice to not rely on success URL, which can be easily deceived, you'd need to handle this situation in some way and accept the payment finally just after the status request, not on success redirection. 

I have anyway this problem solved and the solution is included into the module.


The code

UPDATES: github version https://github.com/l0co/zendMultipayment

Below you can download both the module source and the test application. Using test application you can more thoroughly trace the module flow and test your payment gateways. I've included there also the test payment gateway implementation, which simulates payment gateway inside the application.
Payment module

To use it you just need to unpack the zip into your application /library directory. It creates one Payment directory under Zend library.
Test application

You can use it as standard Zend application, but you need install both Zend Framework and the Payment module into /library directory.