diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp index 5a71c81..d820d5b 100644 --- a/android/dushanbe/GetCardInfoScript.cpp +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -157,11 +157,84 @@ void GetCardInfoScript::doStart() { continue; } + // Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета". + QString accountNumber; + const Node settingsBtn = xmlScreenParser.findCardSettingsButton(); + if (settingsBtn.x() > 0) { + AdbUtils::makeTap(m_deviceId, settingsBtn.x(), settingsBtn.y()); + QThread::msleep(1500); + + bool sheetFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isCardSettingsBottomSheet()) { + sheetFound = true; + break; + } + QThread::msleep(1000); + } + + if (sheetFound) { + const Node requisitesItem = xmlScreenParser.findCardRequisitesMenuItem(); + if (requisitesItem.x() > 0) { + AdbUtils::makeTap(m_deviceId, requisitesItem.x(), requisitesItem.y()); + QThread::msleep(1500); + + bool reqScreenFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isCardRequisitesScreen()) { + reqScreenFound = true; + break; + } + QThread::msleep(1000); + } + + if (reqScreenFound) { + const Node accountTab = xmlScreenParser.findAccountRequisitesTab(); + if (accountTab.x() > 0) { + AdbUtils::makeTap(m_deviceId, accountTab.x(), accountTab.y()); + QThread::msleep(1500); + + // Несколько попыток — данные могут подгружаться + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + accountNumber = xmlScreenParser.parseAccountNumber(); + if (!accountNumber.isEmpty()) break; + QThread::msleep(1000); + } + qDebug() << "[Dushanbe::GetCardInfo] accountNumber for card" << i + << ":" << accountNumber; + } else { + qWarning() << "[Dushanbe::GetCardInfo] Account tab not found for card" << i; + } + } else { + qWarning() << "[Dushanbe::GetCardInfo] Requisites screen not detected for card" << i; + } + } else { + qWarning() << "[Dushanbe::GetCardInfo] 'Реквизиты карты' menu item not found for card" << i; + } + } else { + qWarning() << "[Dushanbe::GetCardInfo] Card settings bottom sheet not detected for card" << i; + } + + // Возврат на экран детали карты — bottom sheet и/или экран Реквизитов + for (int attempt = 0; attempt < 4; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isCardDetailScreen()) break; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1200); + } + } else { + qWarning() << "[Dushanbe::GetCardInfo] 'Настройки карты' button not found for card" << i; + } + MaterialInfo account; account.deviceId = device.id; account.appCode = m_appCode; account.lastNumbers = lastNumbers; account.cardNumber = QString(fullNumber).remove(' '); + account.accountNumber = accountNumber; account.amount = cardBalance; account.currency = "TJS"; account.description = cards[i].description.isEmpty() ? holder : cards[i].description; @@ -187,6 +260,7 @@ void GetCardInfoScript::doStart() { QJsonObject materialBody; materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["card_number"] = QString(fullNumber).remove(' '); + materialBody["account_number"] = accountNumber; materialBody["name"] = updatedApp.fullName; materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled"; materialBody["currency"] = currencyCodes.value(savedAccount.currency.toUpper(), 972); diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp index b15eaee..a45020d 100644 --- a/android/dushanbe/ScreenXmlParser.cpp +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -410,6 +410,86 @@ QString ScreenXmlParser::parseCardDetailHolder() { return {}; } +Node ScreenXmlParser::findCardSettingsButton() { + for (const Node &node : m_nodes) { + if (node.clickable && node.contentDesc == QString::fromUtf8("Настройки карты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isCardSettingsBottomSheet() { + bool hasRequisites = false; + bool hasBlock = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasRequisites = true; + if (node.contentDesc == QString::fromUtf8("Заблокировать карту")) hasBlock = true; + if (hasRequisites && hasBlock) return true; + } + return false; +} + +Node ScreenXmlParser::findCardRequisitesMenuItem() { + // На bottom sheet это ImageView; на экране "Реквизиты и информация" + // одноимённый таб — Button. Берём именно ImageView, чтобы не перепутать. + for (const Node &node : m_nodes) { + if (node.clickable + && node.className == "android.widget.ImageView" + && node.contentDesc == QString::fromUtf8("Реквизиты карты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isCardRequisitesScreen() { + bool hasCardTab = false; + bool hasAccountTab = false; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasCardTab = true; + if (node.contentDesc == QString::fromUtf8("Реквизиты счета")) hasAccountTab = true; + if (hasCardTab && hasAccountTab) return true; + } + return false; +} + +Node ScreenXmlParser::findAccountRequisitesTab() { + for (const Node &node : m_nodes) { + if (node.clickable + && node.className == "android.widget.Button" + && node.contentDesc == QString::fromUtf8("Реквизиты счета")) { + return node; + } + } + return {}; +} + +QString ScreenXmlParser::parseAccountNumber() { + // Берём ноду "Номер счета" (label) и ищем ближайший узел снизу + // с чисто цифровым content-desc — это и есть номер счёта. + Node label; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Номер счета")) { + label = node; + break; + } + } + if (label.y2() < 0) return {}; + + static const QRegularExpression digitsOnly(R"(^\d{10,}$)"); + Node best; + for (const Node &node : m_nodes) { + if (node.y1() < label.y2()) continue; + if (!digitsOnly.match(node.contentDesc).hasMatch()) continue; + if (best.y1() < 0 || node.y1() < best.y1()) { + best = node; + } + } + return best.contentDesc; +} + bool ScreenXmlParser::isHistoryScreen() { bool hasOperacii = false; bool hasVypiska = false; @@ -527,32 +607,15 @@ Node ScreenXmlParser::findEditTextByHint(const QString &hint) { } Node ScreenXmlParser::findSumEditText() { - // Якорь: View "Мин. сумма: ..." расположен под полем "Комментарий", - // которое следует за полем суммы. То есть над якорем два EditText'а: - // ближайший = "Комментарий", следующий вверх = "Сумма". - Node anchor; + // На экране перевода по карте порядок EditText'ов сверху вниз: + // [0] номер карты, [1] сумма, [2] комментарий. + QList editTexts; for (const Node &node : m_nodes) { - if (node.contentDesc.contains(QString::fromUtf8("Мин. сумма"))) { - anchor = node; - break; - } + if (node.className == "android.widget.EditText") editTexts.append(node); } - if (anchor.x1() < 0) return {}; - - QList aboveAnchor; - for (const Node &node : m_nodes) { - if (node.className != "android.widget.EditText") continue; - if (node.y2() > anchor.y1()) continue; - aboveAnchor.append(node); - } - // Сортируем по нижней границе по убыванию: [0] — ближайший (Комментарий), - // [1] — следующий выше (Сумма). - std::sort(aboveAnchor.begin(), aboveAnchor.end(), - [](const Node &a, const Node &b) { return a.y2() > b.y2(); }); - - if (aboveAnchor.size() >= 2) return aboveAnchor[1]; - if (aboveAnchor.size() == 1) return aboveAnchor[0]; // нестандартная разметка - return {}; + std::sort(editTexts.begin(), editTexts.end(), + [](const Node &a, const Node &b) { return a.y1() < b.y1(); }); + return editTexts.size() >= 2 ? editTexts[1] : Node{}; } Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { diff --git a/android/dushanbe/ScreenXmlParser.h b/android/dushanbe/ScreenXmlParser.h index deeedab..d089cc8 100644 --- a/android/dushanbe/ScreenXmlParser.h +++ b/android/dushanbe/ScreenXmlParser.h @@ -82,6 +82,24 @@ public: // Парсит имя держателя с экрана детали QString parseCardDetailHolder(); + // Кнопка "Настройки карты" на экране детали карты + Node findCardSettingsButton(); + + // Bottom sheet с пунктами "Реквизиты карты" / "Заблокировать карту" и т.д. + bool isCardSettingsBottomSheet(); + + // Кнопка "Реквизиты карты" внутри bottom sheet "Настройки карты" + Node findCardRequisitesMenuItem(); + + // Экран "Реквизиты и информация" с табами "Реквизиты карты" / "Реквизиты счета" + bool isCardRequisitesScreen(); + + // Таб "Реквизиты счета" (правый) на экране "Реквизиты и информация" + Node findAccountRequisitesTab(); + + // Значение под лейблом "Номер счета" — длинная цифровая строка + QString parseAccountNumber(); + // Определяет экран истории операций (табы "Операции" + "Выписка") bool isHistoryScreen(); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 974aead..dc55000 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -410,6 +410,105 @@ void GetCardInfoScript::doStart() { } } + // 2.5. Достаём номер счёта (общий для всех карт): Основной счёт → Посмотреть реквизиты + QString accountNumber; + { + bool needFetch = false; + for (const MaterialInfo &c : cards) { + const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers); + if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; } + } + + if (needFetch) { + // Возвращаемся на главный экран — критерий именно по наличию кнопки + // "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий + // (Перевести/Пополнить присутствуют и на других экранах). + Node mainAcc; + for (int attempt = 0; attempt < 6; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + mainAcc = xmlScreenParser.findMainAccountButton(); + if (mainAcc.x() > 0) break; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + } + + if (mainAcc.x() > 0) { + AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y()); + QThread::msleep(2000); + + // Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине, + // чтобы тап не ушёл в системный нав-бар. + Node showReq; + const int targetMaxY = device.screenHeight / 2; + for (int attempt = 0; attempt < 6; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node candidate = xmlScreenParser.findShowRequisitesButton(); + if (candidate.x() > 0 && candidate.y() <= targetMaxY) { + showReq = candidate; + break; + } + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight * 2 / 3, + device.screenWidth / 2, + device.screenHeight / 3); + QThread::msleep(800); + } + + if (showReq.x() > 0) { + AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y()); + QThread::msleep(2000); + + bool reqScreen = false; + for (int attempt = 0; attempt < 6; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isAccountRequisitesScreen()) { + reqScreen = true; + break; + } + QThread::msleep(1000); + } + + if (reqScreen) { + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + accountNumber = xmlScreenParser.parseAccountNumberFromRequisites(); + if (!accountNumber.isEmpty()) break; + QThread::msleep(800); + } + qDebug() << "[GetCardInfo] accountNumber:" << accountNumber; + } else { + qWarning() << "[GetCardInfo] Account requisites screen not detected"; + } + } else { + qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached"; + } + } else { + qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home"; + } + + // Возврат на главный — критерий по кнопке "Основной счёт ... ₽" + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.findMainAccountButton().x() > 0) break; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + } + + // Записываем номер счёта во все материалы device/app + if (!accountNumber.isEmpty()) { + for (const MaterialInfo &c : cards) { + MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers); + if (upd.id == -1) continue; + upd.accountNumber = accountNumber; + upd.updateTime = QDateTime::currentDateTimeUtc(); + if (!MaterialDAO::upsertAccount(upd)) { + qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers; + } + } + } + } + } + // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); if (!updatedApp.bankProfileId.isEmpty()) { @@ -427,6 +526,7 @@ void GetCardInfoScript::doStart() { QJsonObject materialBody; materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["card_number"] = dbCard.cardNumber; + materialBody["account_number"] = dbCard.accountNumber; materialBody["name"] = updatedApp.fullName; materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled"; materialBody["currency"] = currencyCodes.value(dbCard.currency.toUpper(), 643); diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index e233644..b98b32a 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -400,6 +400,50 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { return {}; } +Node ScreenXmlParser::findMainAccountButton() { + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + QString t = node.text; + t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' '); + if (t.contains("Основной", Qt::CaseInsensitive) && t.contains("₽")) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findShowRequisitesButton() { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == QString::fromUtf8("Посмотреть реквизиты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isAccountRequisitesScreen() { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == QString::fromUtf8("Реквизиты счёта")) { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseAccountNumberFromRequisites() { + // text="Номер счёта получателя 40914810100039708263" + // Префикс может варьироваться по регистру/пробелам — берём после первого совпадения цифр в конце. + static const QRegularExpression tailDigits(R"((\d{15,}))"); + static const QString prefix = QString::fromUtf8("Номер счёта получателя"); + for (const Node &node : m_nodes) { + const QString t = node.text; + if (!t.contains(prefix)) continue; + const QRegularExpressionMatch m = tailDigits.match(t); + if (m.hasMatch()) return m.captured(1); + } + return {}; +} + QList ScreenXmlParser::parseCardsOnHomeScreen() { // "Основной счёт X ₽" — общая кнопка с балансом для всех карт. // Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces). diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h index 8b22da5..04df241 100644 --- a/android/ozon/ScreenXmlParser.h +++ b/android/ozon/ScreenXmlParser.h @@ -85,6 +85,18 @@ public: // Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX") QString findFullCardNumber(); + // Кнопка "Основной счёт X ₽" на главном экране + Node findMainAccountButton(); + + // Кнопка/текст "Посмотреть реквизиты" на экране "Основной счёт" + Node findShowRequisitesButton(); + + // Экран "Реквизиты счёта" — заголовок text="Реквизиты счёта" + bool isAccountRequisitesScreen(); + + // Парсит "Номер счёта получателя <20 цифр>" из текстовой ноды + QString parseAccountNumberFromRequisites(); + bool isTransactionListScreen(); // Возвращает кнопки транзакций на экране "Операции" (до maxCount штук) diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index feb65c8..86ab677 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -134,11 +134,14 @@ void DatabaseManager::initSchema() { update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), status TEXT DEFAULT 'active', card_number TEXT DEFAULT '', + account_number TEXT DEFAULT '', material_id TEXT DEFAULT '', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, app_code, last_numbers) ))"); + q.exec("ALTER TABLE materials ADD COLUMN account_number TEXT DEFAULT ''"); + q.exec(R"(CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER, diff --git a/database/dao/MaterialDAO.cpp b/database/dao/MaterialDAO.cpp index 074c62a..0add8bb 100644 --- a/database/dao/MaterialDAO.cpp +++ b/database/dao/MaterialDAO.cpp @@ -19,14 +19,15 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency, material_id) - VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency, :material_id) + INSERT INTO materials (device_id, app_code, last_numbers, card_number, account_number, amount, update_time, status, description, currency, material_id) + VALUES (:device_id, :app_code, :last_numbers, :card_number, :account_number, :amount, :update_time, :status, :description, :currency, :material_id) ON CONFLICT(device_id, app_code, last_numbers) DO UPDATE SET amount = excluded.amount, update_time = excluded.update_time, last_numbers = excluded.last_numbers, card_number = CASE WHEN excluded.card_number != '' THEN excluded.card_number ELSE materials.card_number END, + account_number = CASE WHEN excluded.account_number != '' THEN excluded.account_number ELSE materials.account_number END, status = excluded.status, description = excluded.description, currency = excluded.currency, @@ -37,6 +38,7 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) { query.bindValue(":app_code", info.appCode); query.bindValue(":last_numbers", info.lastNumbers); query.bindValue(":card_number", info.cardNumber); + query.bindValue(":account_number", info.accountNumber); query.bindValue(":currency", info.currency); query.bindValue(":description", info.description); query.bindValue(":amount", info.amount); @@ -133,6 +135,7 @@ MaterialInfo parseAccount(const QSqlQuery &query) { acc.appCode = query.value("app_code").toString(); acc.lastNumbers = query.value("last_numbers").toString(); acc.cardNumber = query.value("card_number").toString(); + acc.accountNumber = query.value("account_number").toString(); acc.materialId = query.value("material_id").toString(); acc.amount = query.value("amount").toDouble(); acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); @@ -151,7 +154,7 @@ QList MaterialDAO::activateExclusive( QSqlQuery sel(db); sel.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE device_id = :device_id AND app_code = :app_code AND status = 'active' AND id != :target_id @@ -199,7 +202,7 @@ QList MaterialDAO::getAccounts(const QString &deviceId, const QStr QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE device_id = :device_id AND app_code = :app_code )"); @@ -223,7 +226,7 @@ MaterialInfo MaterialDAO::getAccountById(const int accountId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE id = :id LIMIT 1 @@ -247,7 +250,7 @@ QList MaterialDAO::getAccountsByAppCode(const QString &appCode) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE app_code = :app_code )"); @@ -270,7 +273,7 @@ MaterialInfo MaterialDAO::findAppAccount(const QString &deviceId, const QString QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId LIMIT 1 @@ -334,7 +337,7 @@ MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency FROM materials WHERE material_id = :material_id LIMIT 1 diff --git a/migrations.sql b/migrations.sql index fba728f..60aa02f 100644 --- a/migrations.sql +++ b/migrations.sql @@ -53,6 +53,7 @@ CREATE TABLE IF NOT EXISTS materials update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), status TEXT DEFAULT 'active', card_number TEXT DEFAULT '', + account_number TEXT DEFAULT '', material_id TEXT DEFAULT '', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, app_code, last_numbers) diff --git a/models/db/MaterialInfo.h b/models/db/MaterialInfo.h index b54020a..1ccf91b 100644 --- a/models/db/MaterialInfo.h +++ b/models/db/MaterialInfo.h @@ -35,6 +35,7 @@ struct MaterialInfo { QString appCode; QString lastNumbers; QString cardNumber; // full 16-digit card number + QString accountNumber; // full bank account number (IBAN-like) QString materialId; QString currency; QString description;