#include "PayByPhoneScript.h" #include #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "dump/TaskDumpRecorder.h" #include "dao/MaterialDAO.h" #include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" #include 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(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 операции 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 { PayByPhoneScript::PayByPhoneScript( MaterialInfo account, QString phone, QString bankName, const double amount, QObject *parent ) : CommonScript(parent), m_account(std::move(account)), m_phone(std::move(phone)), m_bankName(std::move(bankName)), m_amount(amount) { } PayByPhoneScript::~PayByPhoneScript() = default; void PayByPhoneScript::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; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}}; TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); // Закрываем рекламный bottom sheet if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { qDebug() << "[PayByPhone] 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() << "[PayByPhone] 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() << "[PayByPhone] 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() << "[PayByPhone] 'Заказать карту' 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 PayByPhoneScript::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() << "[PayByPhone]" << m_error; return false; } return true; }; // 1. Нажимаем "Перевести" на домашнем экране. // Внимание: поверх правой части кнопки сидит referral-бейдж "badge icon за друга" // (bounds типа [550,90][602,140]), который перекрывает центр кнопки. Тап в центр // открывает промо "До 3 000 за рекомендацию". Тапаем в левую четверть, чтобы // гарантированно попасть мимо бейджа. Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false); if (transferBtn.isEmpty()) { m_error = "Кнопка 'Перевести' не найдена на домашнем экране"; qWarning() << "[PayByPhone]" << m_error; return; } // Тапаем в нижнюю часть кнопки (там, где визуально надпись "Перевести"), // ниже бейджа "за друга" (он заканчивается на y≈140). const int transferTapX = transferBtn.x(); const int transferTapY = transferBtn.y1() + transferBtn.height() * 4 / 5; qDebug() << "[PayByPhone] Tapping 'Перевести' at" << transferTapX << "," << transferTapY << "(below 'за друга' badge)"; AdbUtils::makeTap(deviceId, transferTapX, transferTapY); QThread::msleep(2000); // 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя" bool paymentsPageLoaded = false; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); Node phoneField = xmlScreenParser.findFirstEditText(); if (!phoneField.isEmpty()) { paymentsPageLoaded = true; break; } qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } if (!paymentsPageLoaded) { m_error = "Экран 'Платежи и переводы' не загрузился"; qWarning() << "[PayByPhone]" << m_error; return; } // 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести" Node phoneFieldTap = xmlScreenParser.findFirstEditText(); qDebug() << "[PayByPhone] Tapping 'Телефон получателя'..."; AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y()); QThread::msleep(2000); // 4. Ждём экран "Кому перевести" bool phoneScreenLoaded = false; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); // Закрываем диалог "Разрешить доступ к контактам" Node notNowBtn = xmlScreenParser.findButtonNode("Не сейчас", false); if (!notNowBtn.isEmpty()) { qDebug() << "[PayByPhone] Contacts permission dialog detected, tapping 'Не сейчас'..."; AdbUtils::makeTap(deviceId, notNowBtn.x(), notNowBtn.y()); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); } Node title = xmlScreenParser.findTextNode("Кому перевести"); if (!title.isEmpty()) { phoneScreenLoaded = true; break; } qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } if (!phoneScreenLoaded) { m_error = "Экран 'Кому перевести' не загрузился"; qWarning() << "[PayByPhone]" << m_error; return; } // 5. Тапаем на EditText и вводим номер телефона Node phoneInput; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); phoneInput = xmlScreenParser.findFirstEditText(); if (!phoneInput.isEmpty()) break; qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } if (phoneInput.isEmpty()) { m_error = "Поле ввода телефона не найдено"; qWarning() << "[PayByPhone]" << m_error; return; } AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y()); QThread::msleep(500); AdbUtils::inputText(deviceId, m_phone); QThread::msleep(2000); // 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны bool sumScreenLoaded = false; bool bankSelected = false; bool otherBankClicked = false; FreezeDetector freezeCheck; for (int attempt = 0; attempt < 20; ++attempt) { if (!checkApp()) return; const QString currentDumpXml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter); xmlScreenParser.parseAndSaveXml(currentDumpXml); closeGooglePlayBannerIfVisible(deviceId, width, height); if (freezeCheck.noticeAndCheckFrozen(currentDumpXml)) { qWarning() << "[PayByPhone] 4 identical dumps in a row — screen frozen, aborting"; break; } // Проверяем: уже на экране суммы? Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона"); if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) { sumScreenLoaded = true; break; } // Ищем кнопку с номером телефона (новый перевод / последние переводы) QString phoneDigits = m_phone; phoneDigits.remove(QRegularExpression("[^0-9]")); bool foundPhoneBtn = false; for (const Node &btn : xmlScreenParser.findAllButtons()) { QString btnDigits = btn.text; btnDigits.remove(QRegularExpression("[^0-9]")); if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) { qDebug() << "[PayByPhone] Found button with phone number:" << btn.text; AdbUtils::makeTap(deviceId, btn.x(), btn.y()); QThread::msleep(2000); foundPhoneBtn = true; break; } } if (foundPhoneBtn) continue; // Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка" if (!bankSelected) { // Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой). // На устройствах с маленьким экраном кнопка лежит в конце виртуализированного // списка и приходит с bounds=[0,0][0,0] — Node::isEmpty это не ловит, тап // улетает в (0,0). Скроллим список вниз, пока bounds не станут ненулевыми // (после первого скролла кнопка обычно материализуется у нижнего края, но // тап по ней работает — это её настоящая позиция в layout). auto findOtherBank = [&]() -> Node { for (const Node &n : xmlScreenParser.findAllNodes()) { if (n.contentDesc.contains("other-bank-item")) return n; } return {}; }; bool foundOtherBank = false; Node node = findOtherBank(); for (int scrollAttempt = 0; !node.isEmpty() && (node.width() == 0 || node.height() == 0) && scrollAttempt < 6; ++scrollAttempt) { qDebug() << "[PayByPhone] 'Другой банк' has collapsed bounds, scrolling list down, attempt" << scrollAttempt + 1 << "/ 6"; AdbUtils::makeSwipe(deviceId, width / 2, height * 3 / 4, width / 2, height / 4); QThread::msleep(800); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); node = findOtherBank(); } if (!node.isEmpty() && node.width() > 0 && node.height() > 0) { qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item) at" << node.x() << "," << node.y(); AdbUtils::makeTap(deviceId, node.x(), node.y()); QThread::msleep(2000); foundOtherBank = true; otherBankClicked = true; } if (foundOtherBank) continue; // Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк" if (otherBankClicked) { // После перехода на экран поиска банка поле ввода прячется под верхним // тулбаром (рендерится Svelte WebView'ом). Свайпаем сверху вниз, чтобы // подтянуть содержимое и сделать поле физически видимым/активным. qDebug() << "[PayByPhone] Scrolling up to reveal hidden bank search input..."; AdbUtils::makeSwipe(deviceId, width / 2, height / 4, width / 2, height * 3 / 4); QThread::msleep(800); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); Node bankInput = xmlScreenParser.findFirstEditText(); if (!bankInput.isEmpty()) { qDebug() << "[PayByPhone] Bank search screen detected, entering bank name..."; // Переключаем IME ДО тапа по полю — иначе смена IME после тапа // закроет системную клавиатуру и WebView резайзится, из-за чего // сохранённые координаты bankInput уезжают и повторный тап мимо. if (!AdbUtils::enableAdbIMEKeyboard(deviceId)) { m_error = "Не удалось активировать ADB Keyboard (см. логи 'ime list -s' / default_input_method)"; qWarning() << "[PayByPhone]" << m_error; return; } QThread::msleep(800); // Перечитываем XML — layout с ADBKeyboard (без UI) отличается от layout // с системной клавиатурой. Берём актуальные bounds поля. xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); Node freshBankInput = xmlScreenParser.findFirstEditText(); if (!freshBankInput.isEmpty()) bankInput = freshBankInput; AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y()); QThread::msleep(500); AdbUtils::inputAdbIMEText(deviceId, m_bankName); QThread::msleep(2500); // Верифицируем, что текст действительно попал в поле xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); Node verifyInput = xmlScreenParser.findFirstEditText(); if (verifyInput.text.isEmpty()) { qWarning() << "[PayByPhone] Поле банка пустое после inputAdbIMEText — ретраим"; AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y()); QThread::msleep(500); AdbUtils::inputAdbIMEText(deviceId, m_bankName); QThread::msleep(2500); } AdbUtils::disableAdbIMEKeyboard(deviceId); qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName; QThread::msleep(2000); // Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc) bool bankFound = false; for (int ba = 0; ba < 10; ++ba) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); // Сначала проверяем, что поле поиска реально содержит введённый текст — // иначе мы тапаем по первому попавшемуся банку из несфильтрованного списка. Node searchField = xmlScreenParser.findFirstEditText(); if (searchField.text.trimmed().isEmpty()) { qDebug() << "[PayByPhone] Search field still empty, waiting for filter, attempt" << ba + 1 << "/ 10"; QThread::msleep(1500); continue; } for (const Node &node : xmlScreenParser.findAllNodes()) { if (node.className == "android.widget.Button" && !node.contentDesc.isEmpty() && node.contentDesc != "base-go-back-button" && node.contentDesc != "native-toolbar-close-button" && node.contentDesc != "clear-input-icon") { const int tapX = node.x1() + node.width() / 4; qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "tapX:" << tapX << "y:" << node.y(); AdbUtils::makeTap(deviceId, tapX, node.y()); bankFound = true; break; } } if (bankFound) break; qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10"; QThread::msleep(1500); } if (!bankFound) { m_error = "Банк '" + m_bankName + "' не найден в списке"; qWarning() << "[PayByPhone]" << m_error; return; } bankSelected = true; QThread::msleep(2000); continue; } } // otherBankClicked } qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20"; QThread::msleep(1500); } if (!sumScreenLoaded) { m_error = "Экран ввода суммы не загрузился"; qWarning() << "[PayByPhone]" << m_error; return; } // 7. Вводим сумму // На экране "По номеру телефона" обычно 2 EditText: сумма (сверху) и // сообщение получателю (снизу). Оба имеют rid="input___xxxx" в Svelte UI. // Берём верхний (минимальный y1) — это amount. Node sumInput; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; QList candidateInputs; for (const Node &n : xmlScreenParser.findAllNodes()) { if (n.className != "android.widget.EditText") continue; if (n.height() <= 0 || n.height() > height * 4 / 5) continue; if (!n.resourceId.startsWith("input___")) continue; candidateInputs.append(n); } if (!candidateInputs.isEmpty()) { std::sort(candidateInputs.begin(), candidateInputs.end(), [](const Node &a, const Node &b) { return a.y1() < b.y1(); }); sumInput = candidateInputs.first(); // верхний = amount break; } // Старая логика — если нет ни одного "input___": берём любой EditText. sumInput = xmlScreenParser.findFirstEditText(); if (!sumInput.isEmpty()) break; qDebug() << "[PayByPhone] Waiting for sum input field, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); } if (sumInput.isEmpty()) { m_error = "Поле ввода суммы не найдено"; qWarning() << "[PayByPhone]" << 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); // Закрываем системную софт-клавиатуру, иначе она перекрывает "Продолжить" // (кнопка уезжает за нижнюю границу web_view_holder и имеет нулевую высоту). AdbUtils::hideKeyboard(deviceId); QThread::msleep(800); // 6. Нажимаем "Продолжить" (ретраим, пока bounds перестанут быть нулевыми) Node continueBtn; for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); continueBtn = xmlScreenParser.findButtonNode("Продолжить", false); if (!continueBtn.isEmpty() && continueBtn.height() > 0) break; qDebug() << "[PayByPhone] Waiting for tappable 'Продолжить', attempt" << attempt + 1 << "/ 5" << "(bounds collapsed — клавиатура ещё видна?)"; AdbUtils::hideKeyboard(deviceId); QThread::msleep(1000); } if (continueBtn.isEmpty() || continueBtn.height() == 0) { m_error = "Кнопка 'Продолжить' не найдена или перекрыта клавиатурой"; qWarning() << "[PayByPhone]" << m_error; return; } AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y()); QThread::msleep(2000); // 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..." bool confirmScreenLoaded = false; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод"); if (!confirmTitle.isEmpty()) { confirmScreenLoaded = true; break; } qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } if (!confirmScreenLoaded) { m_error = "Экран подтверждения не загрузился"; qWarning() << "[PayByPhone]" << m_error; return; } // 8. Нажимаем "Перевести ..." (ищем по первому слову) Node transferConfirmBtn; for (int attempt = 0; attempt < 10; ++attempt) { if (!checkApp()) return; transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true); if (!transferConfirmBtn.isEmpty()) break; qDebug() << "[PayByPhone] Waiting for 'Перевести ...' button, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(deviceId, width, height); } if (transferConfirmBtn.isEmpty()) { m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения"; qWarning() << "[PayByPhone]" << m_error; return; } qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text; AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y()); QThread::msleep(3000); // 8.5. Проверяем экран ввода PIN-кода xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); if (const QList pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); !pinNodes.isEmpty()) { qDebug() << "[PayByPhone] 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 pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); !pinNodes.isEmpty()) { qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN..."; inputPinCode(deviceId, pinNodes); QThread::msleep(3000); continue; } if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) { qDebug() << "[PayByPhone] Payment successful!"; m_error.clear(); // Сохраняем транзакцию в БД TransactionInfo tx; tx.accountId = m_account.id; tx.amount = -m_amount; tx.phone = m_phone; tx.bankName = "ozon"; tx.type = TransactionType::Phone; 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() << "[PayByPhone] Saved transaction to DB, id:" << txId; // Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler. // Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR // ради bank_transaction_id, но это медленно (до 30 сек ожидания) и // нестабильно. Теперь полагаемся на fallback-ID из phone+timestamp. QString externalId; if (txId != -1) { const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId); if (!screenshot.isEmpty()) { m_resultScreenshot = screenshot; emit screenshotReady(m_resultScreenshot); TransactionDAO::updateReceiptImage(txId, screenshot); qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId; } } if (externalId.isEmpty() && txId != -1) { externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch()); qDebug() << "[PayByPhone] Generated fallback transaction ID:" << externalId; } if (!externalId.isEmpty() && txId != -1) { TransactionDAO::updateBankTrExternalId(txId, externalId); qDebug() << "[PayByPhone] Transaction ID:" << externalId; } AdbUtils::goBack(deviceId); QThread::msleep(1000); AdbUtils::goBack(deviceId); QThread::msleep(1000); return; } if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) { m_error = "Перевод не удался (экран 'Не удалось')"; qWarning() << "[PayByPhone]" << m_error; AdbUtils::goBack(deviceId); QThread::msleep(1000); AdbUtils::goBack(deviceId); QThread::msleep(1000); return; } qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15"; QThread::msleep(2000); } m_error = "Не дождались результата перевода"; qWarning() << "[PayByPhone]" << m_error; } } // namespace Ozon