# Мастер установки банка (`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 ```