1
0
forked from BRT/arc
arc/android/dushanbe/PayByCardScript.cpp

488 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 <QRegularExpression>
#include <QThread>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "AppLogger.h"
// Точечный OCR: находит строку "операции" и кропает правую часть для digit-only OCR
static QString recognizeOperationId(const QByteArray &pngData) {
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) return {};
const int w = pixGetWidth(pix);
const int h = pixGetHeight(pix);
auto *api = new tesseract::TessBaseAPI();
if (api->Init(nullptr, "rus+eng")) {
pixDestroy(&pix); delete api;
return {};
}
api->SetImage(pix);
api->Recognize(nullptr);
int opLineY = -1, opLineH = 0;
bool prevWasNomer = false;
tesseract::ResultIterator *ri = api->GetIterator();
if (ri) {
do {
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
if (word) {
const QString w_str = QString::fromUtf8(word);
delete[] word;
// Ищем "Номер" (или OCR-артефакты: "Homep", "Номep")
if (w_str.contains(QString::fromUtf8("омер")) ||
w_str.contains(QString::fromUtf8("omep"))) {
prevWasNomer = true;
continue;
}
// После "Номер" ищем "операции"
if (prevWasNomer &&
(w_str.contains(QString::fromUtf8("операции")) ||
w_str.contains(QString::fromUtf8("onepaции")))) {
int x1, y1, x2, y2;
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
opLineY = y1;
opLineH = y2 - y1;
break;
}
prevWasNomer = false;
}
} while (ri->Next(tesseract::RIL_WORD));
}
api->End();
delete api;
if (opLineY < 0) { pixDestroy(&pix); return {}; }
const int cropY = qMax(0, opLineY - opLineH / 2);
const int cropH = opLineH * 2;
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
Pix *cropped = pixClipRectangle(pix, box, nullptr);
boxDestroy(&box);
pixDestroy(&pix);
if (!cropped) return {};
auto *api2 = new tesseract::TessBaseAPI();
if (api2->Init(nullptr, "eng")) {
pixDestroy(&cropped); delete api2;
return {};
}
api2->SetVariable("tessedit_char_whitelist", "0123456789");
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
api2->SetImage(cropped);
char *text = api2->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&cropped);
api2->End();
delete api2;
result.remove(QRegularExpression("[^0-9]"));
if (result.length() >= 8) {
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
return result;
}
return {};
}
namespace Dushanbe {
PayByCardScript::PayByCardScript(
MaterialInfo account,
QString cardNumber,
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;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Dushanbe::PayByCard] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AppLogger::log("dushanbe/PayByCard", device.id, m_error,
AdbUtils::captureScreenshotBytes(device.adbSerial),
"CREATE_TRANSACTION");
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
}
makePayment(device.adbSerial, 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 transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы"));
if (transfersTab.isEmpty()) {
m_error = "Tab 'Переводы' not found on home screen";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByCard] Tapping 'Переводы' tab";
AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y());
QThread::msleep(2000);
// 2. Ждём экран переводов — ищем "DC (по номеру карты)"
bool transfersLoaded = false;
Node cardTransferBtn;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
cardTransferBtn = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("DC (по номеру карты)"), true);
if (!cardTransferBtn.isEmpty()) {
transfersLoaded = true;
break;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for transfers screen, attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!transfersLoaded) {
m_error = "Transfers screen not loaded or 'DC (по номеру карты)' not found";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 3. Тапаем "DC (по номеру карты)"
qDebug() << "[Dushanbe::PayByCard] Tapping 'DC (по номеру карты)'";
AdbUtils::makeTap(deviceId, cardTransferBtn.x(), cardTransferBtn.y());
QThread::msleep(2000);
// 4. Ждём экран ввода — ищем EditText с hint "Номер карты получателя"
bool inputScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node editText = xmlScreenParser.findEditTextByHint(
QString::fromUtf8("Номер карты получателя"));
if (editText.isEmpty()) {
editText = xmlScreenParser.findFirstEditText();
}
if (!editText.isEmpty()) {
inputScreenLoaded = true;
break;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for card input screen, attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!inputScreenLoaded) {
m_error = "Card input screen not loaded";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 5. Вводим номер карты в поле "Номер карты получателя"
Node cardEditText = xmlScreenParser.findEditTextByHint(
QString::fromUtf8("Номер карты получателя"));
if (cardEditText.isEmpty()) {
cardEditText = xmlScreenParser.findFirstEditText();
}
AdbUtils::makeTap(deviceId, cardEditText.x(), cardEditText.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_cardNumber);
QThread::msleep(1000);
// Скрываем клавиатуру чтобы поле суммы стало видимым
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
// 6. Вводим сумму в поле "Сумма" (hint содержит "Сумма")
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма"));
if (sumEditText.isEmpty()) {
m_error = "Sum input field not found";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y());
QThread::msleep(500);
// Сумма приходит с копейками (например 100.50), форматируем как целое или с точкой
const QString amountStr = (m_amount == static_cast<int>(m_amount))
? QString::number(static_cast<int>(m_amount))
: QString::number(m_amount, 'f', 2);
AdbUtils::inputText(deviceId, amountStr);
QThread::msleep(1000);
// 7. Скрываем клавиатуру и нажимаем "Далее"
AdbUtils::goBack(deviceId);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
if (daleeBtn.isEmpty()) {
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
}
if (daleeBtn.isEmpty()) {
m_error = "Button 'Далее' not found";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByCard] Tapping 'Далее'";
AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y());
QThread::msleep(2000);
// 8. После "Далее" кнопка меняется на "Оплатить" — ждём и нажимаем
bool oplatitFound = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить"));
if (oplatitBtn.isEmpty()) {
oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false);
}
if (!oplatitBtn.isEmpty()) {
qDebug() << "[Dushanbe::PayByCard] Tapping 'Оплатить'";
AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y());
oplatitFound = true;
QThread::msleep(3000);
break;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for 'Оплатить', attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!oplatitFound) {
m_error = "Button 'Оплатить' not found";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю"
// content-desc содержит "Подтверждение платежа...Подтверждаю"
// Кнопка "Подтверждаю" визуально внизу bottom sheet
bool confirmed = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// Пин-код может появиться
if (xmlScreenParser.isPinCodeScreen()) {
qDebug() << "[Dushanbe::PayByCard] PIN code screen, entering PIN...";
inputPinCode(deviceId, pinCode);
QThread::msleep(3000);
continue;
}
// Ищем элемент с "Подтверждение платежа" и "Подтверждаю" в content-desc
Node confirmNode = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Подтверждение платежа"), true);
if (!confirmNode.isEmpty() &&
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
// Тапаем внизу этого элемента (кнопка "Подтверждаю" в нижней части)
// Кнопка "Подтверждаю" всегда внизу bottom sheet — тапаем по 92% высоты экрана
const int tapY = height * 92 / 100;
qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at y=" << tapY;
AdbUtils::makeTap(deviceId, confirmNode.x(), tapY);
confirmed = true;
QThread::msleep(4000);
break;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for confirmation sheet, attempt" << attempt + 1;
QThread::msleep(2000);
}
if (!confirmed) {
m_error = "Confirmation sheet not found";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 10. Ждём результат — "Платеж выполнен" или ошибку
bool success = false;
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// Пин-код
if (xmlScreenParser.isPinCodeScreen()) {
qDebug() << "[Dushanbe::PayByCard] PIN code screen (retry), entering PIN...";
inputPinCode(deviceId, pinCode);
QThread::msleep(3000);
continue;
}
// Успех: content-desc начинается с "Платеж выполнен"
if (!xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Платеж выполнен"), true).isEmpty()) {
success = true;
break;
}
// Ошибка
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() ||
!xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) {
m_error = "Transfer failed: error screen detected";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for result, attempt" << attempt + 1;
QThread::msleep(2000);
}
if (!success) {
m_error = "Payment result not received within timeout";
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByCard] Payment successful!";
m_error.clear();
// Скриншот чека
const QByteArray receiptImage = AdbUtils::captureScreenshotBytes(deviceId);
// Парсим детали из content-desc "Платеж выполнен"
// Формат: "Платеж выполнен\nПоставщик:\n...\nКомиссия:\n0.00\nДата:\n05.04.26\nВремя:\n20:19:56\nНа главную"
QString bankTime;
double fee = 0;
Node resultNode = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Платеж выполнен"), true);
if (!resultNode.isEmpty()) {
const QStringList lines = resultNode.contentDesc.split('\n');
for (int i = 0; i < lines.size(); ++i) {
const QString line = lines[i].trimmed();
if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
fee = lines[i + 1].trimmed().toDouble();
} else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) {
bankTime = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) {
bankTime += " " + lines[i + 1].trimmed();
}
}
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
}
// Переходим в историю для получения ID операции через OCR
// 1. Тапаем "На главную" (внизу экрана результата)
Node naGlavnuyuNode = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Платеж выполнен"), true);
if (!naGlavnuyuNode.isEmpty()) {
const int tapY = height * 92 / 100;
AdbUtils::makeTap(deviceId, width / 2, tapY);
QThread::msleep(2000);
}
// 2. Тапаем "История" в нижней навигации
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node historyTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История"));
if (!historyTab.isEmpty()) {
AdbUtils::makeTap(deviceId, historyTab.x(), historyTab.y());
QThread::msleep(3000);
}
// 3. Тапаем первую транзакцию в списке (содержит m_cardNumber)
QString operationId;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node txNode = xmlScreenParser.findNodeByContentDesc(m_cardNumber, true);
if (txNode.isEmpty()) {
// Fallback: ищем "Сегодня" — транзакция обычно сразу после
txNode = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Сегодня"), true);
}
if (!txNode.isEmpty()) {
AdbUtils::makeTap(deviceId, txNode.x(), txNode.y());
QThread::msleep(3000);
// 4. Делаем скриншот детали и OCR
const QByteArray detailScreenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!detailScreenshot.isEmpty()) {
operationId = recognizeOperationId(detailScreenshot);
if (!operationId.isEmpty()) {
qDebug() << "[Dushanbe::PayByCard] Operation ID:" << operationId;
} else {
qWarning() << "[Dushanbe::PayByCard] Could not extract operation ID";
}
}
// Назад из деталей
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
} else {
qWarning() << "[Dushanbe::PayByCard] Transaction not found in history";
}
// Сохраняем транзакцию
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.fee = fee;
tx.status = TransactionStatus::Complete;
tx.type = TransactionType::Card;
tx.phone = m_cardNumber;
tx.bankName = "dushanbe_city_bank";
tx.bankTime = bankTime;
tx.bankTrExternalId = operationId;
tx.timestamp = QDateTime::currentDateTimeUtc();
tx.completeTime = QDateTime::currentDateTimeUtc();
tx.receiptImage = receiptImage;
const int txId = TransactionDAO::insertTransaction(tx);
if (txId != -1) {
qDebug() << "[Dushanbe::PayByCard] Transaction saved, id:" << txId
<< "operationId:" << operationId;
if (!receiptImage.isEmpty()) {
TransactionDAO::updateReceiptImage(txId, receiptImage);
}
} else {
qWarning() << "[Dushanbe::PayByCard] Failed to save transaction";
}
}
} // namespace Dushanbe