1
0
forked from BRT/arc
arc/android/ozon/PayByCardScript.cpp
slava a92610f9e5 Improve "Перевести" button handling in PayByCardScript:
- Add keyboard dismissal and retry logic to ensure button visibility and tappability.
- Update error messages to differentiate between missing or obstructed button scenarios.
2026-04-24 22:10:06 +07:00

464 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PayByCardScript.h"
#include <QCoreApplication>
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.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(QCoreApplication::applicationDirPath().toUtf8().constData(), "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 операции B606513042211200000120011700501"
// "ID операции po-3140eb01-002c-5000-8000-033396b637e1"
// или разбит на 2 строки
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))");
if (auto m = reId.match(ocrText); m.hasMatch()) {
QString id = m.captured(1);
id.remove(' ');
id.remove(QChar(0x00A0));
return id;
}
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
PayByCardScript::PayByCardScript(
MaterialInfo 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 BankProfileInfo app = BankProfileDAO::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;
}
const QString &adbId = device.adbSerial;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByCard] Ad banner detected, closing...";
AdbUtils::makeTap(adbId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByCard] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
qWarning() << m_error;
AdbUtils::tryToKillApplication(adbId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) {
qDebug() << "[PayByCard] Google Play update banner detected, closing...";
AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByCard] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByCardScript::makePayment(
const QString &deviceId,
const QString &packageName,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
auto checkApp = [&]() -> bool {
if (!isAppRunning(deviceId, packageName)) {
m_error = "Приложение было закрыто";
qWarning() << "[PayByCard]" << m_error;
return false;
}
return true;
};
// 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. Ждём экран "Платежи и переводы" И прогрузки кнопки "По номеру карты"
Node cardBtn;
bool paymentsPageLoaded = false;
bool titleSeen = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
if (!title.isEmpty()) titleSeen = true;
cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
if (!title.isEmpty() && !cardBtn.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы' + 'По номеру карты', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = titleSeen
? "Кнопка 'По номеру карты' не найдена на экране платежей"
: "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByCard]" << m_error;
return;
}
// 3. Нажимаем "По номеру карты" в секции "ДРУГИЕ СПОСОБЫ"
qDebug() << "[PayByCard] Tapping 'По номеру карты'...";
AdbUtils::makeTap(deviceId, cardBtn.x(), cardBtn.y());
QThread::msleep(2000);
// 4. Ждём экран "Перевод по номеру карты"
bool cardScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
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;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя");
if (!cardInput.isEmpty()) break;
qDebug() << "[PayByCard] Waiting for 'Номер карты получателя' input, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
}
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. Ждём распознавания карты и вводим сумму
Node sumInput;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
sumInput = xmlScreenParser.findEditTextByHint("Сумма от");
if (!sumInput.isEmpty()) break;
qDebug() << "[PayByCard] Waiting for 'Сумма от' input, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
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";
// Закрываем системную софт-клавиатуру — иначе она перекрывает кнопку "Перевести"
// внизу экрана (bounds схлопываются до нулевой высоты).
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(800);
// 7. Ждём расчёта комиссии (текст после "Комиссия" перестаёт быть "Рассчитаем...")
double fee = 0;
bool feeCalculated = false;
for (int attempt = 0; attempt < 15; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
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 ₽")
// Ретраим, пока bounds кнопки не будут ненулевыми — клавиатура могла всплыть
// заново (напр. если расчёт комиссии сфокусировал поле суммы).
Node transferBtn2;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
transferBtn2 = xmlScreenParser.findButtonNode("Перевести", true);
if (!transferBtn2.isEmpty() && transferBtn2.height() > 0) break;
qDebug() << "[PayByCard] Waiting for tappable 'Перевести', attempt" << attempt + 1 << "/ 5"
<< "(bounds collapsed — клавиатура ещё видна?)";
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(1000);
}
if (transferBtn2.isEmpty() || transferBtn2.height() == 0) {
m_error = "Кнопка 'Перевести' не найдена или перекрыта клавиатурой";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping transfer button:" << transferBtn2.text;
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
QThread::msleep(3000);
// 8.5. Проверяем экран ввода PIN-кода
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByCard] PIN code screen detected after confirm, entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
}
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Если PIN-код появился позже — вводим
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByCard] PIN code screen detected (retry), entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
continue;
}
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 операции
QString externalId;
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()) {
m_resultScreenshot = screenshot;
emit screenshotReady(m_resultScreenshot);
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;
externalId = extractTransactionId(ocrText);
}
}
} else {
qWarning() << "[PayByCard] 'Поделиться' not found, skipping receipt";
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
if (externalId.isEmpty() && txId != -1) {
externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[PayByCard] Generated fallback transaction ID:" << externalId;
}
if (!externalId.isEmpty() && txId != -1) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByCard] Transaction ID:" << externalId;
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByCard]" << m_error;
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
qDebug() << "[PayByCard] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByCard]" << m_error;
}
} // namespace Ozon