Jump to content

[WIP] Using Bankwire as base for a payment gateway module


Beluga
 Share

Recommended Posts

I'm using Bankwire as base to build a Checkout.fi module (link to their reference PHP implementation).

 

I have printed the code of their PHP example, their osCommerce module and PrestaShop's Bankwire on paper and studied them carefully, so I have a pretty good idea of how they work, but I'm a newbie to PrestaShop and I'm having trouble figuring out how to integrate this.

Anyways, I dived into practice to see what type of interesting error messages I can produce and unsurprisingly I soon got a taste of them.

 

When confirming an order and moving to payment.php, this error appears, pointing to the $coData = array(); line (it is near the bottom of the code):

 

Parse error: syntax error, unexpected '$coData' (T_VARIABLE)

 

I have no idea, why this is unexpected and would appreciate any advice.

 

Here are the contents of my payment.php:

class Checkout
{
    private $version        = "0001";
    private $language        = "FI";
    private $country        = "FIN";
    private $currency        = "EUR";
    private $device            = "1";
    private $content        = "1";
    private $type            = "0";
    private $algorithm        = "2";
    private $merchant        = "";
    private $password        = "";
    private $stamp            = 0;
    private $amount            = 0;
    private $reference        = "";
    private $message        = "";
    private $return            = "";
    private $cancel            = "";
    private $reject            = "";
    private $delayed        = "";
    private $delivery_date        = "";
    private $firstname        = "";
    private $familyname        = "";
    private $address        = "";
    private $postcode        = "";
    private $postoffice        = "";
    private $status            = "";
    private $email            = "";
    
    public function __construct($merchant, $password)
    {
        $this->merchant    = $merchant; // merchant id
        $this->password    = $password; // security key (about 80 chars)
    }

    /*
      * generates MAC and prepares values for creating payment
     */    
    public function getCheckoutObject($data)
    {
        // overwrite default values
        foreach($data as $key => $value)
        {
            $this->{$key} = $value;
        }

        $mac =
strtoupper(md5("{$this->version}+{$this->stamp}+{$this->amount}+{$this->reference}+{$this->message}+{$this->language}+{$this->merchant}+{$this->return}+{$this->cancel}+{$this->reject}+{$this->delayed}+{$this->country}+{$this->currency}+{$this->device}+{$this->content}+{$this->type}+{$this->algorithm}+{$this->delivery_date}+{$this->firstname}+{$this->familyname}+{$this->address}+{$this->postcode}+{$this->postoffice}+{$this->password}"));
        $post['VERSION']        = $this->version;
        $post['STAMP']            = $this->stamp;
        $post['AMOUNT']            = $this->amount;
        $post['REFERENCE']        = $this->reference;
        $post['MESSAGE']        = $this->message;
        $post['LANGUAGE']        = $this->language;
        $post['MERCHANT']        = $this->merchant;
        $post['RETURN']            = $this->return;
        $post['CANCEL']            = $this->cancel;
        $post['REJECT']            = $this->reject;
        $post['DELAYED']        = $this->delayed;
        $post['COUNTRY']        = $this->country;
        $post['CURRENCY']        = $this->currency;
        $post['DEVICE']            = $this->device;
        $post['CONTENT']        = $this->content;
        $post['TYPE']            = $this->type;
        $post['ALGORITHM']        = $this->algorithm;
        $post['DELIVERY_DATE']        = $this->delivery_date;
        $post['FIRSTNAME']        = $this->firstname;
        $post['FAMILYNAME']        = $this->familyname;
        $post['ADDRESS']        = $this->address;
        $post['POSTCODE']        = $this->postcode;
        $post['POSTOFFICE']        = $this->postoffice;
        $post['MAC']            = $mac;

        $post['EMAIL']            = $this->email;
        $post['PHONE']            = $this->phone;

        return $post;
    }
    
    /*
     * returns payment information in XML
     */
    public function getCheckoutXML($data)
    {
        $this->device = "10";
        return $this->sendPost($this->getCheckoutObject($data));
    }
    
    private function sendPost($post) {
        $options = array(
                CURLOPT_POST         => 1,
                CURLOPT_HEADER         => 0,
                CURLOPT_URL         => 'https://payment.checkout.fi',
                CURLOPT_FRESH_CONNECT     => 1,
                CURLOPT_RETURNTRANSFER     => 1,
                CURLOPT_FORBID_REUSE     => 1,
                CURLOPT_TIMEOUT     => 20,
                CURLOPT_POSTFIELDS     => http_build_query($post)
        );
        
        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $result = curl_exec($ch);
        curl_close($ch);

        return $result;
    }
    
