2.1. Подключите скрипт PayQR к интернет-сайту

Для подключения скрипта PayQR к интернет-сайту скопируйте в секцию head подключаемого интернет-сайта код скрипта вашего «Магазина» из личного кабинета. Найти его можно в разделе «Магазины» в настройках подключаемого «Магазина» на вкладке «Интеграция» в блоке «Код скрипта для интернет-сайта».

В коде скрипта будет присутствовать уникальный номер подключаемого «Магазина».

Этот скрипт должен присутствовать в head каждой страницы интернет-сайта, где планируется размещение кнопки PayQR

Пример кода скрипта PayQR в коде интернет-сайта  

<!DOCTYPE html>
<html>
  <head>
    <!-- Это пример кода скрипта PayQR. Код для вашего «Магазина» скопируйте из своего личного кабинета в PayQR -->
    <script src="https://payqr.ru/popup.js?merchId=000000-00000"></script>
  </head>
  <body>
  </body>
</html>

Маленький скрипт PayQR располагается на «облачных» серверах PayQR, что позволяет осуществлять доработку и самого скрипта PayQR, и popup-окон PayQR без необходимости переделывать подключение интернет-сайтов к PayQR, что очень удобно. Дополнительно этот подход позволяет снять нагрузку с серверов интернет-сайта, так как интернет-сайт в загрузке скрипта PayQR вообще не участвует. Также архитектура скрипта PayQR спроектирована так, чтобы не увеличивать время загрузки страниц интернет-сайта у покупателей – основные элементы скрипта начинают загружаться только тогда, когда весь базовый контент интернет-сайта уже загружен. На мобильных устройствах скрипт тоже ведет себя корректно, адаптируя свою логику под возможности конкретного мобильного устройства.


2.2. Разместите кнопку PayQR

Кнопка PayQR представляет собой стандартную кнопку интернет-сайта, которая вызывает popup-окно PayQR для быстрого завершения покупки. Рекомендуемые дизайн и название кнопки PayQR позволяют а) узнавать ее тем, кто уже покупал что-то через PayQR, б) не идентифицировать ее однозначно с каким-то сторонним сервисом тем, кто еще не пользовался PayQR. И то, и то другое работает в интересах интернет-сайта на увеличение конверсии в успешные покупки, позволяя покупателям проходить этапы регистрации, оформления заказа и его оплаты с гораздо меньшим сопротивлением.


2.2.1. Определение расположения и атрибутов кнопки PayQR

Разместите коды кнопок PayQR везде, где вы хотите предоставить возможность посетителям быстрее покупать товары/услуги.

В интернет-магазинах кнопка PayQR должна располагаться в корзине, на самом первом этапе совершения покупки – еще до того, как пользователь заполнил хотя бы одно поле в корзине или выбрал какую-нибудь опцию (как правило, на этих же страницах размещаются классические кнопки интернет-магазина «Оформить заказ» или «Зарегистрироваться»). Кнопка должна располагаться в отдельной заметной зоне, вне классических блоков с формами и кнопками, чтобы позиционироваться как альтернатива стандартному процессу совершения покупки. Наиболее эффективное расположение кнопки PayQR в корзине – под суммой «Итого к оплате» или над полями ввода данных покупателем (имя, фамилия, телефон, e-mail, адрес и так далее), если они присутствуют в корзине. Обязательно настраивайте размеры и дизайн кнопки PayQR так, чтобы он соответствовал общей стилистике интернет-сайта, но цвет кнопки должен отличаться от кнопок стандартного процесса оформления заказа («Оформить заказ», «Пересчитать», «Купить в 1 клик», «Купить в кредит», «Продолжить покупки» и тому подобное).

Дополнительно к корзине рекомендуется размещать кнопки PayQR на страницах товаров и в карточках товаров на главной странице интернет-сайта.

Атрибуты кнопок задают параметры совершения покупки. Интернет-сайт самостоятельно «заказывает» какие данные будут получены и сохранены в заказах через PayQR с помощью соответствующих атрибутов кнопок PayQR (data-firstname-required, data-phone-required и другие). Имейте в виду, чем меньше данных будет запрашиваться с покупателей, тем выше будет конверсия вашего интернет-сайта.

Для настройки атрибутов и внешнего вида кнопок PayQR воспользуйтесь специальным конструктором в своем личном кабинете в PayQR. Он расположен в разделе «Магазины» в настройках подключаемого «Магазина» на вкладке «Конструктор и опции кнопки».

Кнопки PayQR идентифицируются скриптом PayQR по классу payqr-button, не удаляйте его для корректной работы сервиса.

Код кнопок PayQR выглядит примерно так:  

<!-- Kласс кнопки PayQR, на который реагирует скрипт PayQR. Обязательно должен быть в коде кнопки! -->
<button class="payqr-button"
  <!--В данном случае кнопка подтверждения заказа в приложении будет называться «Купить» (рекомендовано) --> 
  data-scenario="buy"
  <!--Содержание заказа для демонстрации покупателю, в данном случае в заказе две позиции – на 500 и на 1000 рублей -->
  data-cart='[{
    "article":"123123",
    "name":"Хороший товар",
    "imageUrl":"http://modastuff.ru/item1.jpg",
    "amount":"500.00",
    "quantity":1
    },{
    "article":"321321",
    "name":"Очень хороший товар",
    "imageUrl":"http://modastuff.ru/item2.jpg",
    "amount":"1000.00",
    "quantity":2
  }]' 
  <!-- У покупателя будет запрошено имя для оформления заказа -->
  data-firstname-required="required"
  <!-- У покупателя будет запрошена фамилия для оформления заказа -->
  data-lastname-required="required"
  <!-- У покупателя будет запрошен номер телефона для оформления заказа -->
  data-phone-required="required"
  <!-- У покупателя будет запрошен e-mail для оформления заказа -->
  data-email-required="required"
  <!-- У покупателя будет запрошен адрес доставки, но после оформления заказа и его оплаты (в рекомендательном режиме, так что покупатель сможет пропустить этап) -->
  data-delivery-required="notrequired"
  <!-- Сумма всего заказа, которая и будет оплачиваться покупателем -->
  data-amount="1500"
  <!-- Размеры кнопки PayQR на интернет-сайте -->
  style="width:185px; height:36px;"
  <!-- Рекомендованный текст кнопки PayQR -->
  >Купить быстрее</button>

Имейте в виду, что кнопки PayQR должны быть настроены таким образом, чтобы интернет-сайт смог в дальнейшем на основе полученных данных сформировать заказ так же, как если бы покупатель оформлял заказ в интернет-браузере. Например, если для создания заказа в учетной системе интернет-сайта требуется посчитать содержание заказа (корзины) по артикулам позиций в заказе (в корзине), то необходимо обязательно передавать артикулы всех позиций заказа в кнопки PayQR в предназначенные для этого атрибуты article объекта data-cart каждой позиции заказа.


2.2.2. Заполнение атрибутов кнопки PayQR

После того, как вы определились с размещением кнопок и их содержанием, необходимо реализовать передачу в параметры кнопок соответствующих данных, в частности, содержаний заказов. Если вы используете рекомендованную PHP-библиотеку PayQR, то там предусмотрен файл, дополнительно упрощающий процесс генерации кнопок PayQR на страницах интернет-магазина. Это payqr_button.php в папке classes, а также payqr_product.php из той же папки для формирования товарных позиций в кодах кнопок PayQR.

Не забудьте указать «URL для уведомлений» в личном кабинете PayQR и реализовать обработку уведомлений от PayQR после завершения работ над размещением кнопки PayQR. В противном случае совершение покупок через PayQR на вашем интернет-сайте работать не будет. Двигайтесь по нижеследующим разделам инструкции.

Примеры автоматического заполнения кнопок  

Как динамически собирать данные о заказах и передавать их в кнопки PayQR зависит от учетной системы вашего интернет-сайта. Можно обратиться к базе данных (что рекомендуется), а можно с помощью JavaScript собрать данные на стороне клиента.

Пример как мы делали генерацию кнопок PayQR:

