666 lines
29 KiB
JavaScript
666 lines
29 KiB
JavaScript
// ozon/get_last_transactions.js
|
||
//
|
||
// JS-порт Ozon::GetLastTransactionsScript. Логика 1:1 с C++ (см.
|
||
// android/ozon/GetLastTransactionsScript.cpp). Самый длинный из Ozon-скриптов:
|
||
// навигация до "Операции" → либо поиск конкретной транзакции по сумме+времени,
|
||
// либо сканирование последних N транзакций.
|
||
//
|
||
// Контракт:
|
||
// params.deviceId — серийный или android_id
|
||
// params.appCode — "ozon"
|
||
// params.maxCount — 0 = фильтр по 24ч, >0 = N последних
|
||
// params.searchAmount — сумма в рублях для поиска (0 = scan-mode)
|
||
// params.searchTime — ISO-строка времени (UTC, "" если scan-mode)
|
||
// params.materialId — UUID задачи (для дампа)
|
||
//
|
||
// Особенности порта:
|
||
// - таймзону устройства игнорируем, используем MSK (+3ч) — Ozon только в РФ.
|
||
// В C++ берётся getDeviceTimezone(), но в 99% случаев это всё равно МСК.
|
||
// - host.script.emitTransactionParsed(jsonStr) шлёт результат в EventHandler.
|
||
// - OCR для bank_transaction_id (когда XML не отдал) — через host.ocr.
|
||
|
||
var MSK_OFFSET_MIN = 180; // +03:00
|
||
|
||
var MONTH_MAP = {
|
||
"января": 1, "февраля": 2, "марта": 3, "апреля": 4,
|
||
"мая": 5, "июня": 6, "июля": 7, "августа": 8,
|
||
"сентября": 9, "октября": 10, "ноября": 11, "декабря": 12
|
||
};
|
||
|
||
function run(host) {
|
||
var deviceId = host.params.deviceId;
|
||
var appCode = host.params.appCode;
|
||
var maxCount = host.params.maxCount | 0;
|
||
var searchAmount = +host.params.searchAmount || 0;
|
||
var searchTimeIso = host.params.searchTime || "";
|
||
var materialId = host.params.materialId || "";
|
||
|
||
var device = host.dao.deviceGetById(deviceId);
|
||
if (!device || !device.id) device = host.dao.deviceFindByAdbSerial(deviceId);
|
||
if (device && device.adbSerial) deviceId = device.adbSerial;
|
||
var app = host.dao.bankProfileGet(device.id, appCode);
|
||
if (!device || !device.id || !app || app.id < 0) {
|
||
var msg = "Device or app not found: " + deviceId + " " + appCode;
|
||
host.log.error(msg);
|
||
return msg;
|
||
}
|
||
|
||
host.dump.beginScope("ozon", "FETCH_TRANSACTIONS", deviceId, {
|
||
materialId: materialId, amount: searchAmount,
|
||
});
|
||
|
||
var counter = {n: 0};
|
||
try {
|
||
if (!navigateToTransactionList(host, device, app, deviceId, counter)) {
|
||
var navErr = "Transaction list screen not loaded: " + deviceId;
|
||
host.dump.endScope(navErr);
|
||
return navErr;
|
||
}
|
||
|
||
var accountId = -1;
|
||
var accounts = host.dao.materialGetAccounts(device.id, appCode);
|
||
if (accounts.length > 0) accountId = accounts[0].id;
|
||
else host.log.warn("No account found for " + device.id + " " + appCode);
|
||
|
||
var result;
|
||
if (Math.abs(searchAmount) > 0.01) {
|
||
result = searchTransaction(host, device, deviceId, appCode, accountId,
|
||
searchAmount, searchTimeIso, counter);
|
||
} else {
|
||
result = scanRecentTransactions(host, device, deviceId, appCode, accountId,
|
||
maxCount, counter);
|
||
}
|
||
host.dump.endScope(result || "");
|
||
return result;
|
||
} catch (e) {
|
||
host.dump.endScope(String(e));
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
// ─── navigateToTransactionList ─────────────────────────────────────────────
|
||
|
||
function navigateToTransactionList(host, device, app, deviceId, counter) {
|
||
function tryRefreshIfError() {
|
||
var refreshBtn = host.parser.findNodeByContentDesc
|
||
? host.parser.findNodeByContentDesc("Обновить")
|
||
: findNodeByDescFallback(host, "Обновить");
|
||
if (refreshBtn && refreshBtn.x > 0 && refreshBtn.y > 0) {
|
||
host.log.debug("Page load error, tapping 'Обновить'");
|
||
host.adb.makeTap(deviceId, refreshBtn.x, refreshBtn.y);
|
||
host.timer.sleep(5000);
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
}
|
||
}
|
||
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
tryRefreshIfError();
|
||
|
||
var adNode = host.parser.tryToFindAdBanner();
|
||
if (!adNode.isEmpty) {
|
||
host.adb.makeTap(deviceId, adNode.x, adNode.y);
|
||
host.timer.sleep(1000);
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
var stillAd = host.parser.tryToFindAdBanner();
|
||
if (!stillAd.isEmpty) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
}
|
||
}
|
||
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
|
||
// Уже на экране операций — скроллим к началу
|
||
if (host.parser.isTransactionListScreen()) {
|
||
host.log.debug("Already on tx list, scrolling to top");
|
||
for (var i = 0; i < 5; ++i) {
|
||
host.adb.makeSwipeWithDuration(deviceId,
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight / 3),
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight * 2 / 3),
|
||
200);
|
||
host.timer.sleep(500);
|
||
}
|
||
host.timer.sleep(1000);
|
||
return true;
|
||
}
|
||
|
||
// Не на домашнем → перезапуск
|
||
if (!host.parser.isHomeScreen()) {
|
||
if (!host.helpers.runAppAndGoToHomeScreen(deviceId, app.package, app.pinCode,
|
||
device.screenWidth, device.screenHeight)) {
|
||
host.log.warn("Cannot reach home screen");
|
||
host.adb.tryToKillApplication(deviceId, app.package);
|
||
return false;
|
||
}
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
tryRefreshIfError();
|
||
}
|
||
|
||
host.helpers.tryToCloseAllBannersOnHomePage(deviceId, device.screenWidth, device.screenHeight);
|
||
host.timer.sleep(500);
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
|
||
// Скроллим до "Все" и тапаем
|
||
var navBarTop = device.screenHeight - Math.floor(device.screenHeight / 10);
|
||
var allBtn = {isEmpty: true};
|
||
for (var j = 0; j < 10; ++j) {
|
||
if (host.timer.isInterrupted()) return false;
|
||
var x = Math.floor(device.screenWidth / 2);
|
||
host.adb.makeSwipeWithDuration(deviceId, x, device.screenHeight - 200, x, 200, 100);
|
||
host.timer.sleep(1500);
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
|
||
allBtn = host.parser.findButtonNode("Все", false);
|
||
if (!allBtn.isEmpty && allBtn.y > 0 && allBtn.y < navBarTop) break;
|
||
allBtn = {isEmpty: true};
|
||
}
|
||
if (allBtn.isEmpty) return false;
|
||
|
||
host.adb.makeTap(deviceId, allBtn.x, allBtn.y);
|
||
host.timer.sleep(2000);
|
||
|
||
// Ждём экран "Операции"
|
||
for (var a = 0; a < 10; ++a) {
|
||
if (host.timer.isInterrupted()) return false;
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
if (host.parser.isTransactionListScreen()) return true;
|
||
host.timer.sleep(1500);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Fallback: ищем по content-desc через findAllNodes если bridge не имеет findNodeByContentDesc.
|
||
function findNodeByDescFallback(host, desc) {
|
||
var all = host.parser.findAllNodes();
|
||
for (var i = 0; i < all.length; ++i) {
|
||
if (all[i].contentDesc === desc) return all[i];
|
||
}
|
||
return {isEmpty: true, x: -1, y: -1};
|
||
}
|
||
|
||
// ─── Дата-хелперы ──────────────────────────────────────────────────────────
|
||
|
||
// Парсит заголовок "Сегодня"/"Вчера"/"DD месяца, день_недели" в Date (00:00 MSK).
|
||
function parseDateHeader(text, searchYear) {
|
||
var t = (text || "").trim();
|
||
var nowMsk = nowMskDate();
|
||
if (t.toLowerCase() === "сегодня") return atMidnightMsk(nowMsk);
|
||
if (t.toLowerCase() === "вчера") return atMidnightMsk(addDays(nowMsk, -1));
|
||
var m = t.match(/(\d{1,2})\s+(\S+),\s+\S+/);
|
||
if (!m) return null;
|
||
var day = parseInt(m[1], 10);
|
||
var monName = m[2].toLowerCase();
|
||
var mon = MONTH_MAP[monName] || 0;
|
||
if (mon === 0) return null;
|
||
return new Date(Date.UTC(searchYear, mon - 1, day) - MSK_OFFSET_MIN * 60 * 1000);
|
||
}
|
||
|
||
function nowMskDate() {
|
||
return new Date(Date.now() + MSK_OFFSET_MIN * 60 * 1000); // shift to MSK
|
||
}
|
||
function atMidnightMsk(d) {
|
||
// d уже в MSK-shifted виде
|
||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())
|
||
- MSK_OFFSET_MIN * 60 * 1000);
|
||
}
|
||
function addDays(d, n) {
|
||
return new Date(d.getTime() + n * 86400000);
|
||
}
|
||
function sameDate(a, b) {
|
||
if (!a || !b) return false;
|
||
return a.getTime() === b.getTime();
|
||
}
|
||
function isBefore(a, b) {
|
||
return a && b && a.getTime() < b.getTime();
|
||
}
|
||
function formatDateMsk(d) {
|
||
var msk = new Date(d.getTime() + MSK_OFFSET_MIN * 60 * 1000);
|
||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||
return pad(msk.getUTCDate()) + "." + pad(msk.getUTCMonth() + 1) + "." + msk.getUTCFullYear();
|
||
}
|
||
|
||
// ─── searchTransaction ─────────────────────────────────────────────────────
|
||
|
||
function searchTransaction(host, device, deviceId, appCode, accountId,
|
||
searchAmount, searchTimeIso, counter) {
|
||
var maxScrolls = 30;
|
||
var searchTime = searchTimeIso ? new Date(searchTimeIso) : null;
|
||
if (!searchTime || isNaN(searchTime.getTime())) return "Invalid searchTime";
|
||
var searchDate = atMidnightMsk(new Date(searchTime.getTime() + MSK_OFFSET_MIN * 60 * 1000));
|
||
var searchYear = new Date(searchTime.getTime() + MSK_OFFSET_MIN * 60 * 1000).getUTCFullYear();
|
||
|
||
function detectNavBarTop() {
|
||
var nodes = host.parser.findAllNodes();
|
||
for (var i = 0; i < nodes.length; ++i) {
|
||
var n = nodes[i];
|
||
if (n.resourceId === "ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
|
||
&& n.height > 0) return n.y1;
|
||
}
|
||
return device.screenHeight - Math.floor(device.screenHeight / 10);
|
||
}
|
||
var navBarTop = detectNavBarTop();
|
||
|
||
host.log.debug("Searching: amount=" + searchAmount + " date=" + formatDateMsk(searchDate));
|
||
|
||
// Считаем сколько дат на каждой y-координате (для детекции схлопнутых)
|
||
function computeDateYCount() {
|
||
var cnt = {};
|
||
var nodes = host.parser.findAllNodes();
|
||
for (var i = 0; i < nodes.length; ++i) {
|
||
var n = nodes[i];
|
||
if (n.className !== "android.widget.TextView") continue;
|
||
if (n.y <= 0) continue;
|
||
if (parseDateHeader(n.text, searchYear)) {
|
||
cnt[n.y] = (cnt[n.y] || 0) + 1;
|
||
}
|
||
}
|
||
return cnt;
|
||
}
|
||
function hasDateOnScreen() {
|
||
var yc = computeDateYCount();
|
||
var nodes = host.parser.findAllNodes();
|
||
for (var i = 0; i < nodes.length; ++i) {
|
||
var n = nodes[i];
|
||
if (n.className !== "android.widget.TextView") continue;
|
||
if (n.y <= 0) continue;
|
||
if ((yc[n.y] || 0) !== 1) continue;
|
||
var d = parseDateHeader(n.text, searchYear);
|
||
if (d && sameDate(d, searchDate)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasOlderDateOnScreen() {
|
||
var yc = computeDateYCount();
|
||
var nodes = host.parser.findAllNodes();
|
||
for (var i = 0; i < nodes.length; ++i) {
|
||
var n = nodes[i];
|
||
if (n.className !== "android.widget.TextView") continue;
|
||
if (n.y <= 0) continue;
|
||
if ((yc[n.y] || 0) !== 1) continue;
|
||
var d = parseDateHeader(n.text, searchYear);
|
||
if (d && isBefore(d, searchDate)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Ждём WebView accessibility tree
|
||
for (var w = 0; w < 5; ++w) {
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
if (host.parser.findTransactionButtons(1).length > 0) break;
|
||
if (hasDateOnScreen()) break;
|
||
host.timer.sleep(2000);
|
||
}
|
||
|
||
// Скроллим до нужной даты
|
||
var dateFound = false;
|
||
for (var s = 0; s < maxScrolls; ++s) {
|
||
if (host.timer.isInterrupted()) return "Interrupted";
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
if (hasDateOnScreen()) { dateFound = true; break; }
|
||
if (hasOlderDateOnScreen()) { host.log.debug("Passed target date"); break; }
|
||
host.adb.makeSwipeWithDuration(deviceId,
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight * 2 / 3),
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight / 3),
|
||
600);
|
||
host.timer.sleep(1500);
|
||
}
|
||
if (!dateFound) {
|
||
return "Date not found in transaction list: " + formatDateMsk(searchDate);
|
||
}
|
||
|
||
function findDateYRange() {
|
||
var yc = computeDateYCount();
|
||
function isVisible(y) { return y > 0 && (yc[y] || 0) === 1; }
|
||
var dateY = -1;
|
||
var nextDateY = device.screenHeight;
|
||
var foundOlder = false;
|
||
var nodes = host.parser.findAllNodes();
|
||
for (var i = 0; i < nodes.length; ++i) {
|
||
var n = nodes[i];
|
||
if (n.className !== "android.widget.TextView") continue;
|
||
var d = parseDateHeader(n.text, searchYear);
|
||
if (!d) continue;
|
||
if (sameDate(d, searchDate)) {
|
||
if (isVisible(n.y)) dateY = n.y;
|
||
} else if (isBefore(d, searchDate)) {
|
||
foundOlder = true;
|
||
if (isVisible(n.y) && (dateY < 0 || n.y > dateY) && n.y < nextDateY) {
|
||
nextDateY = n.y;
|
||
}
|
||
}
|
||
}
|
||
if (dateY < 0 && nextDateY < device.screenHeight) dateY = 0;
|
||
return [dateY, nextDateY];
|
||
}
|
||
|
||
var checked = {};
|
||
var noProgress = 0;
|
||
|
||
for (var attempt = 0; attempt < 15; ++attempt) {
|
||
if (host.timer.isInterrupted()) return "Interrupted";
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
|
||
var range = findDateYRange();
|
||
var dateHeaderY = range[0], nextDateHeaderY = range[1];
|
||
if (dateHeaderY < 0) {
|
||
host.adb.makeSwipeWithDuration(deviceId,
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight * 2 / 3),
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight / 3), 500);
|
||
host.timer.sleep(1500);
|
||
continue;
|
||
}
|
||
if (dateHeaderY === 0 && nextDateHeaderY < device.screenHeight) {
|
||
host.adb.makeSwipeWithDuration(deviceId,
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight / 3),
|
||
Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight * 2 / 3), 600);
|
||
host.timer.sleep(1500);
|
||
continue;
|
||
}
|
||
|
||
var txButtons = host.parser.findTransactionButtons(20);
|
||
|
||
var tapped = false, hasNewButton = false, buttonsInRange = 0;
|
||
for (var bi = 0; bi < txButtons.length; ++bi) {
|
||
if (host.timer.isInterrupted()) return "Interrupted";
|
||
var btn = txButtons[bi];
|
||
if (btn.y1 <= dateHeaderY || btn.y1 >= nextDateHeaderY) continue;
|
||
if (btn.y1 <= 0 || btn.y1 >= navBarTop) continue;
|
||
++buttonsInRange;
|
||
|
||
var parsed = host.parser.parseTransactionButtonText(btn.text);
|
||
if (Math.abs(parsed.amount - searchAmount) > 0.01) continue;
|
||
|
||
var safeMaxY = navBarTop - 20;
|
||
var tapY = btn.y;
|
||
if (tapY > safeMaxY) tapY = Math.max(btn.y1 + 30, safeMaxY);
|
||
|
||
host.adb.makeTap(deviceId, btn.x, tapY);
|
||
host.timer.sleep(2000);
|
||
|
||
var detail = {bankTime: ""};
|
||
for (var da = 0; da < 10; ++da) {
|
||
if (host.timer.isInterrupted()) return "Interrupted";
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
detail = host.parser.parseTransactionDetail();
|
||
if (detail.bankTime) break;
|
||
if (host.parser.isTransactionListScreen()) {
|
||
host.adb.makeTap(deviceId, btn.x, btn.y);
|
||
}
|
||
host.timer.sleep(1500);
|
||
}
|
||
|
||
if (!detail.bankTime) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
continue;
|
||
}
|
||
|
||
var dtStr = detail.bankTime.replace(/[ ]/g, ' ');
|
||
var txTime = parseBankTime(dtStr);
|
||
|
||
if (checked[dtStr]) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(500);
|
||
continue;
|
||
}
|
||
checked[dtStr] = true;
|
||
tapped = true;
|
||
hasNewButton = true;
|
||
|
||
// Дата принадлежит нужной?
|
||
if (txTime) {
|
||
var txMsk = new Date(txTime.getTime() + MSK_OFFSET_MIN * 60 * 1000);
|
||
var txDate = new Date(Date.UTC(txMsk.getUTCFullYear(), txMsk.getUTCMonth(),
|
||
txMsk.getUTCDate()) - MSK_OFFSET_MIN * 60 * 1000);
|
||
if (!sameDate(txDate, searchDate)) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
continue;
|
||
}
|
||
// ±10 минут
|
||
if (searchTime) {
|
||
var diffSec = Math.abs((txTime.getTime() - searchTime.getTime()) / 1000);
|
||
if (diffSec > 600) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
host.log.debug("FOUND transaction: amount=" + parsed.amount + " time=" + dtStr);
|
||
|
||
// Скриншот деталей для receipt
|
||
host.adb.captureScreenshotBase64(deviceId);
|
||
|
||
var externalId = detail.bankTrExternalId || "";
|
||
|
||
if (accountId !== -1) {
|
||
var fallbackId = externalId || ("ozon_" + dtStr.replace(/,/g, '').trim().replace(/ /g, '_'));
|
||
var tx = {
|
||
accountId: accountId,
|
||
amount: parsed.amount,
|
||
bankName: appCode,
|
||
bankTime: dtStr,
|
||
bankTrExternalId: fallbackId,
|
||
status: "complete",
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
if (txTime) tx.completeTime = txTime.toISOString();
|
||
if (detail.name) tx.name = detail.name;
|
||
if (detail.description) tx.description = detail.description;
|
||
if (detail.phone) {
|
||
tx.phone = (detail.phone || "").replace(/[^0-9]/g, '');
|
||
tx.type = "phone";
|
||
}
|
||
host.dao.transactionInsert(tx);
|
||
}
|
||
|
||
// Шлём в EventHandler через signalSink
|
||
var finalId = externalId || ("ozon_" + dtStr.replace(/,/g, '').trim().replace(/ /g, '_'));
|
||
var emitObj = {
|
||
bank_transaction_id: finalId,
|
||
amount: Math.round(parsed.amount * 100),
|
||
currency: 643,
|
||
transaction_type: "bank",
|
||
bank_participator_id: detail.phone || detail.name || "",
|
||
status: "completed",
|
||
extra: {fee: 0},
|
||
created_at: txTime ? txTime.toISOString() : null,
|
||
};
|
||
host.script.emitTransactionParsed(JSON.stringify(emitObj));
|
||
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
return ""; // success
|
||
}
|
||
|
||
if (!hasNewButton && tapped) {
|
||
if (++noProgress >= 5) break;
|
||
} else if (hasNewButton) noProgress = 0;
|
||
|
||
if (!tapped || !hasNewButton) {
|
||
var cx = Math.floor(device.screenWidth / 2);
|
||
if (dateHeaderY === 0 && nextDateHeaderY < device.screenHeight) {
|
||
host.adb.makeSwipeWithDuration(deviceId, cx,
|
||
Math.floor(device.screenHeight * 2 / 5), cx,
|
||
Math.floor(device.screenHeight * 3 / 4), 600);
|
||
host.timer.sleep(1500);
|
||
} else {
|
||
var safeBottom = navBarTop - 30;
|
||
var amplitude;
|
||
if (dateHeaderY > 0 && dateHeaderY < safeBottom - 100) {
|
||
var safeTop = Math.max(dateHeaderY + 80, Math.floor(device.screenHeight / 4));
|
||
amplitude = Math.min(safeBottom - safeTop, 700);
|
||
} else {
|
||
amplitude = Math.min(safeBottom - Math.floor(device.screenHeight / 4), 700);
|
||
}
|
||
amplitude = Math.max(amplitude, 300);
|
||
for (var sw = 0; sw < 3; ++sw) {
|
||
host.adb.makeSwipeWithDuration(deviceId, cx, safeBottom,
|
||
cx, safeBottom - amplitude, 250);
|
||
host.timer.sleep(350);
|
||
}
|
||
host.timer.sleep(800);
|
||
}
|
||
}
|
||
}
|
||
|
||
return "Transaction not found: amount=" + searchAmount.toFixed(2)
|
||
+ " date=" + formatDateMsk(searchDate);
|
||
}
|
||
|
||
// Парсит "dd.MM.yyyy, HH:mm" или "dd.MM.yyyy,HH:mm" → UTC Date (MSK-localized).
|
||
function parseBankTime(s) {
|
||
var m = (s || "").match(/(\d{2})\.(\d{2})\.(\d{4}),?\s*(\d{2}):(\d{2})/);
|
||
if (!m) return null;
|
||
var d = parseInt(m[1], 10), mo = parseInt(m[2], 10), y = parseInt(m[3], 10);
|
||
var hh = parseInt(m[4], 10), mm = parseInt(m[5], 10);
|
||
return new Date(Date.UTC(y, mo - 1, d, hh, mm) - MSK_OFFSET_MIN * 60 * 1000);
|
||
}
|
||
|
||
// ─── scanRecentTransactions ────────────────────────────────────────────────
|
||
|
||
function scanRecentTransactions(host, device, deviceId, appCode, accountId, maxCount, counter) {
|
||
var navBarTop = device.screenHeight - Math.floor(device.screenHeight / 10);
|
||
|
||
// Ждём появления транзакций
|
||
var txButtons = [];
|
||
for (var i = 0; i < 5; ++i) {
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
txButtons = host.parser.findTransactionButtons(8);
|
||
if (txButtons.length > 0) break;
|
||
host.timer.sleep(1500);
|
||
}
|
||
|
||
var transactions = [];
|
||
for (var i2 = 0; i2 < txButtons.length; ++i2) {
|
||
var btn = txButtons[i2];
|
||
|
||
if (btn.y <= 0 || btn.y >= navBarTop) {
|
||
for (var s = 0; s < 5; ++s) {
|
||
var x = Math.floor(device.screenWidth / 2);
|
||
var offset = Math.floor(device.screenHeight / 4);
|
||
host.adb.makeSwipe(deviceId, x, device.screenHeight - offset, x, offset);
|
||
host.timer.sleep(1000);
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
txButtons = host.parser.findTransactionButtons(8);
|
||
if (i2 < txButtons.length && txButtons[i2].y > 0 && txButtons[i2].y < navBarTop) break;
|
||
}
|
||
if (i2 >= txButtons.length) break;
|
||
}
|
||
|
||
var visibleBtn = txButtons[i2];
|
||
var tx = host.parser.parseTransactionButtonText(visibleBtn.text);
|
||
|
||
host.adb.makeTap(deviceId, visibleBtn.x, visibleBtn.y);
|
||
host.timer.sleep(2000);
|
||
|
||
var detail = {bankTime: ""};
|
||
for (var da = 0; da < 10; ++da) {
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
detail = host.parser.parseTransactionDetail();
|
||
if (detail.bankTime) break;
|
||
if (host.parser.isTransactionListScreen()) {
|
||
host.adb.makeTap(deviceId, visibleBtn.x, visibleBtn.y);
|
||
}
|
||
host.timer.sleep(1500);
|
||
}
|
||
|
||
tx.bankName = appCode;
|
||
tx.bankTime = detail.bankTime;
|
||
tx.bankTrExternalId = detail.bankTrExternalId || "";
|
||
|
||
// Если ID не найден — пробуем через "Документы" + OCR
|
||
if (!tx.bankTrExternalId) {
|
||
var docBtn = host.parser.findButtonNode("Документы", false);
|
||
if (!docBtn.isEmpty) {
|
||
host.adb.makeTap(deviceId, docBtn.x, docBtn.y);
|
||
host.timer.sleep(2000);
|
||
var docLoaded = false;
|
||
for (var di = 0; di < 15; ++di) {
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
if (!findNodeByDescFallback(host, "Поделиться").isEmpty) {
|
||
docLoaded = true; break;
|
||
}
|
||
host.timer.sleep(1500);
|
||
}
|
||
if (docLoaded) {
|
||
host.timer.sleep(500);
|
||
var ocrText = host.ocr.recognizeFromScreen(deviceId);
|
||
if (ocrText) {
|
||
tx.bankTrExternalId = host.ocr.extractTransactionId(ocrText) || "";
|
||
}
|
||
}
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
}
|
||
}
|
||
|
||
tx.status = "complete";
|
||
tx.timestamp = new Date().toISOString();
|
||
if (detail.name) tx.name = detail.name;
|
||
if (detail.phone) {
|
||
tx.phone = (detail.phone || "").replace(/[^0-9]/g, '');
|
||
tx.type = "phone";
|
||
}
|
||
|
||
if (tx.bankTime) {
|
||
var dtStr = tx.bankTime.replace(/[ ]/g, ' ');
|
||
tx.bankTime = dtStr;
|
||
var parsed = parseBankTime(dtStr);
|
||
if (parsed) tx.completeTime = parsed.toISOString();
|
||
}
|
||
|
||
// Фильтр по времени (24ч)
|
||
if (maxCount <= 0 && tx.completeTime) {
|
||
var ageSec = (Date.now() - new Date(tx.completeTime).getTime()) / 1000;
|
||
if (ageSec > 86400) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (accountId !== -1) {
|
||
tx.accountId = accountId;
|
||
host.dao.transactionInsert(tx);
|
||
// Receipt + OCR-id через "Документы" — упрощённо, без двойного перехода.
|
||
// Если уже извлекли ID выше — не повторяем.
|
||
}
|
||
|
||
transactions.push(tx);
|
||
if (maxCount > 0 && transactions.length >= maxCount) {
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1000);
|
||
break;
|
||
}
|
||
|
||
host.adb.goBack(deviceId);
|
||
host.timer.sleep(1500);
|
||
|
||
for (var wa = 0; wa < 5; ++wa) {
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
host.helpers.closeGooglePlayBannerIfVisible(deviceId, device.screenWidth, device.screenHeight);
|
||
if (host.parser.isTransactionListScreen()) break;
|
||
host.timer.sleep(1000);
|
||
}
|
||
|
||
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter.n));
|
||
txButtons = host.parser.findTransactionButtons(8);
|
||
}
|
||
return ""; // success
|
||
}
|