#include "PayByCardScript.h" #include #include #include #include #include "adb/AdbUtils.h" #include "dump/TaskDumpRecorder.h" #include "ocr/OcrUtils.h" #include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "AppLogger.h" #include // Точечный OCR: находит строку "операции" и кропает правую часть для digit-only OCR static QString recognizeOperationId(const QByteArray &pngData) { Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), static_cast(pngData.size())); if (!pix) return {}; const int w = pixGetWidth(pix); const int h = pixGetHeight(pix); auto *api = new tesseract::TessBaseAPI(); if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "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(QCoreApplication::applicationDirPath().toUtf8().constData(), "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; } const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber}; TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); 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.name + " (" + device.id + ")"; qWarning() << m_error; AppLogger::log("dushanbe/PayByCard", device.id, m_error, {}, "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)); closeGooglePlayBannerIfVisible(deviceId, width, height); 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)); closeGooglePlayBannerIfVisible(deviceId, width, height); 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 содержит "Сумма") Node sumEditText; for (int attempt = 0; attempt < 10; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма")); if (!sumEditText.isEmpty()) break; qDebug() << "[Dushanbe::PayByCard] Waiting for sum input, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } 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(m_amount)) ? QString::number(static_cast(m_amount)) : QString::number(m_amount, 'f', 2); AdbUtils::inputText(deviceId, amountStr); QThread::msleep(1000); // 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий") AdbUtils::goBack(deviceId); QThread::msleep(500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); { const QString cardMask = "****" + m_account.lastNumbers; Node cardNode; for (const Node &n : xmlScreenParser.findAllNodes()) { if (!n.clickable) continue; if (n.contentDesc.contains(cardMask)) { cardNode = n; break; } } if (cardNode.isEmpty()) { m_error = "Source card not found: " + cardMask; qWarning() << "[Dushanbe::PayByCard]" << m_error; AppLogger::log("dushanbe/PayByCard", deviceId, m_error, AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); return; } qDebug() << "[Dushanbe::PayByCard] Selecting source card:" << cardMask; AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y()); QThread::msleep(500); } // 7. Нажимаем "Далее" 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)); closeGooglePlayBannerIfVisible(deviceId, width, height); 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)); closeGooglePlayBannerIfVisible(deviceId, width, height); // Пин-код может появиться 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("Подтверждаю"))) { // Compose flatten-ит весь bottom sheet в одну accessibility-кнопку, // отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом // по скриншоту. Если OCR не нашёл — fallback на геометрию (центр // нижней четверти sheet, выше системного nav-бара). const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId); QPoint tap = OcrUtils::findTextCenter(screen, QString::fromUtf8("Подтверждаю"), confirmNode.y1(), confirmNode.y2()); if (tap.x() < 0) { const int tapY = confirmNode.y2() - qMax(150, height / 14); tap = QPoint(confirmNode.x(), tapY); qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap; } qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at" << tap; AdbUtils::makeTap(deviceId, tap.x(), tap.y()); 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)); closeGooglePlayBannerIfVisible(deviceId, width, height); // Пин-код 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(); // Скриншот чека — сохраняем как m_resultScreenshot и отправляем в мониторинг m_resultScreenshot = AdbUtils::captureScreenshotBytes(deviceId); const QByteArray &receiptImage = m_resultScreenshot; if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); // Парсим детали из 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 транзакции из даты+времени (без перехода в историю) const QString operationId = bankTime.isEmpty() ? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss") : QString("dushanbe_") + QString(bankTime).replace(' ', '_'); qDebug() << "[Dushanbe::PayByCard] Generated operationId:" << operationId; // Сохраняем транзакцию 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