<?php
  if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
      die();

  $arResult['PQR_AMOUNT'] = 0;
  $pqrCart                = array();

  if ($_REQUEST['getCartJson'] != 'Y') {
      $APPLICATION->AddHeadScript('https://payqr.ru/popup.js?merchId=' . $arParams['MERCH_ID']);
  }

  if (CModule::IncludeModule("sale") && CModule::IncludeModule("iblock")) {
      $dbBasketItems = CSaleBasket::GetList(array(
          "NAME" => "ASC",
          "ID" => "ASC"
      ), array(
          "FUSER_ID" => CSaleBasket::GetBasketUserID(),
          "LID" => SITE_ID,
          "ORDER_ID" => "NULL"
      ), false, false, array(
          "ID",
          "PRODUCT_ID",
          "PRODUCT_PRICE_ID",
          "NAME",
          "QUANTITY",
          "CAN_BUY",
          "PRICE",
          "PREVIEW_PICTURE",
          "DELAY"
      ));

      while ($arBasketItem = $dbBasketItems->Fetch()) {
          if ($arBasketItem['CAN_BUY'] == 'Y' && $arBasketItem['DELAY'] == 'N') {
              $rsProduct = CIBlockElement::GetByID($arBasketItem["PRODUCT_ID"]);
              $imageUrl = null;
              if ($arProduct = $rsProduct->Fetch())
                  $imageUrl = 'http://' . SITE_SERVER_NAME . CFile::GetPath($arProduct["PREVIEW_PICTURE"]);
              $replaceArray = array(
                  "'" => "&#039;",
                  '"' => "\""
              );
              $pqrCart[]    = array(
                  "article" => $arBasketItem['PRODUCT_PRICE_ID'],
                  "name" => str_replace(array_keys($replaceArray), array_values($replaceArray), $arBasketItem['NAME']),
                  "imageUrl" => $imageUrl,
                  "quantity" => $arBasketItem['QUANTITY'],
                  "amount" => $arBasketItem['PRICE'] * $arBasketItem['QUANTITY']
              );
              $arResult['PQR_AMOUNT'] += $arBasketItem['PRICE'] * $arBasketItem['QUANTITY'];
          }
      }

      $arResult['PQR_ORDER_GROUP'] = SITE_ID . "." . CSaleBasket::GetBasketUserID() . "." . CUser::GetID();

      /*
        Заносим в дополнительный атрибут кнопки PayQR все идентификаторы этой
        учетной системы, которые могут потребоваться в дальнейшим при создании и
        оплате заказа, через разделитель «точка», в данном случае первым, вторым и
        третьим параметром сохранили ИД сайта, ИД пользователя на интернет-сайте и
        ИД пользователя в учетной системе (это особенность «1С-Битрикс»,
        часто в учетных системах понятия ИД сайта, ИД пользователя на
        интернет-сайте и ИД пользователя в учетной системе отсутствуют,
        а в создании заказа участвуют только артикулы позиций)
      */

      $arResult['PQR_CART'] = json_encode($pqrCart); // Конвертируем заполненную корзину в JSON для передачи в кнопку

      if ($_REQUEST['getCartJson'] == 'Y') {
          $APPLICATION->RestartBuffer();
          echo json_encode(array(
              'cart' => $arResult['PQR_CART'],
              'amount' => $arResult['PQR_AMOUNT']
          ));
          die();
      }
  } else
      $arResult['ERROR'] = 'Вы не можете использовать данный компонент, т.к. не установлен модуль Интернет-магазин';

  $this->IncludeComponentTemplate();
?>
<?php
  function payqr_button_process($element) {
    $merchant_id = variable_get('commerce_payqr_merchant_id', '');
    if (!empty($merchant_id)) {
      $data = array(
        'merchId' => $merchant_id,
      );
      $js_url = url('https://payqr.ru/popup.js', array('query' => $data));
      $options = array(
        'type' => 'external',
        'scope' => 'header',
      );
      drupal_add_js($js_url, $options);
      $module_path = drupal_get_path('module','commerce_payqr');
      drupal_add_js($module_path . '/commerce_payqr.js');
    }
    $element['#attributes']['class'][] = 'payqr-button';
    $button_style = commerce_payqr_get_button_style();
    $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $button_style['class']);
    if (!isset($element['#attributes']['style'])) {
      $element['#attributes']['style'] = array();
    }
    $element['#attributes']['style'] = array_merge($element['#attributes']['style'], $button_style['style']);
    return $element;
  }

  /**
   *  Implements hook_element_info().
   */
  function commerce_payqr_element_info() {
    $types['payqr_button'] = array(
      '#input' => FALSE,
      '#name' => 'payqr',
      '#button_type' => 'button',
      '#executes_submit_callback' => FALSE,
      '#limit_validation_errors' => FALSE,
      '#process' => array('ajax_process_form', 'payqr_button_process'),
      '#theme' => array('payqr_button'),
    );
    return $types;
  }

  /**
   *  Implements hook_theme().
   */
  function commerce_payqr_theme($existing, $type, $theme, $path) {
    return array(
      'payqr_button' => array(
        'render element' => 'element',
      ),
    );
  }

  function theme_payqr_button($variables) {
    $element = $variables['element'];
    $element['#attributes']['type'] = 'button';
    element_set_attributes($element, array('id', 'name', 'value'));

    $element['#attributes']['class'][] = 'form-' . $element['#button_type'];
    if (!empty($element['#attributes']['disabled'])) {
      $element['#attributes']['class'][] = 'form-button-disabled';
    }

    return '<button' . drupal_attributes($element['#attributes']) . '>' . $element['#value'] . '</button>';
  }

  /**
   * @param $scenario
   * @param array $data
   * @return array|bool
   */
  function commerce_payqr_get_data($scenario, $products, $quantity = 1) {
    $allowed_scenario = array('buy', 'pay');
    if (!in_array($scenario, $allowed_scenario)) {
      watchdog('commerce PayQR', 'Unallowed scenario');
      return FALSE;
    }
    global $user;
    $user_data = array();
    $data = array();
    $data['data-scenario'] = $scenario;

    //  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

    //  @todo Номер заказа в учетной системе интернет-сайта.
    //  Если номер заказа не заполняется на уровне кнопки, то его необходимо передать PayQR в дальнейшем ответом на уведомление от PayQR о событии invoice.order.creating.
    //  Пользователь не сможет приступить к оплате в PayQR, пока не получен номер заказа от интернет-сайта.
    //  $data['data-orderid'] = '';
    $data['data-orderid-custom'] = 'deny';
    $data['data-ordergroup'] = session_id();
    //  @todo Сумма заказа - обязательное поле. При необходимости интернет-сайт может изменить указанную в кнопке сумму заказа в дальнейшем ответом на уведомление от PayQR о событии invoice.order.creating.
    //  Пользователь не сможет приступить к оплате в PayQR, пока не получена сумма заказа от интернет-сайта.
    $cart_data = array();
    foreach ($products as $delta => $product) {
      $price = commerce_product_calculate_sell_price($product['product']);
      $product_wrapper = entity_metadata_wrapper('commerce_product', $product['product']);
      $cart_data[] = array(
        'article' => $product_wrapper->sku->value(),
        'name' => $product_wrapper->label(),
        'category' => 'SKU',  // @todo do not know meaning of category
        'quantity' => $product['quantity'],
        'amount' => $product['quantity'] * commerce_currency_amount_to_decimal($price['amount'], $price['currency_code']),
        'imageUrl' => '', // @todo need config image field per product types structure and config
      );
    }
    $data_amount = 0;
    foreach ($cart_data as $item) {
      $data_amount += $item['amount'];
    }
    $data['data-amount'] = $data_amount;
    $data['data-amount-custom'] = 'deny';
    $data['data-cart'] = drupal_json_encode($cart_data);

    $data['data-firstname-required'] = variable_get('commerce_payqr_firstname_required', 'default');
    $data['data-lastname-required'] = variable_get('commerce_payqr_firstname_required', 'default');
    $data['data-phone-required'] = variable_get('commerce_payqr_phone_required', 'default');
    if (user_is_anonymous()) {
      $data['data-email-required'] = 'required';
    }
    else {
      $data['data-email-required'] = 'deny';
      $customer = new stdClass();
      $customer->email = $user->mail;
      $user_data['customer'] = $customer;
      //    $data['data-email-required'] = variable_get('commerce_payqr_email_required', 'default');
    }

    if (module_exists('commerce_shipping')) {
      $data['data-delivery-required'] = variable_get('commerce_payqr_delivery_required', 'default');
      $data['data-deliverycases-required'] = variable_get('commerce_payqr_deliverycases_required', 'default');
      $data['data-pickpoints-required'] = variable_get('commerce_payqr_pickpoints_required', 'default');
    }
    else {
      $data['data-delivery-required'] = 'deny';
      $data['data-deliverycases-required'] = 'deny';
      $data['data-pickpoints-required'] = 'deny';
    }

    if (module_exists('commerce_coupon')) {
      $data['data-promo-required'] = variable_get('commerce_payqr_promo_required', 'default');
    }
    else {
      $data['data-promo-required'] = variable_get('commerce_payqr_promo_required', 'deny');
    }
    $data['data-promo-description'] = variable_get('commerce_payqr_promo_description', '');
    $data['data-commissionpercent'] = variable_get('commerce_payqr_commissionpercent', 0);
    $data['data-userdata'] = drupal_json_encode($user_data);

    $upsale_data = array();
    $upsale_data[] = array(
      'article' => 'SKU',
      'name' => 'SKU',
      'category' => 'SKU',
      'quantity' => 'SKU',
      'amount' => 'SKU',
      'imageUrl' => 'SKU',
    );
    $data['data-upsale'] = drupal_json_encode($upsale_data);

    $crosssale_data = array();
    $crosssale_data[] = array(
      'article' => 'SKU',
      'name' => 'SKU',
      'category' => 'SKU',
      'quantity' => 'SKU',
      'amount' => 'SKU',
      'imageUrl' => 'SKU',
    );
    $data['data-crosssale'] = drupal_json_encode($crosssale_data);

    $data['data-message-text'] = variable_get('commerce_payqr_message_text', '');
    $data['data-message-imageurl'] = variable_get('commerce_payqr_message_imageurl', '');
    $data['data-message-url'] = variable_get('commerce_payqr_message_url', '');

    return $data;
  }

  /**
   * Get PayQR button style.
   *
   * @return array
   */
  function commerce_payqr_get_button_style(){
    $style = &drupal_static(__FUNCTION__);
    if (!isset($style)) {
      $style = array();
      $style['class'][] = variable_get('commerce_payqr_button_color', 'payqr-button_green');
      $style['class'][] = variable_get('commerce_payqr_button_form', 'payqr-button_sharp');
      $style['class'][] = variable_get('commerce_payqr_button_gradient', 'payqr-button_flat');
      $style['class'][] = variable_get('commerce_payqr_button_text_case', 'payqr-button_standardcase');
      $style['class'][] = variable_get('commerce_payqr_button_text_width', 'payqr-button_font_normal');
      $style['class'][] = variable_get('commerce_payqr_button_text_size', 'payqr-button_text_normal');
      $style['style'][] = 'height:' . variable_get('commerce_payqr_button_height', 'auto') . ';';
      $style['style'][] = 'width:' . variable_get('commerce_payqr_button_width', 'auto') . ';';
    }
    return $style;
  }



  // Кнопка в корзине
  function commerce_payqr_form_commerce_cart_add_to_cart_form_alter(&$form, &$form_state, $form_id) {
    $form['payqr_button'] = array(
      '#type' => 'payqr_button',
      '#value' => t('Buy faster'),
      '#weight' => $form['submit']['#weight'] + 1,
    );
    if (isset($form['quantity'])) {
      $form['quantity']['#ajax'] = array(

      );
    }
    $products =  array(
      array(
        'product' => $form_state['default_product'],
        'quantity' => 1,
      )
    );
    $payqr_data = commerce_payqr_get_data('buy', $products);
    if ($payqr_data) {
      $form['payqr_button']['#attributes'] = $payqr_data;
    }
  }
