// ozon/get_card_info.js // // JS-порт Ozon::GetCardInfoScript. Логика 1:1 с C++ (см. // android/ozon/GetCardInfoScript.cpp). Самый длинный из Ozon-скриптов: // сканирует все карты с home screen, для каждой раскрывает полный номер // через "Показать" с retry-логикой и постит на сервер. // // Контракт: // params.deviceId — серийный или android_id // params.appCode — "ozon" // params.lastNumbers — если задан, обрабатывается только одна карта // Возвращает: // "" / null / undefined → success // строка → бизнес-ошибка function run(host) { var deviceId = host.params.deviceId; var appCode = host.params.appCode; var lastNumbers = host.params.lastNumbers || ""; var counter = 0; 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_CARDS", deviceId, {}); try { // 1. Читаем текущий экран host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); // 2. Закрываем рекламный bottom sheet, если открыт var adNode = host.parser.tryToFindAdBanner(); if (!adNode.isEmpty) { host.log.debug("Ad banner detected, closing..."); host.adb.makeTap(deviceId, adNode.x, adNode.y); host.timer.sleep(1000); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); } // 3. Свежий дамп перед проверкой домашнего экрана host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); // 4. Если не на домашнем — перезапускаем приложение if (!host.parser.isHomeScreen()) { host.log.debug("Not on home screen, restarting app..."); if (!host.helpers.runAppAndGoToHomeScreen(deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { var err = "Cannot reach home screen: " + device.name + " (" + deviceId + ")"; host.log.warn(err); host.adb.tryToKillApplication(deviceId, app.package); return err; } host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); } // 5. Закрываем баннеры host.helpers.tryToCloseAllBannersOnHomePage(deviceId, device.screenWidth, device.screenHeight); host.timer.sleep(500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); // 6. Парсим имя/телефон, если в БД ещё нет var hasProfileInfo = !!(app.fullName) && !!(app.phone); if (!hasProfileInfo) { var userNameNode = host.parser.findUserNameOnHomeScreen(device.screenHeight); if (!userNameNode.isEmpty) { host.log.debug("Navigating to profile to parse name/phone..."); host.adb.makeTap(deviceId, userNameNode.x, userNameNode.y); host.timer.sleep(2500); for (var a = 0; a < 5; ++a) { host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); if (host.parser.isProfileScreen()) break; host.timer.sleep(1000); } if (host.parser.isProfileScreen()) { var fullName = host.parser.parseProfileFullName(); var phone = host.parser.parseProfilePhone(); host.log.debug("Profile parsed - fullName: " + fullName + " phone: " + phone); host.dao.bankProfileUpdateProfileInfo(app.id, fullName, phone, ""); } else { host.log.warn("Profile screen not reached, skipping profile parse"); } host.adb.goBack(deviceId); host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); } else { host.log.debug("Username not found on home, skipping profile parse"); } } else { host.log.debug("Profile info already in DB, skipping profile parse"); } // 7. Проверяем что снова на домашнем экране host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); if (!host.parser.isHomeScreen()) { host.log.debug("Not on home screen before card search, restarting..."); if (!host.helpers.runAppAndGoToHomeScreen(deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { var err2 = "Cannot reach home screen before card search: " + device.name + " (" + deviceId + ")"; host.log.warn(err2); host.adb.tryToKillApplication(deviceId, app.package); return err2; } host.helpers.tryToCloseAllBannersOnHomePage(deviceId, device.screenWidth, device.screenHeight); host.timer.sleep(500); } // 8. Ждём пока карты подгрузятся var cards = []; for (var attempt = 0; attempt < 5; ++attempt) { host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); cards = host.parser.parseCardsOnHomeScreen(); if (cards.length > 0) break; host.log.debug("No cards found on home screen, attempt " + (attempt + 1) + "/5"); host.timer.sleep(1500); } if (cards.length === 0) { var noCardsErr = "No cards found on home screen: " + deviceId; host.log.warn(noCardsErr); host.adb.tryToKillApplication(deviceId, app.package); return noCardsErr; } host.log.debug("Found " + cards.length + " card(s) on home screen"); // 9. Апсёртим аккаунты for (var ci = 0; ci < cards.length; ++ci) { cards[ci].deviceId = device.id; cards[ci].appCode = appCode; cards[ci].status = "active"; if (!host.dao.materialUpsert(cards[ci])) { host.log.warn("Failed to upsert account for card " + cards[ci].lastNumbers); } else { host.log.debug("Upserted account: " + cards[ci].lastNumbers + " " + cards[ci].description + " " + cards[ci].amount); } } // 10. Баланс var totalAmount = cards[0].amount; if (totalAmount > 0.0) { host.dao.bankProfileUpdateAmount(app.id, totalAmount); } // 11. createDevice если нет apiId host.helpers.createDeviceIfNeeded(device); // 12. Регистрируем/обновляем bank profile на сервере host.helpers.createBankProfileIfNeeded(deviceId, appCode, totalAmount); // 13. Для каждой карты раскрываем полный номер через "Показать" for (var i = 0; i < cards.length; ++i) { var card = cards[i]; if (lastNumbers && card.lastNumbers !== lastNumbers) continue; host.log.debug("Getting card number for " + card.lastNumbers); var existing = host.dao.materialFindAppAccount(device.id, appCode, card.lastNumbers); if (existing && existing.id >= 0 && existing.cardNumber) { host.log.debug("Card " + card.lastNumbers + " already has number, skipping"); continue; } var cardBtn = host.parser.findCardButtonByLastNumbers(card.lastNumbers); if (cardBtn.isEmpty) { host.log.warn("Card button not found for " + card.lastNumbers); continue; } host.adb.makeTap(deviceId, cardBtn.x, cardBtn.y); // 13a. Скроллим WebView вверх, пока "Показать" не окажется в верхней трети экрана var targetTopY = Math.floor(device.screenHeight / 3); for (var s = 0; s < 6; ++s) { host.timer.sleep(s === 0 ? 2000 : 600); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var cand = host.parser.findButtonNode("Показать", false); if (cand.isEmpty) { if (host.parser.isHomeScreen()) { var rb = host.parser.findCardButtonByLastNumbers(card.lastNumbers); if (!rb.isEmpty) { host.log.debug("Still on home before scroll, re-tapping card " + card.lastNumbers); host.adb.makeTap(deviceId, rb.x, rb.y); } } continue; } if (cand.y <= targetTopY) { host.log.debug("'Показать' already at top y=" + cand.y); break; } var swipeFromY = cand.y; var swipeToY = Math.floor(device.screenHeight / 6); host.log.debug("Scrolling 'Показать' up: " + swipeFromY + " -> " + swipeToY); host.adb.makeSwipe(deviceId, Math.floor(device.screenWidth / 2), swipeFromY, Math.floor(device.screenWidth / 2), swipeToY); } // 13b. Ждём стабильной геометрии: "Показать" в одной строке с "Реквизиты карты" // И координаты не меняются в двух подряд дампах. var showBtn = null; var prevShowY = -1; for (var w = 0; w < 10; ++w) { host.timer.sleep(w === 0 ? 2000 : 500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var c = host.parser.findButtonNode("Показать", false); var req = host.parser.findTextNode("Реквизиты карты"); if (c.isEmpty || req.isEmpty) { if (host.parser.isHomeScreen()) { var rb2 = host.parser.findCardButtonByLastNumbers(card.lastNumbers); if (!rb2.isEmpty) { host.log.debug("Still on home screen, re-tapping card " + card.lastNumbers); host.adb.makeTap(deviceId, rb2.x, rb2.y); } } prevShowY = -1; host.log.debug("Requisites not ready, attempt " + (w + 1) + "/10"); continue; } if (Math.abs(c.y1 - req.y1) > 10) { prevShowY = -1; host.log.debug("'Показать' not aligned with 'Реквизиты карты', waiting reflow"); continue; } if (prevShowY >= 0 && Math.abs(c.y - prevShowY) <= 5) { showBtn = c; break; } prevShowY = c.y; host.log.debug("'Показать' seen at y=" + c.y + ", waiting for stable frame"); } if (!showBtn) { host.log.warn("'Показать' not stable for card " + card.lastNumbers); host.adb.goBack(deviceId); host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); continue; } host.log.debug("'Показать' at " + showBtn.x + " " + showBtn.y); host.adb.makeTap(deviceId, showBtn.x, showBtn.y); // 13c. Ждём появления полного номера карты var cardNumber = ""; for (var t = 0; t < 10; ++t) { host.timer.sleep(2000); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); cardNumber = host.parser.findFullCardNumber(); if (cardNumber) { host.log.debug("Card number found on attempt " + (t + 1)); break; } // Баннер "Понятно" — закрываем и повторно тапаем "Показать" var banner = host.parser.findTextNode("Понятно"); if (!banner.isEmpty) { host.log.debug("Banner 'Понятно' detected, closing..."); host.adb.makeTap(deviceId, banner.x, banner.y); host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var retry = host.parser.findButtonNode("Показать", false); if (!retry.isEmpty) { host.log.debug("Re-tapping 'Показать' after banner close"); host.adb.makeTap(deviceId, retry.x, retry.y); } continue; } // "Подождите" — загрузка идёт if (!host.parser.findButtonNode("Подождите", false).isEmpty) { host.log.debug("'Подождите' visible, waiting... attempt " + (t + 1) + "/10"); continue; } // "Скрыть" видна — номер должен быть, но парсер не нашёл if (!host.parser.findButtonNode("Скрыть", false).isEmpty) { host.log.debug("'Скрыть' visible but card number not parsed, attempt " + (t + 1) + "/10"); continue; } // Повторный тап "Показать" var retry2 = host.parser.findButtonNode("Показать", false); if (!retry2.isEmpty) { host.log.debug("Re-tapping 'Показать', attempt " + (t + 1) + "/10"); host.adb.makeTap(deviceId, retry2.x, retry2.y); } else { host.log.debug("Card number not visible, attempt " + (t + 1) + "/10"); } } if (cardNumber) { host.log.debug("Card " + card.lastNumbers + " full number: " + cardNumber); var saved = host.dao.materialFindAppAccount(device.id, appCode, card.lastNumbers); if (saved && saved.id >= 0) { host.dao.materialUpdateCardNumber(saved.id, cardNumber); } } else { var noNumErr = "Card number not found for " + card.lastNumbers; host.log.warn(noNumErr); var s2 = host.dao.materialFindAppAccount(device.id, appCode, card.lastNumbers); if (s2 && s2.id >= 0) { host.dao.materialUpdateAccountById(s2.id, "off", s2.description, s2.currency); } } // Возвращаемся на домашний для следующей карты if (i < cards.length - 1) { host.adb.goBack(deviceId); host.timer.sleep(2000); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); } } // 14. Отправляем материалы на сервер — после того как все номера в БД var updatedApp = host.dao.bankProfileGet(device.id, appCode); if (updatedApp && updatedApp.bankProfileId) { for (var k = 0; k < cards.length; ++k) { var dbCard = host.dao.materialFindAppAccount(device.id, appCode, cards[k].lastNumbers); if (!dbCard || dbCard.id < 0) continue; if (!dbCard.cardNumber) continue; if (dbCard.materialId) { host.log.debug("material already exists for card " + dbCard.lastNumbers + ", skipping"); continue; } host.helpers.postMaterial(dbCard, updatedApp.bankProfileId, updatedApp.fullName); host.log.debug("postMaterial done for card " + dbCard.lastNumbers); } } else { host.log.warn("bankProfileId empty, skipping materials"); } host.adb.tryToKillApplication(deviceId, app.package); host.dump.endScope(""); return ""; // success } catch (e) { host.dump.endScope(String(e)); throw e; } }