Yii 1.1: ePay Integration - Bulgarian Payment Provider

7 followers

Preparation

  1. Sign up a developer account at https://devep2.datamax.bg/ep2/epay2_demo
  2. Set up merchant and client username and password in ePay and notify URL address in the dev account (the same operations would be done for a live account too).
  3. Edit protected/config/main.php and add blocks of code for LIVE and DEV environment.
// Define LIVE constant as true if 'localhost' is not present in the host name. Configure the detecting of environment as necessary of course.
defined('LIVE') || define('LIVE', strpos($_SERVER['HTTP_HOST'],'localhost')===false ? true : false);
if (LIVE) {
  define('EPAY_URL','https://www.epay.bg/');
  define('EPAY_CLIENT_EMAIL','*********');
  define('EPAY_CLIENT_NUMBER','********');
  define('EPAY_SECRET_KEY','********');
}else{
  define('EPAY_URL','https://devep2.datamax.bg/ep2/epay2_demo/');
  define('EPAY_CLIENT_EMAIL','*********');
  define('EPAY_CLIENT_NUMBER','********');
  define('EPAY_SECRET_KEY','********');
}

Implementation

I. In the view script add the following hidden fields:

<?php
  $form=$this->beginWidget('CActiveForm', array(
    'id'=>'orderForm',
    'htmlOptions'=>array('onsubmit'=>'return false;'),
    'action'=>EPAY_URL,
  ));
  // epay.bg fields
  echo CHtml::hiddenField('PAGE','paylogin');
  echo CHtml::hiddenField('ENCODED','',array('id'=>'epayEncoded'));
  echo CHtml::hiddenField('CHECKSUM','',array('id'=>'epayChecksum'));
  echo CHtml::hiddenField('URL_OK',Yii::app()->createAbsoluteUrl('order/success'));
  echo CHtml::hiddenField('URL_CANCEL',Yii::app()->createAbsoluteUrl('order/canceled'));

II. Further down in the same view file we will put JavaScript code that will send request to OrderController::actionCreate() to create the order record in the database and on success we will send another request to OrderController::actionEpayData() to generate the needed data for the hidden fields ENCODING and CHECKSUM.

$.post('<?php echo Yii::app()->createUrl('order/create'); ?>',$('#orderForm').serializeArray(),function(orderResp) {
  if(orderResp.error === undefined){
    $.post('<?php echo url('order/epayData')?>',{orderId:orderResp.id, price:orderResp.total, productName:'<?php echo $productLang->title?>'}, function(epayResp){
      $('#epayEncoded').val(epayResp.encoded);
      $('#epayChecksum').val(epayResp.checksum);
      $('#orderForm').attr({action:'<?php echo EPAY_URL?>',onsubmit:true}).submit();
    },'json');
  }
}else{
  alert(orderResp.error);
},'json');

III. In the class OrderController add these methods:

public function actionEpayData(){
  $epay=new EpayBg();
  echo CJavaScript::jsonEncode($epay->getInitData($_POST));
  Yii::app()->end();
}
public function actionEpayNotify(){
  $epay = new EpayBg();
  $epay->notify();
}

IV. Create a file in protected/components/EpayBg.php

class EpayBg {
 
    public function getInitData($post){
        $dt=new DateTime('+1 day');
        $expDate=$dt->format('d.m.Y');
        $min=EPAY_CLIENT_NUMBER;
        $data = <<<DATA
MIN={$min}
INVOICE={$post['orderId']}
AMOUNT={$post['price']}
EXP_TIME={$expDate}
DESCR={$post['productName']}
DATA;
 
        # XXX Packet:
        #     (MIN or EMAIL)=     REQUIRED
        #     INVOICE=            REQUIRED
        #     AMOUNT=             REQUIRED
        #     EXP_TIME=           REQUIRED
        #     DESCR=              OPTIONAL
 
        $encoded = base64_encode($data);
        return array(
            'encoded'=>$encoded,
            'checksum'=>$this->hmac('sha1', $encoded, EPAY_SECRET_KEY)
        );
    }
 