?>

<button class="payqr-button payqr-btn-template"
  data-scenario="<?=$arParams['TYPE']?>"
  <?if (!empty($arResult['PQR_ORDER_GROUP'])):?> data-orderGroup='<?=$arResult['PQR_ORDER_GROUP']?>'<?endif;?>
  <?if (!empty($arResult['ORDER_ID'])):?> data-orderId='<?=$arResult['ORDER_ID']?>'<?endif;?>
  <?if ($arParams['FIRSTNAME_REQUIRED'] == 'Y'): ?> data-firstname-required="required"<?endif;?>
  <?if ($arParams['LASTNAME_REQUIRED'] == 'Y'): ?> data-lastname-required="required"<?endif;?>
  <?if ($arParams['PHONE_REQUIRED'] == 'Y'): ?> data-phone-required="required"<?endif;?>
  <?if ($arParams['EMAIL_REQUIRED'] == 'Y'): ?> data-email-required="required"<?endif;?>
  <?if ($arParams['DELIVERY_REQUIRED'] == 'Y'): ?> data-delivery-required="required"<?endif;?>
  <?if (!empty($arResult['PQR_CART'])):?> data-cart='<?=$arResult['PQR_CART']?>'<?endif;?>
  <?if (!empty($arResult['PQR_AMOUNT'])):?> data-amount="<?=$arResult['PQR_AMOUNT']?>"<?endif;?>
  ><?if ($arParams['TYPE'] == 'buy') echo GetMessage('BTN_BUY'); else echo GetMessage('BTN_PAY'); ?></button>

Практически все атрибуты кнопки PayQR являются необязательными, поэтому вы можете передавать в кнопке только те атрибуты, которые актуальны для того или иного расположения кнопки PayQR. Значения отсутствующих атрибутов будут восприниматься как default. Кнопок на странице может быть много, но логику заполнения атрибутов в кодах кнопок нужно реализовать всего один раз, чтобы она автоматически распространялась на все кнопки PayQR.


2.3. Разместите на своем интернет-сайте обработчик уведомлений от PayQR

Определите расположение обработчика уведомлений PayQR на хостинге своего интернет-сайта, куда PayQR будет направлять уведомления обо всех событиях. И укажите URL-адрес на этот файл в личном кабинете в разделе «Магазины» в настройках подключаемого «Магазина» на вкладке «Интеграция» в поле «URL для уведомлений». Адрес может быть указан с протоколом http:// или https://.

Пожалуйста, не путайте URL для уведомлений с URL страницы успешной оплаты (это понятие в рамках PayQR вообще не используется, так как покупка совершается не в интернете, а в мобильном приложении). URL для уведомлений — это ссылка на обработчик уведомлений от PayQR, который использует интернет-сайт. Сюда поступают уведомления о тех или иных событиях PayQR, которые описаны в следующем разделе инструкции. В этом обработчике интернет-сайт должен принимать уведомления, интерпретировать их и вызывать на своей стороне соответствующую логику, чтобы корректно отвечать PayQR на полученные уведомления.

Вы можете собрать обработчик уведомлений PayQR самостоятельно, но мы рекомендуем воспользоваться готовой PHP-библиотекой PayQR. Архив с этой библиотекой можно скачать в разделе готовых решений. В составе PHP-библиотеки файлом, который должен принимать уведомления от PayQR, является payqr_receiver.php из корня библиотеки – именно ссылку на этот файл на вашем интернет-сайте нужно указывать в личном кабинете PayQR в поле «URL для уведомлений». Также библиотека PayQR содержит инструменты для формирования ответов на уведомления от PayQR и много полезных примеров работы с API PayQR.

Для того, чтобы библиотека начала взаимодействовать с PayQR после распаковки архива библиотеки и размещения папки библиотеки на вашем интернет-сайте вам останется только указать данные «Магазина» из своего личного кабинета в PayQR в файле payqr_config.php. К таким данным «Магазина» относятся номер «Магазина» и секретные ключи.

Обязательно: разместите обработчик уведомлений PayQR на стороне интернет-сайта и укажите ссылку на него в поле «URL для уведомлений» в своем личном кабинете в PayQR, иначе совершение покупок через PayQR на вашем интернет-сайте будет невозможным.

Если вы решите собирать обработчик уведомлений PayQR самостоятельно или ваш интернет-сайт написан не на PHP, ориентируйтесь на примеры написания обработчика, представленные ниже.


A. Обработчик уведомлений: распознание header уведомлений PayQR и генерация корректных header своих ответов на эти уведомления

Чтобы однозначно отличать уведомления PayQR от других, в заголовке каждого уведомления будет присутствовать код, известный только PayQR и подключаемому интернет-сайту. Содержание этого кода вы можете получить в личном кабинете в разделе «Магазины» в настройках подключаемого «Магазина» на вкладке «Интеграция» из поля SecretKeyIn.

Аналогично PayQR отличает уведомления интернет-сайта от других по наличию подобного кода в ответах интернет-сайта, который известен только PayQR и подключаемому интернет-сайту. Содержание этого кода вы можете получить в личном кабинете в разделе «Магазины» в настройках подключаемого «Магазина» на вкладке «Интеграция» из поля SecretKeyOut.

Примеры проверки header и заполнения header  

Один из вариантов проверки в header определенного значения SecretKeyIn:

<?php
// Вместо XXX должен быть входящий ключ SecretKeyIn этого «Магазина», указанный в личном кабинете в PayQR
$secretKeyIn = "XXX"; 
// Проверить, что запрос действительно от сервера PayQR по входящему ключу
if (getallheaders()["PQRSecretKey"] == $secretKeyIn) {
}
?>
# .config.secretKeyIn 
# Значение берется для текущего environments, проверка не будет работать если сервер запущен в production, а значения указаны в development
if request.headers["PQRSecretKey"] == CONFIG["secretKeyIn"]
  # Условие выполняется
else
  # Не выполняется
end
import httplib
host = 'example.com'
connection = httplib.HTTPConnection(host)
connection.request("GET", "")
HTTPResponse = connection.getresponse()
###
# Вместо XXX должен быть входящий ключ SecretKeyIn этого «Магазина», указанный в личном кабинете в PayQR
secretKeyIn = "XXX" 
# Проверить, что запрос от сервера PayQR
if HTTPResponse.getheader("PQRSecretKey") == secretKeyIn: 
  pass

Один из вариантов заполнения header в ответах определенным значением SecretKeyOut:

<?php
// Вместо YYY должен быть исходящий ключ SecretKeyOut этого «Магазина», указанный в личном кабинете в PayQR
$secretKeyOut = "YYY"; 
// Подписать ответ исходящим ключом для подтверждения, что ответ действительно от «Магазина»
header("PQRSecretKey:" . $secretKeyOut); 
?>
# Задать заголовок сервера для всего сайта или для определенного контроллера:
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  after_filter :payqrheaders
  private
    def payqrheaders
      response.headers["PQRSecretKey"]= CONFIG["secretKeyOut"]
    end
