Add documentation for BankSetupWizard, detailing lifecycle, UI structure, verification flow, error handling, and worker thread management. Include XML assets for Dushanbe City Bank.
@ -152,20 +152,22 @@ void GetCardInfoScript::doStart() {
|
|||||||
const double cardBalance = xmlScreenParser.parseCardDetailBalance();
|
const double cardBalance = xmlScreenParser.parseCardDetailBalance();
|
||||||
const QString holder = xmlScreenParser.parseCardDetailHolder();
|
const QString holder = xmlScreenParser.parseCardDetailHolder();
|
||||||
const QString fullNumberClean = QString(fullNumber).remove(' ');
|
const QString fullNumberClean = QString(fullNumber).remove(' ');
|
||||||
const QString lastNumbers = (fullNumberClean.length() >= 4)
|
|
||||||
? fullNumberClean.right(4)
|
|
||||||
: (cards[i].lastNumbers.isEmpty() ? QString() : cards[i].lastNumbers);
|
|
||||||
|
|
||||||
qDebug() << "[Dushanbe::GetCardInfo] Card" << i << ":" << fullNumber
|
qDebug() << "[Dushanbe::GetCardInfo] Card" << i << ":" << fullNumber
|
||||||
<< "balance:" << cardBalance << "holder:" << holder;
|
<< "balance:" << cardBalance << "holder:" << holder;
|
||||||
|
|
||||||
if (lastNumbers.isEmpty()) {
|
// Без полного номера карты материал не создаём и на сервер не отправляем —
|
||||||
qWarning() << "[Dushanbe::GetCardInfo] No card number for card" << i;
|
// защита от записей с пустым card_number (см. Ozon::GetCardInfo).
|
||||||
|
if (fullNumberClean.isEmpty()) {
|
||||||
|
qWarning() << "[Dushanbe::GetCardInfo] Full card number not parsed for card" << i
|
||||||
|
<< "(home-screen lastNumbers=" << cards[i].lastNumbers << ") — skipping";
|
||||||
AdbUtils::goBack(m_deviceId);
|
AdbUtils::goBack(m_deviceId);
|
||||||
QThread::msleep(1500);
|
QThread::msleep(1500);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString lastNumbers = fullNumberClean.right(4);
|
||||||
|
|
||||||
// Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета".
|
// Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета".
|
||||||
// ВРЕМЕННО ОТКЛЮЧЕНО
|
// ВРЕМЕННО ОТКЛЮЧЕНО
|
||||||
QString accountNumber;
|
QString accountNumber;
|
||||||
|
|||||||
@ -67,6 +67,14 @@ static QString extractTransactionId(const QString &ocrText) {
|
|||||||
|
|
||||||
namespace Ozon {
|
namespace Ozon {
|
||||||
|
|
||||||
|
static QString sanitizeBankName(const QString &raw) {
|
||||||
|
// "Озон банк (Ozon)" -> "Озон банк"; убираем содержимое круглых скобок
|
||||||
|
// вместе с предшествующими пробелами и обрезаем края.
|
||||||
|
QString s = raw;
|
||||||
|
s.remove(QRegularExpression(R"(\s*\([^)]*\))"));
|
||||||
|
return s.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
PayByPhoneScript::PayByPhoneScript(
|
PayByPhoneScript::PayByPhoneScript(
|
||||||
MaterialInfo account,
|
MaterialInfo account,
|
||||||
QString phone,
|
QString phone,
|
||||||
@ -75,7 +83,7 @@ PayByPhoneScript::PayByPhoneScript(
|
|||||||
QObject *parent
|
QObject *parent
|
||||||
) : CommonScript(parent),
|
) : CommonScript(parent),
|
||||||
m_account(std::move(account)), m_phone(std::move(phone)),
|
m_account(std::move(account)), m_phone(std::move(phone)),
|
||||||
m_bankName(std::move(bankName)), m_amount(amount) {
|
m_bankName(sanitizeBankName(bankName)), m_amount(amount) {
|
||||||
}
|
}
|
||||||
|
|
||||||
PayByPhoneScript::~PayByPhoneScript() = default;
|
PayByPhoneScript::~PayByPhoneScript() = default;
|
||||||
|
|||||||
BIN
assets/dushanbe/3.2.33/card_detail.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
1
assets/dushanbe/3.2.33/card_detail.xml
Normal file
BIN
assets/dushanbe/3.2.33/card_transfer_confirm.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
1
assets/dushanbe/3.2.33/card_transfer_confirm.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.Button" package="tj.dc.next1" content-desc="Подтверждение платежа 1.00 TJS Поставщик: DC (по номеру карты) Номер/Счет 9762000186263942 Способ оплаты: 9762 **** **** 8865 Сумма зачисления: 1.00 TJS Комиссия: 0.00 TJS Сумма списания: 1.00 TJS Подтверждаю" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node></node></node></node><node index="1" text="" resource-id="android:id/navigationBarBackground" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,1544][720,1640]" /></node></hierarchy>
|
||||||
BIN
assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
1
assets/dushanbe/3.2.33/card_transfer_confirm_2.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.Button" package="tj.dc.next1" content-desc="Подтверждение платежа 1.00 TJS Поставщик: DC (по номеру карты) Номер/Счет 9762000104225205 Способ оплаты: 9762 **** **** 8865 Сумма зачисления: 1.00 TJS Комиссия: 0.00 TJS Сумма списания: 1.00 TJS Подтверждаю" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node></node></node></node><node index="1" text="" resource-id="android:id/navigationBarBackground" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,1544][720,1640]" /></node></hierarchy>
|
||||||
BIN
assets/dushanbe/3.2.33/card_transfer_input.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
1
assets/dushanbe/3.2.33/card_transfer_input.xml
Normal file
BIN
assets/dushanbe/3.2.33/card_transfer_success.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
1
assets/dushanbe/3.2.33/card_transfer_success.xml
Normal file
BIN
assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
1
assets/dushanbe/3.2.33/card_transfer_tx_detail.xml
Normal file
BIN
assets/dushanbe/3.2.33/cards_screen.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
1
assets/dushanbe/3.2.33/cards_screen.xml
Normal file
BIN
assets/dushanbe/3.2.33/history_screen.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
1
assets/dushanbe/3.2.33/history_screen.xml
Normal file
BIN
assets/dushanbe/3.2.33/home_screen.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
1
assets/dushanbe/3.2.33/home_screen.xml
Normal file
BIN
assets/dushanbe/3.2.33/launch_screen.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
1
assets/dushanbe/3.2.33/launch_screen.xml
Normal file
BIN
assets/dushanbe/3.2.33/phone_transfer_confirm.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
1
assets/dushanbe/3.2.33/phone_transfer_confirm.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.Button" package="tj.dc.next1" content-desc="Подтверждение платежа 1.00 TJS Поставщик: DC (по номеру телефона) Номер/Счет 559411001 Способ оплаты: 9762 **** **** 8865 Сумма зачисления: 1.00 TJS Комиссия: 0.00 TJS Сумма списания: 1.00 TJS Подтверждаю" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node></node></node></node><node index="1" text="" resource-id="android:id/navigationBarBackground" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,1544][720,1640]" /></node></hierarchy>
|
||||||
BIN
assets/dushanbe/3.2.33/phone_transfer_input.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
1
assets/dushanbe/3.2.33/phone_transfer_input.xml
Normal file
BIN
assets/dushanbe/3.2.33/phone_transfer_success.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
1
assets/dushanbe/3.2.33/phone_transfer_success.xml
Normal file
BIN
assets/dushanbe/3.2.33/side_menu.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
1
assets/dushanbe/3.2.33/side_menu.xml
Normal file
BIN
assets/dushanbe/3.2.33/transfers_screen.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
1
assets/dushanbe/3.2.33/transfers_screen.xml
Normal file
@ -43,4 +43,4 @@ currency=TJS
|
|||||||
country=tajikistan
|
country=tajikistan
|
||||||
name=Dushanbe City
|
name=Dushanbe City
|
||||||
phone_code=992
|
phone_code=992
|
||||||
app_version=0
|
app_version=3.2.33
|
||||||
|
|||||||
120
docs/bank-setup-wizard.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Мастер установки банка (`BankSetupWizard`)
|
||||||
|
|
||||||
|
Файлы: `views/bank/BankSetupWizard.{h,cpp}`
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Диалоговое окно, проводящее пользователя через настройку нового банковского профиля на конкретном устройстве: ввод пин-кода → автоматический вход в приложение → парсинг профиля → парсинг карт → выбор активной карты. Все данные одновременно сохраняются в локальную БД и регистрируются на бэкенде (`localhost:9999/api/v1/`).
|
||||||
|
|
||||||
|
Поддерживаемые банки: **Ozon**, **Black** (Birbank), **Dushanbe City Bank** (`appCode == "ozon" | "black" | "dushanbe_city_bank"`). Остальные банки в мастере не поддерживаются и сразу падают с ошибкой (`BankSetupWizard.cpp:443, 494, 549`).
|
||||||
|
|
||||||
|
## Где открывается
|
||||||
|
|
||||||
|
Два входа, оба создают `BankSetupWizard` как обычный `QDialog` (немодальный):
|
||||||
|
- `views/device/DeviceSettingsWindow.cpp:176` — из карточки настроек устройства.
|
||||||
|
- `views/device/DeviceWidget.cpp:278` — из плитки устройства в главном окне.
|
||||||
|
|
||||||
|
В обоих местах используется свежая запись из БД, если профиль уже есть; иначе — переданный `BankProfileInfo` шаблон.
|
||||||
|
|
||||||
|
## Структура UI: `QStackedWidget` из 5 страниц
|
||||||
|
|
||||||
|
Индексы страниц: `0` Intro, `1` PIN, `2` Progress, `3` Success, `4` Error (`BankSetupWizard.cpp:32-38`).
|
||||||
|
|
||||||
|
| Шаг | Что показывает | Кнопки |
|
||||||
|
|-----|----------------|--------|
|
||||||
|
| 1 Intro (`createStep1`) | Иконка + название банка + список из 3 шагов | **Далее** → шаг 2 |
|
||||||
|
| 2 PIN (`createStep2`) | Поле ввода пин-кода (max 10 символов) | **Назад**, **Далее** → сохраняет `m_app.pinCode`, переходит на шаг 3 и запускает `startVerification()` |
|
||||||
|
| 3 Progress (`createStep3`) | `QProgressBar` + текстовая метка статуса | **Назад** → `stopWorkerThread()`, возврат на шаг 2 |
|
||||||
|
| 4a Success (`createStep4Success`) | ФИО / Телефон / Email + таблица карт с radio-выбором | **Завершить** → `applyActiveCardSelection()`, `accept()` |
|
||||||
|
| 4b Error (`createStep4Error`) | Человекочитаемый текст ошибки | **Назад** → шаг 2, **Начать заново** → очищает БД-запись (если профиль ещё не отправлен) и возвращает на шаг 1 |
|
||||||
|
|
||||||
|
## Жизненный цикл и пауза других профилей
|
||||||
|
|
||||||
|
В **конструкторе** (`BankSetupWizard.cpp:41`) сразу вызывается `pauseActiveProfiles()`:
|
||||||
|
- Берёт все профили устройства через `BankProfileDAO::getApplicationsByDeviceId`.
|
||||||
|
- Все `status == "active"` переводятся в `"off"` локально и **выключаются на сервере** через `NetworkService::toggleBankProfile(enable=false)` (`BankSetupWizard.cpp:297-321`).
|
||||||
|
- Коды паузованных профилей запоминаются в `m_pausedProfiles`.
|
||||||
|
|
||||||
|
Это сделано, чтобы фоновый `EventHandler` / `DeviceScreener` не дёргали телефон параллельно во время работы мастера.
|
||||||
|
|
||||||
|
В **деструкторе** (`BankSetupWizard.cpp:44-47`): `stopWorkerThread()` + `resumePausedProfiles()` — возвращает паузованные профили в `"active"` и шлёт `toggleBankProfile(enable=true)` на сервер.
|
||||||
|
|
||||||
|
## Цепочка верификации (шаги 3 → 4)
|
||||||
|
|
||||||
|
`startVerification()` → `runPinCheck()` → `runProfileCheck()` → `runCardCheck()` → `onVerificationSuccess()`.
|
||||||
|
|
||||||
|
Каждый шаг запускает соответствующий банк-скрипт в **отдельном `QThread`** (`m_workerThread`), результат принимает лямбдой через `Qt::QueuedConnection`. Если шаг вернул непустую строку — `onVerificationError()`, иначе — следующий шаг.
|
||||||
|
|
||||||
|
### 1. `runPinCheck()` — проверка пин-кода (`BankSetupWizard.cpp:384`)
|
||||||
|
|
||||||
|
Сначала `startVerification()` делает базовую проверку, что устройство онлайн (`device.status == "device"`), затем upsert'ит профиль в БД с `status="off"`, `install=true`, чтобы получить `m_app.id` (`BankSetupWizard.cpp:362-382`).
|
||||||
|
|
||||||
|
Дальше создаётся `LoginAndCheckAccountsScript` с флагом **`pinCheckOnly=true`** (3-й или 4-й аргумент в зависимости от банка). Этот флаг говорит скрипту: только дойти до главного экрана, чтобы убедиться что пин валидный, и выйти. Никаких материалов не создаётся.
|
||||||
|
|
||||||
|
При успехе: `BankProfileDAO::updatePinCodeStatus(appId, "yes", "")`, прогресс → 33%, переход к `runProfileCheck()`.
|
||||||
|
|
||||||
|
### 2. `runProfileCheck()` — данные профиля (`BankSetupWizard.cpp:450`)
|
||||||
|
|
||||||
|
Запускает `GetProfileInfoScript`. Скрипт парсит ФИО/телефон/email с главного экрана, обновляет `bank_profiles` в БД и **отправляет на сервер** `POST/PATCH` через `NetworkService` — на этом этапе бэкенд возвращает `bank_profile_id`.
|
||||||
|
|
||||||
|
После завершения обработчик `onProfileResult` дочитывает запись из БД (`BankProfileDAO::getApplication`) и проверяет, что `bankProfileId` непустой (`BankSetupWizard.cpp:463`). Если бэкенд не вернул id — ошибка "Не удалось зарегистрировать банковский профиль на сервере. Возможно, банк ещё не поддерживается сервером." Прогресс → 66%, переход к `runCardCheck()`.
|
||||||
|
|
||||||
|
### 3. `runCardCheck()` — карты (`BankSetupWizard.cpp:501`)
|
||||||
|
|
||||||
|
Запускает `GetCardInfoScript`. Скрипт обходит все карты в приложении, парсит полный номер карты (для Ozon/Dushanbe — через OCR/XML), складывает в БД и отправляет `POST material` на сервер.
|
||||||
|
|
||||||
|
После завершения `onCardResult` читает `materials = MaterialDAO::getAccounts(...)`, и если хотя бы одна карта есть, но **ни у одной нет `materialId`** (т.е. ничего не синхронизировалось с сервером) — ошибка "Не удалось отправить данные карт на сервер" (`BankSetupWizard.cpp:518`). Прогресс → 100%, вызов `onVerificationSuccess()`.
|
||||||
|
|
||||||
|
### 4. `onVerificationSuccess()` (`BankSetupWizard.cpp:556`)
|
||||||
|
|
||||||
|
- Перечитывает профиль из БД, формирует HTML с ФИО/телефоном/email.
|
||||||
|
- Загружает карты, и **если хоть у одной `cardNumber.isEmpty()` — уходит в Error** ("Не удалось получить полный номер карты, распознаны только последние 4 цифры") (`BankSetupWizard.cpp:574-580`). Это страховка поверх защиты в самих скриптах.
|
||||||
|
- Заполняет таблицу: 4 колонки (Номер / Сумма / Валюта / Активна-radio).
|
||||||
|
- Логика автовыбора:
|
||||||
|
- **0 карт** → строка "Карты не найдены", кнопка Завершить активна.
|
||||||
|
- **1 карта** → автовыбор, колонка radio скрыта.
|
||||||
|
- **≥2 карт** → первая карта выбрана по умолчанию, можно перевыбрать.
|
||||||
|
|
||||||
|
### 5. `applyActiveCardSelection()` (`BankSetupWizard.cpp:631`) — при нажатии Завершить
|
||||||
|
|
||||||
|
- `MaterialDAO::activateExclusive(deviceId, appCode, selectedMaterialId)` — делает выбранную карту единственной активной в этом профиле; возвращает список деактивированных.
|
||||||
|
- Для выбранной карты шлёт `NetworkService::toggleMaterial(enable=true)`, для каждой деактивированной — `toggleMaterial(enable=false)`. Каждый запрос — в своём `QThread`, fire-and-forget.
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
`onVerificationError()` (`BankSetupWizard.cpp:690`) проганяет текст через `humanReadableError()` (`BankSetupWizard.cpp:659`), который:
|
||||||
|
1. Определяет **этап** по подстрокам ("пин-кода" / "профиля" / "карт").
|
||||||
|
2. Распознаёт типовые сообщения скриптов (`"Cant open the home screen"`, `"No cards found"`, `"Cannot find user name"`, и т.д.) и подставляет понятный русский текст с подсказкой что делать.
|
||||||
|
3. Всё незнакомое → "Произошла непредвиденная ошибка. Попробуйте повторить позже."
|
||||||
|
|
||||||
|
Кнопка **«Начать заново»** на странице Error дополнительно чистит БД, но **только если профиль ещё не зарегистрирован на сервере** (`current.bankProfileId.isEmpty()`, `BankSetupWizard.cpp:277-283`): удаляет материалы (`MaterialDAO::deleteAccountsByApp`) и сам профиль (`BankProfileDAO::deleteProfile`), сбрасывает `m_app.id = -1`. Это предотвращает дубликаты при повторной попытке.
|
||||||
|
|
||||||
|
## Управление воркер-потоком
|
||||||
|
|
||||||
|
`stopWorkerThread()` (`BankSetupWizard.cpp:342`) — критичный кусок:
|
||||||
|
1. `requestInterruption()` + `quit()`, ждёт до 3 секунд `wait(3000)`.
|
||||||
|
2. Если поток **залип** (обычно — внутри `QProcess::waitForFinished` в `AdbUtils::startProcess`): **отсоединяет** `QThread` от родителя (`setParent(nullptr)`) и вешает `deleteLater` на сигнал `finished`. Без этого `~QWidget` удалит работающий `QThread` → `qFatal`.
|
||||||
|
|
||||||
|
Каждый `run*Check()` сначала зовёт `stopWorkerThread()`, поэтому переход на следующий шаг безопасен даже при гонках.
|
||||||
|
|
||||||
|
## Краткая последовательность данных
|
||||||
|
|
||||||
|
```
|
||||||
|
Пользователь вводит PIN
|
||||||
|
↓
|
||||||
|
BankProfileDAO::upsertApplication (status=off, install=true)
|
||||||
|
↓
|
||||||
|
LoginAndCheckAccountsScript (pinCheckOnly=true) ─→ только верифицирует вход
|
||||||
|
↓
|
||||||
|
BankProfileDAO::updatePinCodeStatus("yes", "")
|
||||||
|
↓
|
||||||
|
GetProfileInfoScript ─→ парсит профиль → POST/PATCH bank_profile → bankProfileId
|
||||||
|
↓
|
||||||
|
GetCardInfoScript ─→ парсит карты → POST material для каждой → materialId
|
||||||
|
↓
|
||||||
|
Таблица карт + выбор активной
|
||||||
|
↓
|
||||||
|
MaterialDAO::activateExclusive + toggleMaterial на сервере
|
||||||
|
↓
|
||||||
|
accept() → resumePausedProfiles() в деструкторе возвращает остальные профили в active
|
||||||
|
```
|
||||||
@ -41,5 +41,5 @@ struct MaterialInfo {
|
|||||||
QString description;
|
QString description;
|
||||||
double amount = 0.0;
|
double amount = 0.0;
|
||||||
QDateTime updateTime;
|
QDateTime updateTime;
|
||||||
MaterialStatus status = MaterialStatus::Active;
|
MaterialStatus status = MaterialStatus::Off;
|
||||||
};
|
};
|
||||||
@ -742,7 +742,7 @@ void NetworkService::syncBankProfilesFromServer() {
|
|||||||
newAcc.currency = currency.isEmpty()
|
newAcc.currency = currency.isEmpty()
|
||||||
? configSettings.value(bankName + "/currency").toString()
|
? configSettings.value(bankName + "/currency").toString()
|
||||||
: currency;
|
: currency;
|
||||||
newAcc.status = MaterialStatus::Active;
|
newAcc.status = MaterialStatus::Off;
|
||||||
newAcc.updateTime = QDateTime::currentDateTime();
|
newAcc.updateTime = QDateTime::currentDateTime();
|
||||||
|
|
||||||
if (MaterialDAO::upsertAccount(newAcc)) {
|
if (MaterialDAO::upsertAccount(newAcc)) {
|
||||||
@ -1517,6 +1517,19 @@ void NetworkService::start() {
|
|||||||
// Проверяем токен при старте и планируем рефреш по времени жизни
|
// Проверяем токен при старте и планируем рефреш по времени жизни
|
||||||
ensureValidToken();
|
ensureValidToken();
|
||||||
scheduleNextRefresh();
|
scheduleNextRefresh();
|
||||||
|
|
||||||
|
// Периодическая синхронизация статусов bank_profiles/materials
|
||||||
|
scheduleNextStatusSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::scheduleNextStatusSync() {
|
||||||
|
if (!m_running) return;
|
||||||
|
|
||||||
|
QTimer::singleShot(60 * 1000, this, [this]() {
|
||||||
|
if (!m_running) return;
|
||||||
|
syncCurrentProfile();
|
||||||
|
scheduleNextStatusSync();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::deleteDeviceFromServer() {
|
void NetworkService::deleteDeviceFromServer() {
|
||||||
|
|||||||
@ -119,6 +119,7 @@ private:
|
|||||||
bool refreshToken();
|
bool refreshToken();
|
||||||
bool ensureValidToken();
|
bool ensureValidToken();
|
||||||
void scheduleNextRefresh();
|
void scheduleNextRefresh();
|
||||||
|
void scheduleNextStatusSync();
|
||||||
void syncDevicesFromServer(const QString &desktopId);
|
void syncDevicesFromServer(const QString &desktopId);
|
||||||
void syncBankProfilesFromServer();
|
void syncBankProfilesFromServer();
|
||||||
void syncBanksIfEmpty();
|
void syncBanksIfEmpty();
|
||||||
|
|||||||