    public function notify(){
        $logCat='epay';
        if(empty($_POST['encoded']) || empty($_POST['checksum'])){
            Yii::log('Missing encoded or checksum POST variables', CLogger::LEVEL_INFO, $logCat);
        }else{
            $encoded = $_POST['encoded'];
            $checksum = $_POST['checksum'];
            $hmac = $this->hmac('sha1', $encoded, EPAY_SECRET_KEY); # XXX SHA-1 algorithm REQUIRED
            if ($hmac == $checksum) { # XXX Check if the received CHECKSUM is OK
                $data = base64_decode($encoded);
                $lines_arr = split("\n", $data);
                $infoData = '';
                foreach ($lines_arr as $line) {
                    if (preg_match("/^INVOICE=(\d+):STATUS=(PAID|DENIED|EXPIRED)(:PAY_TIME=(\d+):STAN=(\d+):BCODE=([0-9a-zA-Z]+))?$/",
                            $line, $regs)) {
                        Yii::log($line,CLogger::LEVEL_INFO,$logCat);
                        $invoice = $regs[1]; // order id
                        $status = $regs[2];
                        $payDate = $regs[4]; # YYYYMMDDHHIISS
                        $stan = $regs[5]; # XXX if PAID
                        $bcode = $regs[6]; # XXX if PAID
                        # XXX process $invoice, $status, $payDate, $stan, $bcode here
                        # XXX if OK for this invoice
                        $infoData .= "INVOICE=$invoice:STATUS=OK\n";
                        if($status==='PAID'){
                            $model=Order::model()->findByPk($invoice);
                            if($model===null){
                                Yii::log($invoice.' order not found',CLogger::LEVEL_INFO,$logCat);
                            }else{
                                $model->setAttributes(array(
                                    'payDate'=>implode('-',array(substr($payDate,0,4),substr($payDate,4,2),substr($payDate,6,2))).' '.
                                        implode(':',array(substr($payDate,8,2),substr($payDate,10,2),substr($payDate,12,2))),
                                    'stan'=>$stan,
                                    'bcode'=>$bcode,
                                    'statusId'=>Order::STATUS_PAID
                                ));
                                $model->save();
                                Product::deductQty($model);
                                Product::sendSuccessEmails($model);
                            }
                        }
                    }
                }
                echo $infoData, "\n";
            }
            else {
                echo "ERR=Not valid CHECKSUM\n";
                Yii::log('ERR=Not valid CHECKSUM',CLogger::LEVEL_ERROR,$logCat);
            }
        }
    }
 
    private function hmac($algo,$data,$passwd){
        /* md5 and sha1 only */
        $algo=strtolower($algo);
        $p=array('md5'=>'H32','sha1'=>'H40');
        if(strlen($passwd)>64)
            $passwd=pack($p[$algo],$algo($passwd));
        if(strlen($passwd)<64)
            $passwd=str_pad($passwd,64,chr(0));
 
        $ipad=substr($passwd,0,64) ^ str_repeat(chr(0x36),64);
        $opad=substr($passwd,0,64) ^ str_repeat(chr(0x5C),64);
        return($algo($opad.pack($p[$algo],$algo($ipad.$data))));
    }
 
}

V. The Order model would start with:

class Order extends CActiveRecord
{
    const STATUS_INITIATED = 1;
    const STATUS_CANCELED = 2;
    const STATUS_EXPIRED = 3;
    const STATUS_PAID = 4;
    public $statuses = array(
        self::STATUS_INITIATED => 'Initiated',
        self::STATUS_CANCELED => 'Canceled',
        self::STATUS_EXPIRED => 'Expired',
        self::STATUS_PAID => 'Paid',
    );
// more code of the model

Final words

The classes OrderController and EpayBg have methods for handling both initializing the payment and the notification they send about the payment status. Epay will be sending notifications until the notify script sends correct message i.e. "INVOICE=$invoice:STATUS=OK\n". Make sure there's no other output or they will continue sending notifications. Good luck.

Total 2 comments

#11359 report it
Arlekino at 2013/01/09 03:58am
Полезно

Браво, наистина много полезно.

#11181 report it
ProXy at 2012/12/24 02:27am
Браво

Много полезно четиво. Браво!!!

Leave a comment

Please to leave your comment.

Write new article