end
# Или добавить header в определеннный экшн-контроллер:
def show
  @product = Product.find(params[:id])
  response.headers["PQRSecretKeyOut-only-for-specefic-controller"]= CONFIG["secretKeyOut"]
end
import httplib
host = 'example.com'
conn = httplib.HTTPConnection(host)
###
# Вместо YYY должен быть исходящий ключ SecretKeyOut этого «Магазина», указанный в личном кабинете в PayQR
secretKeyOut = "YYY" 
headers = {"PQRSecretKey": secretKeyOut}
conn.request("GET", "", headers=headers)


B. Обработчик уведомлений: определение типа события уведомлений PayQR

Каждое уведомление о событии PayQR имеет определенную структуру. В атрибуте type объекта события присутствует однозначное определение того или иного события в PayQR, о котором сейчас уведомляется интернет-сайт (например, invoice.order.creating). Обрабатывайте значения атрибута type, чтобы правильно реагировать на те или иные события.

Примеры определения типа события PayQR  

Один из вариантов выделения события PayQR по значению type:

<?php
// Получаем уведомление о событии из тела пришедшего POST-запроса от PayQR
$event = json_decode($HTTP_RAW_POST_DATA); 
// Проверяем тип события на соответствие типу события для создания заказа
if (isset($event->type) && $event->type == "invoice.order.creating") {
  // Успех!
}
?>
# Проверяем тип события на соответствие типу события для создания заказа
def postfrompayqr
@payqrpostrequest = JSON.parse(request.raw_post)
respond_to do |format|
  if @payqrpostrequest.has_key?('type') && @payqrpostrequest['type'] == 'invoice.order.creating'
    format.json { render json: @payqrpostrequest, status: :created, location: @payqrpostrequest }
  else
    format.any { redirect_to :action => "new"}
  end
end
  # Посылаем POST:
  curl -XPOST 'http://yourdomain:port/products/postfrompayqr' --data '{"foo":"bar","type":"invoice.order.creating","abcdefghijklmnop":"qrstuvwxyz"}'
  # содержащий "type":"invoice.order.creating" в ответ рендерится посланный JSON (условие выполняется)
  # А если посылаем POST:
  curl -XPOST 'http://yourdomain:port/products/postfrompayqr' --data '{"foo":"bar","blah":"wat","abcdefghijklmnop":"qrstuvwxyz"}'
  # не содержащий нужного параметра, проверка не выполняется и происходит редирект.
import json
###
# request.POST - достаточно условная запись, конкретный способ получения содержимого POST будет зависеть от способа запуска приложения и фреймворка
event = json.loads(request.POST)
# Проверяем тип события на соответствие типу события для создания заказа
if "type" in event and event["type"] == "invoice.order.creating": 
  pass

Структура объекта «Событие» (event) в уведомлении PayQR, но может прийти больше параметров:

{
  "id": "evt_14EccV2eZvKYlo2C2j1B18pF", // Уникальный идентификатор события, создаваемый PayQR
  "object": "event", // Имя объекта, в данном случае «Событие»
  "created": "2015-08-09T18:31:42.201+04:00", // Дата генерации события
  "type": "invoice.order.creating", // Тип события
  "data": {
    ... // Объект, по которому произошло событие
  }
}


C. Обработчик уведомлений: интерпретация уведомлений PayQR и ответы на эти уведомления

Для обработки уведомлений PayQR интернет-сайту необходимо разбирать (десериализовывать) и собирать (сериализовывать) классы объектов, содержащихся в уведомлениях PayQR. В перечисленных в этой инструкции уведомлениях о событиях PayQR содержатся только объекты вида invoice (условно называются в PayQR «Счет на оплату»). Все объекты в PayQR используются в формате JSON.

Примеры десериализации уведомлений и сериализации ответов  

Один из вариантов десериализации объекта «Событие» (event) с вложенным объектом «Счет на оплату» (invoice) – для принятия уведомлений PayQR:

<?php
// Получаем уведомление о событии из тела пришедшего POST-запроса от PayQR
$event = json_decode($HTTP_RAW_POST_DATA);
if (isset($event->data) && isset($event->data->object) && $event->data->object == "invoice") {
  // Объект «Счет на оплату» (invoice)
  $invoice = $event->data; 
}
?>
def postfrompayqr
@payqrpostrequest = JSON.parse(request.raw_post)
  respond_to do |format|
    if @payqrpostrequest.has_key ? ('data') && @payqrpostrequest.has_key ? ('data').include ? ('object') && @payqrpostrequest['data', 'object'] == 'invoice'
      @invoice = @payqrpostrequest['data']
  end
end
import json
###
# request.POST - достаточно условная запись, конкретный способ получения содержимого POST будет зависеть от способа запуска приложения и фреймворка
# Получаем уведомление о событии из тела пришедшего POST-запроса от PayQR
event = json.loads(request.POST) 
if "data" in event and "object" in event["data"] and event["data"]["object"] == "invoice":
  # Объект «Счет на оплату» (invoice)
  invoice = event["data"]  

Один из вариантов сериализации ответов на уведомление PayQR – объект «Событие» (event) с вложенным объектом «Счет на оплату» (invoice):

<?php
// Получаем уведомление о событии из тела пришедшего POST-запроса от PayQR
$event = json_decode($HTTP_RAW_POST_DATA);
var_dump(json_encode(array(
  "id" => $event->id,
  "data" => array(
  "orderId" => "00000000" /* В данном случае в качестве сериализуемого ответа интернет-сайт направил параметр orderId со значением 00000000(и, тем самым, либо дополнил, либо обновил этот параметр в объекте «Счет на оплату») */
  ))));
?>
def postfrompayqr
  @payqrpostrequest = JSON.parse(request.raw_post)
  serialize = Array({
    :id => "#{@payqrpostrequest['id']}",
    :data => Array({
    :order_id => "0000000"})
  }).to_json
 puts YAML::dump(serialize)
end
import json
###
# request.POST - достаточно условная запись, конкретный способ получения содержимого POST будет зависеть от способа запуска приложения и фреймворка
event = json.loads(request.POST) 
# Получаем уведомление о событии из тела пришедшего POST-запроса от PayQR
json.dumps({
  "id": event["id"],
  "data": {
    "orderId": "00000000"
  }
})

Структура объекта «Счет на оплату» (invoice), но может прийти больше параметров:

{
  "id": "inv_14EeCA2eZvKYlo2C8nDrcXdp",  // Номер счета в PayQR(номера счетов создаются PayQR)
  "object": "invoice", // Имя объекта, в данном случае «Счет на оплату»
  "livemode": false, // Режим тестирования с эмуляцией платежей (false) или реальные платежи (true)
  "created": "2015-08-09T18:31:42.201+04:00", // Дата и время создания объекта по московскому времени
  "modified": "2015-08-09T18:33:42.201+04:00", // Дата и время изменения объекта по московскому времени
  "payqrNumber": "3213326680056410", // Уникальный номер конкретного объекта «Счет на оплату», отличается от id тем, что id является технической сущностью, а с данным номером работают покупатели (номера счетов создаются PayQR)
  "payqrUserId": "000000000000", // Уникальный номер абонента в PayQR, который совершает операции по этому объекту «Счет на оплату», т.е. оформляет заказ и совершает оплату (номера абонентов создаются PayQR)
  "orderGroup": "3241.8954", // Какие-то дополнительные идентификаторы конкретного интернет-сайта, которые понадобятся интернет-сайту на стадии оформления заказа или оплаты заказа
  "amount": 5000.00, // Сумма заказа
  "cart":[ // Содержание заказа в PayQR
    {
      "article": "5675657", // Артикул товара/услуги в учетной системе интернет-сайта
      "name": "Товар 1", // Название товара/услуги
      "imageUrl": "http://goods.ru/item1.jpg", // Абсолютная ссылка на изображение товара/услуги
      "quantity": 5, // Количество товара/услуги
      "amount": 5000.00 // Стоимость товара/услуги с учетом количества
    },
    {
      "article": "0", // Артикул товара/услуги в учетной системе интернет-сайта
      "name": "PROMO акция", // Название товара/услуги
      "imageUrl": "http://goods.ru/promo.jpg" // Абсолютная ссылка на изображение товара/услуги
    }
  ],
  "customer": { // Данные о покупателе, который передал сам покупатель через PayQR (набор запрашиваемых данных интернет-сайт определил на уровне кнопки PayQR)
    "firstName": "Иван", // Имя покупателя
    "lastName": "Иванов", // Фамилия покупателя
    "phone": "+79111111111", // Номер телефона покупателя
    "email": "test@user.com" // E-mail покупателя
  },
  "delivery": { // Адрес доставки покупки покупателю, который передал сам покупатель через PayQR (интернет-сайт запросил получение адреса доставки покупателя на уровне кнопки PayQR)
    "country": "Россия", // Страна адреса доставки
    "region": "Москва", // Регион адреса доставки
    "city": "Москва", // Населенный пункт адреса доставки
    "zip": "115093", // Индекс адреса доставки
    "street": "Дубининская ул.", // Улица адреса доставки
    "house": "80", // Дом адреса доставки
    "comment": "У входа в автосалон Хонда" // Комментарий к адресу доставки
  },
  "validityInMinutes": 129600, // Срок действия счета, в течение которого покупатель может оплатить счет после его получения в PayQR, в минутах, не более 259200 минут (с момента подтверждения счета в PayQR покупателем до его оплаты)
  "confirmWaitingInMinutes": 4320, // Срок автоматического подтверждения оплаты по счету, по истечении которого возможные возвраты от интернет-сайта будут осуществляться не на первоначальное средство платежа покупателя, а на его остаток в PayQR, в минутах, не более 4320 минут (с момента технического осуществления операций списания до момента запуска расчетов с интернет-сайтом)
  "promo": "TRADE", // Промо-идентификатор, который передал сам покупатель через PayQR, может быть промо-кодом или номером карты лояльности (интернет-сайт предусмотрел возможность указать покупателю промо-идентификатор на уровне кнопки PayQR)
  "status": "new" // Текущий статус объекта «Счет на оплату», в данном случае «Создан»
}

