1
0
forked from BRT/arc
arc/scripts/ozon/pay_by_card.js

360 lines
16 KiB
JavaScript
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.

// ozon/pay_by_card.js
//
// JS-порт Ozon::PayByCardScript. Логика 1:1 с C++ (см.
// android/ozon/PayByCardScript.cpp).
//
// Контракт:
// params.account — {id, materialId, appCode, deviceId, lastNumbers, cardNumber, currency}
// params.cardNumber — номер карты получателя
// params.amount — сумма перевода
// Возвращает:
// "" / null / undefined → success
// строка → бизнес-ошибка
//
// OCR для bank_transaction_id здесь не используется (см. комментарий в
// PayByCardScript.cpp:453). Externalid формируется как fallback "ozon_<phone>_<ts>".
function run(host) {
var account = host.params.account;
var cardNumber = host.params.cardNumber;
var amount = host.params.amount;
var device = host.dao.deviceGetById(account.deviceId);
var app = host.dao.bankProfileGet(account.deviceId, account.appCode);
if (!device || !device.id || !app || app.id < 0) {
var msg = "Device or app not found: " + account.deviceId;
host.log.error(msg);
return msg;
}
var adbId = device.adbSerial;
var counter = 0;
host.dump.beginScope("ozon", "CREATE_TRANSACTION_CARD", adbId, {
materialId: account.materialId,
amount: amount,
card: cardNumber,
});
try {
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
// Закрываем рекламный bottom sheet
var adNode = host.parser.tryToFindAdBanner();
if (!adNode.isEmpty) {
host.log.debug("Ad banner detected, closing...");
host.adb.makeTap(adbId, adNode.x, adNode.y);
host.timer.sleep(1000);
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
}
// Если не на домашнем — перезапускаем
if (!host.parser.isHomeScreen()) {
if (!host.helpers.runAppAndGoToHomeScreen(adbId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
var err = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
host.adb.tryToKillApplication(adbId, app.package);
return err;
}
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
}
// Закрываем баннеры
host.helpers.tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight);
host.timer.sleep(500);
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
// Google Play "Доступно обновление"
var gp = host.parser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight);
if (!gp.isEmpty) {
host.adb.makeTap(adbId, gp.x, gp.y);
host.timer.sleep(1500);
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
}
// Баннер "Заказать карту"
var orderCard = host.parser.findTextNode("Заказать карту");
if (!orderCard.isEmpty) {
host.adb.makeTap(adbId, Math.floor(device.screenWidth / 2),
Math.floor(device.screenHeight / 20));
host.timer.sleep(1000);
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter));
}
var result = makePayment(host, adbId, app.package, app.pinCode, app.phone,
device.screenWidth, device.screenHeight,
account, cardNumber, amount, counter);
host.dump.endScope(result || "");
return result;
} catch (e) {
host.dump.endScope(String(e));
throw e;
}
}
// Возвращает m_error (пустая строка = success).
function makePayment(host, deviceId, packageName, pinCode, bankProfilePhone,
width, height, account, cardNumber, amount, counter) {
function checkApp() {
if (!host.helpers.isAppRunning(deviceId, packageName)) {
return "Приложение было закрыто";
}
return null;
}
// 1. "Перевести" на домашнем экране
var transferBtn = host.parser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty) return "Кнопка 'Перевести' не найдена на домашнем экране";
host.adb.makeTap(deviceId, transferBtn.x, transferBtn.y);
host.timer.sleep(2000);
// 2. Ждём экран "Платежи и переводы"
var cardBtn = {isEmpty: true};
var paymentsPageLoaded = false;
var titleSeen = false;
{
var freezePrev = "", freezeStreak = 0;
for (var i = 0; i < 10; ++i) {
var e = checkApp(); if (e) return e;
var xml = host.adb.getScreenDumpAsXml(deviceId, ++counter);
host.parser.parseAndSaveXml(xml);
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
if (host.helpers.isUsefulDump(xml)) {
if (freezePrev && xml === freezePrev) {
if (++freezeStreak >= 3) { host.log.warn("screen frozen waiting payments"); break; }
} else freezeStreak = 0;
freezePrev = xml;
}
var title = host.parser.findTextNode("Платежи и переводы");
if (!title.isEmpty) titleSeen = true;
cardBtn = host.parser.findButtonNode("По номеру карты", false);
if (!title.isEmpty && !cardBtn.isEmpty) { paymentsPageLoaded = true; break; }
host.timer.sleep(1500);
}
}
if (!paymentsPageLoaded) {
return titleSeen ? "Кнопка 'По номеру карты' не найдена на экране платежей"
: "Экран 'Платежи и переводы' не загрузился";
}
// 3. Тапаем "По номеру карты"
host.adb.makeTap(deviceId, cardBtn.x, cardBtn.y);
host.timer.sleep(2000);
// 4. Ждём "Перевод по номеру карты"
var cardScreenLoaded = false;
{
var fp = "", fs = 0;
for (var j = 0; j < 10; ++j) {
var e2 = checkApp(); if (e2) return e2;
var xml2 = host.adb.getScreenDumpAsXml(deviceId, ++counter);
host.parser.parseAndSaveXml(xml2);
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
if (host.helpers.isUsefulDump(xml2)) {
if (fp && xml2 === fp) { if (++fs >= 3) { host.log.warn("screen frozen waiting card screen"); break; } }
else fs = 0;
fp = xml2;
}
if (!host.parser.findTextNode("Перевод по номеру карты").isEmpty) { cardScreenLoaded = true; break; }
host.timer.sleep(1500);
}
}
if (!cardScreenLoaded) return "Экран 'Перевод по номеру карты' не загрузился";
// 5. Поле "Номер карты получателя" + PAN fallback
var cardInput = {isEmpty: true};
for (var k = 0; k < 10; ++k) {
var e3 = checkApp(); if (e3) return e3;
cardInput = host.parser.findEditTextByHint("Номер карты получателя");
if (cardInput.isEmpty) cardInput = host.parser.findPanCardNumberInput();
if (!cardInput.isEmpty) break;
host.timer.sleep(1500);
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter));
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
}
if (cardInput.isEmpty) return "Поле 'Номер карты получателя' не найдено";
host.adb.makeTap(deviceId, cardInput.x, cardInput.y);
host.timer.sleep(500);
host.adb.inputText(deviceId, cardNumber);
host.timer.sleep(2000);
host.adb.hideKeyboard(deviceId);
host.timer.sleep(800);
// 6. Поле суммы (с fallback по input___ rid / bottom-most EditText)
var sumInput = {isEmpty: true};
for (var s = 0; s < 10; ++s) {
var e4 = checkApp(); if (e4) return e4;
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter));
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
sumInput = host.parser.findEditTextByHint("Сумма от");
if (!sumInput.isEmpty) break;
var candidates = [];
var allNodes = host.parser.findAllNodes();
for (var ni = 0; ni < allNodes.length; ++ni) {
var n = allNodes[ni];
if (n.className !== "android.widget.EditText") continue;
if (n.height <= 0 || n.height > height * 4 / 5) continue;
candidates.push(n);
}
// Strategy 1: rid="input___"
for (var c1 = 0; c1 < candidates.length; ++c1) {
if (String(candidates[c1].resourceId || "").indexOf("input___") === 0) {
sumInput = candidates[c1]; break;
}
}
// Strategy 2: bottom-most
if (sumInput.isEmpty && candidates.length >= 2) {
candidates.sort(function(a, b) { return a.y1 - b.y1; });
sumInput = candidates[candidates.length - 1];
}
if (!sumInput.isEmpty) break;
host.timer.sleep(1500);
}
if (sumInput.isEmpty) return "Поле ввода суммы не найдено";
host.adb.makeTap(deviceId, sumInput.x, sumInput.y);
host.timer.sleep(500);
host.adb.inputText(deviceId, String(Math.floor(amount)));
host.timer.sleep(1000);
host.adb.hideKeyboard(deviceId);
host.timer.sleep(800);
// 7. Ждём расчёта комиссии
var fee = 0;
var feeCalculated = false;
for (var f = 0; f < 15; ++f) {
var e5 = checkApp(); if (e5) return e5;
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter));
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
var foundKomissiya = false;
var nodesf = host.parser.findAllNodes();
for (var nf = 0; nf < nodesf.length; ++nf) {
var node = nodesf[nf];
if (node.className === "android.widget.TextView" && (node.text || "").trim() === "Комиссия") {
foundKomissiya = true; continue;
}
if (foundKomissiya && node.className === "android.widget.TextView" && node.text) {
var feeText = (node.text || "").trim();
if (feeText.indexOf("Рассчитаем") === 0) { foundKomissiya = false; break; }
var digits = feeText.replace(/[^0-9,\.]/g, '').replace(',', '.');
fee = parseFloat(digits) || 0;
feeCalculated = true;
host.log.debug("Fee parsed: " + fee + " from text: " + feeText);
break;
}
}
if (feeCalculated) break;
host.timer.sleep(1500);
}
if (!feeCalculated) host.log.warn("Fee not calculated, proceeding with fee=0");
// 8. Тапаем "Перевести" (retry если bounds 0)
var transferBtn2 = {isEmpty: true};
for (var a = 0; a < 5; ++a) {
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter));
transferBtn2 = host.parser.findButtonNode("Перевести", true);
if (!transferBtn2.isEmpty && transferBtn2.height > 0) break;
host.adb.hideKeyboard(deviceId);
host.timer.sleep(1000);
}
if (transferBtn2.isEmpty || transferBtn2.height === 0) {
return "Кнопка 'Перевести' не найдена или перекрыта клавиатурой";
}
host.adb.makeTap(deviceId, transferBtn2.x, transferBtn2.y);
host.timer.sleep(3000);
// 8.5. Возможный PIN-экран после confirm
host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter));
var pinNodes = host.parser.tryToFindPinCode(pinCode, "Введите ПИН");
if (pinNodes && pinNodes.length > 0) {
host.helpers.inputPinCode(deviceId, pinNodes);
host.timer.sleep(3000);
}
// 9. Ждём результат
var freezePrev2 = "", freezeStreak2 = 0;
for (var w = 0; w < 15; ++w) {
var e6 = checkApp(); if (e6) return e6;
var xml3 = host.adb.getScreenDumpAsXml(deviceId, ++counter);
host.parser.parseAndSaveXml(xml3);
host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height);
if (host.helpers.isUsefulDump(xml3)) {
if (freezePrev2 && xml3 === freezePrev2) {
if (++freezeStreak2 >= 3) { host.log.warn("screen frozen waiting result"); break; }
} else freezeStreak2 = 0;
freezePrev2 = xml3;
}
// PIN-экран мог появиться позже
var pinLater = host.parser.tryToFindPinCode(pinCode, "Введите ПИН");
if (pinLater && pinLater.length > 0) {
host.helpers.inputPinCode(deviceId, pinLater);
host.timer.sleep(3000);
continue;
}
if (!host.parser.findTextNode("Успешно").isEmpty) {
// Сохраняем транзакцию
var nowMs = Date.now();
var tx = {
accountId: account.id,
amount: -amount,
fee: fee,
phone: cardNumber,
bankName: "ozon",
type: "card",
status: "complete",
timestamp: new Date().toISOString(),
completeTime: new Date().toISOString(),
bankTime: formatBankTime(new Date()),
};
var txId = host.dao.transactionInsert(tx);
host.log.debug("Saved tx to DB, id: " + txId + " fee: " + fee);
// Receipt + external_id
if (txId !== -1) {
host.helpers.saveTransactionReceipt(deviceId, txId);
var externalId = "ozon_" + bankProfilePhone + "_" + Math.floor(nowMs / 1000);
host.dao.transactionUpdateBankTrExternalId(txId, externalId);
}
host.adb.goBack(deviceId);
host.timer.sleep(1000);
host.adb.goBack(deviceId);
host.timer.sleep(1000);
return ""; // success
}
if (!host.parser.findTextNode("Не удалось").isEmpty) {
host.adb.goBack(deviceId);
host.timer.sleep(1000);
host.adb.goBack(deviceId);
host.timer.sleep(1000);
return "Перевод не удался (экран 'Не удалось')";
}
host.timer.sleep(2000);
}
return "Не дождались результата перевода";
}
// "dd.MM.yyyy, HH:mm" — MSK-таймзона как в C++
// (.toTimeZone(QTimeZone("Europe/Moscow"))).
function formatBankTime(d) {
// Берём смещение MSK (+03:00) от UTC и форматируем вручную, чтобы не зависеть
// от локали JS-движка.
var mskMs = d.getTime() + (3 * 60 - d.getTimezoneOffset() * -1) * 60 * 1000 + d.getTimezoneOffset() * 60 * 1000;
// Проще: добавляем 3 часа к UTC.
var utc = d.getTime();
var msk = new Date(utc + 3 * 3600 * 1000);
function pad(n) { return n < 10 ? '0' + n : '' + n; }
return pad(msk.getUTCDate()) + "." + pad(msk.getUTCMonth() + 1) + "." + msk.getUTCFullYear()
+ ", " + pad(msk.getUTCHours()) + ":" + pad(msk.getUTCMinutes());
}