diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 008f1e8..a579877 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -489,6 +489,14 @@ QSet AdbUtils::getListApps(const QString &deviceId) { return apps; } +QString AdbUtils::getDeviceTimezone(const QString &deviceId) { + QString out; + if (!startProcess(adbPath(), {"-s", deviceId, "shell", "getprop", "persist.sys.timezone"}, &out)) { + return {}; + } + return out.trimmed(); +} + void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) { const QString adb = adbPath(); diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index 38d8cc7..3adb5a4 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -49,6 +49,9 @@ public: static QSet getListApps(const QString &deviceId); + // Возвращает таймзону устройства (напр. "Asia/Bangkok"), либо пустую строку при ошибке + static QString getDeviceTimezone(const QString &deviceId); + private: static QString adbDir(); static bool startProcess(const QString &program, const QStringList &args, diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 5c6a606..efd3bb1 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -316,6 +316,36 @@ void GetLastTransactionsScript::doStart() { return; } + // Переключаемся на вкладку "Выписка" (по умолчанию открыта "Операции") + Node vypiskaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Выписка")); + if (vypiskaBtn.isEmpty()) { + m_error = "Cannot find 'Выписка' tab: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + qDebug() << "[Dushanbe::GetLastTransactions] Switching to 'Выписка' tab"; + AdbUtils::makeTap(m_deviceId, vypiskaBtn.x(), vypiskaBtn.y()); + QThread::msleep(3000); + + // Сохраняем дамп Выписки для анализа формата + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем возможный диалог "Ошибка" на Выписке + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка")).isEmpty()) { + Node okBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("ОК")); + if (!okBtn.isEmpty()) { + qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK"; + AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); + QThread::msleep(1000); + } + 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 int accountId = -1; const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); @@ -443,9 +473,9 @@ void GetLastTransactionsScript::doStart() { saveTx.fee = detail.fee; saveTx.bankName = m_appCode; saveTx.bankTime = detail.date + " " + detail.time; - saveTx.bankTrExternalId = detail.operationId; + saveTx.bankTrExternalId = "dushanbe_" + detail.date + "_" + detail.time; saveTx.description = detail.provider; - saveTx.phone = detail.receiverAccount; + saveTx.phone = ""; saveTx.name = detail.receiverAccount; saveTx.status = detail.status.contains(QString::fromUtf8("Успешн")) ? TransactionStatus::Complete : TransactionStatus::Unknown; diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp index 5359639..097b6ae 100644 --- a/android/ozon/CommonScript.cpp +++ b/android/ozon/CommonScript.cpp @@ -134,8 +134,8 @@ bool CommonScript::runAppAndGoToHomeScreen( // Проверяем, что это экран ввода пин-кода if (!findAndInputPincode) { - if (const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); - pincode.size() == 4) { + const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + if (pincode.size() == pinCode.length() && pincode.size() >= 4) { // чтобы пробудить экран if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) { AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y()); diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index 3176d22..01a0670 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -203,17 +203,21 @@ bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &devi } void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) { - const QTimeZone msk("Europe/Moscow"); + // Время в Ozon показывается в таймзоне устройства, а не в MSK + const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId); + QTimeZone deviceTz(tzName.toUtf8()); + if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow"); const int maxScrolls = 30; const int navBarTop = device.screenHeight - (device.screenHeight / 10); - // Целевая дата в МСК для поиска заголовка дня в списке - const QDateTime searchMsk = m_searchTime.toTimeZone(msk); - const QDate searchDate = searchMsk.date(); + // Целевая дата в таймзоне устройства для поиска заголовка дня в списке + const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz); + const QDate searchDate = searchLocal.date(); qDebug() << "[FetchTransactions] Searching: amount=" << m_searchAmount << "time=" << m_searchTime.toString(Qt::ISODate) - << "date(MSK)=" << searchDate.toString("dd.MM.yyyy"); + << "tz=" << tzName + << "date(local)=" << searchDate.toString("dd.MM.yyyy"); // ── Шаг 1: скроллим до нужной даты ────────────────────────────── bool dateFound = false; @@ -231,18 +235,28 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int {"сентября", 9}, {"октября", 10}, {"ноября", 11}, {"декабря", 12} }; + const QDate today = QDateTime::currentDateTimeUtc().toTimeZone(deviceTz).date(); bool hasOlderDate = false; for (const Node &node : xmlScreenParser.getNodes()) { if (node.className != "android.widget.TextView") continue; const QString t = node.text.trimmed(); - // Парсим "4 января, вс" → QDate - static const QRegularExpression dateRx(R"((\d{1,2})\s+(\S+),\s+\S+)"); - const auto match = dateRx.match(t); - if (!match.hasMatch()) continue; - const int day = match.captured(1).toInt(); - const int month = monthMap.value(match.captured(2), 0); - if (month == 0) continue; - const QDate headerDate(searchDate.year(), month, day); + QDate headerDate; + + // Относительные заголовки "Сегодня" / "Вчера" + if (t.compare("Сегодня", Qt::CaseInsensitive) == 0) { + headerDate = today; + } else if (t.compare("Вчера", Qt::CaseInsensitive) == 0) { + headerDate = today.addDays(-1); + } else { + // Абсолютный формат "4 января, вс" + static const QRegularExpression dateRx(R"((\d{1,2})\s+(\S+),\s+\S+)"); + const auto match = dateRx.match(t); + if (!match.hasMatch()) continue; + const int day = match.captured(1).toInt(); + const int month = monthMap.value(match.captured(2), 0); + if (month == 0) continue; + headerDate = QDate(searchDate.year(), month, day); + } if (!headerDate.isValid()) continue; if (headerDate == searchDate) { @@ -356,7 +370,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int } QDateTime txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); if (!txTime.isValid()) txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); - if (txTime.isValid()) txTime.setTimeZone(msk); + if (txTime.isValid()) txTime.setTimeZone(deviceTz); // Проверяем что дата и время совпадают (±1 час от m_searchTime) if (txTime.isValid()) { @@ -370,7 +384,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC())); if (diffSecs > 3600) { qDebug() << "[FetchTransactions] Time mismatch:" << dtStr - << "expected" << searchMsk.toString("dd.MM.yyyy, HH:mm") + << "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm") << "diff=" << diffSecs << "sec"; AdbUtils::goBack(m_deviceId); QThread::msleep(1000); @@ -434,7 +448,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int tx.amount = parsed.amount; tx.bankName = m_appCode; tx.bankTime = dtStr; - tx.bankTrExternalId = externalId; + tx.bankTrExternalId = externalId.isEmpty() + ? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_') + : externalId; tx.status = TransactionStatus::Complete; tx.timestamp = DateUtils::getUtcNow(); if (txTime.isValid()) tx.completeTime = txTime; @@ -474,7 +490,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int } void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) { - const QTimeZone msk("Europe/Moscow"); + const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId); + QTimeZone deviceTz(tzName.toUtf8()); + if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow"); const int navBarTop = device.screenHeight - (device.screenHeight / 10); // Ждём появления транзакций @@ -581,7 +599,7 @@ void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); if (!parsed.isValid()) parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); if (parsed.isValid()) { - parsed.setTimeZone(msk); + parsed.setTimeZone(deviceTz); tx.completeTime = parsed; } } diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index 01c4cd8..6c9dd1a 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -427,6 +427,10 @@ QString ScreenXmlParser::parseProfilePhone() { QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { const QString cleanPin = QString(pinCode).remove(' '); + if (cleanPin.isEmpty()) { + qWarning() << "[findPinCodeNodes] pinCode is empty — check bank_profiles.pin_code in DB"; + return {}; + } // Способ 1: ищем кликабельные ноды с текстом-цифрой (0-9) // Поддерживаем форматы: "1", "key 1", и т.п. QMap digitMap; @@ -499,8 +503,11 @@ QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { // Сначала квадратные, потом любые QList candidates = fuzzyGroup(true); - if (candidates.size() < 10) + qDebug() << "[findPinCodeNodes] fallback square candidates:" << candidates.size(); + if (candidates.size() < 10) { candidates = fuzzyGroup(false); + qDebug() << "[findPinCodeNodes] fallback any candidates:" << candidates.size(); + } if (candidates.size() < 10) { // Дебаг: выводим все clickable ноды и их размеры @@ -575,8 +582,12 @@ QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { for (const QChar &c : cleanPin) { if (const QString d = QString(c); digitMap.contains(d)) { result.append(digitMap[d]); + } else { + qWarning() << "[findPinCodeNodes] fallback: digit not in digitMap:" << d; } } + qDebug() << "[findPinCodeNodes] fallback result size:" << result.size() + << "digitMap size:" << digitMap.size(); return result; }