📜 ⬆️ ⬇️

Organization of on-line payments on the site. For those who have never done this, but are afraid that they will have to. Part 2: Architecture

Following the first part , designed primarily to show that "the devil is not so bad as he is painted"

An article about the architecture of the part of the project that deals with online payments. Intentionally, I would not like to describe in detail the API of a particular billing or the registration procedure in it. The subtleties of specific billing need to be discussed separately, otherwise the topic simply does not reveal. The purpose of the article: to discuss the option of architecture that allows stringing new types of billing and types of payments, with the least headache.

So, for a start, imagine, we thought a little and made on our website a very simple sale of goods through one of the billing systems.
  1. We have information about the product: Product ID, price, <specifications>.
  2. Users go to the site and click on the "buy" button. Store information about the purchase: purchase ID, product ID, price of the product at that moment, <customer information>;
  3. The user watches his purchases, presses "pay" for one of them. We save the payment information: payment ID, purchase ID, payment date, payment status (, payment amount), and send the user to the billing system;
  4. The script processing billing responses saves response data: response ID, <all billing sent>, response date, response status. Checks the validity of the answer, according to the result of the check it maintains the status of the answer. If everything is ok, then put the desired purchase status "paid"
  5. information about the paid purchase is displayed at the moderators marked "must be delivered"

* Information about the buyer - it can be a user number, by which you can find all the necessary data, and can be directly data (address, phone, ...), if you do not want to burden your users with registration.

We debugged all this, and for a while we were even satisfied with our work. But, more and more often we hear: it would be necessary to tie such a billing. In addition, we want to sell not only products, but also different types of accounts to our users, and even let them pay separately if they want to raise their rating by 10 points, and so on and so forth. Call it shopping, but we will keep in mind that purchases are now of a different type.
')
As you know, a good idea comes afterwards. Only when I had to screw a couple of billing systems and organize several different types of purchases, in my system the purchase processing and billing processing were separated, the common parts in working with different billing systems, patterns in processing purchases of different types were distinguished.

The separation of the processing of purchases from the processing of billing operations makes it possible

When processing different types of purchases, you can see that all of them can be divided into components:
  1. Information about a specific product is available in the system: ID (unique for this type), price, <specifications>. This may be a description of the goods in the store, or a description of the type of account and the period of its validity, or a description of the service of increasing the rating by N positions;
  2. Saving information about the user's choice (which user, which type of product and which product number he chose);
  3. Change of purchase status (paid, deleted, ...);
  4. Realization of the purchase, let's call it that. (for example: delivery of goods, or a change in account type for a specified period, or an increase in rating by N positions);

Now it is clear that there are fundamental differences only in paragraphs 1 and 4. If the interface of the class describing the type of purchase and the actions for making a purchase is observed, the processing scheme for various types of purchase becomes unified.

Work with the billing system can be divided into points:
  1. Saving payment information: payment ID, billing type, purchase ID, payment status, <other characteristics>;
  2. Redirecting the user to the billing system, indicating the payment number and the amount of the purchase;
  3. Check the validity of the response from the billing;
  4. Payment status change;
  5. If everything is OK, the call for processing the purchase (change the status of the purchase, the realization of the purchase, ...)
Points 2 and 3 for different billing will be yours. T.O. while observing the interface of the class describing the type of billing that implements functions 2 and 3, the scheme of working with different billing systems is also unified.

Class diagram for visual display of the structure described:

image

These are general principles that I try to follow in my work. I think this scheme can and should be improved. I hope for a constructive discussion.



I remember, in the first part of the article, that not only common words are expected of me, but also specific lines of code. Approximately, the code of this construction is given below. A real example, pulled out of context and trimmed to the maximum to highlight the main point. Unfortunately, even with this, it turned out a bit too much code :)

Immediately make a reservation, in another language, it was possible to get along with the abstract class and its heirs, but since in PHP you cannot redefine the static function, the ancestors were divided into an interface + base class.

Shopping interface and an example for the implementation of a paid member policy:
interface InterfacePurchase {
public function getId();

public function getItemId();
public function setItemId ($val);

public function getItemType();
public function setItemType ($val);

public function getPrice();
public function setPrice ($val);

public function getUserId();
public function setUserId($val);

public function getStatus();
public function setStatus($val);

public function save ();

/**
*
*/
public function callbackPayment ();

/**
* -. ,
*/
public function getItem ();
}

