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 as terrible as he is painted"

    An article on the architecture of the part of the project that deals with online payments. Intentionally, I would not want to now describe in detail the API of a specific billing or the registration procedure in it. The subtleties of specific billing needs to be discussed separately, otherwise the topic simply cannot be revealed. The purpose of the article: to discuss a variant of architecture that allows stringing new types of billing and types of payments, with the least headache.

    So, for starters, imagine, we thought a little and made on our site a very simple sale of goods through one of the billing systems.
    1. We have product information: Product ID, Price, <Specifications>.
    2. Users go to the site and click on the buy button. We save the purchase information: purchase ID, product ID, product price at that moment, <customer information>;
    3. The user watches their 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 that processes billing responses saves the response data: response ID, <all that billing sent>, response date, response status. Checks the validity of the response, the result of the verification saves the status of the response. If everything is ok, then sets the status of "paid" to the desired purchase
    5. information about a paid purchase is displayed by moderators marked "must be delivered"

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

    We debugged all this, and for some time we were even pleased with our work. But, more and more often we hear: it would be necessary to tighten billing so-and-so. In addition, we want to sell not only goods, 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. We call it shopping, but we will keep in mind that purchases are now of a different type.

    As you know, good thought comes afterwards. Only when I had to fasten a couple of billing and organize several different types of purchases, in my system the processing of the purchase from the processing of billing was separated, the general parts in working with different billing, the patterns in the processing of purchases of different types were distinguished.

    Separating purchase processing from processing billing operations enables
    • connect new billing once and in one place, regardless of how many types of purchases exist in the system;
    • connect a new type of purchase once and in one place, regardless of how many billing should work with it;

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

    Now it is clear that there are fundamental differences only in paragraphs 1 and 4. Subject to the interface of the class that describes the type of purchase and the actions for the purchase, the processing scheme for various types of purchases becomes one.

    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. Redirect the user to the billing system, indicating the payment number and purchase amount;
    3. Checking the validity of the response from billing;
    4. Change of payment status;
    5. If everything is ok, call the purchase processing (change of purchase status, implementation of a purchase, ...).
    Points 2 and 3 for different billing will be your own. T.O. subject to the class interface that describes the type of billing that implements functions 2 and 3, the scheme for working with various billing systems is also unified.

    Class diagram for visual display of the described structure:

    image

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



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

    I must make a reservation in a different language, it was possible to get by with the abstract class and its descendants, but since you cannot override the static function in PHP, the ancestors were divided into an interface + base class.

    Shopping interface and example for implementing a paid member chip:
    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 implementing work 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.

    Also popular now: