#include "PayByCardScript.h" #include #include #include #include #include #include #include #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(pngData.constData()), static_cast(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, device.screenHeight / 20); 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(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