ePay Integration - Bulgarian Payment Provider

  1. Preparation
  2. Implementation
  3. Final words

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.

6 0
5 followers
Viewed: 11 931 times
Version: 1.1
Category: Tutorials
Written by: yasen
Last updated by: yasen
Created on: Dec 22, 2012
Last updated: 12 years ago
Update Article

Revisions

View all history

Related Articles