class CPurchase {
protected $_mPurchase = null ;

/**
* @return InterfacePurchase
**/
public static function createPurchaseByType ($type) {
$purchase = null ;
switch ($type){
case PURCHASE_SHOP: $purchase = new CPurchaseShop(); break;
case PURCHASE_ACCOUNT: $purchase = new CPurchaseAccount(); break;
case PURCHASE_RAIT: $purchase = new CPurchaseRait(); break;
// ...
default : throw new ExceptionUnknownPurchaseType (__CLASS__);
}
$purchase->_mPurchase = new CPurchaseItem ();
return $purchase;
}

/**
* @return InterfacePurchase
**/
public static function loadPurchaseById($id){
$purchase_item = CPurchaseItem::getById($id);
$purchase = self::createPurchaseByType($purchase_item->getType());
$purchase->_mPurchase = $purchase_item;
}

public function getId() { return $ this ->_mPurchase->getId(); }

public function getItemId() { return $ this ->_mPurchase->getItemId();}
public function setItemId ($val) { return $ this ->_mPurchase->setItemId( $val ); }

public function getItemType() { return $ this ->_mPurchase->getItemType(); }
public function setItemType ($val) { return $ this ->_mPurchase->setItemType( $val ); }

public function getPrice() { return $ this ->_mPurchase->getPrice (); }
public function setPrice ($val) { return $ this ->_mPurchase->setPrice ( $val ); }

public function getUserId() { return $ this ->_mPurchase->getUserId(); }
public function setUserId($val) { return $ this ->_mPurchase->setUserId($val); }

public function getStatus() { return $ this ->_mPurchase->getStatus(); }
public function setStatus($val) { return $ this ->_mPurchase->setStatus($val); }

public function save () { $ this ->_mPurchase->save(); }

}

Class CPurchaseAccount extends CPurchase implements InterfacePurchase {

public function getItem (){
$item = null ;
If ($item_id = $ this ->getItemId()) {
$item = CMembership::getById($item_id);
}
return $item;
}
public function callbackPayment () {
$ this ->setStatus(PURCHASE_STATUS_OK);
ServiceAccount::setMembership($ this ->getUserId(), $ this ->getItemId());
}
}

* This source code was highlighted with Source Code Highlighter .


Billing interface and an example for the implementation of working with Robox:
interface InterfaceBilling {
public function getId();

public function getPurchaseId();
public function setPurchaseId ($val);

public function getBillingType();
public function setBillingType ($val);

public function getStatus();
public function setStatus($val);

public function save ();

/**
*
*/
public function redirectToBilling ();

/**
* , ,
*/
public static function checkResponseFormat ($data);

/**
*
*/
public function checkResult ($data);

/**
* . , .
*/
public function addResultInView ($view, $results);
}

class CBilling {
protected $_mBilling = null ;

/**
* @return InterfaceBilling
**/
public static function createBillingByType( $type ) {
switch ($type){
case BILLING_ROBOX: $billing = new CBillingRobox(); break;
case BILLING_WM: $billing = new CBillingWM(); break;
// ...
default : throw new ExceptionUnknownBillingType (__CLASS__);
}
$billing->_mBilling = new CBillingItem();
$ this ->setBillingType($type);
}

public static function getBillingTypeByRequest($response_data) {
$billing_type = null ;
if (CBillingRobox::checkResponseFormat($response_data)) {
$billing_type = self::BILLING_ROBOX;
}
if (CBillingWM::checkResponseFormat($response_data)) {
$billing_type = self::BILLING_WM;
}

return $billing_type;
}

public function getId() { return $ this ->_mBilling->getId(); }

public function getPurchaseId() { return $ this ->_mBilling->getPurchaseId(); }
public function setPurchaseId ($val) { return $ this ->_mBilling->setPurchaseId($val); }

public function getBillingType() { return $ this ->_mBilling->getBillingType(); }
public function setBillingType ($val) { return $ this ->_mBilling->setBillingType($val); }

public function getStatus() { return $ this ->_mBilling->getStatus(); }
public function setStatus($val) { return $ this ->_mBilling->setStatus($val); }

public function save () { $ this ->_mBilling->save(); }

public function checkSumm($summ) {
$purchase = CPurchaseItem::getById($ this ->getPurchaseId());
return intval($purchase->getPrice()) == intval($summ);
}

public function checkStatusNotFinish() {
$purchase = CPurchaseItem::getById($ this ->getPurchaseId());
return PURCHASE_STATUS_OK != $purchase->getStatus();
}
}

