1
0
forked from BRT/arc

Add PayByCardScript implementation and XML asset for payment automation in Ozon

Implemented `PayByCardScript` to enable automated card payments on the Ozon platform. Includes ad banner detection/handling, OCR-based transaction ID extraction, fee calculation, and database updates for transactions. Added XML asset `pay_by_card_success.xml` for payment result handling.
This commit is contained in:
slava 2026-03-10 09:30:25 +07:00
parent f95dabca77
commit 23d404b9c5
13 changed files with 1279 additions and 6 deletions

View File

@ -0,0 +1,375 @@
#include "PayByCardScript.h"
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(nullptr, "rus+eng")) {
qWarning() << "[OCR] Failed to init Tesseract";
delete api;
return {};
}
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции A60130542076330K0000040011670301" или "ID операции = Аб60650..."
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-я0-9]+))");
if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1);
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
PayByCardScript::PayByCardScript(
AccountInfo account,
QString cardNumber,
const double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)), m_cardNumber(std::move(cardNumber)),
m_amount(amount) {
}
PayByCardScript::~PayByCardScript() = default;
void PayByCardScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByCard] Ad banner detected, closing...";
AdbUtils::makeTap(device.id, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByCard] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.id, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(device.id, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
qDebug() << "[PayByCard] Google Play update banner detected, closing...";
AdbUtils::makeTap(device.id, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByCard] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(device.id, device.screenWidth / 2, 100);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByCardScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const int width,
const int height
) {
// 1. Нажимаем "Перевести" на домашнем экране
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping 'Перевести'...";
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
if (!title.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByCard]" << m_error;
return;
}
// 3. Нажимаем "По номеру карты" в секции "ДРУГИЕ СПОСОБЫ"
Node cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
if (cardBtn.isEmpty()) {
m_error = "Кнопка 'По номеру карты' не найдена на экране платежей";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping 'По номеру карты'...";
AdbUtils::makeTap(deviceId, cardBtn.x(), cardBtn.y());
QThread::msleep(2000);
// 4. Ждём экран "Перевод по номеру карты"
bool cardScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Перевод по номеру карты");
if (!title.isEmpty()) {
cardScreenLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Перевод по номеру карты', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!cardScreenLoaded) {
m_error = "Экран 'Перевод по номеру карты' не загрузился";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Card payment screen loaded successfully";
// 5. Вводим номер карты в поле "Номер карты получателя"
Node cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя");
if (cardInput.isEmpty()) {
m_error = "Поле 'Номер карты получателя' не найдено";
qWarning() << "[PayByCard]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, cardInput.x(), cardInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_cardNumber);
QThread::msleep(2000);
// 6. Ждём распознавания карты и вводим сумму
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node sumInput = xmlScreenParser.findEditTextByHint("Сумма от");
if (sumInput.isEmpty()) {
m_error = "Поле ввода суммы не найдено";
qWarning() << "[PayByCard]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
QThread::msleep(1000);
qDebug() << "[PayByCard] Card number and amount entered";
// 7. Ждём расчёта комиссии (текст после "Комиссия" перестаёт быть "Рассчитаем...")
double fee = 0;
bool feeCalculated = false;
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
bool foundKomissiya = false;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.TextView" && node.text.trimmed() == "Комиссия") {
foundKomissiya = true;
continue;
}
if (foundKomissiya && node.className == "android.widget.TextView" && !node.text.isEmpty()) {
const QString feeText = node.text.trimmed();
if (feeText.startsWith("Рассчитаем")) {
foundKomissiya = false;
break; // ещё не рассчитана
}
// Парсим "45 ₽" → 45.0
QString digits = feeText;
digits.remove(QRegularExpression("[^0-9,\\.]"));
digits.replace(',', '.');
fee = digits.toDouble();
feeCalculated = true;
qDebug() << "[PayByCard] Fee parsed:" << fee << "from text:" << feeText;
break;
}
}
if (feeCalculated) break;
qDebug() << "[PayByCard] Waiting for fee calculation, attempt" << attempt + 1 << "/ 15";
QThread::msleep(1500);
}
if (!feeCalculated) {
qWarning() << "[PayByCard] Fee not calculated after timeout, proceeding with fee=0";
}
// 8. Нажимаем "Перевести" (или "Перевести X ₽")
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node transferBtn2 = xmlScreenParser.findButtonNode("Перевести", true);
if (transferBtn2.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на экране ввода суммы";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping transfer button:" << transferBtn2.text;
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
QThread::msleep(3000);
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
qDebug() << "[PayByCard] Payment successful!";
m_error.clear();
// Сохраняем транзакцию в БД
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.fee = fee;
tx.phone = m_cardNumber;
tx.bankName = "ozon";
tx.type = TransactionType::Card;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
const int txId = TransactionDAO::insertTransaction(tx);
qDebug() << "[PayByCard] Saved transaction to DB, id:" << txId << "fee:" << fee;
// Ищем кнопку "Документы" и достаём ID операции
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty() && txId != -1) {
qDebug() << "[PayByCard] Tapping 'Документы'...";
AdbUtils::makeTap(deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 20; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
QThread::msleep(1500);
}
if (docLoaded) {
QThread::msleep(500);
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!screenshot.isEmpty()) {
TransactionDAO::updateReceiptImage(txId, screenshot);
qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId;
const QString ocrText = recognizeTextFromImage(screenshot);
if (!ocrText.isEmpty()) {
qDebug() << "[PayByCard] OCR result:\n" << ocrText;
const QString externalId = extractTransactionId(ocrText);
if (!externalId.isEmpty()) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByCard] Extracted transaction ID:" << externalId;
} else {
qWarning() << "[PayByCard] Could not extract transaction ID from OCR text";
}
} else {
qWarning() << "[PayByCard] OCR returned empty text";
}
} else {
qWarning() << "[PayByCard] Screenshot is empty";
}
} else {
qWarning() << "[PayByCard] 'Поделиться' not found, skipping receipt";
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByCard]" << m_error;
}
} // namespace Ozon

