Difference between #3 and #2 of ePay Integration - Bulgarian Payment Provider

unchanged
Title
ePay Integration - Bulgarian Payment Provider
unchanged
Category
Tutorials
unchanged
Tags
epay, payments, bulgaria
changed
Content
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.
~~~
[php]
// 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
--------------
1.I. In the view script add the following hidden fields:
~~~
[php]
<?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'));
~~~
 2.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.
~~~
[php]
$.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');
~~~
 3.III. In the class OrderController add these methods:
~~~
[php]
public function actionEpayData(){
  $epay=new EpayBg();
  echo CJavaScript::jsonEncode($epay->getInitData($_POST));
  Yii::app()->end();
}
public function actionEpayNotify(){
  $epay = new EpayBg();
  $epay->notify();
}
~~~
 4.IV. Create a file in protected/components/EpayBg.php
~~~
[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))));
	}

}
~~~
 5.V. The Order model would start with:
~~~
[php]
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.
Write new article