    public function validateCheckout($data)
    {
        $generatedMac =
strtoupper(md5("{$this->password}&{$data['VERSION']}&{$data['STAMP']}&{$data['REFERENCE']}&{$data['PAYMENT']}&{$data['STATUS']}&{$data['ALGORITHM']}"));
        
        if($data['MAC'] == $generatedMac)
            return true;
        else
            return false;
    }
    
    public function isPaid($status)
    {
        if(in_array($status, array(2, 4, 5, 6, 7, 8, 9, 10)))
            return true;
        else
            return false;
    }
}  // class Checkout

class checkoutfiPaymentModuleFrontController extends ModuleFrontController
{
    public $ssl = true;
    
    /**
     * @see FrontController::initContent()
     */
    public function initContent()
    {
        $this->display_column_left = false;
        parent::initContent();

        $cart = $this->context->cart;
        /*if (!$this->module->checkCurrency($cart))
            Tools::redirect('index.php?controller=order');*/
        $summa = number_format($this->context->cart->getOrderTotal * 100, 0, '','');
        $co = new Checkout(375917, "SAIPPUAKAUPPIAS"); // merchantID and securitykey (normally about 80 chars)
        $return = $this->context->link->getModuleLink('checkoutfi', 'validation', [], true)
        
        // Order information
        $coData = array();
        $coData["stamp"]             = time();
        $coData["reference"]         = $coData['stamp'];
        $coData["message"]             = "testiviesti";
        $coData["return"]             = $return;
        $coData["cancel"]             = $cancel;
        $coData["reject"]             = $reject;
        $coData["delayed"]             = $delayed;
        $coData["amount"]             = $summa;
        $coData["delivery_date"]     = date("Ymd");
        $coData["firstname"]         = "testietunimi";
        $coData["familyname"]        = "testisukunimi";
        $coData["address"]             = "testiosoite";
        $coData["postcode"]         = "00340";
        $coData["postoffice"]         = "testipostikonttori";

        // change stamp for xml method
        $coData['stamp'] = time() + 1;
        $response =    $co->getCheckoutXML($coData); // get payment button data
        $xml = simplexml_load_string($response);
        
        $this->context->smarty->assign(array(
            'nbProducts' => $cart->nbProducts(),
            'cust_currency' => $cart->id_currency,
            'xmlbanks' => $xml->payments->payment->banks,
            'total' => $cart->getOrderTotal(true, Cart::BOTH),
            'this_path' => $this->module->getPathUri(),
            'this_path_bw' => $this->module->getPathUri(),
            'this_path_ssl' => Tools::getShopDomainSsl(true, true).__PS_BASE_URI__.'modules/'.$this->module->name.'/'
        ));

        $this->setTemplate('payment_execution.tpl');
    }
}
Edited by Beluga (see edit history)

Share this post


Link to post
Share on other sites

I love you, Pascal!

 

Now I got into the real errors. First I realized that cURL over SSL won't work on my local (Windows 7 64-bit) machine, not with Bitnami Wampstack or XAMPP. So I copied my PS to a server where it works (I mean the PHP example from Checkout.fi renders just fine).

 

The first error was about the '[' character in

$return = $this->context->link->getModuleLink('checkoutfi', 'validation', [], true);

Well, I simplified things a bit and used the "dummy data" from the PHP example.

Now I'd like to see Smarty working.

I have this in my payment_execution.tpl:

    {foreach $xmlbanks as $bankX}
    {foreach $bankX as $bank}
    <div class='C1'>
        <form action='{[email protected]}' method='post'>
            {foreach $bank as $key=>$value}
            <input type='hidden' name='{$key}' value='{$value|escape:"html"}'>
            {/foreach}
            <span><input type='image' src='{[email protected]}'> </span>
            <div>
                {[email protected]}
            </div>
        </form>
    </div>
    {/foreach}
    {/foreach}

And it throws:

 

Notice: Undefined property: Smarty_Variable::$name ... on line 87

 

And doesn't render the bank buttons (the page renders otherwise fine).

 

Here is how Smarty has processed it in the cached file:

<?php  $_smarty_tpl->tpl_vars['bankX'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['bankX']->_loop = false;
 $_from = $_smarty_tpl->tpl_vars['xmlbanks']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');}
foreach ($_from as $_smarty_tpl->tpl_vars['bankX']->key => $_smarty_tpl->tpl_vars['bankX']->value){
$_smarty_tpl->tpl_vars['bankX']->_loop = true;
?>
    <?php  $_smarty_tpl->tpl_vars['bank'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['bank']->_loop = false;
 $_from = $_smarty_tpl->tpl_vars['bankX']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');}
foreach ($_from as $_smarty_tpl->tpl_vars['bank']->key => $_smarty_tpl->tpl_vars['bank']->value){
$_smarty_tpl->tpl_vars['bank']->_loop = true;
?>
    <div class='C1'>
        <form action='<?php echo $_smarty_tpl->tpl_vars['bank']->url;?>
' method='post'>
            <?php  $_smarty_tpl->tpl_vars['value'] = new Smarty_Variable; $_smarty_tpl->tpl_vars['value']->_loop = false;
 $_smarty_tpl->tpl_vars['key'] = new Smarty_Variable;
 $_from = $_smarty_tpl->tpl_vars['bank']->value; if (!is_array($_from) && !is_object($_from)) { settype($_from, 'array');}
foreach ($_from as $_smarty_tpl->tpl_vars['value']->key => $_smarty_tpl->tpl_vars['value']->value){
$_smarty_tpl->tpl_vars['value']->_loop = true;
 $_smarty_tpl->tpl_vars['key']->value = $_smarty_tpl->tpl_vars['value']->key;
?>
            <input type='hidden' name='<?php echo $_smarty_tpl->tpl_vars['key']->value;?>
' value='<?php echo htmlspecialchars($_smarty_tpl->tpl_vars['value']->value, ENT_QUOTES, 'UTF-8', true);?>
'>
            <?php } ?>
            <span><input type='image' src='<?php echo $_smarty_tpl->tpl_vars['bank']->icon;?>
'> </span>
            <div>
                <?php echo $_smarty_tpl->tpl_vars['bank']->name;?>

            </div>
        </form>

Line 87 is

<?php echo $_smarty_tpl->tpl_vars['bank']->name;?>

What might be the correct syntax?

Share this post


Link to post
Share on other sites

I think the @ is causing the problem here, as it doesn't get escaped.

Try the way they use it in the sample code:

$bank['name']

 

(and $bank['icon'] and $bank['url'] for that matter)

 

give it a try

pascal

Thank you, it worked! I used the @ because I read from the Smarty docs

Although you can retrieve the array key with the syntax {foreach $myArray as $myKey => $myValue}, the key is always available as [email protected] within the foreach loop.

 

I feel like I'm making some progress. I figured out the way to get the order total in cents is:

$summa = number_format($cart->getOrderTotal(true, 3) * 100, 0, '','');

But now I'm interested in validating the order as paid in PrestaShop.

 

I moved the "class Checkout" from payment.php to the main checkoutfi.php and it worked ok, but when I initially tried to install the module it didn't allow me to have the class Checkout in checkoutfi.php.. so I wonder why it works now or if the cache hasn't been updated?

Here is the error I get in the Modules page, if I have "class Checkout" in checkoutfi.php:

Parse error: syntax error, unexpected end of file in ..\classes\module\Module.php(1077) : eval()'d code on line 237

The following module(s) could not be loaded::

 

    checkoutfi (parse error in /modules/checkoutfi/checkoutfi.php)

    checkoutfi (class missing in /modules/checkoutfi/checkoutfi.php)

 

  I have this in validation.php:

class checkoutfiValidationModuleFrontController extends ModuleFrontController
{
    /**
     * @see FrontController::postProcess()
     */
    public function postProcess()
    {
        $cart = $this->context->cart;
        if ($cart->id_customer == 0 || $cart->id_address_delivery == 0 || $cart->id_address_invoice == 0 || !$this->module->active)
            Tools::redirect('index.php?controller=order&step=1');

        // Check that this payment option is still available in case the customer changed his address just before the end of the checkout process
        $authorized = false;
        foreach (Module::getPaymentModules() as $module)
            if ($module['name'] == 'checkoutfi')
            {
                $authorized = true;
                break;
            }
        if (!$authorized)
            die($this->module->l('This payment method is not available.', 'validation'));

        $customer = new Customer($cart->id_customer);
        if (!Validate::isLoadedObject($customer))
            Tools::redirect('index.php?controller=order&step=1');

        $currency = $this->context->currency;
        $total = (float)$cart->getOrderTotal(true, Cart::BOTH);

        if(isset($_GET['MAC'])) {
            if($co->validateCheckout($_GET)) {
                if($co->isPaid($_GET['STATUS'])) {
                    $this->module->validateOrder($cart->id, Configuration::get('PS_OS_CHECKOUTFI'), $total, $this->module->displayName, NULL, $mailVars, (int)$currency->id, false, $customer->secure_key);
                    Tools::redirect('index.php?controller=order-confirmation&id_cart='.$cart->id.'&id_module='.$this->module->id.'&id_order='.$this->module->currentOrder.'&key='.$customer->secure_key);
                }
            }
        }
    }
}

I'm passing this as the return url for the bank through payment.php:

$return = $this->context->link->getModuleLink('checkoutfi', 'validate');

It outputs the url http://mysite.com/index.php?fc=module&module=checkoutfi&controller=validate&id_lang=2

 

Then when the payment gateway is supposed to redirect back to the store, it produces an error about infinite redirects and I can see this URL in the address bar:

https://payment.checkout.fi/IEsuX02FRS/fi/done?VERSION=0001&STAMP=1380520435&REFERENCE=12344&PAYMENT=10247204&STATUS=2&ALGORITHM=2&MAC=0306F690FE65627D1594F65BAFF9643E

So the status is ok (2), but is my validation.php set up incorrectly to receive the info?

Edited by Beluga (see edit history)

Share this post


Link to post
Share on other sites

It seems to be that Prestashop does not accept the redirect from the gateway because of the lack of session info or cookie. How can I get PS to accept the redirect? It is explicitly mentioned in the gateway API that the ecommerce software must accept the HTTP GET request even without cookie or session info.

Share this post


Link to post
Share on other sites

Just popping in to say that the problem was entirely due to my sloppiness or code blindness. I fully expect to get this working in the next couple of days and I will publish the module for review.

Share this post


Link to post
Share on other sites

Ok, here is a work in progress version of the module:

checkoutfi.zip

It still needs a bit more polish. It has a config with the testing merchant id and security key prefilled.

 

Most crucially, I'm interested in how I could pass the session info to validate.php.

In payment.php I have

        $tilaustunniste = time().rand(0,9999); // unique order identification
        $tilaustunniste = str_pad($tilaustunniste, 9, "1", STR_PAD_RIGHT);

        session_start();
        $_SESSION['timestamp'] = $tilaustunniste;
        $psStamp = md5($_SESSION['timestamp'] . $this->lisaaViitenumeroonTarkiste($tilaustunniste) . "SAIPPUAKAUPPIAS");
        if(substr_count($return, '?'))
            $return     .= '&psStamp='.$psStamp;
        else
            $return     .= '?psStamp='.$psStamp;

Then in validation.php I have:

if (isset($_SESSION['timestamp']) && $_SESSION['timestamp'] != null)
                {
                    $psStamp = md5($_SESSION['timestamp'] . $_GET['STAMP'] . Configuration::get('CHECKOUTFI_PASSWORD'));

                    if ($psStamp != $_GET['psStamp']){
                        die("Tilauksen tunnisteesta '" . $_GET['STAMP'] . "' laskettu md5 '" . $psStamp . "' tilauksen md5:n '" . $_GET['psStamp'] . "' kanssa.");
                    }
                }

These come from an osCommerce module implementation of the Checkout.fi payment gateway.

In the payment_return.tpl I have Session: {$smarty.session.timestamp} which causes Smarty to throw an error that session is not defined.

 

I'd like to get rid of useless info being passed (firstname, lastname etc.), but had some trouble when I simply removed them from payment.php and checkoutdata.php. I'll investigate this later.

I'll probably have to make a new payment status type for the delayed payment.

Share this post


Link to post
Share on other sites

Why can PayPal module's install do this:

$orderState = new OrderState();
$orderState->name = array();

but when I do it in my install, I get "Property OrderState->name is not valid" ? It does, however, create the new order state - it just doesn't create the lang description in the ps_order_state_lang table.
 
Here is my full install function for now:

public function install()
    {
        if (!parent::install() || !$this->registerHook('payment') || !$this->registerHook('paymentReturn'))
            return false;

            Configuration::updateValue('CHECKOUTFI_PASSWORD', "SAIPPUAKAUPPIAS");
            Configuration::updateValue('CHECKOUTFI_MERCHANT', 375917);
            if (!Configuration::get('PS_OS_DELAYED'))
            {
            $orderState = new OrderState();
            $orderState->name = array();

            foreach (Language::getLanguages() as $language)
            {
                if (strtolower($language['iso_code']) == 'fi')
                    $orderState->name[$language['id_lang']] = 'Checkout.fi-maksun tila viivästetty';
                else
                    $orderState->name[$language['id_lang']] = 'Checkout.fi payment state: delayed';
            }

            $orderState->send_email = false;
            $orderState->color = '#DDEEFF';
            $orderState->hidden = false;
            $orderState->delivery = false;
            $orderState->logable = true;
            $orderState->invoice = false;

            if ($orderState->add())
            {
                $source = dirname(__FILE__).'/logo.gif';
                $destination = dirname(__FILE__).'/../../img/os/'.(int)$orderState->id.'.gif';
                copy($source, $destination);
            }
            Configuration::updateValue('PS_OS_DELAYED', (int)$orderState->id);            
            }
            
            return true;
    }

EDIT: Found this solution: http://www.prestashop.com/forums/topic/7643-how-to-change-default-order-status-for-some-payment-module/?view=findpost&p=1304944
So now I have this working code in my install function:

if (!Configuration::get('PS_OS_DELAYED'))
{
$values_to_insert = array(
'invoice' => 1,
'send_email' => 1,
'module_name' => $this->name,
'color' => 'RoyalBlue',
'unremovable' => 0,
'hidden' => 0,
'logable' => 1,
'delivery' => 0,
'shipped' => 0,
'paid' => 1,
'deleted' => 0);

if(!Db::getInstance()->autoExecute(_DB_PREFIX_.'order_state', $values_to_insert, 'INSERT'))
return false;
$id_order_state = (int)Db::getInstance()->Insert_ID();
$languages = Language::getLanguages(false);
foreach ($languages as $language)
Db::getInstance()->autoExecute(_DB_PREFIX_.'order_state_lang', array('id_order_state'=>$id_order_state, 'id_lang'=>$language['id_lang'], 'name'=>'Checkout.fi payment state: delayed', 'template'=>''), 'INSERT');
if ([email protected](dirname(__FILE__).DIRECTORY_SEPARATOR.'logo.gif', _PS_ROOT_DIR_.DIRECTORY_SEPARATOR.'img'.DIRECTORY_SEPARATOR.'os'.DIRECTORY_SEPARATOR.$id_order_state.'.gif'))
return false;
Configuration::updateValue('PS_OS_DELAYED', $id_order_state);
unset($id_order_state);
}
Edited by Beluga (see edit history)

Share this post


Link to post
Share on other sites

I realized it was useless for me to obsess with the session and I did it with Prestashop's cookie method, in payment.php I have:

        $idCookie = $this->context->cookie->id_customer;
        $psStamp = md5($idCookie . $this->lisaaViitenumeroonTarkiste($tilaustunniste) . Configuration::get('CHECKOUTFI_PASSWORD'));

and in validation.php I have:

        $idCookie = $this->context->cookie->id_customer;
        $md5 = md5(Configuration::get('CHECKOUTFI_PASSWORD')."&{$_GET['VERSION']}&{$_GET['STAMP']}&{$_GET['REFERENCE']}&{$_GET['PAYMENT']}&{$_GET['STATUS']}&{$_GET['ALGORITHM']}");
        if (strtoupper($md5) == $_GET['MAC'])
        {
            if ($_GET['STATUS'] == 2 || $_GET['STATUS'] == 3)
            {
                // 2 = maksu suoritettu / payment done
                // 3 = maksu viivästetty / payment delayed

                
                $psStamp = md5($idCookie . $_GET['STAMP'] . Configuration::get('CHECKOUTFI_PASSWORD'));

                if ($psStamp != $_GET['psStamp']){
                    die("Tilauksen tunnisteesta '" . $_GET['STAMP'] . "' laskettu md5 '" . $psStamp . "' tilauksen md5:n '" . $_GET['psStamp'] . "' kanssa.");
                }

Here is the module so far, including the broken order state adding at install:

checkoutfi.zip

Please only use it on a test install of PS.

Edited by Beluga (see edit history)

Share this post


Link to post
Share on other sites

  • 1 year later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...

Important Information

Cookies ensure the smooth running of our services. Using these, you accept the use of cookies. Learn More