From ffb206303e2c0bb6fe9fe533ea07e59cb8eb3269 Mon Sep 17 00:00:00 2001 From: slava Date: Fri, 10 Apr 2026 18:46:33 +0700 Subject: [PATCH] Refine transaction ID generation by replacing history-based retrieval with timestamp-based fallback logic. Adjust source card selection and transaction grouping in payment scripts. --- .../dushanbe/GetLastTransactionsScript.cpp | 107 ++++++++++++------ android/dushanbe/PayByCardScript.cpp | 75 +++++------- android/dushanbe/PayByPhoneScript.cpp | 69 +++++------ 3 files changed, 129 insertions(+), 122 deletions(-) diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 0ec4445..8004a1b 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -294,10 +294,6 @@ void GetLastTransactionsScript::doStart() { AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); QThread::msleep(1000); } - // Свайп вниз для обновления (pull-to-refresh) - const int x = device.screenWidth / 2; - AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight / 4, x, device.screenHeight * 3 / 4); - QThread::msleep(3000); continue; } @@ -339,11 +335,8 @@ void GetLastTransactionsScript::doStart() { qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK"; AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } - const int x = device.screenWidth / 2; - AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight / 4, x, device.screenHeight * 3 / 4); - QThread::msleep(3000); - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } // Находим accountId @@ -364,43 +357,93 @@ void GetLastTransactionsScript::doStart() { if (!tjTz.isValid()) tjTz = QTimeZone("Asia/Dushanbe"); const int maxScrolls = 10; + struct VypiskaItem { + double amount = 0.0; + QString time; // "21:31:21" + QString date; // "05.04.2026" + Node node; + }; + for (int scroll = 0; scroll <= maxScrolls; ++scroll) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - const QList items = xmlScreenParser.parseTransactionItems(); + + // 1. Собираем список дат-групп из родительского ImageView "Обновлено: ...\n05.04.2026\n..." + // Поддерживаем "DD.MM.YYYY", "Сегодня", "Вчера" + const QDate todayLocal = QDateTime::currentDateTimeUtc().toTimeZone(tjTz).date(); + QStringList groupDates; + static const QRegularExpression reDate(R"(^\d{2}\.\d{2}\.\d{4}$)"); + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.widget.ImageView") continue; + if (!n.contentDesc.startsWith(QString::fromUtf8("Обновлено"))) continue; + for (const QString &line : n.contentDesc.split('\n')) { + const QString t = line.trimmed(); + if (reDate.match(t).hasMatch()) { + groupDates.append(t); + } else if (t.compare(QString::fromUtf8("Сегодня"), Qt::CaseInsensitive) == 0) { + groupDates.append(todayLocal.toString("dd.MM.yyyy")); + } else if (t.compare(QString::fromUtf8("Вчера"), Qt::CaseInsensitive) == 0) { + groupDates.append(todayLocal.addDays(-1).toString("dd.MM.yyyy")); + } + } + break; + } + + // 2. Собираем View-транзакции (формат Выписки) + static const QRegularExpression reTime(R"(\d{2}:\d{2}:\d{2})"); + QList items; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.view.View") continue; + if (!n.contentDesc.contains("DCWallet") && !n.contentDesc.contains("***")) continue; + const QStringList lines = n.contentDesc.split('\n'); + if (lines.size() < 5) continue; + const QString last = lines.last().trimmed(); + if (!reTime.match(last).hasMatch()) continue; + + VypiskaItem it; + bool ok = false; + it.amount = lines[2].trimmed().toDouble(&ok); + if (!ok) continue; + it.time = last; + it.node = n; + items.append(it); + } + + // 3. Сортируем по Y и группируем по разрывам (> 50px → новая группа-день) + std::sort(items.begin(), items.end(), [](const VypiskaItem &a, const VypiskaItem &b) { + return a.node.y1() < b.node.y1(); + }); + int groupIdx = 0; + int prevY2 = -1; + for (VypiskaItem &it : items) { + if (prevY2 >= 0 && it.node.y1() - prevY2 > 50) ++groupIdx; + if (groupIdx < groupDates.size()) it.date = groupDates[groupIdx]; + prevY2 = it.node.y2(); + } + + qDebug() << "[Dushanbe::GetLastTransactions] Vypiska items:" << items.size() + << "groups:" << groupDates; for (const auto &tx : items) { if (qAbs(tx.amount - m_searchAmount) > 0.01) continue; qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount - << "phone:" << tx.phone << "time:" << tx.time; + << "date:" << tx.date << "time:" << tx.time; - // Предварительная проверка времени из списка (±1 час) - if (m_searchTime.isValid() && !tx.time.isEmpty()) { - const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss"); - const QTime searchLocal = m_searchTime.toTimeZone(tjTz).time(); - if (listTime.isValid() && searchLocal.isValid()) { - const int diffSecs = qAbs(listTime.secsTo(searchLocal)); - if (diffSecs > 3600 && diffSecs < 82800) { // 82800 = 24h - 1h (для полуночи) - qDebug() << "[Dushanbe::GetLastTransactions] Time pre-filter skip:" - << tx.time << "vs" << searchLocal.toString("HH:mm:ss") - << "diff=" << diffSecs << "sec"; + // Проверка даты+времени (±1 час) + if (m_searchTime.isValid() && !tx.date.isEmpty() && !tx.time.isEmpty()) { + QDateTime txDt = QDateTime::fromString(tx.date + " " + tx.time, + "dd.MM.yyyy HH:mm:ss"); + if (txDt.isValid()) { + txDt.setTimeZone(tjTz); + const qint64 diffSecs = qAbs(txDt.toUTC().secsTo(m_searchTime.toUTC())); + if (diffSecs > 3600) { + qDebug() << "[Dushanbe::GetLastTransactions] Time diff too big:" << diffSecs << "sec, skipping"; continue; } } } - // Ищем соответствующий node чтобы по нему тапнуть - Node txNode; - for (const Node &node : xmlScreenParser.findAllNodes()) { - if (node.className != "android.widget.ImageView") continue; - if (!node.contentDesc.contains("***")) continue; - // Проверяем что содержит нужную сумму и время - const QString amountStr = QString::number(tx.amount, 'f', 2); - if (node.contentDesc.contains(amountStr) && node.contentDesc.contains(tx.time)) { - txNode = node; - break; - } - } + Node txNode = tx.node; if (txNode.x() == 0 && txNode.y() == 0) { qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction"; diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp index 398820d..4beda0d 100644 --- a/android/dushanbe/PayByCardScript.cpp +++ b/android/dushanbe/PayByCardScript.cpp @@ -250,10 +250,31 @@ void PayByCardScript::makePayment( AdbUtils::inputText(deviceId, amountStr); QThread::msleep(1000); - // 7. Скрываем клавиатуру и нажимаем "Далее" + // 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); @@ -409,53 +430,11 @@ void PayByCardScript::makePayment( 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"; - } + // Генерируем 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; diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp index 83e2951..4f231e6 100644 --- a/android/dushanbe/PayByPhoneScript.cpp +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -250,10 +250,31 @@ void PayByPhoneScript::makePayment( AdbUtils::inputText(deviceId, amountStr); QThread::msleep(1000); - // 7. Скрываем клавиатуру и нажимаем "Далее" + // 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::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + qDebug() << "[Dushanbe::PayByPhone] 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); @@ -400,47 +421,11 @@ void PayByPhoneScript::makePayment( qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee; } - // Переходим в историю для получения ID операции через OCR - 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); - } - - 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); - } - - QString operationId; - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - Node txNode = xmlScreenParser.findNodeByContentDesc(phoneDigits, true); - if (txNode.isEmpty()) { - txNode = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Сегодня"), true); - } - if (!txNode.isEmpty()) { - AdbUtils::makeTap(deviceId, txNode.x(), txNode.y()); - QThread::msleep(3000); - - const QByteArray detailScreenshot = AdbUtils::captureScreenshotBytes(deviceId); - if (!detailScreenshot.isEmpty()) { - operationId = recognizeOperationId(detailScreenshot); - if (!operationId.isEmpty()) { - qDebug() << "[Dushanbe::PayByPhone] Operation ID:" << operationId; - } else { - qWarning() << "[Dushanbe::PayByPhone] Could not extract operation ID"; - } - } - - AdbUtils::goBack(deviceId); - QThread::msleep(1000); - } else { - qWarning() << "[Dushanbe::PayByPhone] Transaction not found in history"; - } + // Генерируем ID транзакции из даты+времени (без перехода в историю) + const QString operationId = bankTime.isEmpty() + ? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss") + : QString("dushanbe_") + QString(bankTime).replace(' ', '_'); + qDebug() << "[Dushanbe::PayByPhone] Generated operationId:" << operationId; // Сохраняем транзакцию TransactionInfo tx;