Объекты «Счет на оплату» содержат в себе большое количество различных данных, которые могут учитываться или не учитываться интернет-сайтом. Интернет-сайт самостоятельно выбирает значимые для него данные для реализации необходимой бизнес-логики интернет-сайтом. В уведомлениях интернет-сайта от PayQR объект «Счет на оплату» всегда приходит целиком, но в ответах интернет-сайт может использовать как объект «Счет на оплату» целиком, так и только ту часть объекта, по которой интернет-сайт направляет дополнения или которую интернет-сайт желает изменить.


2.4. Настройте обработку уведомлений PayQR

PayQR для интернет-сайта является всего лишь интерфейсом получения информации от покупателя и передачи информации покупателю. По сути, PayQR выступает аналогом обычного интернет-браузера:

1. Раньше после наполнения корзины покупатели регистрировались на интернет-сайте и заполняли данные о себе в браузере. И туда же в браузер интернет-сайт возвращал результат обработки данных покупателя и его корзины, а именно номер заказа и сумму к оплате. Сейчас эту же информацию от покупателя интернет-сайт может получать еще и через PayQR, и в PayQR же интернет-сайт потом возвращает номер заказа и сумму к оплате. Все бизнес-процессы интернет-сайта остаются без изменений – он все так же создает заказ на основе данных покупателя и содержания корзины, осуществляет какие-то свои проверки и процедуры в процессе создания заказа, в том числе пересчитывает стоимость заказа, и так далее. И только после создания заказа и передачи его параметров в PayQR покупатель приступает к оплате.

2. По самой оплате аналогичная параллель – раньше покупатель выбирал способ оплаты и оплачивал покупки в браузере, сейчас за него эту процедуру может осуществить PayQR. Интернет-сайт точно так же, как из браузера, получает в свою учетную систему информацию об оплате, чтобы изменить статус конкретного заказа на «Оплачен» (или любой другой для такого случая).

PayQR необходимо взаимодействовать с интернет-сайтом на определенных этапах осуществления покупки покупателем. Такое взаимодействие реализуется посредством направления интернет-сайту обычных уведомлений (http/https-запросов типа POST) от PayQR и ответов интернет-сайта на такие уведомления. Уведомления отправляются на адрес, указанный в поле «URL для уведомлений» в личном кабинете PayQR (описано в предыдущем разделе инструкции).

Основными двумя этапами, по которым PayQR направляет уведомления, являются события:

  1. invoice.order.creating (интернет-сайту требуется создать заказ)
  2. invoice.paid (интернет-сайту требуется пометить созданный заказ как оплаченный)

Как реагировать на уведомления об этих событиях описано далее.


2.4.1. Реализуйте обработку формирования заказа

Ответы на уведомления PayQR должны носить чисто технический характер и формироваться без какой-либо сложной логики на стороне интернет-сайта, иначе в PayQR может сработать таймаут. Уведомления PayQR должны обрабатываться так же быстро, как стандартные аналогичные действия покупателя в интернет-браузере (формирование нового заказа).

Событие invoice.order.creating означает, что PayQR собрал все данные с покупателя, которые запрашивал интернет-сайт через параметры кнопки PayQR, и теперь интернет-сайту нужно сформировать заказ на основе полученных данных от PayQR.

После получения уведомления о событии invoice.order.creating интернет-сайту требуется создать заказ на своей стороне по стандартному механизму, то есть выполнить все необходимые проверки в рамках процедуры создания заказа и другие возможные процессы. Если учетная система интернет-сайта функционирует так, что заказ создается уже на уровне генерации кнопки PayQR, то после получения invoice.order.creating можно просто актуализировать его на основе данных, пришедших от PayQR.

Вместе с уведомлением invoice.order.creating будут переданы а) все данные, которые интернет-сайт сообщил в кнопке PayQR, б) все данные, которые покупатель сообщил о себе интернет-сайту в соответствии с требованиями, сформированными интернет-сайтом на уровне кода кнопки PayQR.

Важно, что если на уровне кнопки интернет-сайт не передавал номер заказа (data-orderid), потому что заказа еще не было, а была только корзина с каким-то содержимым, то в ответе на уведомление о событии invoice.order.creating номер заказа должен присутствовать обязательно (в атрибуте orderId объекта «Счет на оплату»). По правилам PayQR покупатель не может приступить к оплате заказа, у которого неизвестен номер.

Если вы используете рекомендованную PHP-библиотеку PayQR, то там уже предусмотрен файл, который запускается при получении уведомления о событии invoice.order.creating. Это invoice.order.creating.php в папке handlers. Также в библиотеке предусмотрены некоторые методы по объекту «Счет на оплату», содержащемуся в уведомлении invoice.order.creating, которые могут существенно облегчить написания своего кода реакции на это событие. Они описаны в файле payqr_invoice.php в папке classes.

Обязательно: в ответе на уведомление о событии invoice.order.creating от PayQR возвращайте номер заказа в вашей учетной системе в параметре orderId объекта PayQR «Счет на оплату», если на этапе формирование кнопки PayQR номер заказа был еще неизвестен, иначе совершение покупок через PayQR на вашем интернет-сайте будет невозможным.

Ответ на уведомление о событии invoice.order.creating – это первая и последняя возможность интернет-сайта изменить какие-либо данные в объекте «Счет на оплату» в процессе покупки (увеличить или уменьшить сумму, добавить позиции в содержание заказа и тому подобное). Дальше начнется процесс оплаты и все изменения будут игнорироваться сервером PayQR.

Примеры обработки уведомления для создания заказа  

Как формировать заказы в учетной системе вашего интернет-сайта зависит от этой учетной системы (обратитесь к документации учетной системы, если затрудняетесь дифференцировать этот функционал). Можно создать сущность заказа в базе данных учетной системы и поочередно заполнять ее значениями, а можно сразу вызвать штатную команду создания заказа со всеми известными значениями. Фактически это всегда имитация заполнения всех необходимых полей покупателем на самом интернет-сайте (как если бы покупатель совершал покупку старым способом в интернет-браузере) и нажатия покупателем кнопки «Оформить заказ» (или другой похожей). Если бизнес-логикой вашей учетной системы предусмотрена обязательная регистрация покупателей перед формированием заказов, то на основе запрошенных в кнопке PayQR и полученных сейчас данных покупателя запустите такую регистрацию перед обработкой формирования заказа.

Пример уведомления о событии, направленное интернет-сайту:

{
  "id": "evt_14EccV2eZvKYlo2C2j1B18pF",
  "object": "event",
  "created": "2015-08-09T18:31:42.201+04:00",
  "type": "invoice.order.creating",
  "data": {
    "id": "inv_14EeCA2eZvKYlo2C8nDrcXdp",
    "object": "invoice",
    "livemode": false,
    "created": "2015-08-09T18:31:42.201+04:00",
    "modified": "2015-08-09T18:33:42.201+04:00",
    "payqrNumber": "3213326680056410",
    "payqrUserId": "000000000000",
    "orderGroup": "123.456",
    "amount": 5000.00,
    "cart": [
      {
        "article": "5675657",
        "name": "Товар 1",
        "imageUrl": "http://goods.ru/item1.jpg",
        "quantity": 5,
        "amount": 5000.00
      }
    ],
    "customer": {
       "firstName": "Иван",
       "lastName": "Иванов",
       "phone": "+79111111111",
       "email": "test@user.com"
    },
    "delivery": {
       "country": "Россия",
       "region": "Москва",
       "city": "Москва",
       "zip": "115093",
       "street": "Партийный пер.",
       "house": "1",
       "unit": "46",
       "hallway": "2",
       "floor": "4",
       "intercom": "#1234",
       "comment": "Сказать охране, что в PayQR"
    },
    "validityInMinutes": 129600,
    "confirmWaitingInMinutes": 4320,
    "status": "new"
  }
}

Пример ответа на уведомление о событии от интернет-сайта:

{
  "id": "evt_14EccV2eZvKYlo2C2j1B18pF",
  "data": {
      "orderId": "0000000000",
      "amount": 5000.00
  }
}

Пример как мы делали оплату заказа:

<?php
  if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
      die();

  $raw_data = file_get_contents("php://input");
  //кастомная функция логирования, для отладки
  payQrLogToFile($raw_data);
  $event = json_decode($raw_data);

  $secretKeyIn  = $arParams['SECRET_KEY_IN']; // Входящий ключ SecretKeyIn этого «Магазина», указанный в личном кабинете
  $secretKeyOut = $arParams['SECRET_KEY_OUT']; // Исходящий ключ SecretKeyOut этого «Магазина», указанный в личном кабинете

  header("PQRSecretKey:" . $secretKeyOut); // Подписать ответ исходящим ключом для подтверждения, что ответ действительно от «Магазина»

  if ($event->type == "invoice.order.creating" && CModule::IncludeModule("iblock") && CModule::IncludeModule("sale") && CModule::IncludeModule("catalog")) /* Проверяем тип события на соответствие типу события для создания заказа и задействуем нужные компоненты*/ {
      $invoice         = $event->data; // Объект «Счет на оплату» (invoice)
      $orderGroupParts = explode(".", $invoice->orderGroup);
      /* Получаем пришедшие идентификаторы учетной системы ИД сайта ИД пользователя на сайте и ИД пользователя в учетной системе (это редкая особенность «1С-Битрикс», часто в учетных системах понятия ИД сайта и ИД пользователя отсутствуют, а в формировании участвуют только артикулы позиций в заказе) */
      $siteId          = $orderGroupParts[0]; // ИД сайта (был сохранен на уровне кнопки PayQR в data-orderGroup первым параметром)
      $fuserId         = $orderGroupParts[1]; // ИД пользователя на сайте (был сохранен на уровне кнопки PayQR в data-orderGroup вторым параметром)
      $userId          = $orderGroupParts[2]; // ИД пользователя в учетной системе (был сохранен на уровне кнопки PayQR в data-orderGroup третьим параметром)
      if (empty($userId))
          $userId = $arParams['DEFAULT_USER_ID'];

      $arFieldsOrder = array();

      $arFieldsOrder[$arParams['FIELD_FIRSTNAME_ID']] .= ', ' . $invoice->customer->firstName;
      $arFieldsOrder[$arParams['FIELD_LASTNAME_ID']] .= ', ' . $invoice->customer->lastName;
      $arFieldsOrder[$arParams['FIELD_PHONE_ID']] .= ', ' . $invoice->customer->phone;
      $arFieldsOrder[$arParams['FIELD_EMAIL_ID']] .= ', ' . $invoice->customer->email;

      $arFieldsOrder[$arParams['FIELD_COUNTRY_ID']] .= ', ' . $invoice->delivery->country;
      $arFieldsOrder[$arParams['FIELD_CITY_ID']] .= ', ' . $invoice->delivery->city;
      $arFieldsOrder[$arParams['FIELD_ZIP_ID']] .= ', ' . $invoice->delivery->zip;

      if ($arParams['FIELD_ADDRESS_ID'] > 0):
          $arFieldsOrder[$arParams['FIELD_ADDRESS_ID']] .= ', ';
          if (!empty($invoice->delivery->street))
              $arAddress[] = 'ул. ' . $invoice->delivery->street;
          if (!empty($invoice->delivery->house))
              $arAddress[] = 'д. ' . $invoice->delivery->house;
          if (!empty($invoice->delivery->unit))
              $arAddress[] = 'корп.. ' . $invoice->delivery->unit;
          if (!empty($invoice->delivery->building))
              $arAddress[] = 'стр. ' . $invoice->delivery->building;
          if (!empty($invoice->delivery->flat))
              $arAddress[] = 'кв. ' . $invoice->delivery->flat;
          if (!empty($invoice->delivery->hallway))
              $arAddress[] = 'подъезд ' . $invoice->delivery->hallway;
          if (!empty($invoice->delivery->floor))
              $arAddress[] = 'этаж ' . $invoice->delivery->floor;
          if (!empty($invoice->delivery->intercom))
              $arAddress[] = 'домофон ' . $invoice->delivery->intercom;
          $arFieldsOrder[$arParams['FIELD_ADDRESS_ID']] .= implode(', ', $arAddress);
      endif;

      //$arFieldsOrder[$arParams['FIELD_PAYQR_ID']] = $invoice->payqrNumber;
      $commentOrder = $invoice->delivery->comment;

      AddMessage2Log('FUSERID: ' . $fuserId . ' | USERID: ' . $userId . ' | ');

      $dbBasketItems = CSaleBasket::GetList( // Получаем корзину этого пользователя из интернет-сайта
          array(
          "NAME" => "ASC",
          "ID" => "ASC"
      ), array(
          "FUSER_ID" => $fuserId,
          "LID" => SITE_ID,
          "ORDER_ID" => "NULL"
      ), false, false, array(
          "ID",
          "PRODUCT_ID",
          "PRODUCT_PRICE_ID",
          "NAME",
          "QUANTITY",
          "CAN_BUY",
          "PRICE"
      ));
      $arBasketItems = array();
      while ($arBasketItem = $dbBasketItems->Fetch()) /* На всякий случай, приводим состояние корзины на интернет-сайте в соответствие с содержанием заказа в PayQR (заказ, который начал оформляться в PayQR, приоритетнее) */
          $arBasketItems[] = $arBasketItem;
      $removingPositionKeys = array();
      /* Массив удаляемых из корзины PayQR элементов, которые не удалось поместить в корзину магазина (нет такого товара или не удалось добавить к корзину) */
      $invoice->amount      = 0; // Очищаем итоговую сумму счета для пересчета по текущим ценам интернет-сайта, так как они могли измениться

      payQrLogToFile("\\narBasketItems: " . var_export($arBasketItems, true));
      payQrLogToFile("\\ninvoice->cart: " . var_export($invoice->cart, true));

      foreach ($invoice->cart as $positionKey => $position) // Проверяем корзину PayQR и обновляем корзину магазина
          {
          $arPrice                   = CPrice::GetByID(intval($position->article));
          $arDiscounts               = CCatalogDiscount::GetDiscountByPrice($arPrice["ID"], $USER->GetUserGroupArray(), "N", SITE_ID);
          $discountPrice             = CCatalogProduct::CountPriceWithDiscount($arPrice["PRICE"], $arPrice["CURRENCY"], $arDiscounts);
          $arPrice["DISCOUNT_PRICE"] = $discountPrice;
          payQrLogToFile("\\narPrice: " . var_export($arPrice, true));
          if ($arProduct = CIBlockElement::GetByID(intval($arPrice["PRODUCT_ID"]))->Fetch()) {
              $position->amount = $arPrice["DISCOUNT_PRICE"] * $position->quantity; // Актуализируем сумму по позиции
              $position->name   = $arProduct["NAME"]; // Актуализируем название позиции
              $removingIndex    = -1;
              foreach ($arBasketItems as $index => $arBasketItem) /* Проверяем текущую корзину на интернет-сайте на соответствие заказу в приложении */
                  if ($arBasketItem["PRODUCT_PRICE_ID"] == $position->article) {
                      if ($arBasketItem["QUANTITY"] != $position->quantity) // Проверяем совпадение количества
                          CSaleBasket::Update($arBasketItem["ID"], array(
                              "QUANTITY" => $position->quantity
                          ));
                      /* Приводим в соответствие с тем, что в приложении, если есть расхождения */
                      $removingIndex = $index;
                      break;
                  }
              if ($removingIndex >= 0)
                  unset($arBasketItems[$removingIndex]);
                  /* Удаляем элементы из списка, которые успешно обработали, за ненадобностью */
              else if (!CSaleBasket::Add(array(
                      /* Заполняем корзину в учетной системе для создания заказа (это редкая особенность «1С-Битрикс», часто в учетных системах можно сразу создать заказ, а потом заполнить его содержимое, без необходимости эмуляции предварительного создания корзины) */
                      "PRODUCT_ID" => $arPrice["PRODUCT_ID"],
                      "PRODUCT_PRICE_ID" => $arPrice["ID"],
                      "PRICE" => $arPrice["PRICE"],
                      "CURRENCY" => $arParams['CURRENCY'],
                      "QUANTITY" => $position->quantity,
                      "LID" => $siteId,
                      "FUSER_ID" => $fuserId,
                      "DELAY" => "N",
                      "CAN_BUY" => "Y",
                      "NAME" => $arProduct["NAME"],
                      "CALLBACK_FUNC" => "",
                      "MODULE" => "sale",
                      "NOTES" => "",
                      "ORDER_CALLBACK_FUNC" => "",
                      "DETAIL_PAGE_URL" => "",
                      "CANCEL_CALLBACK_FUNC" => "",
                      "PAY_CALLBACK_FUNC" => ""
                  )))
                  $removingPositionKeys[] = $positionKey;
                  /* Удаляем позицию из заказа в приложении, если учетная система отказала в добавлении этой позиции в корзину по любым причинам */
              $invoice->amount += $position->amount; // Пересчитываем итоговую сумму заказа на основе данных учетной системы
          } else
              $removingPositionKeys[] = $positionKey;
              /* Удаляем позицию из заказа в приложении, если указанный товар не был найден в учетной системе (например, закончился на складе) */
      }
      foreach ($arBasketItems as $arBasketItem) /* Удаляем из корзины на интернет-сайте те товары, которые отсутствуют в заказе в приложении PayQR */
          CSaleBasket::Delete($arBasketItem["ID"]);
      foreach ($removingPositionKeys as $positionKey) /* Удаляем из корзины на интернет-сайте те товары, которые отсутствуют в учетной системе интернет-сайта */
          unset($invoice->cart[$positionKey]);

      payQrLogToFile("\\narBasketItems: " . var_export($arBasketItems, true));
      payQrLogToFile("\\ninvoice->amount: " . var_export($invoice->amount, true));

      $dbBasketItems = CSaleBasket::GetList( // Получаем корзину этого пользователя из интернет-сайта
          array(
          "NAME" => "ASC",
          "ID" => "ASC"
      ), array(
          "FUSER_ID" => $fuserId,
          "LID" => SITE_ID,
          "ORDER_ID" => "NULL"
      ), false, false, array(
          "ID",
          "PRODUCT_ID",
          "PRODUCT_PRICE_ID",
          "NAME",
          "QUANTITY",
          "CAN_BUY",
          "PRICE"
      ));
      $arBasketItems = array();
      while ($arBasketItem = $dbBasketItems->Fetch()) /* На всякий случай, приводим состояние корзины на интернет-сайте в соответствие с содержанием заказа в PayQR (заказ, который начал оформляться в PayQR, приоритетнее) */
          $arBasketItems[] = $arBasketItem;
      payQrLogToFile("\\narBasketItems: " . var_export($arBasketItems, true));
      //exit;
      if (count($invoice->cart) > 0) /* Создаем заказ в учетной системе интернет-сайта, если оформляемая корзина после всех проверок осталась непустой, иначе не создаем заказ */ {
          $invoice->orderId = CSaleOrder::Add(array( // Добавляем заказ в учетную систему
              "LID" => $siteId,
              "PERSON_TYPE_ID" => 1,
              "PAYED" => "N",
              "CANCELED" => "N",
              "STATUS_ID" => "N",
              "PRICE" => $invoice->amount,
              "CURRENCY" => $arParams['CURRENCY'],
              "USER_ID" => intval($userId),
              "USER_DESCRIPTION" => $commentOrder
          ));

          if ($invoice->orderId == false)
              AddMessage2Log('Ошибка добавления заказа');


          if (count($arFieldsOrder) > 0) {
              foreach ($arFieldsOrder as $id => $value) {

                  $value = substr($value, 2);

                  if ($id > 0 && !empty($value) && $arOrderProps = CSaleOrderProps::GetByID($id)) {
                      CSaleOrderPropsValue::Add(array(
                          "ORDER_ID" => $invoice->orderId,
                          "ORDER_PROPS_ID" => $id,
                          "NAME" => $arOrderProps['NAME'],
                          "CODE" => $arOrderProps['CODE'],
                          "VALUE" => $value
                      ));
                  }

              }
          }


          CSaleBasket::OrderBasket($invoice->orderId, $fuserId, $siteId);
          /* Связываем только что созданный заказ с текущей корзиной на интернет-сайте, чтобы корзина на интернет-сайте не осталась «брошенной» */
      }
      //------- Формирование заказа завершено в 1C-Битрикс -------
      $raw_data = json_encode($event);
      payQrLogToFile($raw_data);
      echo $raw_data;
  } else if ($event->type == "invoice.paid" && CModule::IncludeModule("sale")) /* Проверяем тип события на соответствие типу события для оплаты заказа и задействуем нужные компоненты этой учетной системы */ {
      /* логика оплаты заказа */
  } else
      echo json_encode(array(
          "id" => $event->id
      ));
?>
<?php
  function commerce_payqr_reciver() {
    payqr_config::$enabledLog =  FALSE;
    payqr_config::$merchantID = variable_get('commerce_payqr_merchant_id', '');
    payqr_config::$secretKeyIn = variable_get('commerce_payqr_secret_key_in', '');
    payqr_config::$secretKeyOut = variable_get('commerce_payqr_secret_key_out', '');
    try{
      $Payqr = new payqr_receiver(); // создаем объект payqr_receiver
      $Payqr->receiving(); // получаем идентификатор счета на оплату в PayQR
      // проверяем тип уведомления от PayQR
      $type = $Payqr->getType();
      $handler = '_commerce_payqr_'. str_replace('.', '_', $type);
      watchdog('$handler', '<pre>!data</pre>', array('!data' => print_r($handler, 1)));
      module_load_include('inc','commerce_payqr','includes/handlers');
      if (function_exists($handler)) {
        $handler($Payqr);
      }
      watchdog('pre response', '<pre>!data</pre>', array('!data' => print_r($Payqr, 1)));
      $Payqr->response();
    }
    catch (payqr_exeption $e){
      if(file_exists(PAYQR_ERROR_HANDLER.'invoice_action_error.php'))
      {
        $response = $e->response;
        require PAYQR_ERROR_HANDLER.'receiver_error.php';
      }
    }
  }


  /**
   * нужно создать заказ в своей учетной системе, если заказ еще не был создан, и вернуть в PayQR полученный номер заказа (orderId), если его еще не было
   *
   * @param payqr_receiver $Payqr
   */
  function _commerce_payqr_invoice_order_creating(payqr_receiver $Payqr) {
    watchdog('debug', '<pre>!data</pre>', array('!data' => print_r($Payqr, 1)));
    global $user;
    // @todo create anon\\auth logic set order owner
    $cartObject = $Payqr->objectOrder->getCart();
    if ($cartObject) {
      // Create the new order in checkout; you might also check first to
      // see if your user already has an order to use instead of a new one.
      $order = commerce_order_new($user->uid, 'checkout_checkout');
      // Save the order to get its ID.
      commerce_order_save($order);
      // Link anonymous user session to the cart
      if (!$user->uid) {
        //  $customerObject = $Payqr->objectOrder->getCustomer();
        //  if ($customerObject) {
        //    $order->$customerObject
        //  }
        commerce_cart_order_session_save($order->order_id);
      }

      foreach ($cartObject as $payqr_product) {
        // Load whatever product represents the item the customer will be
        // paying for and create a line item for it.0
        $product = commerce_product_load_by_sku($payqr_product->article);
        $line_item = commerce_product_line_item_new($product, $payqr_product->quantity, $order->order_id);
        // Save the line item to get its ID.
        commerce_line_item_save($line_item);
        // Add the line item to the order using fago's rockin' wrapper.
        $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
        $order_wrapper->commerce_line_items[] = $line_item;
      }

      //    $profile_types = commerce_customer_profile_types();

      $profile = commerce_customer_profile_new('billing', $user->uid);

      //    // Address field
      //    $profile->commerce_customer_address['und'][0]['country'] = "BE";
      //    $profile->commerce_customer_address['und'][0]['administrative_area'] = NULL;
      //    $profile->commerce_customer_address['und'][0]['sub_administrative_area'] = NULL;
      //    $profile->commerce_customer_address['und'][0]['locality'] = "Antwerpen";
      //    $profile->commerce_customer_address['und'][0]['dependent_locality'] = NULL;
      //    $profile->commerce_customer_address['und'][0]['postal_code'] = "2000";
      //    $profile->commerce_customer_address['und'][0]['thoroughfare'] = "Markt 1";
      //    $profile->commerce_customer_address['und'][0]['premise'] = "";
      //    $profile->commerce_customer_address['und'][0]['sub_premise'] = "";
      //    $profile->commerce_customer_address['und'][0]['organisation_name'] = "";
      //    $profile->commerce_customer_address['und'][0]['name_line'] = "Jimmy Henderickx";
      //    $profile->commerce_customer_address['und'][0]['first_name'] = "Jimmy";
      //    $profile->commerce_customer_address['und'][0]['last_name'] = "Henderickx";
      //    $profile->commerce_customer_address['und'][0]['data'] = NULL;

      commerce_customer_profile_save($profile);

      //create the right array for the save controller
      $profile_object = array (
        'und' => array (
          array (
            'profile_id' => $profile->profile_id,
          ),
        ),
      );

      $order->commerce_customer_billing = $profile_object;

      if (module_exists('commerce_shipping')) {
        $profile = commerce_customer_profile_new('shipping', $user->uid);
        commerce_customer_profile_save($profile);
        //create the right array for the save controller
        $profile_object = array (
          'und' => array (
            array (
              'profile_id' => $profile->profile_id,
            ),
          ),
        );

        $order->commerce_customer_shipping = $profile_object;

        // Add shipping to order if any selected.
        $shipping_service = $Payqr->objectOrder->getDeliveryCasesSelected();
        if ($shipping_service) {
          $service_name = $shipping_service->article;
          // Make the chosen service available to the order.
          commerce_shipping_service_rate_order($service_name, $order);
          // Delete any existing shipping line items from the order.
          commerce_shipping_delete_shipping_line_items($order, TRUE);

          // Extract the unit price from the calculated rate.
          $rate_line_item = $order->shipping_rates[$service_name];
          $rate_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $rate_line_item);
          $unit_price = $rate_line_item_wrapper->commerce_unit_price->value();

          // Create a new shipping line item with the calculated rate from the form.
          $line_item = commerce_shipping_line_item_new($service_name, $unit_price, $order->order_id, $rate_line_item->data, $rate_line_item->type);

          // Save and add the line item to the order.
          commerce_shipping_add_shipping_line_item($line_item, $order, TRUE);
        }
      }



      // Save the order again to update its line item reference field.
      commerce_order_save($order);
      $wrapper = entity_metadata_wrapper('commerce_order', $order);
      $transaction = commerce_payment_transaction_new('commerce_payqr', $order->order_id);
      $transaction->instance_id = 'commerce_payqr|commerce_payment_commerce_payqr';
      $transaction->amount =  commerce_currency_decimal_to_amount($Payqr->objectOrder->getAmount(), $wrapper->commerce_order_total->currency_code->value());
      $transaction->currency_code = $wrapper->commerce_order_total->currency_code->value();
      $transaction->status = COMMERCE_PAYMENT_STATUS_PENDING;
      $transaction->remote_id = $Payqr->objectOrder->getInvId();
      $transaction->message = $Payqr->getType();
      $transaction->remote_status = $Payqr->getType();
      $transaction->payload = $Payqr;
      $transaction->revision = TRUE;
      commerce_payment_transaction_save($transaction);
      $Payqr->objectOrder->setOrderId($order->order_id);
      $Payqr->objectOrder->setOrderGroup($transaction->transaction_id);
    }
  }
