487 lines
21 KiB
C++
487 lines
21 KiB
C++
#include "PayByCardScript.h"
|
||
|
||
#include <QCoreApplication>
|
||
#include <QProcess>
|
||
#include <QRegularExpression>
|
||
#include <QThread>
|
||
#include <QTimeZone>
|
||
#include <utility>
|
||
|
||
#include <tesseract/baseapi.h>
|
||
#include <leptonica/allheaders.h>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "dump/TaskDumpRecorder.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "dao/BankProfileDAO.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(QCoreApplication::applicationDirPath().toUtf8().constData(), "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 операции B606513042211200000120011700501"
|
||
// "ID операции po-3140eb01-002c-5000-8000-033396b637e1"
|
||
// или разбит на 2 строки
|
||
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))");
|
||
if (auto m = reId.match(ocrText); m.hasMatch()) {
|
||
QString id = m.captured(1);
|
||
id.remove(' ');
|
||
id.remove(QChar(0x00A0));
|
||
return id;
|
||
}
|
||
|
||
// "№ Ф-2026-25235663"
|
||
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
|
||
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
|
||
|
||
return {};
|
||
}
|
||
|
||
namespace Ozon {
|
||
|
||
PayByCardScript::PayByCardScript(
|
||
MaterialInfo 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 BankProfileInfo app = BankProfileDAO::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;
|
||
}
|
||
|
||
const QString &adbId = device.adbSerial;
|
||
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id};
|
||
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error);
|
||
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
|
||
// Закрываем рекламный bottom sheet
|
||
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
|
||
qDebug() << "[PayByCard] Ad banner detected, closing...";
|
||
AdbUtils::makeTap(adbId, adNode.x(), adNode.y());
|
||
QThread::msleep(1000);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
}
|
||
|
||
// Если не на домашнем экране — перезапускаем
|
||
if (!xmlScreenParser.isHomeScreen()) {
|
||
qDebug() << "[PayByCard] Not on home screen, restarting app...";
|
||
if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode,
|
||
device.screenWidth, device.screenHeight)) {
|
||
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
|
||
qWarning() << m_error;
|
||
AdbUtils::tryToKillApplication(adbId, app.package);
|
||
emit finishedWithResult(m_error);
|
||
return;
|
||
}
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
}
|
||
|
||
// Закрываем баннеры
|
||
tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight);
|
||
QThread::msleep(500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
|
||
// Закрываем баннер Google Play "Доступно обновление"
|
||
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) {
|
||
qDebug() << "[PayByCard] Google Play update banner detected, closing...";
|
||
AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
}
|
||
|
||
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
|
||
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
|
||
qDebug() << "[PayByCard] 'Заказать карту' banner detected, closing...";
|
||
AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20);
|
||
QThread::msleep(1000);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||
}
|
||
|
||
makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
|
||
|
||
emit finishedWithResult(m_error);
|
||
}
|
||
|
||
void PayByCardScript::makePayment(
|
||
const QString &deviceId,
|
||
const QString &packageName,
|
||
const QString &pinCode,
|
||
const QString &bankProfilePhone,
|
||
const int width,
|
||
const int height
|
||
) {
|
||
auto checkApp = [&]() -> bool {
|
||
if (!isAppRunning(deviceId, packageName)) {
|
||
m_error = "Приложение было закрыто";
|
||
qWarning() << "[PayByCard]" << m_error;
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
// 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. Ждём экран "Платежи и переводы" И прогрузки кнопки "По номеру карты"
|
||
Node cardBtn;
|
||
bool paymentsPageLoaded = false;
|
||
bool titleSeen = false;
|
||
{
|
||
FreezeDetector freezeCheck;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
if (!checkApp()) return;
|
||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||
xmlScreenParser.parseAndSaveXml(xml);
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
if (freezeCheck.noticeAndCheckFrozen(xml)) {
|
||
qWarning() << "[PayByCard] screen frozen waiting Платежи и переводы";
|
||
break;
|
||
}
|
||
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
|
||
if (!title.isEmpty()) titleSeen = true;
|
||
cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
|
||
if (!title.isEmpty() && !cardBtn.isEmpty()) {
|
||
paymentsPageLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы' + 'По номеру карты', attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
}
|
||
|
||
if (!paymentsPageLoaded) {
|
||
m_error = titleSeen
|
||
? "Кнопка 'По номеру карты' не найдена на экране платежей"
|
||
: "Экран 'Платежи и переводы' не загрузился";
|
||
qWarning() << "[PayByCard]" << m_error;
|
||
return;
|
||
}
|
||
|
||
// 3. Нажимаем "По номеру карты" в секции "ДРУГИЕ СПОСОБЫ"
|
||
qDebug() << "[PayByCard] Tapping 'По номеру карты'...";
|
||
AdbUtils::makeTap(deviceId, cardBtn.x(), cardBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 4. Ждём экран "Перевод по номеру карты"
|
||
bool cardScreenLoaded = false;
|
||
{
|
||
FreezeDetector freezeCheck;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
if (!checkApp()) return;
|
||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||
xmlScreenParser.parseAndSaveXml(xml);
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
if (freezeCheck.noticeAndCheckFrozen(xml)) {
|
||
qWarning() << "[PayByCard] screen frozen waiting 'Перевод по номеру карты'";
|
||
break;
|
||
}
|
||
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;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
if (!checkApp()) return;
|
||
cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя");
|
||
if (cardInput.isEmpty()) {
|
||
// На части сборок Ozon WebView hint у поля PAN пустой, а лейбла
|
||
// "Номер карты получателя" на экране нет (есть только "КУДА").
|
||
// Поле PAN всегда внутри элемента resource-id="movable-pan-iframe".
|
||
cardInput = xmlScreenParser.findPanCardNumberInput();
|
||
}
|
||
if (!cardInput.isEmpty()) break;
|
||
qDebug() << "[PayByCard] Waiting for 'Номер карты получателя' input, attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
}
|
||
|
||
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);
|
||
|
||
// Закрываем системную клавиатуру — она перекрывает поле суммы и кнопку
|
||
// "Перевести" внизу, а на TECNO ещё мешает Ozon WebView дорендерить AT.
|
||
AdbUtils::hideKeyboard(deviceId);
|
||
QThread::msleep(800);
|
||
|
||
// 6. Ждём распознавания карты и вводим сумму
|
||
Node sumInput;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
if (!checkApp()) return;
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
sumInput = xmlScreenParser.findEditTextByHint("Сумма от");
|
||
if (!sumInput.isEmpty()) break;
|
||
|
||
// Fallback для устройств, где Ozon WebView не отдаёт hint (TECNO).
|
||
// На экране "Перевод по номеру карты" всегда 2 EditText в DOM-порядке:
|
||
// карта (сверху), сумма (снизу). Оба имеют resource-id="input___xxxx",
|
||
// поэтому отличать по rid нельзя — берём второй по порядку обхода XML.
|
||
QList<Node> editTexts;
|
||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||
if (n.className != "android.widget.EditText") continue;
|
||
if (n.height() <= 0 || n.height() > height * 4 / 5) continue;
|
||
editTexts.append(n);
|
||
}
|
||
if (editTexts.size() >= 2) {
|
||
sumInput = editTexts[1];
|
||
}
|
||
if (!sumInput.isEmpty()) break;
|
||
|
||
qDebug() << "[PayByCard] Waiting for 'Сумма от' input, attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
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";
|
||
|
||
// Закрываем системную софт-клавиатуру — иначе она перекрывает кнопку "Перевести"
|
||
// внизу экрана (bounds схлопываются до нулевой высоты).
|
||
AdbUtils::hideKeyboard(deviceId);
|
||
QThread::msleep(800);
|
||
|
||
// 7. Ждём расчёта комиссии (текст после "Комиссия" перестаёт быть "Рассчитаем...")
|
||
double fee = 0;
|
||
bool feeCalculated = false;
|
||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||
if (!checkApp()) return;
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
|
||
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 ₽")
|
||
// Ретраим, пока bounds кнопки не будут ненулевыми — клавиатура могла всплыть
|
||
// заново (напр. если расчёт комиссии сфокусировал поле суммы).
|
||
Node transferBtn2;
|
||
for (int attempt = 0; attempt < 5; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
transferBtn2 = xmlScreenParser.findButtonNode("Перевести", true);
|
||
if (!transferBtn2.isEmpty() && transferBtn2.height() > 0) break;
|
||
qDebug() << "[PayByCard] Waiting for tappable 'Перевести', attempt" << attempt + 1 << "/ 5"
|
||
<< "(bounds collapsed — клавиатура ещё видна?)";
|
||
AdbUtils::hideKeyboard(deviceId);
|
||
QThread::msleep(1000);
|
||
}
|
||
if (transferBtn2.isEmpty() || transferBtn2.height() == 0) {
|
||
m_error = "Кнопка 'Перевести' не найдена или перекрыта клавиатурой";
|
||
qWarning() << "[PayByCard]" << m_error;
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[PayByCard] Tapping transfer button:" << transferBtn2.text;
|
||
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
|
||
QThread::msleep(3000);
|
||
|
||
// 8.5. Проверяем экран ввода PIN-кода
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||
!pinNodes.isEmpty()) {
|
||
qDebug() << "[PayByCard] PIN code screen detected after confirm, entering PIN...";
|
||
inputPinCode(deviceId, pinNodes);
|
||
QThread::msleep(3000);
|
||
}
|
||
|
||
// 9. Ждём результат: "Успешно" или "Не удалось"
|
||
FreezeDetector resultFreeze;
|
||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||
if (!checkApp()) return;
|
||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||
xmlScreenParser.parseAndSaveXml(xml);
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
if (resultFreeze.noticeAndCheckFrozen(xml)) {
|
||
qWarning() << "[PayByCard] screen frozen while waiting payment result";
|
||
break;
|
||
}
|
||
|
||
// Если PIN-код появился позже — вводим
|
||
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||
!pinNodes.isEmpty()) {
|
||
qDebug() << "[PayByCard] PIN code screen detected (retry), entering PIN...";
|
||
inputPinCode(deviceId, pinNodes);
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
|
||
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;
|
||
|
||
// Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler.
|
||
// Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR
|
||
// ради bank_transaction_id, но это медленно (до 30 сек ожидания) и
|
||
// нестабильно. Теперь полагаемся на fallback-ID.
|
||
QString externalId;
|
||
if (txId != -1) {
|
||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||
if (!screenshot.isEmpty()) {
|
||
m_resultScreenshot = screenshot;
|
||
emit screenshotReady(m_resultScreenshot);
|
||
TransactionDAO::updateReceiptImage(txId, screenshot);
|
||
qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId;
|
||
}
|
||
}
|
||
|
||
if (externalId.isEmpty() && txId != -1) {
|
||
externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch());
|
||
qDebug() << "[PayByCard] Generated fallback transaction ID:" << externalId;
|
||
}
|
||
if (!externalId.isEmpty() && txId != -1) {
|
||
TransactionDAO::updateBankTrExternalId(txId, externalId);
|
||
qDebug() << "[PayByCard] Transaction ID:" << externalId;
|
||
}
|
||
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
return;
|
||
}
|
||
|
||
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
|
||
m_error = "Перевод не удался (экран 'Не удалось')";
|
||
qWarning() << "[PayByCard]" << m_error;
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[PayByCard] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
|
||
QThread::msleep(2000);
|
||
}
|
||
|
||
m_error = "Не дождались результата перевода";
|
||
qWarning() << "[PayByCard]" << m_error;
|
||
}
|
||
|
||
} // namespace Ozon
|