В активе имелось:
- openapi.tinkoff.ru
- Телефон службы поддержки (учитывая занятость отдела техподдержки — дело спасения утопающего, дело рук самого утопающего).
- Нагугленный документ: 24386_policy.pdf (с русским буквами внутри, с занятными выражениями, оборотами колдовскими, малопригодная но, все же вещь...)
В ходе гуглежа были также найдены отзывы о том, что настройка API банка Тинькофф дело весьма занимательное и нетривиальное (см. статью на banki.ru «API Тинькофф — мы слишком глупы для этого»).
Да, пришлось малость повозиться, поэтому, дабы сэкономить время другим товарищам по цеху, была написана данная статья.
Отмечу, что API банка Тинькофф использует Oauth 2.0 для авторизации.
Зачем же нужен openapi.tinkoff.ru?
- для теста (см. ниже);
- для того чтобы догадаться что чего и как; прямого тутора там нет; работаем на уровне интуиции!...
Приступим. В разделе «SSO Авторизация», кликнем на «how/Hide», и затем /secure/token#refresh-token («Выдача токена по рефреш токену»), в качестве параметра выбираем grant_type, далее в поле refresh_token (его можно получить в Личном кабинете пользователя). Жмем кнопку «Try it out!» Результатом этих действий является получение такой важной вещи как access_token (т.е. openapi.tinkoff.ru демонстрирует возможность ее получения).
Далее смотрим раздел «Счета и платежи», кликаем /partner/company/{INN}/excerpt («Получение выписки»). Изучаем, какие параметры необходимы для того чтобы ее заполучить: Authorization, INN, accountNumber, from, till.
Authorization — догадываемся, что Authorization это не что иное как access_token, который был получен нами в разделе «SSO Авторизация»;
INN — ИНН организации для которой настраиваем API;
from — с какого дня (период выписки);
till — по какой день (период выписки).
Таким образом (смотрим матчасть Oauth 2.0), получение данных выписки происходит в два этапа — сначала получаем access_token, затем имея на руках access_token, получаем данные этой самой выписки. Отлично. Алгоритм ясен, пишем код (параметры доступа в коде значения для $user, $pass, $refresh_token, $inn, $accountNumber — в приведенном ниже коде изменены, по понятным причинам).
Создадим следующие файлы:
- Первый файл настроек — StartSettings.php
- Второй файл стартовый — Start.php
- Третий файл постинга/парсинга данных в/из API — TinkoffInsertData.php; используем CURL(php).
- Пустой дамп базы данных, куда можно заливать данные ваших выписок: bank.sql; база MySQL (данные в базу направляем через PDO).
Итак, смотрим код и комментарии к нему!
Файл настроек — StartSettings.php:
$host = '127.0.0.1';
$db = 'bank';
$user = 'root';
$pass = '';
$charset = 'utf8';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $user, $pass, $opt);
$user="IKu0jn98kllkI90kklii"; //20 символов
$pass="ds4234SDFsdfsdijoijslkkdjfoIOi"; //30 символов
$refresh_token='dsfh345kljlkjsdf098sdfkljklj098sdfkklKKLjhjihiKL90909llkrre5345dfFDDFretertERTERETfdgd==';// 88 символов
$inn = '750151513135';
$accountNumber = '40802810300000121212';//20 символов
$from_year = '1980';
$from_month = '01';
$from_day = '01';
$till_year = date('Y');
$till_month = date('m');
$till_day = date('d');
Файл стартовый — Start.php:
session_start();
error_reporting(E_ALL);
include 'StartSettings.php';
include 'TinkoffInsertData.php';
TinkoffInsertData($user,$pass,$refresh_token, $inn, $accountNumber, $from_year, $from_month, $from_day, $till_year, $till_month, $till_day, $pdo);
$stmt = $pdo->prepare("INSERT INTO `bank`.`dateofwork` (dateofwork) VALUES (NOW())");
$stmt->execute();
Файл постинга/парсинга данных в/из API — TinkoffInsertData.php:
function TinkoffInsertData($user,$pass,$refresh_token, $inn, $accountNumber, $from_year, $from_month, $from_day, $till_year, $till_month, $till_day, $pdo){
//Первый этап - поход джедаев за access_token
$from_date = $from_year."-".$from_month."-".$from_day.'%2B03%3A00%3A00';
$till_date = $till_year."-".$till_month."-".$till_day.'%2B03%3A00%3A00';
$params=['grant_type'=>'refresh_token',
'refresh_token'=>$refresh_token
];
$headers = [
'POST /secure/token HTTP/1.1',
'Content-Type: application/x-www-form-urlencoded'
];
$curlURL='https://sso.tinkoff.ru/secure/token';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$curlURL);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $user . ":" . $pass);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_VERBOSE, true);
$curl_res = curl_exec($ch);
if($curl_res) {
$server_output = json_decode($curl_res);
}
//Считываем access_token - он нужен для реализации 2 этапа
$access_token_pos_start = strpos ($curl_res, 'access_token', 0);
$access_token_pos_start = $access_token_pos_start + 15;
$token_type_pos_start = strpos ($curl_res, "token_type", 0);
$access_token = mb_substr($curl_res, $access_token_pos_start, ($token_type_pos_start-$access_token_pos_start-3));
//Ура!.... мы сделали это.....
//По желанию, можете расскомментировать данный sleep, но в принципе работает и без него
//sleep(1);
//Второй этап - поход джедаев за данными
$params=[
'Authorization'=>$access_token,
'INN'=>$inn,
'accountNumber'=>$accountNumber
];
$headers = [
'Authorization: Bearer '.$access_token
];
$curlURL='https://sme-partner.tinkoff.ru/api/v1/partner/company/'.$inn.'/excerpt?accountNumber='.$accountNumber.'&from='.$from_date.'&till='.$till_date;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$curlURL);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $user . ":" . $pass);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, false);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_POSTFIELDS,http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_VERBOSE, true);
$curl_res = curl_exec($ch);
if($curl_res) {
$server_output = json_decode($curl_res);
}
$IE_Edge_pos_start = strpos ($curl_res, 'IE=Edge', 0);
$IE_Edge_pos_start = $IE_Edge_pos_start + 7;
$tinkoff_json = mb_substr($curl_res, $IE_Edge_pos_start);
$tinkoff_json = trim($tinkoff_json);
$tinkoff_json = json_decode($tinkoff_json);
//а тот ли счет мы считываем, собсно ;)
foreach ($tinkoff_json as $k=>$v){
if($k=='accountNumber'){
if(!($v==$accountNumber)) die('not that accountNumber');
}
}
//$tinkoff_array - записываем данные из json в массив
foreach ($tinkoff_json as $k=>$v){
if($k=='operation'){
$i=0;
foreach ($v as $t=>$s){
foreach ($s as $e=>$f){
$tinkoff_array[$i][$e]=$f;
}
$i++;
}
}
}
//заливаем данные из $tinkoff_array в базу данных
for ($i=0;$i<count($tinkoff_array);$i++){
$temp_id = $pdo->query("SELECT count(*) FROM `justtin`.`tinkoff` WHERE id=".$tinkoff_array[$i]['id'].";")->fetchColumn();
if ($temp_id==0){
if (Get_highly_likely_is_number_bill($tinkoff_array[$i]['paymentPurpose'])!=""){
$stmt = $pdo->prepare("INSERT INTO `justtin`.`tinkoff` (id, date, amount, drawDate, payerName, payerInn, payerAccount, payerCorrAccount, payerBic, payerBank, chargeDate, recipient, recipientInn, recipientAccount, recipientCorrAccount, recipientBic, recipientBank, operationType, uin, paymentPurpose, creatorStatus, payerKpp, executionOrder, date_of_save) VALUES (:id, :date, :amount, :drawDate, :payerName, :payerInn, :payerAccount, :payerCorrAccount, :payerBic, :payerBank, :chargeDate, :recipient, :recipientInn, :recipientAccount, :recipientCorrAccount, :recipientBic, :recipientBank, :operationType, :uin, :paymentPurpose, :creatorStatus, :payerKpp, :executionOrder, NOW())");
$stmt->bindParam(':id', $tinkoff_array[$i]['id']);
$stmt->bindParam(':date', $tinkoff_array[$i]['date']);
$stmt->bindParam(':amount', $tinkoff_array[$i]['amount']);
$stmt->bindParam(':drawDate', $tinkoff_array[$i]['drawDate']);
$stmt->bindParam(':payerName', $tinkoff_array[$i]['payerName']);
$stmt->bindParam(':payerInn', $tinkoff_array[$i]['payerInn']);
$stmt->bindParam(':payerAccount', $tinkoff_array[$i]['payerAccount']);
$stmt->bindParam(':payerCorrAccount', $tinkoff_array[$i]['payerCorrAccount']);
$stmt->bindParam(':payerBic', $tinkoff_array[$i]['payerBic']);
$stmt->bindParam(':payerBank', $tinkoff_array[$i]['payerBank']);
$stmt->bindParam(':chargeDate', $tinkoff_array[$i]['chargeDate']);
$stmt->bindParam(':recipient', $tinkoff_array[$i]['recipient']);
$stmt->bindParam(':recipientInn', $tinkoff_array[$i]['recipientInn']);
$stmt->bindParam(':recipientAccount', $tinkoff_array[$i]['recipientAccount']);
$stmt->bindParam(':recipientCorrAccount', $tinkoff_array[$i]['recipientCorrAccount']);
$stmt->bindParam(':recipientBic', $tinkoff_array[$i]['recipientBic']);
$stmt->bindParam(':recipientBank', $tinkoff_array[$i]['recipientBank']);
$stmt->bindParam(':operationType', $tinkoff_array[$i]['operationType']);
$stmt->bindParam(':uin', $tinkoff_array[$i]['uin']);
$stmt->bindParam(':paymentPurpose', $tinkoff_array[$i]['paymentPurpose']);
$stmt->bindParam(':creatorStatus', $tinkoff_array[$i]['creatorStatus']);
$stmt->bindParam(':payerKpp', $tinkoff_array[$i]['payerKpp']);
$stmt->bindParam(':executionOrder', $tinkoff_array[$i]['executionOrder']);
$stmt->execute();
}
}
}
}
Читателям: надеюсь, данный материал поможет в монетизации ваших веб-сервисов и сервисов ваших заказчиков. Да прибудет с вами сила!
Ребятам из техподдержки Банка Тинькофф: надеюсь, данная статья снизит нагрузку на вас! Удачи!
Комментарии (12)
optimusqp Автор
03.12.2018 16:44От банка, думаю атак не будет ) Поэтому информация записывается напрямую в базу. В статье дан минимум кода, который позволил бы реализовать функционал. За советы спасибо!
optimusqp Автор
03.12.2018 19:05-9Мда… вот и 0 под статусом статьи… Совесть есть? Минусовать под контентом, которого нет в сети… Вот и пиши туторы. В закладки 7 человек добавили статью к себе в закладки(!)… Кто на ап статьи? Ау… В этом мире Добро приветствуется....?..
Igor_Shumilov
03.12.2018 20:35В закладки 7 человек добавили статью к себе в закладки
Это для «потом прочитаю».
dmitry_dvm
03.12.2018 21:16Ад какой-то. В пхп нет десериализации в класс? Непонятно в чем сложность была. Да, OAuth2, да, рефреш и акцессорные токены, и что?
MaximChistov
04.12.2018 13:48есть, конечно. последнюю его портянку с вставкой в базу можно раз в 10 сократить.
Если остальной код не рефакторить, а только последний циклforeach ($tinkoff_array as $tinkoff){ $temp_id = $pdo->query("SELECT count(*) FROM `justtin`.`tinkoff` WHERE id=".$tinkoff['id'].";")->fetchColumn(); if ($temp_id==0){ if (Get_highly_likely_is_number_bill($tinkoff['paymentPurpose'])!=""){ $stmt = $pdo->prepare("INSERT INTO `justtin`.`tinkoff` (id, date, amount, drawDate, payerName, payerInn, payerAccount, payerCorrAccount, payerBic, payerBank, chargeDate, recipient, recipientInn, recipientAccount, recipientCorrAccount, recipientBic, recipientBank, operationType, uin, paymentPurpose, creatorStatus, payerKpp, executionOrder, date_of_save) VALUES (:id, :date, :amount, :drawDate, :payerName, :payerInn, :payerAccount, :payerCorrAccount, :payerBic, :payerBank, :chargeDate, :recipient, :recipientInn, :recipientAccount, :recipientCorrAccount, :recipientBic, :recipientBank, :operationType, :uin, :paymentPurpose, :creatorStatus, :payerKpp, :executionOrder, NOW())"); foreach($tinkoff as $key => $val) { $stmt->bindParam(":$key", $val); } $stmt->execute(); }
peresada
04.12.2018 07:32Насколько я знаю, у Тинькофф много различных SDK и API, которые «работаю, но не поддерживаются, и вообще их трудно найти, и вообще, откуда вы это откопали», как-то прикручивал попап на получение кредита по товару, использовал SDK с нормальной документацией, обычная .js библиотечка, в объект просто передаем нужные данные и получаем готовое решение в виде кнопки и попапа.
Спустя какое-то время это все перестало работать, при обращение в техподдержку ответ был как раз из серии «Не поддерживается, мы не знаем, вот другое решение», другое решение заключалось в добавлении на сайт кнопки со скрытыми инпутами. в которых внесена та же самая информация, форма просто отправляется на нужную страницу этого банка, а документация в .docx
А вообще, что касается документаций и работы с авторизацией по Oauth — у Твиттера самые большие проблемы с этим, слишком мудрят они по сравнению с остальными соцсетями.
D01
05.12.2018 09:34Прошу сильно не пинать, когда надо было загрузить данные за 2017й год, набросал такой модуль за несколько часов
const async = require('async'); const request = require('request'); const dateFormat = require('dateformat'); const MongoClient = require('mongodb').MongoClient; const config = require('./config.js'); const datasaver = require('./datasaver'); const client_id = ''; const client_secret = ''; const refresh_token = '' const myINN = 'ИНН своей компании'; const LOADER_TYPE='tinkof'; const URL = 'https://openapi.tinkoff.ru/sso/secure/token'; const ACCOUNT_URL = 'https://openapi.tinkoff.ru/sme/api/v1/partner/company/' + myINN + '/accounts'; const EXCERPT_URL = 'https://openapi.tinkoff.ru/sme/api/v1/partner/company/' + myINN + '/excerpt'; var startDate = new Date(); startDate.setFullYear(2017); startDate.setMonth(0); startDate.setDate(1); startDate.setHours(0); startDate.setMinutes(0); startDate.setSeconds(0); startDate.setMilliseconds(0); var endDate = new Date(); function work() { var access_token; var sessionId; async.waterfall([ function auth(callback) { const req = { form: { grant_type: 'refresh_token', refresh_token: refresh_token, client_id: client_id, client_secret: client_secret, sessionId: 'ANTIRIUS_WEB', } }; request.post({ url: URL, formData: req.form, json: true, }, function optionalCallback(err, httpResponse, body) { if (err) { console.log(err); callback(err, { code: 400, message: 'Ошибка авторизации: ' + err.message }); } else { access_token = body.access_token; sessionId = body.sessionId; callback(); } }); }, function(callback) { const req = { form: { grant_type: 'refresh_token', refresh_token: refresh_token, client_id: client_id, client_secret: client_secret, sessionId: 'ANTIRIUS_WEB', } }; request.get({ url: ACCOUNT_URL, json: true, headers: { Authorization: 'Bearer ' + access_token } }, function optionalCallback(err, httpResponse, body) { if (err) { console.log(err); callback(err, { code: 400, message: 'Ошибка получения списка счетов: ' + err.message }); } else { async.mapLimit(body, 1, function(item, callback) { loadDataForAccount(item, access_token, function() { callback(); }); }, function done(err, result) { callback(); }); } }); }, ], function done(err, result) { if (err) console.dir(err); if (result) console.dir(result, { colors: true }); console.log('DONE'); }); } work(); function loadDataForAccount(account, access_token, callback) { // console.log(account.accountNumber); request.get({ url: EXCERPT_URL + '?accountNumber=' + account.accountNumber + '&from=' + encodeURIComponent(dateFormat(startDate, "yyyy-mm-dd+HH:MM:ss")) + '&till=' + encodeURIComponent(dateFormat(endDate, "yyyy-mm-dd+HH:MM:ss")), json: true, headers: { Authorization: 'Bearer ' + access_token } }, function optionalCallback(err, httpResponse, body) { if (err) { console.log(err); callback(err); } else { save_all(account, body, callback); } }); } function save_all(account, data, callback) { var db; var client; async.waterfall([ function connect(callback) { MongoClient.connect('mongodb://' + config.db.server, function(err, _client) { if (err) { callback(err, { code: 500, message: 'Ошибка подключения к базе данных', }); } else { client = _client; db = client.db(config.db.name); callback(null); } }); }, function save(callback) { async.mapLimit(data.operation, 1, function(item, callback) { prepare_save_data(db, item, function() { callback(); }); }, function done(err, result) { callback(); }); }, ], function done(err, result) { callback(err, result); if (client) client.close(); }); } function prepare_save_data(db, obj, callback) { console.dir(obj, { colors: true }); var data1; var data2; async.waterfall([ function platel(callback) { save_plat_data(db, obj, function(err, result) { data1 = result; callback(null); }); }, function poluch(callback) { save_pol_data(db, obj, function(err, result) { data2 = result; callback(null); }); }, function save_plat(callback) { create_move(db, obj, data1, data2, callback); }, ], function done(err, result) { console.dir(data1); console.dir(data2); callback(null); }); } function getDate(date_string) { console.log(date_string); var arr = date_string.split('-'); var day = parseInt(arr[2]); var month = parseInt(arr[1]); var year = parseInt(arr[0]); var sort = arr[0] + arr[1] + arr[2]; return { y: year, m: month, d: day, sort: sort, } } function create_move(db, obj, payer, recipient, callback) { var move = {}; move.number = obj['id']; move.sum = obj['amount']; move.date_string = obj['date']; move.date = getDate(move.date_string); move.payer = payer; move.recipient = recipient; move.purpose = obj['paymentPurpose']; move.raw = obj; move.id = move.date_string + '.' + move.number + '.' + move.sum; move.loader = LOADER_TYPE; datasaver.save_move(db, move, function(err, result) { console.dir(result, { colors: true }); callback(null); }) } function save_plat_data(db, obj, callback) { var obj1; var obj2; var obj3; async.waterfall([ function save_1(callback) { var op = {}; op.name = obj['payerName']; op.inn = obj['payerInn']; op.kpp = obj['payerKpp']; op.loader = LOADER_TYPE; if (op.name && op.name.toLowerCase().indexOf('синюков') !== -1) { delete op.kpp; } datasaver.save_company(db, op, function(err, result) { obj1 = result.object; callback(null); }); }, function save_bank_1(callback) { var op = {}; op.name = obj['payerBank']; op.bik = obj['payerBic']; op.loader = LOADER_TYPE; datasaver.save_bank(db, op, function(err, result) { obj2 = result.object; callback(null); }); }, function _save_bank_account(callback) { var op = {}; op.number = obj['payerAccount']; op.bank = obj2; op.owner = obj1.id; op.loader = LOADER_TYPE; datasaver.save_bank_account(db, op, function(err, result) { obj3 = result.object; callback(null); }); }, ], function done(err, result) { callback(null, { company: obj1, bank: obj2, account: obj3, }); }); } function save_pol_data(db, obj, callback) { var obj1; var obj2; var obj3; async.waterfall([ function save_1(callback) { var op = {}; op.name = obj['recipient']; op.inn = obj['recipientInn']; op.kpp = obj['recipientKpp']; op.loader = LOADER_TYPE; if (op.name && op.name.toLowerCase().indexOf('синюков') !== -1) { delete op.kpp; } datasaver.save_company(db, op, function(err, result) { obj1 = result.object; callback(null); }); }, function save_bank_1(callback) { var op = {}; op.name = obj['recipientBank']; op.bik = obj['recipientBic']; op.loader = LOADER_TYPE; datasaver.save_bank(db, op, function(err, result) { obj2 = result.object; callback(null); }); }, function _save_bank_account(callback) { var op = {}; op.number = obj['recipientAccount']; op.bank = obj2; op.owner = obj1.id; op.loader = LOADER_TYPE; datasaver.save_bank_account(db, op, function(err, result) { obj3 = result.object; callback(null); }); }, ], function done(err, result) { callback(null, { company: obj1, bank: obj2, account: obj3, }); }); }
p/s Некоторые модули отсутствуют, но по названию понятно, что делают и все равно это придется переделывать под свой формат.
mykolaim
Код очень нечитабельный.
Переменные то написаны в camelCase то в snake_case.
Есть ещё такая штука — Exception
Для запросов советую использовать guzzle
А ну и запись данных доступа напрямую в код — это просто фейспалм.
vlreshet
Ладно бы просто нечитабельный код. Это отличный пример того, как делать никогда не надо
edogs
Ох отхватим мы сейчас, но честное слово.
«If It Looks Stupid but Works It Ain't Stupid»©
У ТС была цель — продемонстрировать работу с тиньков апи (хз в чем там была сложность, но раз автор заявляет что была — просто поверим). Цель выполнена? Да.
Почему с него требуют еще и код по всем стандартам оформлять? Ему зачем еще и на это время тратить, если его цель не идеально оформленный код показать, а просто концепт рабочий показать?
Реально анекдот получается
vlreshet
А в том то и прикол, что нет там сложностей никаких, незачем какие-то «рабочие концепты» показывать. Обычный OAuth, никакой экзотики, любой адекватный разработчик прекрасно знает что это, и как с этим работать. Что имеем в итоге: человек написал о том как решить тривиальную задачу крайне хреновым методом. За что хвалить?
T_Sun
Ну демонстрация должна быть читабильной. Сколько сотен или тысяч человек будут анализировать этот код? Если бы он использовал его сам внутри своего проекта, то критерия «работает и ладно» было бы достаточно.
Смотря какая цель.
Написать работающую программу — выполнена.
Написать статью и поделиться с сообществом — тут другие критерии.