?>


2.4.2. Реализуйте обработку оплаты заказа

Ответы на уведомления PayQR должны носить чисто технический характер и формироваться без какой-либо сложной логики на стороне интернет-сайта, иначе в PayQR может сработать таймаут. Уведомления PayQR должны обрабатываться так же быстро, как стандартные аналогичные действия покупателя в интернет-браузере (оплата сформированного заказа).

Событие invoice.paid означает, что PayQR успешно обработал оплату заказа покупателем и теперь интернет-сайту нужно обработать оплату конкретного заказа на своей стороне.

После получения уведомления о событии invoice.paid интернет-сайту необходимо изменить статус конкретного заказа на «Оплачен» или любой другой, предназначенный для фиксации оплаты по заказу в учетной системе интернет-сайта.

Если вы используете рекомендованную PHP-библиотеку PayQR, то там уже предусмотрен файл, который запускается при получении уведомления о событии invoice.paid. Это invoice.paid.php в папке handlers. Также в библиотеке предусмотрены некоторые методы по объекту «Счет на оплату», содержащемуся в уведомлении invoice.paid, которые могут существенно облегчить написания своего кода реакции на это событие. Они описаны в файле payqr_invoice.php в папке classes.

Обязательно: подтверждайте получение уведомления о событии invoice.paid ответом интернет-сайта на это уведомление от PayQR (после фиксирования оплаты конкретного заказа в своей учетной системе), иначе PayQR будет направлять это уведомление о событии invoice.paid на URL для уведомлений до тех пор, пока не получит нужный ответ от вашего интернет-сайта.

Примеры обработки уведомления для оплаты заказа  

Как обрабатывать оплату заказов в учетной системе вашего интернет-сайта зависит от этой учетной системы (обратитесь к документации учетной системы, если затрудняетесь дифференцировать этот функционал). Если на интернет-сайте уже используются какие-то системы безналичных платежей (интернет-эквайринг, электронные кошельки и тому подобное), то обработка уведомлений об оплате от PayQR должна осуществляться аналогично этим системам.

Пример уведомления о событии, направленное интернет-сайту:

{
  "id": "evt_8902534ko0435i2F34kd04K",
  "object": "event",
  "created": "2015-08-09T18:31:42.201+04:00",
  "type": "invoice.paid",
  "data": {
      "id": "inv_14EeCA2eZvKYlo2C8nDrcXdp",
      "object": "invoice",
      "livemode": false,
      "created": "2015-08-09T18:31:42.201+04:00",
      "modified": "2015-08-09T18:33:42.201+04:00",
      "payqrNumber": "3213326680056410",
      "payqrUserId": "000000000000",
      "orderGroup": "123.456",
      "orderId": "789",
      "amount": 5000.00,
      "cart": [
          {
              "article": "5675657",
              "name": "Товар 1",
              "imageUrl": "http://goods.ru/item1.jpg",
              "quantity": 5,
              "amount": 5000.00
          }
      ],
      "customer": {
          "firstName": "Иван",
          "lastName": "Иванов",
          "phone": "+79111111111",
          "email": "test@user.com"
      },
      "delivery": {
          "country": "Россия",
          "region": "Москва",
          "city": "Москва",
          "zip": "115093",
          "street": "Партийный пер.",
          "house": "1",
          "unit": "46",
          "hallway": "2",
          "floor": "4",
          "intercom": "#1234",
          "comment": "Сказать охране, что в PayQR"
      },
      "validityInMinutes": 129600,
      "confirmWaitingInMinutes": 4320,
      "status": "paid"
  }
}

Пример ответа на уведомление о событии от интернет-сайта:

{
  "id": "evt_8902534ko0435i2F34kd04K",
}

Пример как мы делали оплату заказа

<?php
  if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();

  $raw_data = file_get_contents("php://input");
  //кастомная функция логирования, для отладки
  payQrLogToFile($raw_data);
  $event = json_decode($raw_data);

  $secretKeyIn = $arParams['SECRET_KEY_IN']; // Входящий ключ SecretKeyIn этого «Магазина», указанный в личном кабинете
  $secretKeyOut = $arParams['SECRET_KEY_OUT']; // Исходящий ключ SecretKeyOut этого «Магазина», указанный в личном кабинете

  header("PQRSecretKey:" . $secretKeyOut); // Подписать ответ исходящим ключом для подтверждения, что ответ действительно от «Магазина»

  if ($event->type == "invoice.order.creating" &&
      CModule::IncludeModule("iblock") && CModule::IncludeModule("sale") && CModule::IncludeModule("catalog")) /* Проверяем тип события на соответствие типу события для создания заказа и задействуем нужные компоненты*/
  {

      /* логика формирования заказа */
  }
  else if ($event->type == "invoice.paid" && CModule::IncludeModule("sale")) /* Проверяем тип события на соответствие типу события для оплаты заказа и задействуем нужные компоненты этой учетной системы */
  {
      $invoice = $event->data; // Объект «Счет на оплату» (invoice)
      //------- Начинаем процедуру оплаты заказа в 1C-Битрикс -------
      $orderGroupParts = explode(".", $invoice->orderGroup); /* Получаем пришедшие идентификаторы учетной системы ИД сайта ИД пользователя на сайте и ИД пользователя в учетной системе (это редкая особенность «1С-Битрикс», часто в учетных системах понятия ИД сайта и ИД пользователя отсутствуют, а в формировании участвуют только артикулы позиций в заказе) */
      $fuserId = $orderGroupParts[1]; // ИД пользователя на сайте (был сохранен на уровне кнопки PayQR в data-orderGroup вторым параметром)
      CSaleOrder::PayOrder(intval($invoice->orderId), "Y"); // Пометить заказ как оплаченный в учетной системе интернет-сайта
      CSaleBasket::DeleteAll(intval($fuserId), false); // Очищаем корзину на интернет-сайте для удобства покупателя
      //------- Оплата заказа завершена в 1C-Битрикс -------
      $raw_data = json_encode($event);
      payQrLogToFile($raw_data);
      echo $raw_data;
  }
  else
      echo json_encode(array("id" => $event->id));
?>
<?php
  /**
   * нужно зафиксировать успешную оплату конкретного заказа
   *
   * @param payqr_receiver $Payqr
   */
  function _commerce_payqr_invoice_paid(payqr_receiver $Payqr) {
    watchdog("debug", "<pre>!data</pre>", array("!data" => print_r($Payqr, 1)));
    $transaction_id = $Payqr->objectOrder->getOrderGroup();
    $transaction = commerce_payment_transaction_load($transaction_id);
    if ($transaction) {
      $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
      $transaction->remote_id = $Payqr->objectOrder->getInvId();
      $transaction->message = $Payqr->getType();
      $transaction->remote_status = $Payqr->getType();
      $transaction->payload = $Payqr;
      $transaction->revision = TRUE;
      $transaction->log = $Payqr->getType();

      commerce_payment_transaction_save($transaction);
      $order = commerce_order_load($transaction->order_id);
      if ($order) {
        commerce_order_status_update($order,'checkout_complete');
        commerce_checkout_complete($order);
      }
    }
  }
?>

Продолжить


Вы можете совершить тестовую покупку через PayQR со специальной консолью, которая будет сопровождать процесс обмена информацией между виртуальным магазином и PayQR по ходу совершения покупки. Это позволит ознакомиться с примерным циклом получения интернет-сайтом уведомлений от PayQR и типовых ответов интернет-сайта на такие уведомления.