View File

@ -0,0 +1,36 @@
#pragma once
#include "CommonScript.h"
namespace Ozon {
class PayByCardScript final : public CommonScript {
Q_OBJECT
public:
~PayByCardScript() override;
PayByCardScript(
AccountInfo account,
QString cardNumber,
double amount,
QObject *parent = nullptr
);
protected:
void doStart() override;
private:
const AccountInfo m_account;
const QString m_cardNumber;
const double m_amount;
QString m_error;
void makePayment(
const QString &deviceId,
const QString &pinCode,
int width,
int height
);
};
} // namespace Ozon

View File

@ -142,6 +142,15 @@ Node ScreenXmlParser::findFirstEditText() {
return {}; return {};
} }
Node ScreenXmlParser::findEditTextByHint(const QString &hintPrefix) {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText" && node.hint.contains(hintPrefix)) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) {
for (const Node &node: m_nodes) { for (const Node &node: m_nodes) {
if (contains) { if (contains) {
@ -591,6 +600,9 @@ void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
if (reader.attributes().hasAttribute("resource-id")) { if (reader.attributes().hasAttribute("resource-id")) {
node.resourceId = reader.attributes().value("resource-id").toString(); node.resourceId = reader.attributes().value("resource-id").toString();
} }
if (reader.attributes().hasAttribute("hint")) {
node.hint = reader.attributes().value("hint").toString();
}
m_nodes.append(node); m_nodes.append(node);
} }

View File

@ -28,6 +28,8 @@ public:
Node findFirstEditText(); Node findFirstEditText();
Node findEditTextByHint(const QString &hintPrefix);
Node findButtonNode(const QString &text, bool contains); Node findButtonNode(const QString &text, bool contains);
QList<Node> findAllButtons(); QList<Node> findAllButtons();

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,820 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="0" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/action_bar_root"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="2" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/main_activity_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/over_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="4" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/over_fragment_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1"
hint=""/>
</node>
<node index="3" text="" resource-id="" class="android.view.ViewGroup"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/web_view_top"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="2" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]" drawing-order="0"
hint="">
<node index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="true"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="0" hint="">
<node index="0" text="" resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="MFTransferByCardNumber@lk-pay"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="movable-pan-iframe"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,779][1039,942]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="frame-container"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][1039,929]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][1039,929]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][1039,929]"
drawing-order="0"
hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][1039,929]"
drawing-order="0"
hint="">
<node NAF="true"
index="0"
text=""
resource-id=""
class="android.widget.EditText"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,847][987,903]"
drawing-order="0"
hint="Номер карты получателя"/>
<node index="1"
text=""
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][1039,929]"
drawing-order="0"
hint=""/>
<node index="2"
text="Номер карты получателя"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,826][601,884]"
drawing-order="0"
hint=""/>
</node>
<node index="1" text=""
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,779][47,787]"
drawing-order="0"
hint=""/>
</node>
</node>
</node>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,262]"
drawing-order="0" hint="">
<node NAF="true" index="0" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[28,154][118,241]"
drawing-order="0" hint=""/>
<node index="1"
text="Перевод по номеру карты"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[133,165][945,231]"
drawing-order="0" hint=""/>
</node>
<node index="2" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[0,259][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="СЧЁТ СПИСАНИЯ"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,343][1039,388]"
drawing-order="0" hint=""/>
<node index="1" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,417][1039,606]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,417][231,606]"
drawing-order="0"
hint=""/>
<node index="1" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[231,417][1039,606]"
drawing-order="0" hint="">
<node index="0"
text="1 037,50₽"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[231,506][430,572]"
drawing-order="0"
hint=""/>
<node index="1"
text="Основной счёт"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[231,448][819,504]"
drawing-order="0"
hint=""/>
</node>
<node index="2" text=""
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,417][1039,606]"
drawing-order="0"
hint=""/>
</node>
<node index="2" text=""
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[0,601][1080,711]"
drawing-order="0" hint=""/>
<node index="3" text="ПОЛУЧАТЕЛЬ"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,706][1039,753]"
drawing-order="0" hint=""/>
<node index="4" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,958][1039,1110]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,958][924,1110]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,1005][924,1060]"
drawing-order="0"
hint="">
<node NAF="true"
index="0" text=""
resource-id="input___eEQR_Pc"
class="android.widget.EditText"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,1005][924,1060]"
drawing-order="0"
hint="Сумма от 100₽ до 500 000₽"/>
</node>
</node>
<node NAF="true" index="1"
text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[931,1000][997,1068]"
drawing-order="0"
hint=""/>
</node>
<node index="5" text="Комиссия"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,1168][997,1218]"
drawing-order="0" hint=""/>
<node index="6"
text="Рассчитаем после ввода всех данных"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[84,1215][997,1281]"
drawing-order="0" hint=""/>
<node index="7" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,2105][1039,2254]"
drawing-order="0" hint="">
<node index="0" text="Перевести"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,2105][1039,2254]"
drawing-order="0"
hint=""/>
</node>
</node>
</node>
</node>
</node>
<node index="1" text=""
resource-id="modal-container"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,2336][1080,2336]"
drawing-order="0" hint=""/>
<node index="2" text=""
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][5,5]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="2" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
<node index="3" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_toolbar"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,147]" drawing-order="1" hint=""/>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,2201][1080,2400]" drawing-order="3"
hint=""/>
</node>
</node>
</node>
</node>
<node index="1" text="" resource-id="" class="android.view.View" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[818,0][1080,262]" drawing-order="2" hint=""/>
</node>
</node>
</node>
</hierarchy>

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -42,6 +42,7 @@ public:
QString contentDesc; QString contentDesc;
QString resourceId; QString resourceId;
QString className; QString className;
QString hint;
bool clickable = false; bool clickable = false;
bool focusable = false; bool focusable = false;

