- Introduce `OcrUtils` with fuzzy-matching logic for text recognition on screenshots. - Implement dual-pass OCR to handle light-on-dark and dark-on-light text cases. - Replace hardcoded "Подтверждаю" button tap logic with OCR-based detection and fallback geometry. - Update `TaskDumpRecorder` to utilize `NetworkService` for uploading screen dumps, images, and metadata to the backend. - Add support for `/monitoring/dump` endpoint in `NetworkService`. - Enhance UI functionality with new error handling and progress feedback during dump creation and upload.
496 lines
21 KiB
C++
496 lines
21 KiB
C++
#include "PayByCardScript.h"
|
||
|
||
#include <QRegularExpression>
|
||
#include <QThread>
|
||
|
||
#include <tesseract/baseapi.h>
|
||
#include <leptonica/allheaders.h>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "dump/TaskDumpRecorder.h"
|
||
#include "ocr/OcrUtils.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/TransactionDAO.h"
|
||
#include "AppLogger.h"
|
||
|
||
#include <QCoreApplication>
|
||
// Точечный OCR: находит строку "операции" и кропает правую часть для digit-only OCR
|
||
static QString recognizeOperationId(const QByteArray &pngData) {
|
||
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||
static_cast<size_t>(pngData.size()));
|
||
if (!pix) return {};
|
||
|
||
const int w = pixGetWidth(pix);
|
||
const int h = pixGetHeight(pix);
|
||
|
||
auto *api = new tesseract::TessBaseAPI();
|
||
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
|
||
pixDestroy(&pix); delete api;
|
||
return {};
|
||
}
|
||
api->SetImage(pix);
|
||
api->Recognize(nullptr);
|
||
|
||
int opLineY = -1, opLineH = 0;
|
||
bool prevWasNomer = false;
|
||
tesseract::ResultIterator *ri = api->GetIterator();
|
||
if (ri) {
|
||
do {
|
||
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
|
||
if (word) {
|
||
const QString w_str = QString::fromUtf8(word);
|
||
delete[] word;
|
||
// Ищем "Номер" (или OCR-артефакты: "Homep", "Номep")
|
||
if (w_str.contains(QString::fromUtf8("омер")) ||
|
||
w_str.contains(QString::fromUtf8("omep"))) {
|
||
prevWasNomer = true;
|
||
continue;
|
||
}
|
||
// После "Номер" ищем "операции"
|
||
if (prevWasNomer &&
|
||
(w_str.contains(QString::fromUtf8("операции")) ||
|
||
w_str.contains(QString::fromUtf8("onepaции")))) {
|
||
int x1, y1, x2, y2;
|
||
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
|
||
opLineY = y1;
|
||
opLineH = y2 - y1;
|
||
break;
|
||
}
|
||
prevWasNomer = false;
|
||
}
|
||
} while (ri->Next(tesseract::RIL_WORD));
|
||
}
|
||
api->End();
|
||
delete api;
|
||
|
||
if (opLineY < 0) { pixDestroy(&pix); return {}; }
|
||
|
||
const int cropY = qMax(0, opLineY - opLineH / 2);
|
||
const int cropH = opLineH * 2;
|
||
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
|
||
Pix *cropped = pixClipRectangle(pix, box, nullptr);
|
||
boxDestroy(&box);
|
||
pixDestroy(&pix);
|
||
if (!cropped) return {};
|
||
|
||
auto *api2 = new tesseract::TessBaseAPI();
|
||
if (api2->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "eng")) {
|
||
pixDestroy(&cropped); delete api2;
|
||
return {};
|
||
}
|
||
api2->SetVariable("tessedit_char_whitelist", "0123456789");
|
||
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
|
||
api2->SetImage(cropped);
|
||
char *text = api2->GetUTF8Text();
|
||
QString result = QString::fromUtf8(text).trimmed();
|
||
delete[] text;
|
||
pixDestroy(&cropped);
|
||
api2->End();
|
||
delete api2;
|
||
|
||
result.remove(QRegularExpression("[^0-9]"));
|
||
if (result.length() >= 8) {
|
||
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
|
||
return result;
|
||
}
|
||
return {};
|
||
}
|
||
|
||
namespace Dushanbe {
|
||
|
||
PayByCardScript::PayByCardScript(
|
||
MaterialInfo account,
|
||
QString cardNumber,
|
||
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 TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber};
|
||
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error);
|
||
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||
|
||
if (!xmlScreenParser.isHomeScreen()) {
|
||
qDebug() << "[Dushanbe::PayByCard] Not on home screen, restarting app...";
|
||
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
|
||
device.screenWidth, device.screenHeight)) {
|
||
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
|
||
qWarning() << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", device.id, m_error, {}, "CREATE_TRANSACTION");
|
||
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
|
||
emit finishedWithResult(m_error);
|
||
return;
|
||
}
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||
}
|
||
|
||
makePayment(device.adbSerial, 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 transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы"));
|
||
if (transfersTab.isEmpty()) {
|
||
m_error = "Tab 'Переводы' not found on home screen";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[Dushanbe::PayByCard] Tapping 'Переводы' tab";
|
||
AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 2. Ждём экран переводов — ищем "DC (по номеру карты)"
|
||
bool transfersLoaded = false;
|
||
Node cardTransferBtn;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
cardTransferBtn = xmlScreenParser.findNodeByContentDesc(
|
||
QString::fromUtf8("DC (по номеру карты)"), true);
|
||
if (!cardTransferBtn.isEmpty()) {
|
||
transfersLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for transfers screen, attempt" << attempt + 1;
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!transfersLoaded) {
|
||
m_error = "Transfers screen not loaded or 'DC (по номеру карты)' not found";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
// 3. Тапаем "DC (по номеру карты)"
|
||
qDebug() << "[Dushanbe::PayByCard] Tapping 'DC (по номеру карты)'";
|
||
AdbUtils::makeTap(deviceId, cardTransferBtn.x(), cardTransferBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 4. Ждём экран ввода — ищем EditText с hint "Номер карты получателя"
|
||
bool inputScreenLoaded = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
Node editText = xmlScreenParser.findEditTextByHint(
|
||
QString::fromUtf8("Номер карты получателя"));
|
||
if (editText.isEmpty()) {
|
||
editText = xmlScreenParser.findFirstEditText();
|
||
}
|
||
if (!editText.isEmpty()) {
|
||
inputScreenLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for card input screen, attempt" << attempt + 1;
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!inputScreenLoaded) {
|
||
m_error = "Card input screen not loaded";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
// 5. Вводим номер карты в поле "Номер карты получателя"
|
||
Node cardEditText = xmlScreenParser.findEditTextByHint(
|
||
QString::fromUtf8("Номер карты получателя"));
|
||
if (cardEditText.isEmpty()) {
|
||
cardEditText = xmlScreenParser.findFirstEditText();
|
||
}
|
||
AdbUtils::makeTap(deviceId, cardEditText.x(), cardEditText.y());
|
||
QThread::msleep(500);
|
||
AdbUtils::inputText(deviceId, m_cardNumber);
|
||
QThread::msleep(1000);
|
||
|
||
// Скрываем клавиатуру чтобы поле суммы стало видимым
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
|
||
// 6. Вводим сумму в поле "Сумма" (hint содержит "Сумма")
|
||
Node sumEditText;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма"));
|
||
if (!sumEditText.isEmpty()) break;
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for sum input, attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
if (sumEditText.isEmpty()) {
|
||
m_error = "Sum input field not found";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y());
|
||
QThread::msleep(500);
|
||
|
||
// Сумма приходит с копейками (например 100.50), форматируем как целое или с точкой
|
||
const QString amountStr = (m_amount == static_cast<int>(m_amount))
|
||
? QString::number(static_cast<int>(m_amount))
|
||
: QString::number(m_amount, 'f', 2);
|
||
AdbUtils::inputText(deviceId, amountStr);
|
||
QThread::msleep(1000);
|
||
|
||
// 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий")
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
{
|
||
const QString cardMask = "****" + m_account.lastNumbers;
|
||
Node cardNode;
|
||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||
if (!n.clickable) continue;
|
||
if (n.contentDesc.contains(cardMask)) { cardNode = n; break; }
|
||
}
|
||
if (cardNode.isEmpty()) {
|
||
m_error = "Source card not found: " + cardMask;
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
qDebug() << "[Dushanbe::PayByCard] Selecting source card:" << cardMask;
|
||
AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y());
|
||
QThread::msleep(500);
|
||
}
|
||
|
||
// 7. Нажимаем "Далее"
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
|
||
if (daleeBtn.isEmpty()) {
|
||
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
|
||
}
|
||
if (daleeBtn.isEmpty()) {
|
||
m_error = "Button 'Далее' not found";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[Dushanbe::PayByCard] Tapping 'Далее'";
|
||
AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 8. После "Далее" кнопка меняется на "Оплатить" — ждём и нажимаем
|
||
bool oplatitFound = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить"));
|
||
if (oplatitBtn.isEmpty()) {
|
||
oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false);
|
||
}
|
||
if (!oplatitBtn.isEmpty()) {
|
||
qDebug() << "[Dushanbe::PayByCard] Tapping 'Оплатить'";
|
||
AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y());
|
||
oplatitFound = true;
|
||
QThread::msleep(3000);
|
||
break;
|
||
}
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for 'Оплатить', attempt" << attempt + 1;
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!oplatitFound) {
|
||
m_error = "Button 'Оплатить' not found";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
// 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю"
|
||
// content-desc содержит "Подтверждение платежа...Подтверждаю"
|
||
// Кнопка "Подтверждаю" визуально внизу bottom sheet
|
||
bool confirmed = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
|
||
// Пин-код может появиться
|
||
if (xmlScreenParser.isPinCodeScreen()) {
|
||
qDebug() << "[Dushanbe::PayByCard] PIN code screen, entering PIN...";
|
||
inputPinCode(deviceId, pinCode);
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
|
||
// Ищем элемент с "Подтверждение платежа" и "Подтверждаю" в content-desc
|
||
Node confirmNode = xmlScreenParser.findNodeByContentDesc(
|
||
QString::fromUtf8("Подтверждение платежа"), true);
|
||
if (!confirmNode.isEmpty() &&
|
||
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
|
||
// Compose flatten-ит весь bottom sheet в одну accessibility-кнопку,
|
||
// отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом
|
||
// по скриншоту. Если OCR не нашёл — fallback на геометрию (центр
|
||
// нижней четверти sheet, выше системного nav-бара).
|
||
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
|
||
QPoint tap = OcrUtils::findTextCenter(screen,
|
||
QString::fromUtf8("Подтверждаю"),
|
||
confirmNode.y1(), confirmNode.y2());
|
||
if (tap.x() < 0) {
|
||
const int tapY = confirmNode.y2() - qMax(150, height / 14);
|
||
tap = QPoint(confirmNode.x(), tapY);
|
||
qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap;
|
||
}
|
||
qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at" << tap;
|
||
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
|
||
confirmed = true;
|
||
QThread::msleep(4000);
|
||
break;
|
||
}
|
||
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for confirmation sheet, attempt" << attempt + 1;
|
||
QThread::msleep(2000);
|
||
}
|
||
|
||
if (!confirmed) {
|
||
m_error = "Confirmation sheet not found";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
// 10. Ждём результат — "Платеж выполнен" или ошибку
|
||
bool success = false;
|
||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||
|
||
// Пин-код
|
||
if (xmlScreenParser.isPinCodeScreen()) {
|
||
qDebug() << "[Dushanbe::PayByCard] PIN code screen (retry), entering PIN...";
|
||
inputPinCode(deviceId, pinCode);
|
||
QThread::msleep(3000);
|
||
continue;
|
||
}
|
||
|
||
// Успех: content-desc начинается с "Платеж выполнен"
|
||
if (!xmlScreenParser.findNodeByContentDesc(
|
||
QString::fromUtf8("Платеж выполнен"), true).isEmpty()) {
|
||
success = true;
|
||
break;
|
||
}
|
||
|
||
// Ошибка
|
||
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() ||
|
||
!xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) {
|
||
m_error = "Transfer failed: error screen detected";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[Dushanbe::PayByCard] Waiting for result, attempt" << attempt + 1;
|
||
QThread::msleep(2000);
|
||
}
|
||
|
||
if (!success) {
|
||
m_error = "Payment result not received within timeout";
|
||
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[Dushanbe::PayByCard] Payment successful!";
|
||
m_error.clear();
|
||
|
||
// Скриншот чека — сохраняем как m_resultScreenshot и отправляем в мониторинг
|
||
m_resultScreenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||
const QByteArray &receiptImage = m_resultScreenshot;
|
||
if (!m_resultScreenshot.isEmpty())
|
||
emit screenshotReady(m_resultScreenshot);
|
||
|
||
// Парсим детали из content-desc "Платеж выполнен"
|
||
// Формат: "Платеж выполнен\nПоставщик:\n...\nКомиссия:\n0.00\nДата:\n05.04.26\nВремя:\n20:19:56\nНа главную"
|
||
QString bankTime;
|
||
double fee = 0;
|
||
|
||
Node resultNode = xmlScreenParser.findNodeByContentDesc(
|
||
QString::fromUtf8("Платеж выполнен"), true);
|
||
if (!resultNode.isEmpty()) {
|
||
const QStringList lines = resultNode.contentDesc.split('\n');
|
||
for (int i = 0; i < lines.size(); ++i) {
|
||
const QString line = lines[i].trimmed();
|
||
if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
|
||
fee = lines[i + 1].trimmed().toDouble();
|
||
} else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) {
|
||
bankTime = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) {
|
||
bankTime += " " + lines[i + 1].trimmed();
|
||
}
|
||
}
|
||
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(' ', '_');
|
||
qDebug() << "[Dushanbe::PayByCard] Generated operationId:" << operationId;
|
||
|
||
// Сохраняем транзакцию
|
||
TransactionInfo tx;
|
||
tx.accountId = m_account.id;
|
||
tx.amount = -m_amount;
|
||
tx.fee = fee;
|
||
tx.status = TransactionStatus::Complete;
|
||
tx.type = TransactionType::Card;
|
||
tx.phone = m_cardNumber;
|
||
tx.bankName = "dushanbe_city_bank";
|
||
tx.bankTime = bankTime;
|
||
tx.bankTrExternalId = operationId;
|
||
tx.timestamp = QDateTime::currentDateTimeUtc();
|
||
tx.completeTime = QDateTime::currentDateTimeUtc();
|
||
tx.receiptImage = receiptImage;
|
||
|
||
const int txId = TransactionDAO::insertTransaction(tx);
|
||
if (txId != -1) {
|
||
qDebug() << "[Dushanbe::PayByCard] Transaction saved, id:" << txId
|
||
<< "operationId:" << operationId;
|
||
if (!receiptImage.isEmpty()) {
|
||
TransactionDAO::updateReceiptImage(txId, receiptImage);
|
||
}
|
||
} else {
|
||
qWarning() << "[Dushanbe::PayByCard] Failed to save transaction";
|
||
}
|
||
}
|
||
|
||
} // namespace Dushanbe
|