diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp index 39b806a..c7b0d6b 100644 --- a/android/dushanbe/GetCardInfoScript.cpp +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -152,20 +152,22 @@ void GetCardInfoScript::doStart() { const double cardBalance = xmlScreenParser.parseCardDetailBalance(); const QString holder = xmlScreenParser.parseCardDetailHolder(); 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 << "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); QThread::msleep(1500); continue; } + const QString lastNumbers = fullNumberClean.right(4); + // Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета". // ВРЕМЕННО ОТКЛЮЧЕНО QString accountNumber; diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp index de4780e..0099b14 100644 --- a/android/ozon/PayByPhoneScript.cpp +++ b/android/ozon/PayByPhoneScript.cpp @@ -67,6 +67,14 @@ static QString extractTransactionId(const QString &ocrText) { namespace Ozon { +static QString sanitizeBankName(const QString &raw) { + // "Озон банк (Ozon)" -> "Озон банк"; убираем содержимое круглых скобок + // вместе с предшествующими пробелами и обрезаем края. + QString s = raw; + s.remove(QRegularExpression(R"(\s*\([^)]*\))")); + return s.trimmed(); +} + PayByPhoneScript::PayByPhoneScript( MaterialInfo account, QString phone, @@ -75,7 +83,7 @@ PayByPhoneScript::PayByPhoneScript( QObject *parent ) : CommonScript(parent), 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; diff --git a/assets/dushanbe/3.2.33/card_detail.jpg b/assets/dushanbe/3.2.33/card_detail.jpg new file mode 100644 index 0000000..da9231e Binary files /dev/null and b/assets/dushanbe/3.2.33/card_detail.jpg differ diff --git a/assets/dushanbe/3.2.33/card_detail.xml b/assets/dushanbe/3.2.33/card_detail.xml new file mode 100644 index 0000000..1f84e9e --- /dev/null +++ b/assets/dushanbe/3.2.33/card_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm.jpg b/assets/dushanbe/3.2.33/card_transfer_confirm.jpg new file mode 100644 index 0000000..aa5cd06 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_confirm.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm.xml b/assets/dushanbe/3.2.33/card_transfer_confirm.xml new file mode 100644 index 0000000..a3a46f6 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_confirm.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg b/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg new file mode 100644 index 0000000..821cf9e Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml b/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml new file mode 100644 index 0000000..be76870 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_input.jpg b/assets/dushanbe/3.2.33/card_transfer_input.jpg new file mode 100644 index 0000000..9921626 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_input.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_input.xml b/assets/dushanbe/3.2.33/card_transfer_input.xml new file mode 100644 index 0000000..f6b537f --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_input.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_success.jpg b/assets/dushanbe/3.2.33/card_transfer_success.jpg new file mode 100644 index 0000000..595c632 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_success.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_success.xml b/assets/dushanbe/3.2.33/card_transfer_success.xml new file mode 100644 index 0000000..4b62c7f --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg b/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg new file mode 100644 index 0000000..5721895 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml b/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml new file mode 100644 index 0000000..465c902 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/cards_screen.jpg b/assets/dushanbe/3.2.33/cards_screen.jpg new file mode 100644 index 0000000..0ff9ccb Binary files /dev/null and b/assets/dushanbe/3.2.33/cards_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/cards_screen.xml b/assets/dushanbe/3.2.33/cards_screen.xml new file mode 100644 index 0000000..e7b77cb --- /dev/null +++ b/assets/dushanbe/3.2.33/cards_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/history_screen.jpg b/assets/dushanbe/3.2.33/history_screen.jpg new file mode 100644 index 0000000..bd6145b Binary files /dev/null and b/assets/dushanbe/3.2.33/history_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/history_screen.xml b/assets/dushanbe/3.2.33/history_screen.xml new file mode 100644 index 0000000..75852e7 --- /dev/null +++ b/assets/dushanbe/3.2.33/history_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/home_screen.jpg b/assets/dushanbe/3.2.33/home_screen.jpg new file mode 100644 index 0000000..9520cb2 Binary files /dev/null and b/assets/dushanbe/3.2.33/home_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/home_screen.xml b/assets/dushanbe/3.2.33/home_screen.xml new file mode 100644 index 0000000..6deef18 --- /dev/null +++ b/assets/dushanbe/3.2.33/home_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/launch_screen.jpg b/assets/dushanbe/3.2.33/launch_screen.jpg new file mode 100644 index 0000000..1959e7e Binary files /dev/null and b/assets/dushanbe/3.2.33/launch_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/launch_screen.xml b/assets/dushanbe/3.2.33/launch_screen.xml new file mode 100644 index 0000000..f9e1e85 --- /dev/null +++ b/assets/dushanbe/3.2.33/launch_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg b/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg new file mode 100644 index 0000000..18b37db Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_confirm.xml b/assets/dushanbe/3.2.33/phone_transfer_confirm.xml new file mode 100644 index 0000000..8fdb3db --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_confirm.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_input.jpg b/assets/dushanbe/3.2.33/phone_transfer_input.jpg new file mode 100644 index 0000000..97752f9 Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_input.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_input.xml b/assets/dushanbe/3.2.33/phone_transfer_input.xml new file mode 100644 index 0000000..e69407b --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_input.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_success.jpg b/assets/dushanbe/3.2.33/phone_transfer_success.jpg new file mode 100644 index 0000000..af35587 Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_success.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_success.xml b/assets/dushanbe/3.2.33/phone_transfer_success.xml new file mode 100644 index 0000000..b45518d --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/side_menu.jpg b/assets/dushanbe/3.2.33/side_menu.jpg new file mode 100644 index 0000000..b3f5ee8 Binary files /dev/null and b/assets/dushanbe/3.2.33/side_menu.jpg differ diff --git a/assets/dushanbe/3.2.33/side_menu.xml b/assets/dushanbe/3.2.33/side_menu.xml new file mode 100644 index 0000000..69a71e5 --- /dev/null +++ b/assets/dushanbe/3.2.33/side_menu.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/transfers_screen.jpg b/assets/dushanbe/3.2.33/transfers_screen.jpg new file mode 100644 index 0000000..cf1420f Binary files /dev/null and b/assets/dushanbe/3.2.33/transfers_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/transfers_screen.xml b/assets/dushanbe/3.2.33/transfers_screen.xml new file mode 100644 index 0000000..2ed8618 --- /dev/null +++ b/assets/dushanbe/3.2.33/transfers_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/config.ini b/config.ini index 4123663..1074df0 100644 --- a/config.ini +++ b/config.ini @@ -43,4 +43,4 @@ currency=TJS country=tajikistan name=Dushanbe City phone_code=992 -app_version=0 +app_version=3.2.33 diff --git a/docs/bank-setup-wizard.md b/docs/bank-setup-wizard.md new file mode 100644 index 0000000..d7dbe76 --- /dev/null +++ b/docs/bank-setup-wizard.md @@ -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 +``` \ No newline at end of file diff --git a/models/db/MaterialInfo.h b/models/db/MaterialInfo.h index 1ccf91b..9623c94 100644 --- a/models/db/MaterialInfo.h +++ b/models/db/MaterialInfo.h @@ -41,5 +41,5 @@ struct MaterialInfo { QString description; double amount = 0.0; QDateTime updateTime; - MaterialStatus status = MaterialStatus::Active; + MaterialStatus status = MaterialStatus::Off; }; \ No newline at end of file diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index eadf3e8..502b58f 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -742,7 +742,7 @@ void NetworkService::syncBankProfilesFromServer() { newAcc.currency = currency.isEmpty() ? configSettings.value(bankName + "/currency").toString() : currency; - newAcc.status = MaterialStatus::Active; + newAcc.status = MaterialStatus::Off; newAcc.updateTime = QDateTime::currentDateTime(); if (MaterialDAO::upsertAccount(newAcc)) { @@ -1517,6 +1517,19 @@ void NetworkService::start() { // Проверяем токен при старте и планируем рефреш по времени жизни ensureValidToken(); 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() { diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index d551118..0ddfb3b 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -119,6 +119,7 @@ private: bool refreshToken(); bool ensureValidToken(); void scheduleNextRefresh(); + void scheduleNextStatusSync(); void syncDevicesFromServer(const QString &desktopId); void syncBankProfilesFromServer(); void syncBanksIfEmpty();