View File

@ -29,6 +29,7 @@
#include "toss/LoginAndCheckAccountsTossScript.h" #include "toss/LoginAndCheckAccountsTossScript.h"
#include "ziraat/GetLastDaysHistoryZiraatScript.h" #include "ziraat/GetLastDaysHistoryZiraatScript.h"
#include "ziraat/LoginAndCheckAccountsZiraatScript.h" #include "ziraat/LoginAndCheckAccountsZiraatScript.h"
#include "ozon/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h" #include "ozon/PayByPhoneScript.h"
#include "rshb/PayByPhoneScript.h" #include "rshb/PayByPhoneScript.h"
@ -273,7 +274,9 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
txObj["transaction_type"] = (tx.type == TransactionType::Phone) ? "top-up" : "bank"; txObj["transaction_type"] = (tx.type == TransactionType::Phone) ? "top-up" : "bank";
txObj["bank_participator_id"]= tx.phone; txObj["bank_participator_id"]= tx.phone;
txObj["status"] = tx.status == TransactionStatus::Complete ? "completed" : (tx.status == TransactionStatus::Wait ? "progress" : "completed"); txObj["status"] = tx.status == TransactionStatus::Complete ? "completed" : (tx.status == TransactionStatus::Wait ? "progress" : "completed");
txObj["extra"] = QJsonObject(); QJsonObject extraObj;
extraObj["fee"] = static_cast<int>(tx.fee * 100);
txObj["extra"] = extraObj;
if (tx.timestamp.isValid()) { if (tx.timestamp.isValid()) {
txObj["created_at"] = tx.timestamp.toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs); txObj["created_at"] = tx.timestamp.toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs);
} else if (!tx.bankTime.isEmpty()) { } else if (!tx.bankTime.isEmpty()) {
@ -292,23 +295,33 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
} }
taskData = arr; taskData = arr;
} else if (event.type == EventType::CreateTransaction) { } else if (event.type == EventType::CreateTransaction) {
// Определяем тип транзакции из JSON задачи
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
const QString txType = taskJson["body"].toObject()["transaction_type"].toString();
QJsonObject obj; QJsonObject obj;
obj["amount"] = -static_cast<int>(event.amount * 100); obj["amount"] = -static_cast<int>(event.amount * 100);
obj["currency"] = 643; obj["currency"] = 643;
obj["transaction_type"] = "top-up"; obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType;
obj["bank_participator_id"]= event.phone; obj["bank_participator_id"]= event.phone;
obj["status"] = "completed"; obj["status"] = "completed";
obj["created_at"] = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs); obj["created_at"] = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs);
// Берём bank_transaction_id из последней сохранённой транзакции // Берём bank_transaction_id и fee из последней сохранённой транзакции
QList<TransactionInfo> txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1); QList<TransactionInfo> txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1);
int feeInCents = 0;
for (const auto &tx : txList) { for (const auto &tx : txList) {
if (!tx.bankTrExternalId.isEmpty() && qAbs(tx.amount) == event.amount && tx.phone == event.phone) { if (!tx.bankTrExternalId.isEmpty() && qAbs(tx.amount) == event.amount && tx.phone == event.phone) {
obj["bank_transaction_id"] = tx.bankTrExternalId; obj["bank_transaction_id"] = tx.bankTrExternalId;
feeInCents = static_cast<int>(tx.fee * 100);
break; break;
} }
} }
QJsonObject extraObj;
extraObj["fee"] = feeInCents;
obj["extra"] = extraObj;
taskData = obj; taskData = obj;
} else { } else {
QJsonObject obj; QJsonObject obj;
@ -390,9 +403,21 @@ void EventHandler::startThreadForTask(const AccountInfo &account, const EventInf
if (event.type == EventType::CreateTransaction) { if (event.type == EventType::CreateTransaction) {
const QString recipientBank = event.comment; // резолвленное имя банка-получателя const QString recipientBank = event.comment; // резолвленное имя банка-получателя
if (appCode == "ozon") setup(new Ozon::PayByPhoneScript(account, event.phone, recipientBank, event.amount));
else if (appCode == "rshb") setup(new Rshb::PayByPhoneScript(account, event.phone, recipientBank, event.amount)); // Определяем тип транзакции из JSON задачи
else qWarning() << "[EventHandler] CREATE_TRANSACTION: unknown appCode:" << appCode; const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
const QString txType = taskJson["body"].toObject()["transaction_type"].toString();
if (txType == "bank") {
// Перевод по номеру карты
if (appCode == "ozon") setup(new Ozon::PayByCardScript(account, event.phone, event.amount));
else qWarning() << "[EventHandler] CREATE_TRANSACTION bank: unknown appCode:" << appCode;
} else {
// Перевод по номеру телефона (top-up)
if (appCode == "ozon") setup(new Ozon::PayByPhoneScript(account, event.phone, recipientBank, event.amount));
else if (appCode == "rshb") setup(new Rshb::PayByPhoneScript(account, event.phone, recipientBank, event.amount));
else qWarning() << "[EventHandler] CREATE_TRANSACTION: unknown appCode:" << appCode;
}
} }
// // Watchdog-таймер: 5 минут — убиваем зависший поток // // Watchdog-таймер: 5 минут — убиваем зависший поток