class CBillingRobox extends CBilling implements InterfaceBilling {
public function redirectToBilling () {
$redirect_uri = Config::getKey( 'pay_uri' , 'robox' );
$purchase = CPurchaseItem::getById($ this ->getPurchaseId());
$hash = array(
'MrchLogin' => Config::getKey( 'merchant_login' , 'robox' ),
'OutSum' => $purchase->getPrice(),
'InvId' => $ this ->getId(),
'SignatureValue' => $ this ->_getSignatureValue()
);

MyApplication::redirect($redirect_uri, $hash);
}

public static function checkResponseFormat ($data) {
$is_id = isset($data[ 'InvId' ]);
$is_summ = isset($data[ 'OutSum' ]);
$is_resp_crc = isset($data[ 'SignatureValue' ]);
$result = $is_id && $is_summ && $is_resp_crc;
return $result;
}

public function checkResult ($data) {
$billing_item_id = isset($data[ 'InvId' ])? $data[ 'InvId' ]:0;
$summ = isset($data[ 'OutSum' ])? $data[ 'OutSum' ]:0;
$result = FALSE;
$purchase = null ;
try {
$ this ->_mBilling = CBillingItem::sgetById($billing_item_id);
$purchase = CPurchase::loadPurchaseById($ this ->getPurchaseId());
} catch (ExObjectNotFound $e) {}

if ($ this ->_mBilling && $purchase) {

$is_valid_control_summ = $ this ->_checkControlSumm($data);
$is_valid_summ = $ this ->_checkSumm($summ);
$is_valid_status = $ this ->_checkStatusNotFinish();

if ($is_valid_control_summ && $is_valid_summ && $is_valid_status) {
$result = TRUE;
$ this ->callbackPayment();
$purchase->callbackPayment();

}
}

return $result;
}
public function addResultInView ($view, $result) {
if ($result && $ this ->getId()) {
$view->addText( "OK" );
$view->addText($ this ->getId());
} else {
$view->addText( "ERROR" );
}
}

private function _getSignatureValue() {
$purchase = CPurchaseItem::getById($ this ->getPurchaseId());
$hash = array(
Config::getKey( 'merchant_login' , 'robox' ) ,
$purchase->getPrice(),
$ this ->getId(),
Config::getKey( 'merchant_password1' , 'robox' )
);

return md5(join( ':' , $hash));
}
private function checkControlSumm($data) {
$resp_crc = isset($data[ 'SignatureValue' ])? $data[ 'SignatureValue' ]:0;
return strtoupper(self::getControlSumm($data)) == strtoupper($resp_crc);
}
static public function getControlSumm($data) {
$hash = array(
isset($data[ 'OutSum' ])? $data[ 'OutSum' ]: '' ,
isset($data[ 'InvId' ])? $data[ 'InvId' ]: '' ,
Config::getKey( 'merchant_password2' , 'robox' )
);
return md5(join( ':' , $hash));
}
}

* This source code was highlighted with Source Code Highlighter .


An example of using this architecture:
class ModuleBilling {
private function _createResponse(){
// ,
}
// , :
public function actionResultPage () {
$response = $ this ->_createResponse();
$response_data = $_REQUEST;
$view = new View();

if ( $billing_type = CBilling::getBillingTypeByRequest( $response_data ) ) {

$billing = CBilling::createBillingByType($billing_type);
$result = $billing->checkResult($response_data);
if ($result){
$response->setStatus(CResponse::STATUS_OK);
} else {
$response->setStatus(CResponse::STATUS_ERROR);
}
$response->save();
$billing->addResultInView($view, $result);
}
return $view;
}

// :
public function actionBilling($req = array()){
$user = ServiceUser::checkAccess();
$billing_type = Request::getQueryVar( 'type' );
$purchase_id = Request::getQueryVar( 'purchase' );
$purchase = CPurchase::loadPurchaseById($purchase_id);
$purchase->setStatus(PURCHASE_STATUS_WAITMONEY);
$purchase->save();

$billing = CBilling::createBillingByType($billing_type);
$billing->setPurchaseId($purchase_id);
$billing->setStatus(BILLING_STATUS_WAITMONEY);
$billing->save();
$billing->redirectToBilling();
}
}

// :
...
$action = new ModuleBilling ();
$action->actionResultPage();
...

* This source code was highlighted with Source Code Highlighter .

Source: https://habr.com/ru/post/76742/


All Articles