1
0
forked from BRT/arc
arc/android/ozon/GetCardInfoScript.cpp

648 lines
35 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "GetCardInfoScript.h"
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/SettingsDAO.h"
#include "dump/TaskDumpRecorder.h"
#include "net/NetworkService.h"
#include "net/MaterialPayload.h"
#include "AppLogger.h"
namespace Ozon {
GetCardInfoScript::GetCardInfoScript(
QString deviceId,
QString appCode,
QString lastNumbers,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)),
m_lastNumbers(std::move(lastNumbers)) {
}
GetCardInfoScript::~GetCardInfoScript() = default;
void GetCardInfoScript::doStart() {
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Версионный трек приложения → версионно-зависимый парсинг (см. AppVersionTrack.h).
xmlScreenParser.setVersionTrack(Ozon::trackForVersion(app.appVersion));
qDebug() << "[Ozon::GetCardInfo] appVersion=" << app.appVersion
<< "→ track" << Ozon::trackName(xmlScreenParser.versionTrack());
// Читаем текущий экран
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Закрываем рекламный bottom sheet, если открыт
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[GetCardInfo] Ad banner detected, closing...";
AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Свежий дамп перед проверкой домашнего экрана
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Если не на домашнем экране — перезапускаем приложение
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetCardInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")";
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Закрываем баннеры на главной
tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Переходим в профиль и парсим имя и телефон (только если ещё нет в БД)
const bool hasProfileInfo = !app.fullName.isEmpty() && !app.phone.isEmpty();
Node userNameNode;
if (!hasProfileInfo) {
userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight);
}
if (!hasProfileInfo && !userNameNode.isEmpty()) {
qDebug() << "[GetCardInfo] Navigating to profile to parse name/phone...";
AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y());
QThread::msleep(2500);
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
if (xmlScreenParser.isProfileScreen()) break;
QThread::msleep(1000);
}
if (xmlScreenParser.isProfileScreen()) {
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone();
qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone;
BankProfileDAO::updateProfileInfo(app.id, fullName, phone);
} else {
qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse";
}
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
} else {
qDebug() << "[GetCardInfo] Profile info already in DB or username not found, skipping profile parse";
}
// Проверяем что на домашнем экране перед поиском карт
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetCardInfo] Not on home screen before card search, restarting...";
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen before card search: " + device.name + " (" + m_deviceId + ")";
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
}
// Ждём пока все карты подгрузятся — до 5 попыток
QList<MaterialInfo> cards;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight)) {
// баннер закрыт, дамп уже обновлён внутри helper'а
}
cards = xmlScreenParser.parseCardsOnHomeScreen();
if (!cards.isEmpty()) break;
qDebug() << "[GetCardInfo] No cards found on home screen, attempt" << attempt + 1 << "/ 5";
QThread::msleep(1500);
}
if (cards.isEmpty()) {
m_error = "No cards found on home screen: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen";
// Заполняем метаданные на найденных картах. Новые карты (которых ещё нет в БД)
// не создаём здесь — запись появится только после успешного OCR полного номера
// в success-ветке ниже. Это защищает от записей из ленты переводов и не оставляет
// в БД материал с пустым card_number.
//
// Для карт, которые уже в БД, обновляем только amount/update_time —
// прочие поля (cardNumber, materialId, status, ...) сохраняем как было.
for (MaterialInfo &card : cards) {
card.deviceId = device.id;
card.appCode = m_appCode;
card.updateTime = QDateTime::currentDateTimeUtc();
card.status = MaterialStatus::Off;
const MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (existing.id == -1) continue;
MaterialInfo refresh = existing;
refresh.amount = card.amount;
refresh.updateTime = card.updateTime;
if (!MaterialDAO::upsertAccount(refresh)) {
qWarning() << "[GetCardInfo] Failed to refresh amount for card" << card.lastNumbers;
} else {
qDebug() << "[GetCardInfo] Refreshed amount for card"
<< card.lastNumbers << "to" << card.amount;
}
}
// Сохраняем баланс домашнего экрана в application
const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount;
if (totalAmount > 0.0) {
BankProfileDAO::updateAmount(app.id, totalAmount);
}
// Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри)
NetworkService ns;
ns.setDeviceId(m_deviceId);
// 1. Создаём девайс если нет api_id
if (device.apiId.isEmpty()) {
QJsonObject deviceBody;
deviceBody["desktop_id"] = SettingsDAO::get("desktop_id");
deviceBody["status"] = "ONLINE";
deviceBody["name"] = device.name;
deviceBody["screen_width"] = device.screenWidth;
deviceBody["screen_height"]= device.screenHeight;
deviceBody["battery"] = device.battery;
ns.setPayload(QJsonDocument(deviceBody).toJson());
ns.createDevice();
qDebug() << "[GetCardInfo] createDevice done";
}
// 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены)
{
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts);
// ISO 4217 numeric currency codes
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972}
};
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : "";
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
profileBody["bank_name"] = m_appCode;
// Телефон: убираем пробелы, оставляем "+"
const QString phone = freshApp.phone.simplified().remove(' ');
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(totalAmount * 100));
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
ns.setPayload(QJsonDocument(profileBody).toJson());
if (freshApp.bankProfileId.isEmpty()) {
ns.addExtra("app_id", freshApp.id);
ns.postBankProfile();
// Проверяем что bankProfileId сохранился
const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode);
if (afterPost.bankProfileId.isEmpty()) {
QString err;
if (ns.lastResponseBody().contains("already exists", Qt::CaseInsensitive)) {
err = "PROFILE_ALREADY_EXISTS: " + ns.lastServerMessage();
} else {
err = "postBankProfile failed for " + m_deviceId;
}
qWarning() << "[GetCardInfo]" << err;
AppLogger::log("ozon/GetCardInfo", m_deviceId, err,
AdbUtils::captureScreenshotBytes(m_deviceId),
"FETCH_PROFILE");
m_error = err;
emit finishedWithResult(m_error);
return;
}
qDebug() << "[GetCardInfo] postBankProfile done";
} else {
ns.addExtra("bank_profile_id", freshApp.bankProfileId);
ns.patchBankProfile();
qDebug() << "[GetCardInfo] patchBankProfile done";
}
}
// Для каждой карты переходим на её экран и получаем полный номер через OCR
for (int i = 0; i < cards.size(); ++i) {
const MaterialInfo &card = cards[i];
// Если передан конкретный lastNumbers — обрабатываем только его
if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue;
qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers;
// Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна
MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (existing.id != -1 && !existing.cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping";
continue;
}
// Тапаем по кнопке конкретной карты — каждая открывает свой экран
Node cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers);
if (cardBtn.isEmpty()) {
qWarning() << "[GetCardInfo] Card button not found for" << card.lastNumbers;
continue;
}
// Открываем экран нужной карты.
if (xmlScreenParser.versionTrack() >= AppVersionTrack::V19_19_1) {
// Новый UI: тап по НИЖНЕЙ (чиповой) части тайла "Основной счёт" открывает экран
// счёта с РАЗДЕЛЬНЫМИ кнопками карт ("Bank Card NNNN"). Тапаем кнопку нужной
// карты — открывается именно она. (Тап в центр тайла открывал бы переднюю карту
// карусели, и для всех материалов читался номер одной карты.)
const int accY = cardBtn.y2() - cardBtn.height() / 5; // зона чипов карт внизу тайла
qDebug() << "[GetCardInfo] Opening account screen via tile bottom at" << cardBtn.x() << accY;
AdbUtils::makeTap(m_deviceId, cardBtn.x(), accY);
Node cardTile;
for (int a = 0; a < 8; ++a) {
QThread::msleep(a == 0 ? 1500 : 700);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
cardTile = xmlScreenParser.findAccountCardButton(card.lastNumbers);
if (!cardTile.isEmpty()) break;
qDebug() << "[GetCardInfo] Waiting account card button" << card.lastNumbers
<< "attempt" << a + 1 << "/ 8";
}
if (!cardTile.isEmpty()) {
qDebug() << "[GetCardInfo] Tapping account card button" << card.lastNumbers
<< "at" << cardTile.x() << cardTile.y();
AdbUtils::makeTap(m_deviceId, cardTile.x(), cardTile.y());
} else {
qWarning() << "[GetCardInfo] Account card button not found for" << card.lastNumbers
<< "— fallback to tile center tap";
AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y());
}
} else {
// Старый UI: тап по тайлу открывает карту напрямую.
AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y());
}
// Подводим "Показать" в зону тапа [minTapY, midY]: ниже статус-бара/тулбара и
// ВЫШЕ середины экрана (после раскрытия номер/срок/CVV появляются ниже кнопки —
// ей нужно место снизу). Свайпы делаем МЕДЛЕННЫМИ (durationMs), иначе быстрый
// ф_линг перелистывает "Показать" под самый верх (y≈3, под статус-бар) и тап мимо.
// - cy > midY → кнопка ниже середины (или за нижней навигацией): листаем вверх.
// - cy < minTapY → перелистнули под статус-бар: листаем обратно вниз.
const int minTapY = device.screenHeight / 8;
const int midY = device.screenHeight / 2;
const int swipeMs = 700;
for (int scrollAttempt = 0; scrollAttempt < 8; ++scrollAttempt) {
QThread::msleep(scrollAttempt == 0 ? 2000 : 600);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
Node candidate = xmlScreenParser.findButtonNode("Показать", false);
if (candidate.isEmpty()) {
if (xmlScreenParser.isHomeScreen()) {
cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers);
if (!cardBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Still on home before scroll, re-tapping card" << card.lastNumbers;
AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y());
}
}
continue;
}
const int cy = candidate.y();
if (cy >= minTapY && cy <= midY) {
qDebug() << "[GetCardInfo] 'Показать' in tap zone (above middle) y=" << cy;
break;
}
if (cy > midY) {
// Ниже середины — листаем вверх, целясь в верхнюю треть (медленно, без перелёта).
const int swipeToY = device.screenHeight / 3;
qDebug() << "[GetCardInfo] Scrolling 'Показать' up:" << cy << "->" << swipeToY;
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, cy,
device.screenWidth / 2, swipeToY, swipeMs);
} else {
// Перелистнули под статус-бар (y≈3) — листаем обратно вниз в верхнюю треть.
const int swipeToY = device.screenHeight / 3;
qDebug() << "[GetCardInfo] 'Показать' overshot to y=" << cy << ", scrolling down to" << swipeToY;
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, device.screenHeight / 8,
device.screenWidth / 2, swipeToY, swipeMs);
}
}
// Ждём экран карты и стабильной геометрии блока реквизитов.
// Без этого тап по "Показать" во время reflow уезжает ниже — в "Защиту от мошенников".
// Стабильность: "Показать" совпадает по y1 с "Реквизиты карты" И координаты не меняются в двух подряд дампах.
Node showBtn;
int prevShowY = -1;
for (int attempt = 0; attempt < 10; ++attempt) {
QThread::msleep(attempt == 0 ? 2000 : 500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
Node candidate = xmlScreenParser.findButtonNode("Показать", false);
Node requisites = xmlScreenParser.findTextNode("Реквизиты карты");
if (candidate.isEmpty() || requisites.isEmpty()) {
// Если всё ещё на домашнем экране — тап по карточке не сработал, повторяем
if (xmlScreenParser.isHomeScreen()) {
cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers);
if (!cardBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Still on home screen, re-tapping card" << card.lastNumbers;
AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y());
}
}
prevShowY = -1;
qDebug() << "[GetCardInfo] Requisites not ready, attempt" << attempt + 1 << "/ 10";
continue;
}
// "Показать" должен быть в одной строке с "Реквизиты карты"
if (qAbs(candidate.y1() - requisites.y1()) > 10) {
prevShowY = -1;
qDebug() << "[GetCardInfo] 'Показать' not aligned with 'Реквизиты карты', waiting reflow";
continue;
}
// Y должна совпасть в двух подряд дампах (layout устаканился)
if (prevShowY >= 0 && qAbs(candidate.y() - prevShowY) <= 5) {
showBtn = candidate;
break;
}
prevShowY = candidate.y();
qDebug() << "[GetCardInfo] 'Показать' seen at y=" << candidate.y() << ", waiting for stable frame";
}
if (showBtn.isEmpty()) {
qWarning() << "[GetCardInfo] 'Показать' not stable for card" << card.lastNumbers;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
continue;
}
qDebug() << "[GetCardInfo] 'Показать' at" << showBtn.x() << showBtn.y();
AdbUtils::makeTap(m_deviceId, showBtn.x(), showBtn.y());
// Ждём появления полного номера карты в XML дампе
QString cardNumber;
for (int attempt = 0; attempt < 10; ++attempt) {
QThread::msleep(2000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
cardNumber = xmlScreenParser.findFullCardNumber();
if (!cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1;
break;
}
// Закрываем баннер "Понятно" и повторно нажимаем "Показать"
Node bannerBtn = xmlScreenParser.findTextNode("Понятно");
if (!bannerBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing...";
AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false);
if (!retryShowBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Re-tapping 'Показать' after banner close";
AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y());
}
continue;
}
// "Подождите" — загрузка в процессе, просто ждём
if (!xmlScreenParser.findButtonNode("Подождите", false).isEmpty()) {
qDebug() << "[GetCardInfo] 'Подождите' visible, waiting... attempt" << attempt + 1 << "/ 10";
continue;
}
// "Скрыть" видна — номер должен быть раскрыт, но парсер не нашёл
if (!xmlScreenParser.findButtonNode("Скрыть", false).isEmpty()) {
qDebug() << "[GetCardInfo] 'Скрыть' visible but card number not parsed, attempt" << attempt + 1 << "/ 10";
continue;
}
// Повторно нажимаем "Показать" если тап не сработал
Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false);
if (!retryShowBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Re-tapping 'Показать', attempt" << attempt + 1 << "/ 10";
AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y());
} else {
qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10";
}
}
if (!cardNumber.isEmpty() && !cardNumber.endsWith(card.lastNumbers)) {
// Прочитан номер ЧУЖОЙ карты (карусель отдала не ту) — НЕ сохраняем, иначе
// материал получит неверный номер. Раньше из-за этого все карты получали
// номер передней карты карусели.
const QString err = "Card number " + cardNumber + " does not end with target "
+ card.lastNumbers + " — skipping save";
qWarning() << "[GetCardInfo]" << err;
AppLogger::log("ozon/GetCardInfo", m_deviceId, err,
AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE");
} else if (!cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber;
// Апсёртим запись только теперь — с полным номером карты.
MaterialInfo toSave = card;
toSave.cardNumber = cardNumber;
toSave.updateTime = QDateTime::currentDateTimeUtc();
if (!MaterialDAO::upsertAccount(toSave)) {
qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers;
} else {
qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers
<< card.description << card.amount;
}
} else {
const QString err = "Card number not found for " + card.lastNumbers;
qWarning() << "[GetCardInfo]" << err;
// Без полного номера материал в БД не создаём.
AppLogger::log("ozon/GetCardInfo", m_deviceId, err,
AdbUtils::captureScreenshotBytes(m_deviceId),
"FETCH_PROFILE");
}
// Возвращаемся на домашний экран для следующей карты. В новом UI путь длиннее
// (карта → "Основной счёт" → главная), поэтому жмём "назад" до главной (макс 3).
if (i < cards.size() - 1) {
for (int b = 0; b < 3; ++b) {
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
if (xmlScreenParser.isHomeScreen()) break;
}
}
}
// 2.5. Достаём номер счёта (общий для всех карт): Основной счёт → Посмотреть реквизиты
// ВРЕМЕННО ОТКЛЮЧЕНО
// QString accountNumber;
// {
// bool needFetch = false;
// for (const MaterialInfo &c : cards) {
// const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
// if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; }
// }
//
// if (needFetch) {
// // Возвращаемся на главный экран — критерий именно по наличию кнопки
// // "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий
// // (Перевести/Пополнить присутствуют и на других экранах).
// Node mainAcc;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// mainAcc = xmlScreenParser.findMainAccountButton();
// if (mainAcc.x() > 0) break;
// AdbUtils::goBack(m_deviceId);
// QThread::msleep(1500);
// }
//
// if (mainAcc.x() > 0) {
// AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y());
// QThread::msleep(2000);
//
// // Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине,
// // чтобы тап не ушёл в системный нав-бар.
// Node showReq;
// const int targetMaxY = device.screenHeight / 2;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Node candidate = xmlScreenParser.findShowRequisitesButton();
// if (candidate.x() > 0 && candidate.y() <= targetMaxY) {
// showReq = candidate;
// break;
// }
// AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
// device.screenHeight * 2 / 3,
// device.screenWidth / 2,
// device.screenHeight / 3);
// QThread::msleep(800);
// }
//
// if (showReq.x() > 0) {
// AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y());
// QThread::msleep(2000);
//
// bool reqScreen = false;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.isAccountRequisitesScreen()) {
// reqScreen = true;
// break;
// }
// QThread::msleep(1000);
// }
//
// if (reqScreen) {
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// accountNumber = xmlScreenParser.parseAccountNumberFromRequisites();
// if (!accountNumber.isEmpty()) break;
// QThread::msleep(800);
// }
// qDebug() << "[GetCardInfo] accountNumber:" << accountNumber;
// } else {
// qWarning() << "[GetCardInfo] Account requisites screen not detected";
// }
// } else {
// qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached";
// }
// } else {
// qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home";
// }
//
// // Возврат на главный — критерий по кнопке "Основной счёт ... ₽"
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.findMainAccountButton().x() > 0) break;
// AdbUtils::goBack(m_deviceId);
// QThread::msleep(1500);
// }
//
// // Записываем номер счёта во все материалы device/app
// if (!accountNumber.isEmpty()) {
// for (const MaterialInfo &c : cards) {
// MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
// if (upd.id == -1) continue;
// upd.accountNumber = accountNumber;
// upd.updateTime = QDateTime::currentDateTimeUtc();
// if (!MaterialDAO::upsertAccount(upd)) {
// qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers;
// }
// }
// }
// }
// }
// 3. Отправляем материалы (карты) на сервер.
// Инвариант: одновременно живёт максимум один материал на (deviceId, appCode) —
// тот, что локально active. Пушим только active-карту и только если другой
// материал ещё не синхронизирован (нет с непустым materialId).
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
if (!updatedApp.bankProfileId.isEmpty()) {
const QList<MaterialInfo> dbCards = MaterialDAO::getAccounts(device.id, m_appCode);
bool alreadySynced = false;
for (const MaterialInfo &m : dbCards) {
if (!m.materialId.isEmpty()) { alreadySynced = true; break; }
}
if (alreadySynced) {
qDebug() << "[GetCardInfo] another material already synced, skipping POST for app" << m_appCode;
} else {
for (const MaterialInfo &card : cards) {
const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (dbCard.cardNumber.isEmpty()) continue;
if (dbCard.status != MaterialStatus::Active) continue;
ns.setPayload(buildMaterialPayload(dbCard, updatedApp));
ns.addExtra("account_id", dbCard.id);
ns.postMaterial();
qDebug() << "[GetCardInfo] postMaterial done for active card" << dbCard.lastNumbers;
break; // не больше одного материала на сервер
}
}
} else {
qWarning() << "[GetCardInfo] bankProfileId empty, skipping materials";
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}
} // namespace Ozon