Работать с интернет-банкингом большого количества предприятий может оказаться достаточно рутинной задачей. Для создания платежей и их контроля необходимо каждый раз переключаться между кабинетами. А если таких организаций больше 50, то можно с легкостью сбиться и наделать ошибок, не говоря уже о портаченном времени. Давайте посмотрим, как можно облегчить жизнь бухгалтера на примере API ПриватБанка для бизнеса.

Для работы со счетом необходимо создать Автоклиент и получить данные для авторизации. Эти данные, а так же платежную информацию будем хранить в Google-таблице.





Инструменты / Редактор скриптов. Здесь будем писать бекенд на Google Apps Script, в основе которого лежит Java Script. Наша задача — создать http-запрос, указав необходимые реквизиты.

Скрипт будет реагировать на изменения в последнем столбце. Для этого добавим триггер при изменении (onEdit) — выполнять функцию onEditt(). И саму функцию:

var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var lastRow = sheet.getLastRow();
var lastCol = sheet.getLastColumn();


function onEditt(e) {
  
  var range = e.range;
  var col = range.getColumn();
  var row = range.getRow();

  if (row > 1) {
    
    if (col == lastCol) {
      
      var value = range.getValue();
      
      if (value == 'Отправить') {
        createPayment(range.getRow());
      }
      
      else if (value == 'Проверить') {
        checkPayment(range.getRow(), sheet);
      }        
      
      range.clearContent();    
    } 
}

Создание платежек


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

var url = "https://acp.privatbank.ua/api/proxy/payment/create_pred";
var day = getDay();

//передать номер строки в таблице
function createPayment(i) {
  
  var iRange = sheet.getRange(i, 1, 1, lastCol);
  var values = iRange.getDisplayValues();
  var docNumRange = sheet.getRange(i, 3);
  var stateRange = sheet.getRange(i, 10);
  var state = stateRange.getValue();  
  
  if (state == 'Создано') {
    iRange.setBackground('#66ff66');
    return;
  } else if (state == 'Оплачено') {
    iRange.setBackground('#ebebe0');
    return;
  }
  
  docNumRange.clearContent();
  
  var name = values[0][0];
  var userData = getUserData(name);
  
  if (!userData) {
    iRange.setBackground('#ffff80');
    stateRange.setValue('Нет данных пользователя');
    return;
  }
  
  var inn = userData[1];
  var id = userData[2];
  var token = userData[3];
  var payer_account = userData[4];
  var doc_type = values[0][1];
  var document_number = "" + day + (i + 10) + Math.floor(Math.random() * 90 + 10);
  var payment_amount = values[0][3];
  var payment_naming = values[0][4];    
  var recipient_account = values[0][5];
  var recipient_nceo = values[0][6]; 
  var recipient_ifi = values[0][7];  
  var payment_destination = values[0][8];
  
  if (!inn || !id || !token || !payer_account) 
  {
    iRange.setBackground('#ffff80');
    stateRange.setValue('Нет данных пользователя');
    return;
  }
    
  if (!document_number || ! recipient_account || !recipient_nceo || 
      !payment_naming || !recipient_ifi || !payment_amount || !payment_destination) 
  {
    iRange.setBackground('#ffff80');
    stateRange.setValue('Ошибка заполнения');
    return;
  }
    
  var headers = {
    'User-Agent' : 'GooApps',
    'id' : id,
    'token' : token,      
    'Connection' : 'close'
  }
  
  var payload = {
    'document_number' : document_number,
    'payer_account' : payer_account,
    'recipient_account' : recipient_account,
    'recipient_nceo' : recipient_nceo,
    'payment_naming' : payment_naming,
    'recipient_ifi' : recipient_ifi,
    'payment_amount' : payment_amount,
    'payment_destination' : payment_destination    
  };
  
  var options = {
    'method' : 'post',
    'contentType' : 'application/json;charset=utf8',
    'headers' : headers,
    'payload' : JSON.stringify(payload),
    'muteHttpExceptions' : true
  };    

  var response = UrlFetchApp.fetch(url, options);  
  var respCode = response.getResponseCode();
  
  if (respCode == 201) {
    docNumRange.setValue(document_number);      
    iRange.setBackground('#66ff66');  
    stateRange.setValue('Создано');
  } else {    
    iRange.setBackground('#ff9980');   
    var resp = JSON.parse(response.getContentText());
    var error = resp.code;

    if (!error) {
      error = resp.error_code;
    }

    stateRange.setValue(error);    
  }    
}


//создать все платежки
function createPayments() {

  var range = sheet.getRange(2, 1, lastRow, lastCol);
  range.clearFormat();  
    
  for (var i = 2; i <= lastRow; i++) {
    createPayment(i);
  }
}


function getUserData(name) {        
  var uSheet = ss.getSheetByName('Users');
  var uRange = usersSheet.getRange(2, 1, uSheet.getLastRow(), uSheet.getLastColumn());
  var values = uRange.getDisplayValues();
  
  for (var i = 0; i < values.length; i++) {
    if (name == values[i][0]) {
      return values[i];
    }
  }  
}


function getDay() {   
  var now = new Date();
  var start = new Date('January 1, 2018 00:00:00 +0200');
  var number = Math.round((now.getTime() - start.getTime()) / (1000 * 3600 * 24));    
  return number;
}

Контроль оплат


Для контроля оплат будем получать все выписки за последние 30 дней и искать в них номер документа, статус и сумму оплаты. Если все совпадает — платежка оплачена. В этом случае используется GET-запрос.

var urlGet = 'https://acp.privatbank.ua/api/proxy/transactions';
var stDate = Utilities.formatDate(substMonth() , '+0200', 'dd-MM-yyyy');
var endDate = Utilities.formatDate(new Date(), '+0200', 'dd-MM-yyyy');

//передать номер строки в таблице
function checkPayment(i) {
  
  var iRange = sheet.getRange(i, 1, 1, lastCol);
  var values = iRange.getDisplayValues();  
  var stateRange = sheet.getRange(i, 10);
  var state = stateRange.getValue();
  
  if (state != 'Создано') {
    return;
  }  
  
  var name = values[0][0];
  var userData = getUserData(name);
  
  if (!userData) {
    iRange.setBackground('#ffe066');
    stateRange.setValue('Нет данных пользователя');
    return;
  }
  
  var id = userData[2];
  var token = userData[3];
  var payer_account = userData[4];
  var document_number = values[0][2];
  var payment_amount = values[0][3];  
 
  if (!id || !token || !payer_account) {
    iRange.setBackground('#ffe066');
    stateRange.setValue('Нет данных пользователя');
    return;
  }

  if (!document_number || !payment_amount) {
    iRange.setBackground('#ffe066');
    stateRange.setValue('Ошибка заполнения');  
    return;
  }
  
  var headers = {
    'User-Agent' : 'GooApps',
    'id' : id,
    'token' : token,      
    'Connection' : 'close'
  }
  
  var options = {
    'method' : 'get',
    'contentType' : 'application/json;charset=utf8',
    'headers' : headers,
    'muteHttpExceptions' : true
  };      
  
  var url = Utilities.formatString('%s?acc=%s&startDate=%s&endDate=%s', urlGet, payer_account, stDate, endDate);  
  var response = UrlFetchApp.fetch(url, options);  
  var resp = JSON.parse(response);
  var statements = resp.StatementsResponse.statements;
  
  for (var j = 0; j < statements.length; j++) {    
    for (var name in statements[j]) {
      var bill = statements[j][name];
      
      if (document_number == bill.BPL_NUM_DOC) {
        var pay = payment_amount.toString().replace(',','.');
        if (pay == bill.BPL_SUM) {
          if ('r' == bill.BPL_PR_PR) {
            iRange.setBackground('#ebebe0');
            stateRange.setValue('Оплачено');
          } else {
            iRange.setBackground('#df80ff');
            stateRange.setValue('Не оплачено');
          }
        } else {
          Logger.log(pay + ' ' + bill.BPL_SUM)
          iRange.setBackground('#df80ff');
          stateRange.setValue('Сумма не совпадает');
        }      
      }        
    }
  }    
}


function substMonth() {
  var now = new Date();
  var start = new Date(now.getTime() - 30 * (1000 * 3600 * 24));
  return start;
}

Проверку можно поставить на триггер. Останется только наблюдать, как выполняются оплаты.



P.S. Попробуйте, как это работает на тестовой таблице. Скопируйте ее на свой диск и добавьте триггер onEdit на выполнение функции onEditt().

Комментарии (15)


  1. braker07 Автор
    24.04.2018 23:26

    Post платежка формируется бухгалтером по предприятиям, которые он ведёт. А сами предприниматели оплачивают сформированные платежки.
    С помощью выписок, они же логи оплаты, бухгалтер контролирует выполнение оплат.


  1. iHelper
    24.04.2018 23:29

    Все же управление ожидаемо сводится к внутреннему документообороту и чтению логов из банков.
    т.е. POST платёжки все ещё направляется бухгалтеру, или в лучшем случае – секретарше?


    1. braker07 Автор
      24.04.2018 23:30

      Post платежка формируется бухгалтером по предприятиям, которые он ведёт. А сами предприниматели оплачивают сформированные платежки.
      С помощью выписок, они же логи оплаты, бухгалтер контролирует выполнение оплат.


  1. iHelper
    24.04.2018 23:48

    Подходящая статья, что бы сверить часы: в Беларуси та же ситуация. слать токены в банк безполезно, декларировать можно/нужно, а учитывать приходится в любом случае.


  1. gabin8
    25.04.2018 00:34

    данный сервис бесплатный? Просто насколько я помню за G Suite Business гугл просил денег


    1. braker07 Автор
      25.04.2018 00:35

      Сервис бесплатный


  1. TimsTims
    25.04.2018 00:40

    Несколько вопросов для тех, кто не в теме:
    1) Как происходит авторизация в банкинге? Вы эмулируете браузер, или это можно делать из коробки Google Apis, или есть готовые решения от Приватбанка? Или Приватбанк сам даёт публичный API?
    2) В России большинство интернет-банков для юр.лиц используют usb-токены для генерации усиленной ЭЦП. Поэтому довольно непонятно как вы подписываете платежи, или Google-таблицы тоже это умеют делать?
    3) Ну и последний вопрос: а не боитесь хранить пароли от 50 компаний со всеми деньгами на счетах, и паролями от любых операций (отправить деньги, ага) в онлайновой таблице google? Злоумышленнику не нужно даже взламывать ваш комп, ему нужен только ваш пароль от аккаунта google, где хранятся все доки…


    1. braker07 Автор
      25.04.2018 01:06

      Взаимодействие происходит с помощью публичного API ПриватБанка. Создаётся Автоклиент — подобие пользователя с ограниченными правами. Зная его id и токен можно получить доступ к тому счету или группе счетов, на который настроен Автоклиент. Его права ограничены созданием платежек и просмотром выписок. Подпись платежек электронным ключом и их проведение осуществляется самим предпринимателем. Т.е. автоклиент не влияет на движение денежных средств, а лишь создаёт черновик документа. В самой худшей ситуации, злоумышленник получит доступ к уже оплаченным выпискам.
      Остаётся только создать http-запрос, что apps script умеет делать из коробки.


      1. TimsTims
        25.04.2018 09:36

        Интересно, спасибо.


  1. braker07 Автор
    25.04.2018 01:04

    ...


  1. bit_rainbow
    25.04.2018 10:43

    Пожалуйста поделитесь тестовой таблицей.


    1. braker07 Автор
      25.04.2018 14:44

      Добавил в конец статьи.


  1. adeptoleg
    25.04.2018 11:43

    Неплохо. можно ещё заюзать идею старого лайфхака с бесплатными смс с календаря для платежек которые не прошли или других важных вещей. В общем годно спс. В мемориз


    1. braker07 Автор
      25.04.2018 12:21

      Думал добавить эл. ящики пользователей и делать рассылки по событиям. Про смс-сервис не знал. Спасибо за подсказку.


      1. adeptoleg
        25.04.2018 12:25

        Не совсем смс сервис :) количество в день ограничено и только на номер привязанный к акку. Но на чтото очень важное вполне хватает