1
0
forked from BRT/arc
arc/android/ozon/PayByCardScript.cpp
slava 23d404b9c5 Add PayByCardScript implementation and XML asset for payment automation in Ozon
Implemented `PayByCardScript` to enable automated card payments on the Ozon platform. Includes ad banner detection/handling, OCR-based transaction ID extraction, fee calculation, and database updates for transactions. Added XML asset `pay_by_card_success.xml` for payment result handling.
2026-03-10 09:30:25 +07:00

376 lines
16 KiB
C++
Raw Permalink 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 "PayByCardScript.h"
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(nullptr, "rus+eng")) {
qWarning() << "[OCR] Failed to init Tesseract";
delete api;
return {};
}
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции A60130542076330K0000040011670301" или "ID операции = Аб60650..."
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-я0-9]+))");
if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1);
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
PayByCardScript::PayByCardScript(
AccountInfo account,
QString cardNumber,
const double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)), m_cardNumber(std::move(cardNumber)),
m_amount(amount) {
}
PayByCardScript::~PayByCardScript() = default;
void PayByCardScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByCard] Ad banner detected, closing...";
AdbUtils::makeTap(device.id, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByCard] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.id, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(device.id, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
qDebug() << "[PayByCard] Google Play update banner detected, closing...";
AdbUtils::makeTap(device.id, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByCard] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(device.id, device.screenWidth / 2, 100);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByCardScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const int width,
const int height
) {
// 1. Нажимаем "Перевести" на домашнем экране
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping 'Перевести'...";
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
if (!title.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByCard]" << m_error;
return;
}
// 3. Нажимаем "По номеру карты" в секции "ДРУГИЕ СПОСОБЫ"
Node cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
if (cardBtn.isEmpty()) {
m_error = "Кнопка 'По номеру карты' не найдена на экране платежей";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping 'По номеру карты'...";
AdbUtils::makeTap(deviceId, cardBtn.x(), cardBtn.y());
QThread::msleep(2000);
// 4. Ждём экран "Перевод по номеру карты"
bool cardScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Перевод по номеру карты");
if (!title.isEmpty()) {
cardScreenLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Перевод по номеру карты', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!cardScreenLoaded) {
m_error = "Экран 'Перевод по номеру карты' не загрузился";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Card payment screen loaded successfully";
// 5. Вводим номер карты в поле "Номер карты получателя"
Node cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя");
if (cardInput.isEmpty()) {
m_error = "Поле 'Номер карты получателя' не найдено";
qWarning() << "[PayByCard]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, cardInput.x(), cardInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_cardNumber);
QThread::msleep(2000);
// 6. Ждём распознавания карты и вводим сумму
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node sumInput = xmlScreenParser.findEditTextByHint("Сумма от");
if (sumInput.isEmpty()) {
m_error = "Поле ввода суммы не найдено";
qWarning() << "[PayByCard]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
QThread::msleep(1000);
qDebug() << "[PayByCard] Card number and amount entered";
// 7. Ждём расчёта комиссии (текст после "Комиссия" перестаёт быть "Рассчитаем...")
double fee = 0;
bool feeCalculated = false;
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
bool foundKomissiya = false;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.TextView" && node.text.trimmed() == "Комиссия") {
foundKomissiya = true;
continue;
}
if (foundKomissiya && node.className == "android.widget.TextView" && !node.text.isEmpty()) {
const QString feeText = node.text.trimmed();
if (feeText.startsWith("Рассчитаем")) {
foundKomissiya = false;
break; // ещё не рассчитана
}
// Парсим "45 ₽" → 45.0
QString digits = feeText;
digits.remove(QRegularExpression("[^0-9,\\.]"));
digits.replace(',', '.');
fee = digits.toDouble();
feeCalculated = true;
qDebug() << "[PayByCard] Fee parsed:" << fee << "from text:" << feeText;
break;
}
}
if (feeCalculated) break;
qDebug() << "[PayByCard] Waiting for fee calculation, attempt" << attempt + 1 << "/ 15";
QThread::msleep(1500);
}
if (!feeCalculated) {
qWarning() << "[PayByCard] Fee not calculated after timeout, proceeding with fee=0";
}
// 8. Нажимаем "Перевести" (или "Перевести X ₽")
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node transferBtn2 = xmlScreenParser.findButtonNode("Перевести", true);
if (transferBtn2.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на экране ввода суммы";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Tapping transfer button:" << transferBtn2.text;
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
QThread::msleep(3000);
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
qDebug() << "[PayByCard] Payment successful!";
m_error.clear();
// Сохраняем транзакцию в БД
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.fee = fee;
tx.phone = m_cardNumber;
tx.bankName = "ozon";
tx.type = TransactionType::Card;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
const int txId = TransactionDAO::insertTransaction(tx);
qDebug() << "[PayByCard] Saved transaction to DB, id:" << txId << "fee:" << fee;
// Ищем кнопку "Документы" и достаём ID операции
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty() && txId != -1) {
qDebug() << "[PayByCard] Tapping 'Документы'...";
AdbUtils::makeTap(deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 20; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
}
qDebug() << "[PayByCard] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
QThread::msleep(1500);
}
if (docLoaded) {
QThread::msleep(500);
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!screenshot.isEmpty()) {
TransactionDAO::updateReceiptImage(txId, screenshot);
qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId;
const QString ocrText = recognizeTextFromImage(screenshot);
if (!ocrText.isEmpty()) {
qDebug() << "[PayByCard] OCR result:\n" << ocrText;
const QString externalId = extractTransactionId(ocrText);
if (!externalId.isEmpty()) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByCard] Extracted transaction ID:" << externalId;
} else {
qWarning() << "[PayByCard] Could not extract transaction ID from OCR text";
}
} else {
qWarning() << "[PayByCard] OCR returned empty text";
}
} else {
qWarning() << "[PayByCard] Screenshot is empty";
}
} else {
qWarning() << "[PayByCard] 'Поделиться' not found, skipping receipt";
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByCard]" << m_error;
return;
}
qDebug() << "[PayByCard] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByCard]" << m_error;
}
} // namespace Ozon