1
0
forked from BRT/arc

Handle missing timezone in created_at field for transaction parsing by enforcing UTC interpretation. Improve date range detection and scrolling logic in GetLastTransactionsScript to handle sticky headers. Prevent unregistered devices from displaying bank profiles in DeviceWidget.

This commit is contained in:
slava 2026-05-20 18:22:17 +05:00
parent 14e46cac84
commit 56ba89522c
3 changed files with 54 additions and 10 deletions

View File

@ -379,37 +379,53 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
// Хелпер: находит Y-диапазон [dateY, nextDateY) для нужной даты на экране.
// Кнопки транзакций между этими Y принадлежат нужному дню.
// Если заголовок нужной даты ушёл за верх экрана, но следующая дата ещё
// видна ниже (или не видна вовсе), dateY = 0 — все кнопки выше nextDateY
// принадлежат нашему дню.
// Возвращает специальные коды через dateY:
// dateY >= 0 : header виден на этом Y (нормальный случай)
// dateY == 0 : header выше viewport, есть более старая видимая дата
// dateY == -1 : header не виден / лежит в нижней sticky-пачке
// (скролл вниз, чтобы target выехал в видимую часть)
// dateY == -2 : header в верхней sticky-пачке (несколько дат свёрнуто
// над viewport) — скролл вверх, чтобы пачка распалась
auto findDateYRange = [&]() -> QPair<int, int> {
const QMap<int, int> yCount = computeDateYCount();
auto isVisible = [&](int y) { return y > 0 && yCount.value(y, 0) == 1; };
auto isAnchored = [&](int y) { return y > 0 && yCount.value(y, 0) == 1; };
auto isPiled = [&](int y) { return y > 0 && yCount.value(y, 0) > 1; };
const int viewportMid = device.screenHeight / 2;
int dateY = -1;
int nextDateY = device.screenHeight;
int targetPileY = -1;
bool foundOlderDate = false;
for (const Node &node : xmlScreenParser.getNodes()) {
if (node.className != "android.widget.TextView") continue;
const QDate d = parseDateHeader(node.text.trimmed());
if (!d.isValid()) continue;
if (d == searchDate) {
if (isVisible(node.y())) dateY = node.y();
if (isAnchored(node.y())) dateY = node.y();
else if (isPiled(node.y())) targetPileY = node.y();
} else if (d < searchDate) {
foundOlderDate = true;
if (isVisible(node.y()) && (dateY < 0 || node.y() > dateY)
if (isAnchored(node.y())
&& (dateY < 0 || node.y() > dateY)
&& node.y() < nextDateY) {
nextDateY = node.y();
}
}
}
// Target в sticky-пачке: пачка над viewport (малый Y) → секция уже
// прокручена выше, надо вернуться (dateY = -2). Пачка снизу viewport
// (большой Y) → target ещё не доехал до экрана, скролл вниз (dateY = -1).
if (dateY < 0 && targetPileY >= 0) {
dateY = (targetPileY < viewportMid) ? -2 : -1;
}
// Target-header не виден, но видна более старая дата → target ушёл вверх:
// всё выше nextDateY принадлежит нашему дню.
if (dateY < 0 && nextDateY < device.screenHeight) {
if (dateY < 0 && nextDateY < device.screenHeight && targetPileY < 0) {
dateY = 0;
}
qDebug() << "[FetchTransactions] findDateYRange: dateY=" << dateY
<< "nextDateY=" << nextDateY << "foundOlderDate=" << foundOlderDate;
<< "nextDateY=" << nextDateY << "foundOlderDate=" << foundOlderDate
<< "targetPileY=" << targetPileY;
return {dateY, nextDateY};
};
@ -440,6 +456,19 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
// Находим Y-диапазон заголовка нужной даты
const auto [dateHeaderY, nextDateHeaderY] = findDateYRange();
if (dateHeaderY == -2) {
// Target застрял в sticky-пачке (Ozon стекает заголовки выше viewport
// в одну Y-позицию). Ниже пачки есть anchored более старая дата →
// секцию target уже проскроллили мимо. Скроллим обратно вверх,
// чтобы target распался из пачки и занял реальный Y.
qDebug() << "[FetchTransactions] Target piled in sticky band — scrolling up to detach";
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
device.screenHeight / 3,
device.screenWidth / 2,
device.screenHeight * 2 / 3, 600);
QThread::msleep(1500);
continue;
}
if (dateHeaderY < 0) {
// Target-header не виден и ни одной более старой даты не видно →
// target скорее всего ниже вьюпорта. Скроллим вниз (swipe вверх).

View File

@ -514,7 +514,18 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
const QJsonObject body = taskJson["body"].toObject();
const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака)
const QDateTime searchTime = QDateTime::fromString(body["created_at"].toString(), Qt::ISODate);
const QString createdAtStr = body["created_at"].toString();
QDateTime searchTime = QDateTime::fromString(createdAtStr, Qt::ISODate);
// Backend контракт — UTC ISO с суффиксом 'Z' (как мы сами пишем
// на строках 317 и 373). Если 'Z'/смещение отсутствует — Qt парсит
// как LocalTime, что даёт off-by-one для вечерних транзакций
// (см. task_dump (26): Ozon группировал +100 на 14.05, backend
// прислал 13.05 без TZ). Жёстко переинтерпретируем как UTC.
if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) {
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера"
<< createdAtStr << "— интерпретирую как UTC";
searchTime.setTimeSpec(Qt::UTC);
}
if (appCode == "ozon") {
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId);

View File

@ -626,7 +626,11 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
const bool isOffline = device.status != "device";
QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
// Пока устройство не зарегистрировано на сервере (нет apiId) — банки не показываем
QList<BankProfileInfo> apps;
if (!device.apiId.isEmpty()) {
apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
}
// Фильтр по стране профиля
const QString profileCountry = SettingsDAO::get("country");