1
0
forked from BRT/arc

Handle declined transactions and enhance payment scripts:

- Add unique `bank_transaction_id` generation for declined transactions in `EventHandler`.
- Extend timeout and iteration logic to better handle intermediate statuses ("Выполняется") in `PayByPhoneScript` and `PayByCardScript`.
- Update
This commit is contained in:
slava 2026-04-30 23:41:06 +07:00
parent d58100083f
commit c144305240
6 changed files with 106 additions and 34 deletions

View File

@ -50,21 +50,21 @@ void GetProfileInfoScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Парсим баланс с домашнего экрана. Если баланс не подгрузился (==0),
// тапаем кнопку обновления и перечитываем дамп. Лоадера у refresh нет,
// Всегда тапаем кнопку обновления — баланс на экране может быть закэширован
// и не отражать актуальное состояние счёта. Лоадера у refresh нет,
// поэтому ждём фиксированно.
double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance:" << parsedBalance;
if (parsedBalance <= 0.0) {
Node refreshBtn = xmlScreenParser.findRefreshButtonOnHomeScreen();
if (refreshBtn.x() > 0) {
qDebug() << "[Dushanbe::GetProfileInfo] Balance is 0, tapping refresh and retrying";
qDebug() << "[Dushanbe::GetProfileInfo] Tapping refresh to get up-to-date balance";
AdbUtils::makeTap(m_deviceId, refreshBtn.x(), refreshBtn.y());
QThread::msleep(3000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance after refresh:" << parsedBalance;
} else {
qWarning() << "[Dushanbe::GetProfileInfo] Refresh button not found, parsing as-is";
}
const double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance after refresh:" << parsedBalance;
if (parsedBalance <= 0.0) {
m_error = "Balance is zero after refresh: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
@ -72,7 +72,6 @@ void GetProfileInfoScript::doStart() {
emit finishedWithResult(m_error);
return;
}
}
BankProfileDAO::updateAmount(app.id, parsedBalance);
// Тапаем по кнопке меню (гамбургер, левый верхний угол)

View File

@ -142,13 +142,14 @@ void PayByCardScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
}
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
makePayment(device.adbSerial, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByCardScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
@ -386,9 +387,11 @@ void PayByCardScript::makePayment(
return;
}
// 10. Ждём результат — "Платеж выполнен" или ошибку
// 10. Ждём результат — "Платеж выполнен" или ошибку.
// Промежуточный статус "Выполняется" может висеть до ~1.5 мин, пока банк
// подтверждает перевод, поэтому общий таймаут — 60 итераций × 2 сек = 2 мин.
bool success = false;
for (int attempt = 0; attempt < 15; ++attempt) {
for (int attempt = 0; attempt < 60; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
@ -417,6 +420,35 @@ void PayByCardScript::makePayment(
return;
}
// Промежуточный статус — банк ещё обрабатывает перевод
if (!xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Выполняется"), true).isEmpty()) {
qDebug() << "[Dushanbe::PayByCard] Status 'Выполняется', waiting for completion, attempt"
<< attempt + 1;
QThread::msleep(2000);
continue;
}
// Если bottom sheet "Подтверждение платежа" всё ещё виден — первый тап
// не сработал (мимо OCR-координат), повторяем его той же логикой.
Node retryConfirm = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Подтверждение платежа"), true);
if (!retryConfirm.isEmpty() &&
retryConfirm.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
qDebug() << "[Dushanbe::PayByCard] Confirm sheet still visible, re-tapping 'Подтверждаю'";
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
QPoint tap = OcrUtils::findTextCenter(screen,
QString::fromUtf8("Подтверждаю"),
retryConfirm.y1(), retryConfirm.y2());
if (tap.x() < 0) {
const int tapY = retryConfirm.y2() - qMax(150, height / 14);
tap = QPoint(retryConfirm.x(), tapY);
}
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
QThread::msleep(4000);
continue;
}
qDebug() << "[Dushanbe::PayByCard] Waiting for result, attempt" << attempt + 1;
QThread::msleep(2000);
}
@ -460,10 +492,10 @@ void PayByCardScript::makePayment(
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
}
// Генерируем ID транзакции из даты+времени (без перехода в историю)
const QString operationId = bankTime.isEmpty()
? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss")
: QString("dushanbe_") + QString(bankTime).replace(' ', '_');
// bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю).
// Уникальность за счёт <bankProfilePhone, секунда>.
const QString operationId = QString("dushanbe_") + bankProfilePhone + "_"
+ QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[Dushanbe::PayByCard] Generated operationId:" << operationId;
// Сохраняем транзакцию

View File

@ -28,6 +28,7 @@ private:
void makePayment(
const QString &deviceId,
const QString &pinCode,
const QString &bankProfilePhone,
int width,
int height
);

View File

@ -139,13 +139,14 @@ void PayByPhoneScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
}
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
makePayment(device.adbSerial, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
@ -382,9 +383,11 @@ void PayByPhoneScript::makePayment(
return;
}
// 10. Ждём результат — "Платеж выполнен" или ошибку
// 10. Ждём результат — "Платеж выполнен" или ошибку.
// Промежуточный статус "Выполняется" может висеть до ~1.5 мин, пока банк
// подтверждает перевод, поэтому общий таймаут — 60 итераций × 2 сек = 2 мин.
bool success = false;
for (int attempt = 0; attempt < 15; ++attempt) {
for (int attempt = 0; attempt < 60; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
@ -410,6 +413,34 @@ void PayByPhoneScript::makePayment(
return;
}
if (!xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Выполняется"), true).isEmpty()) {
qDebug() << "[Dushanbe::PayByPhone] Status 'Выполняется', waiting for completion, attempt"
<< attempt + 1;
QThread::msleep(2000);
continue;
}
// Если bottom sheet "Подтверждение платежа" всё ещё виден — первый тап
// не сработал (мимо OCR-координат), повторяем его той же логикой.
Node retryConfirm = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Подтверждение платежа"), true);
if (!retryConfirm.isEmpty() &&
retryConfirm.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
qDebug() << "[Dushanbe::PayByPhone] Confirm sheet still visible, re-tapping 'Подтверждаю'";
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
QPoint tap = OcrUtils::findTextCenter(screen,
QString::fromUtf8("Подтверждаю"),
retryConfirm.y1(), retryConfirm.y2());
if (tap.x() < 0) {
const int tapY = retryConfirm.y2() - qMax(150, height / 14);
tap = QPoint(retryConfirm.x(), tapY);
}
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
QThread::msleep(4000);
continue;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for result, attempt" << attempt + 1;
QThread::msleep(2000);
}
@ -452,10 +483,10 @@ void PayByPhoneScript::makePayment(
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
}
// Генерируем ID транзакции из даты+времени (без перехода в историю)
const QString operationId = bankTime.isEmpty()
? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss")
: QString("dushanbe_") + QString(bankTime).replace(' ', '_');
// bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю).
// Уникальность за счёт <bankProfilePhone, секунда>.
const QString operationId = QString("dushanbe_") + bankProfilePhone + "_"
+ QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[Dushanbe::PayByPhone] Generated operationId:" << operationId;
// Сохраняем транзакцию

View File

@ -28,6 +28,7 @@ private:
void makePayment(
const QString &deviceId,
const QString &pinCode,
const QString &bankProfilePhone,
int width,
int height
);

View File

@ -297,6 +297,14 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
break;
}
}
// Скрипт мог упасть до записи транзакции в БД — сервер всё равно
// ждёт уникальный bank_transaction_id. Генерируем по той же схеме,
// что и в success-ветке скриптов: <bankName>_<bankProfilePhone>_<unix>.
if (declTxId.isEmpty()) {
const BankProfileInfo declApp = BankProfileDAO::getApplication(declAcc.deviceId, event.bankName);
declTxId = event.bankName + "_" + declApp.phone + "_"
+ QString::number(QDateTime::currentSecsSinceEpoch());
}
QJsonObject declined;
declined["bank_transaction_id"] = declTxId;