From c2fc0104c6a47fdad923bcccb637893e5b63b59a Mon Sep 17 00:00:00 2001 From: slava Date: Sun, 12 Apr 2026 15:17:28 +0700 Subject: [PATCH] Fix for windows --- .gitignore | 1 + CMakeLists.txt | 21 + android/black/CommonScript.cpp | 5 +- android/black/CommonScript.h | 2 +- android/dushanbe/CommonScript.cpp | 5 +- android/dushanbe/CommonScript.h | 2 +- android/dushanbe/GetProfileInfoScript.cpp | 10 +- android/ozon/CommonScript.cpp | 5 +- android/ozon/CommonScript.h | 2 +- android/ozon/GetProfileInfoScript.cpp | 21 +- android/ozon/LoginAndCheckAccountsScript.cpp | 13 +- build_x64.bat | 85 +++ config.ini | 6 +- services/EventHandler.cpp | 24 +- split_zip.ps1 | 48 ++ tests/mock_server/WINDOWS.md | 293 ++++++++-- tests/mock_server/gen_fetch_transactions.py | 31 +- tests/mock_server/gen_mock_data.py | 16 +- tests/mock_server/gen_transactions.ps1 | 16 + tests/mock_server/mock_server.py | 172 +++++- tests/mock_server/run_server.ps1 | 2 + .../pixel7_dushanbe_fetch_transactions.json | 6 - .../pixel7_ozon_fetch_transactions.json | 6 - ...el_7_dushanbe_city_bank_fetch_profile.json | 6 - ...dushanbe_city_bank_fetch_transactions.json | 6 - .../scenarios/pixel_7_ozon_fetch_profile.json | 6 - .../pixel_7_ozon_fetch_transactions.json | 6 - ...ixel7_dushanbe_ft_01_embedded_minus_1.json | 10 - .../pixel7_dushanbe_ft_02_minus_1_2131.json | 10 - .../pixel7_dushanbe_ft_03_minus_1_2125.json | 10 - .../pixel7_dushanbe_ft_04_minus_1_1836.json | 10 - .../pixel7_dushanbe_ft_05_minus_1_1819.json | 10 - .../pixel7_dushanbe_ft_06_plus_1_2052.json | 10 - .../pixel7_ozon_ft_01_evgeny_plus_100.json | 10 - .../pixel7_ozon_ft_02_vlad_minus_100.json | 10 - .../pixel7_ozon_ft_03_vlad_minus_300.json | 10 - .../pool/pixel7_ozon_ft_04_vlad_plus_100.json | 10 - .../pixel7_ozon_ft_05_vlad_plus_100_b.json | 10 - .../pixel7_ozon_ft_06_evgeny_plus_100_b.json | 10 - .../tecno_ozon_ft_01_galina_plus_100.json | 10 - .../tecno_ozon_ft_02_galina_plus_300.json | 10 - .../tecno_ozon_ft_03_vlad_minus_100_a.json | 10 - .../tecno_ozon_ft_04_vlad_minus_100_b.json | 10 - .../tecno_ozon_ft_05_galina_minus_100_a.json | 10 - .../tecno_ozon_ft_06_galina_minus_100_b.json | 10 - .../pool/tecno_ozon_ft_07_vlad_plus_550.json | 10 - .../tecno_km4_ozon_fetch_profile.json | 6 - .../tecno_km4_ozon_fetch_transactions.json | 6 - .../tecno_ozon_fetch_transactions.json | 6 - tests/mock_server/setup.ps1 | 552 ++++++++++++++++++ 50 files changed, 1231 insertions(+), 345 deletions(-) create mode 100644 build_x64.bat create mode 100644 split_zip.ps1 create mode 100644 tests/mock_server/gen_transactions.ps1 create mode 100644 tests/mock_server/run_server.ps1 delete mode 100644 tests/mock_server/scenarios/pixel7_dushanbe_fetch_transactions.json delete mode 100644 tests/mock_server/scenarios/pixel7_ozon_fetch_transactions.json delete mode 100644 tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_profile.json delete mode 100644 tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_transactions.json delete mode 100644 tests/mock_server/scenarios/pixel_7_ozon_fetch_profile.json delete mode 100644 tests/mock_server/scenarios/pixel_7_ozon_fetch_transactions.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_01_embedded_minus_1.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_02_minus_1_2131.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_03_minus_1_2125.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_04_minus_1_1836.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_05_minus_1_1819.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_06_plus_1_2052.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_01_evgeny_plus_100.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_02_vlad_minus_100.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_03_vlad_minus_300.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_04_vlad_plus_100.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_05_vlad_plus_100_b.json delete mode 100644 tests/mock_server/scenarios/pool/pixel7_ozon_ft_06_evgeny_plus_100_b.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_01_galina_plus_100.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_02_galina_plus_300.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_03_vlad_minus_100_a.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_04_vlad_minus_100_b.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_05_galina_minus_100_a.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_06_galina_minus_100_b.json delete mode 100644 tests/mock_server/scenarios/pool/tecno_ozon_ft_07_vlad_plus_550.json delete mode 100644 tests/mock_server/scenarios/tecno_km4_ozon_fetch_profile.json delete mode 100644 tests/mock_server/scenarios/tecno_km4_ozon_fetch_transactions.json delete mode 100644 tests/mock_server/scenarios/tecno_ozon_fetch_transactions.json create mode 100644 tests/mock_server/setup.ps1 diff --git a/.gitignore b/.gitignore index 127330c..066d717 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Makefile /Contents/ /application.log /database/app_data.db +/build_x64/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 99537f9..d724db3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,27 @@ if (WIN32) ) endforeach() + # Copy mock-server test scripts into the build so they ship with the archive + set(_MOCK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests/mock_server") + set(_MOCK_FILES + "${_MOCK_DIR}/mock_server.py" + "${_MOCK_DIR}/gen_mock_data.py" + "${_MOCK_DIR}/gen_fetch_transactions.py" + "${_MOCK_DIR}/inject_random.py" + "${_MOCK_DIR}/run.sh" + "${_MOCK_DIR}/inject.sh" + "${_MOCK_DIR}/README.md" + "${_MOCK_DIR}/WINDOWS.md" + "${_MOCK_DIR}/seed_profile.example.json" + "${_MOCK_DIR}/setup.ps1" + ) + foreach(_f ${_MOCK_FILES}) + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/tests/mock_server" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_f}" "$/tests/mock_server/" + ) + endforeach() + # Copy Tesseract, Leptonica, and their transitive dependency DLLs file(GLOB _tesseract_dlls "${VCPKG_BIN_DIR}/tesseract*.dll" "${VCPKG_BIN_DIR}/leptonica*.dll" diff --git a/android/black/CommonScript.cpp b/android/black/CommonScript.cpp index 6374132..6b214ec 100644 --- a/android/black/CommonScript.cpp +++ b/android/black/CommonScript.cpp @@ -200,7 +200,7 @@ void CommonScript::updateTransactionData( thread->start(); } -void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) { +void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { @@ -236,7 +236,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = appCode; profileBody["phone_number"] = phone; - profileBody["balance"] = QString::number(qRound64(app.amount * 100)); + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; diff --git a/android/black/CommonScript.h b/android/black/CommonScript.h index 1949cea..7cdfff1 100644 --- a/android/black/CommonScript.h +++ b/android/black/CommonScript.h @@ -47,7 +47,7 @@ protected: ); // Создаёт bank_profile на сервере если ещё не создан - void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode); + void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); bool m_running = true; ScreenXmlParser xmlScreenParser; diff --git a/android/dushanbe/CommonScript.cpp b/android/dushanbe/CommonScript.cpp index 1ac0778..11d2ebb 100644 --- a/android/dushanbe/CommonScript.cpp +++ b/android/dushanbe/CommonScript.cpp @@ -235,7 +235,7 @@ void CommonScript::updateTransactionData( thread->start(); } -void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) { +void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { @@ -270,7 +270,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = appCode; profileBody["phone_number"] = phone; - profileBody["balance"] = QString::number(qRound64(app.amount * 100)); + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; diff --git a/android/dushanbe/CommonScript.h b/android/dushanbe/CommonScript.h index ea13920..42e35dc 100644 --- a/android/dushanbe/CommonScript.h +++ b/android/dushanbe/CommonScript.h @@ -45,7 +45,7 @@ protected: const QString &newDesc ); - void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode); + void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); bool m_running = true; ScreenXmlParser xmlScreenParser; diff --git a/android/dushanbe/GetProfileInfoScript.cpp b/android/dushanbe/GetProfileInfoScript.cpp index d4a570f..eef24bf 100644 --- a/android/dushanbe/GetProfileInfoScript.cpp +++ b/android/dushanbe/GetProfileInfoScript.cpp @@ -51,9 +51,11 @@ void GetProfileInfoScript::doStart() { } // Парсим баланс с домашнего экрана - const double balance = xmlScreenParser.parseBalanceFromHomeScreen(); - qDebug() << "[Dushanbe::GetProfileInfo] balance:" << balance; - BankProfileDAO::updateAmount(app.id, balance); + const double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen(); + qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance:" << parsedBalance; + if (parsedBalance > 0.0) { + BankProfileDAO::updateAmount(app.id, parsedBalance); + } // Тапаем по кнопке меню (гамбургер, левый верхний угол) // Это первый clickable ImageView в верхней панели @@ -105,7 +107,7 @@ void GetProfileInfoScript::doStart() { BankProfileDAO::updateProfileInfo(app.id, fullName, phone, ""); } - createBankProfileIfNeeded(device.id, m_appCode); + createBankProfileIfNeeded(device.id, m_appCode, parsedBalance); const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); QList> accountsToPost; diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp index fcf4c0e..2bed3fa 100644 --- a/android/ozon/CommonScript.cpp +++ b/android/ozon/CommonScript.cpp @@ -330,7 +330,7 @@ void CommonScript::updateTransactionData( thread->start(); } -void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) { +void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { @@ -366,7 +366,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = appCode; profileBody["phone_number"] = phone; - profileBody["balance"] = QString::number(qRound64(app.amount * 100)); + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; diff --git a/android/ozon/CommonScript.h b/android/ozon/CommonScript.h index d3dbbac..7768c24 100644 --- a/android/ozon/CommonScript.h +++ b/android/ozon/CommonScript.h @@ -58,7 +58,7 @@ protected: ); // Создаёт bank_profile на сервере если ещё не создан - void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode); + void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); // Проверяет что приложение ещё запущено на устройстве static bool isAppRunning(const QString &deviceId, const QString &packageName); diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp index 127f890..ae9a367 100644 --- a/android/ozon/GetProfileInfoScript.cpp +++ b/android/ozon/GetProfileInfoScript.cpp @@ -71,6 +71,23 @@ void GetProfileInfoScript::doStart() { QThread::msleep(500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // Парсим баланс с домашнего экрана + const QList parsedCards = xmlScreenParser.parseCardsOnHomeScreen(); + double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount; + qDebug() << "[GetProfileInfo] parseCardsOnHomeScreen returned" + << parsedCards.size() << "cards, balance:" << parsedBalance; + // Fallback: если парсинг не нашёл баланс, берём из materials + if (parsedBalance <= 0.0) { + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + for (const auto &acc : accounts) { + if (acc.amount > 0.0) { parsedBalance = acc.amount; break; } + } + qDebug() << "[GetProfileInfo] Fallback balance from materials:" << parsedBalance; + } + if (parsedBalance > 0.0) { + BankProfileDAO::updateAmount(app.id, parsedBalance); + } + // Находим имя пользователя в верхней части домашнего экрана и тапаем по нему Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); if (userNameNode.isEmpty()) { @@ -118,7 +135,7 @@ void GetProfileInfoScript::doStart() { qWarning() << m_error; } - // Отправляем bank_profile на сервер + // Отправляем bank_profile на сервер — используем свежепарсенные данные const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); const QMap currencyCodes = { @@ -133,7 +150,7 @@ void GetProfileInfoScript::doStart() { profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = m_appCode; profileBody["phone_number"] = phone; - profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100)); + profileBody["balance"] = QString::number(qRound64(parsedBalance * 100)); profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index 6449128..b21bf51 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -63,8 +63,17 @@ void LoginAndCheckAccountsScript::doStart() { if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); - // Создаём bank profile если ещё не создан - createBankProfileIfNeeded(device.id, m_appCode); + // Парсим баланс с домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + const QList parsedCards = xmlScreenParser.parseCardsOnHomeScreen(); + double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount; + qDebug() << "[LoginAndCheck] Parsed balance:" << parsedBalance << "cards:" << parsedCards.size(); + if (parsedBalance > 0.0) { + BankProfileDAO::updateAmount(app.id, parsedBalance); + } + + // Создаём/обновляем bank profile на сервере + createBankProfileIfNeeded(device.id, m_appCode, parsedBalance); if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) { QList > accounts = xmlScreenParser.parseAccountsInfo(); diff --git a/build_x64.bat b/build_x64.bat new file mode 100644 index 0000000..7f2fe92 --- /dev/null +++ b/build_x64.bat @@ -0,0 +1,85 @@ +@echo off +setlocal enabledelayedexpansion + +set PROJECT_DIR=%~dp0 +set BUILD_DIR=%PROJECT_DIR%build_x64 +set RELEASE_DIR=%BUILD_DIR%\Release +set QT_DIR=C:\Qt\6.9.0\msvc2022_64 +set WINDEPLOYQT=%QT_DIR%\bin\windeployqt.exe +set CONFIG_FILE=%PROJECT_DIR%config.ini +set TG_BOT_TOKEN=8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8 +set TG_CHAT_ID=-5177086220 + +for /f "tokens=2 delims==" %%V in ('findstr /i "version=" "%CONFIG_FILE%"') do set OLD_VERSION=%%V +set CURRENT_VERSION=%OLD_VERSION% + +if "%~1"=="-up-version" ( + echo === [0] Incrementing build version === + for /f "tokens=1-3 delims=." %%A in ("!OLD_VERSION!") do ( + set MAJOR=%%A + set MINOR=%%B + set /a PATCH=%%C+1 + ) + set CURRENT_VERSION=!MAJOR!.!MINOR!.!PATCH! + powershell -NoProfile -Command "(Get-Content '!CONFIG_FILE!') -replace 'version=!OLD_VERSION!', 'version=!CURRENT_VERSION!' | Set-Content '!CONFIG_FILE!'" + echo Version: !OLD_VERSION! -^> !CURRENT_VERSION! +) else ( + echo Version: !CURRENT_VERSION! +) + +set ZIP_NAME=ARC_x64_!CURRENT_VERSION!.zip + +echo === [1/5] Configuring CMake for x64 Release === +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" +cd /d "%BUILD_DIR%" +cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="%QT_DIR%" -DCMAKE_BUILD_TYPE=Release +if %ERRORLEVEL% neq 0 ( + echo ERROR: CMake configure failed + exit /b 1 +) + +echo === [2/5] Building Release === +cmake --build . --config Release +if %ERRORLEVEL% neq 0 ( + echo ERROR: Build failed + exit /b 1 +) + +echo === [3/5] Deploying Qt dependencies === +"%WINDEPLOYQT%" "%RELEASE_DIR%\ARC.exe" +if %ERRORLEVEL% neq 0 ( + echo ERROR: windeployqt failed + exit /b 1 +) + +echo === [4/5] Creating zip archive === +cd /d "%PROJECT_DIR%" +if exist "%ZIP_NAME%" del "%ZIP_NAME%" +powershell -NoProfile -Command "Compress-Archive -Path '%RELEASE_DIR%\*' -DestinationPath '%ZIP_NAME%' -Force" +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to create zip archive + exit /b 1 +) + +echo === [5/5] Uploading and sending link to Telegram === +for /f "delims=" %%B in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N').Substring(0,16)"') do set BIN_ID=arc-%%B +curl -s -H "Content-Type: application/octet-stream" -X POST --data-binary "@%PROJECT_DIR%%ZIP_NAME%" "https://filebin.net/!BIN_ID!/%ZIP_NAME%" > nul +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to upload archive + exit /b 1 +) +set DOWNLOAD_URL=https://filebin.net/!BIN_ID!/%ZIP_NAME% +echo Upload URL: !DOWNLOAD_URL! + +curl -s -X POST "https://api.telegram.org/bot%TG_BOT_TOKEN%/sendMessage" -d "chat_id=%TG_CHAT_ID%" -d "text=ARC x64 v!CURRENT_VERSION!: !DOWNLOAD_URL!" +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to send message to Telegram + exit /b 1 +) + +echo. +echo === Build complete! === +echo Version: %NEW_VERSION% +echo Archive: %PROJECT_DIR%%ZIP_NAME% +echo Download: !DOWNLOAD_URL! +endlocal diff --git a/config.ini b/config.ini index 785f64c..4842e63 100644 --- a/config.ini +++ b/config.ini @@ -3,10 +3,10 @@ list=ozon, black, dushanbe_city_bank [common] currencies=USD, ARS, RUB, TJS -version=0.0.1 +version=0.0.9 [db] -dbPath=~/CLionProjects/ARCDeskProject/ARCDeskProject/database/app_data.db +dbPath=./database/app_data.db [net] #apiBase=http://93.183.75.134:8005 @@ -35,4 +35,4 @@ phone_code=7 android_package_name=tj.dc.next1 currency=TJS name=Dushanbe City -phone_code=992 \ No newline at end of file +phone_code=992 diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 87ed40b..b05a92d 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -278,16 +278,12 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, QJsonValue taskData; if (event.type == EventType::FetchProfile) { - // FETCH_PROFILE всегда один объект. Amount читаем актуальный: - // - для Ozon скрипт обновляет MaterialDAO → берём из account; - // - для остальных (Dushanbe и т.п.) скрипт обновляет BankProfileDAO - // через updateAmount → берём из bank_profiles. - double freshAmount = account.amount; - if (event.bankName != "ozon") { - const BankProfileInfo freshApp = BankProfileDAO::getApplication( - account.deviceId, event.bankName); - if (freshApp.id != -1) freshAmount = freshApp.amount; - } + // FETCH_PROFILE: баланс из bank_profiles (скрипт обновляет через + // BankProfileDAO::updateAmount), fallback на materials. + const BankProfileInfo freshApp = BankProfileDAO::getApplication( + account.deviceId, event.bankName); + double freshAmount = (freshApp.id != -1 && freshApp.amount > 0.0) + ? freshApp.amount : account.amount; EventDAO::updateEventAmount(event.id, freshAmount); QJsonObject obj; obj["bank_account_id"] = account.materialId; @@ -569,8 +565,12 @@ void EventHandler::clearAll() { for (QThread *thread : m_threads) { if (thread->isRunning()) { thread->requestInterruption(); - thread->terminate(); - thread->wait(); + thread->quit(); + if (!thread->wait(3000)) { + qWarning() << "[EventHandler] Thread did not stop in 3s, terminating"; + thread->terminate(); + thread->wait(2000); + } } thread->deleteLater(); } diff --git a/split_zip.ps1 b/split_zip.ps1 new file mode 100644 index 0000000..6664c79 --- /dev/null +++ b/split_zip.ps1 @@ -0,0 +1,48 @@ +param( + [string]$SourceDir, + [string]$OutputDir, + [string]$Prefix = "ARC_x64_part", + [long]$MaxSize = 48000000 +) + +$SourceDir = $SourceDir.Trim('"', "'", ' ') +$OutputDir = $OutputDir.Trim('"', "'", ' ') + +$part = 1 +$currentSize = 0 +$currentFiles = @() +$allItems = Get-ChildItem -Path $SourceDir -Recurse -File | Sort-Object Length + +function Flush-Part { + param($files, $partNum, $size) + $zipName = Join-Path $OutputDir "$Prefix$partNum.zip" + Write-Host "Creating part $partNum ($([math]::Round($size/1MB,1)) MB, $($files.Count) files)" + $tempDir = Join-Path $env:TEMP "arc_split_$partNum" + if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force } + foreach ($cf in $files) { + $dest = Join-Path $tempDir $cf.Rel + $destDir = Split-Path $dest -Parent + if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } + Copy-Item $cf.Full $dest + } + Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zipName -Force + Remove-Item $tempDir -Recurse -Force +} + +foreach ($f in $allItems) { + $rel = $f.FullName.Substring($SourceDir.TrimEnd('\').Length) + if ($currentSize + $f.Length -gt $MaxSize -and $currentFiles.Count -gt 0) { + Flush-Part $currentFiles $part $currentSize + $part++ + $currentSize = 0 + $currentFiles = @() + } + $currentFiles += @{ Full = $f.FullName; Rel = $rel } + $currentSize += $f.Length +} + +if ($currentFiles.Count -gt 0) { + Flush-Part $currentFiles $part $currentSize +} + +Write-Host "Done: $part part(s) created." diff --git a/tests/mock_server/WINDOWS.md b/tests/mock_server/WINDOWS.md index 5b9ac59..7256417 100644 --- a/tests/mock_server/WINDOWS.md +++ b/tests/mock_server/WINDOWS.md @@ -1,20 +1,203 @@ # Запуск тестов на Windows +## Оглавление + +1. [Быстрый старт](#1-быстрый-старт) +2. [Вспомогательные скрипты](#2-вспомогательные-скрипты) +3. [Генерация FETCH_TRANSACTIONS задач](#3-генерация-fetch_transactions-задач) +4. [Ручной запуск (пошагово)](#4-ручной-запуск-пошагово) +5. [Мониторинг и отчёт](#5-мониторинг-и-отчёт) +6. [Ручной инжект задач](#6-ручной-инжект-задач) +7. [ADB на Windows](#7-adb-на-windows) +8. [Типовые проблемы](#8-типовые-проблемы) +9. [Быстрый чеклист](#9-быстрый-чеклист) + --- -## 1. Установка зависимостей +## 1. Быстрый старт ```powershell -# Python 3.9+ (если нет — скачать с python.org, при установке включить «Add to PATH») +cd tests\mock_server +powershell -ExecutionPolicy Bypass -File setup.ps1 +``` + +Скрипт `setup.ps1` автоматически: +1. Проверяет / устанавливает **Python 3.12+** (через winget) +2. Устанавливает **Flask** +3. Создаёт вспомогательные скрипты `gen_transactions.ps1` и `run_server.ps1` +4. Находит `config.ini` и проверяет **базу данных** +5. Удаляет старые сгенерированные данные (профиль + scenarios, пул не трогает) +6. Генерирует `default_profile.json` и FETCH_PROFILE scenarios из БД +7. Если порт занят — убивает старый процесс +8. Переключает `config.ini` на mock (`apiBase=http://127.0.0.1:8888`) +9. Показывает сводку: устройства, профили, материалы, пул транзакций +10. Запускает mock-сервер и открывает HTML-отчёт в браузере + +При первом логине ARC мок автоматически ставит **FETCH_PROFILE** задачи +на каждый материал. **FETCH_TRANSACTIONS** задачи нужно генерировать отдельно +(см. [раздел 3](#3-генерация-fetch_transactions-задач)). + +### Параметры + +| Параметр | По умолчанию | Описание | +|---|---|---| +| `-Port` | 8888 | Порт mock-сервера | +| `-FtPerMaterial` | 3 | FETCH_TRANSACTIONS на материал при бутстрапе (из пула) | +| `-SkipInstall` | — | Пропустить установку Python/Flask | +| `-GenerateOnly` | — | Только сгенерировать данные, не запускать сервер | +| `-Clean` | — | Также очистить пул транзакций (`scenarios/pool/`) | + +### Примеры + +```powershell +# Всё автоматически (установка + генерация + запуск) +.\setup.ps1 + +# На другом порту с 5 транзакциями на материал +.\setup.ps1 -Port 9999 -FtPerMaterial 5 + +# Пропустить установку (Python/Flask уже есть) +.\setup.ps1 -SkipInstall + +# Только подготовить данные, сервер не запускать +.\setup.ps1 -GenerateOnly + +# Полная очистка (включая пул транзакций) +.\setup.ps1 -Clean +``` + +--- + +## 2. Вспомогательные скрипты + +После первого запуска `setup.ps1` в папке `tests\mock_server\` появляются +скрипты с зашитым путём до Python — искать `python.exe` вручную не нужно: + +| Скрипт | Назначение | Запуск | +|---|---|---| +| `gen_transactions.ps1` | Генерация FETCH_TRANSACTIONS с устройства | `powershell -ExecutionPolicy Bypass -File gen_transactions.ps1` | +| `run_server.ps1` | Запуск mock-сервера | `powershell -ExecutionPolicy Bypass -File run_server.ps1` | +| `setup.ps1` | Полная установка + генерация + запуск | `powershell -ExecutionPolicy Bypass -File setup.ps1` | + +Скрипты принимают дополнительные аргументы, которые передаются в Python-скрипт: + +```powershell +# Генерация с явным серийником +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --serial 28291FDH300712 + +# Запуск сервера на другом порту +powershell -ExecutionPolicy Bypass -File run_server.ps1 --port 9999 +``` + +--- + +## 3. Генерация FETCH_TRANSACTIONS задач + +FETCH_TRANSACTIONS задачи генерируются **только вручную** с живого устройства. +Скрипт подключается по ADB, проходит по списку транзакций на экране +(тап -> снятие суммы/даты -> назад) и сохраняет JSON-файлы в `scenarios/pool/`. + +### Подготовка + +1. Подключите устройство по USB +2. Убедитесь что ADB видит устройство: + ```powershell + tools\adb\windows\adb.exe devices + # Должно показать серийник со статусом "device" + ``` +3. Откройте на устройстве банковское приложение на экране **списка транзакций** + - Ozon: раздел "Операции" -> список переводов/пополнений + - Dushanbe City Bank: раздел "Операции" -> список P2P операций + +### Запуск + +```powershell +# Через вспомогательный скрипт (рекомендуется) +cd tests\mock_server +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 + +# Или напрямую (если Python в PATH) +python tests\mock_server\gen_fetch_transactions.py +``` + +Скрипт: +1. Покажет меню с активными материалами: + ``` + Available materials (3): + 1. Pixel 7 / ozon / Галина Терехина (material 30bc106c... serial=28291FDH30xxxx) + 2. Pixel 7 / dushanbe_city_bank / Мехрубон (material 61d967d1...) + 3. TECNO KM4 / ozon / Владислав Касаткин (material 49001bb5...) + Pick [1-3] or 'all': + ``` +2. Выберите нужный материал (номер) или `all` для всех +3. Скрипт попросит подтвердить что список транзакций открыт -> нажмите **Enter** +4. Автоматически пройдёт по каждой транзакции на экране (**не трогайте телефон!**) +5. Сохранит задачи в `scenarios/pool/` +6. Если mock-сервер запущен — сразу инжектнёт задачи в него + +### Флаги + +| Флаг | Описание | +|---|---| +| `--serial 28291FDH30xxxx` | Явный ADB-серийник (если несколько устройств) | +| `--kopecks 100` | Множитель суммы (по умолчанию 100: рубли -> копейки) | +| `--config path` | Путь к config.ini | + +### Примеры + +```powershell +# Интерактивный выбор материала +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 + +# Для конкретного устройства +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --serial 28291FDH300712 + +# С другим config +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --config cmake-build-debug\config.ini +``` + +### Результат + +После генерации в `scenarios/pool/` появятся файлы вида: +``` +pixel_7_ozon_ft_01_plus_15000_143022.json +pixel_7_ozon_ft_02_minus_500_150133.json +tecno_km4_ozon_ft_01_plus_3000_091500.json +``` + +Каждый файл содержит задачу с реальной суммой (в копейках) и временем (UTC): +```json +{ + "bank_name": "ozon", + "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", + "type": "FETCH_TRANSACTIONS", + "body": { + "amount": 1500000, + "created_at": "2026-04-10T11:30:22Z" + } +} +``` + +При следующем запуске `setup.ps1` пул сохраняется (если не указать `-Clean`), +и мок будет раздавать задачи из него при бутстрапе. + +--- + +## 4. Ручной запуск (пошагово) + +Если не хотите использовать `setup.ps1`: + +### 4.1. Установка зависимостей + +```powershell +# Python 3.9+ (если нет — скачать с python.org, включить «Add to PATH») python --version # Flask pip install flask ``` ---- - -## 2. Подготовка config.ini +### 4.2. Подготовка config.ini В `config.ini` (рядом с `ARC.exe`) переключить `apiBase`: @@ -23,12 +206,23 @@ pip install flask apiBase=http://127.0.0.1:8888 ``` -> Совет: держите отдельную копию `config.ini.mock`, а перед тестом +> Совет: держите копию `config.ini.mock`, а перед тестом > `copy config.ini.mock config.ini`. ---- +### 4.3. Генерация профиля из БД -## 3. Запуск mock-сервера +```powershell +# Из корня проекта +python tests\mock_server\gen_mock_data.py +``` + +Создаёт `default_profile.json` и FETCH_PROFILE scenarios. + +### 4.4. Генерация пула транзакций (опционально) + +См. [раздел 3](#3-генерация-fetch_transactions-задач). + +### 4.5. Запуск mock-сервера ```powershell cd tests\mock_server @@ -43,26 +237,23 @@ python mock_server.py --port 8888 * Running on http://127.0.0.1:8888 ``` -Флаги: -- `--ft-per-material 3` — сколько FETCH_TRANSACTIONS на материал (default 3) -- `--seed 42` — фиксированный RNG для воспроизводимости +### 4.6. Запуск ARC ---- - -## 4. Запуск ARC - -В отдельном терминале (cmd/PowerShell): +В отдельном терминале: ```powershell cd build\Release ARC.exe ``` -Или через CLion: Run → ARC. +Или через CLion: Run -> ARC. -При логине мок автоматически поставит задачи (1 FETCH_PROFILE + 3 FETCH_TRANSACTIONS -на каждый материал). Задачи раздаются по 1 на material за поллинг — следующая -только после результата предыдущей. +При логине мок автоматически ставит задачи: +- 1 x FETCH_PROFILE на каждый материал (всегда) +- N x FETCH_TRANSACTIONS из пула (если пул не пуст) + +Задачи раздаются по 1 на material за поллинг — следующая только +после результата предыдущей. --- @@ -89,20 +280,25 @@ curl -s http://127.0.0.1:8888/_admin/report Invoke-WebRequest http://127.0.0.1:8888/_admin/report -OutFile report.json ``` +На главной странице отчёта (`/_admin/report/html`) отображаются: +- Счётчики задач (всего / выполнено / в очереди) +- **Загруженные данные** — устройства, профили, материалы с количеством задач в пуле (группировка по устройствам) +- Задачи с результатами, сгруппированные по устройствам + --- ## 6. Ручной инжект задач ```powershell -# Одна задача +# Одна задача из файла curl -X POST http://127.0.0.1:8888/_admin/inject ^ -H "Content-Type: application/json" ^ - --data-binary @scenarios\tecno_ozon_fetch_profile.json + --data-binary @scenarios\pixel_7_ozon_fetch_profile.json -# 5 случайных из пула +# N случайных из пула python inject_random.py --count 5 -# Повторный бутстрап +# Повторный бутстрап (1 FP + N FT на material) curl -X POST http://127.0.0.1:8888/_admin/bootstrap # Сброс (очистка очереди + результатов) @@ -113,15 +309,15 @@ curl -X POST http://127.0.0.1:8888/_admin/reset ## 7. ADB на Windows -ARC использует ADB из `tools\adb\windows\`. Убедитесь, что: +ARC использует ADB из `tools\adb\windows\`. Проверка: ```powershell tools\adb\windows\adb.exe devices ``` -показывает устройства в статусе `device`. +Должно показать устройства в статусе `device`. -Если ADB занят другим процессом (Android Studio, scrcpy), закройте его: +Если ADB занят другим процессом (Android Studio, scrcpy): ```powershell tools\adb\windows\adb.exe kill-server tools\adb\windows\adb.exe start-server @@ -130,37 +326,32 @@ tools\adb\windows\adb.exe devices --- -## 8. Типовые проблемы (Windows-специфичные) +## 8. Типовые проблемы -- **`python` не найден** → использовать `py` или `python3`. Или добавить - Python в PATH. -- **Порт 8888 занят** → `netstat -ano | findstr :8888`, затем - `taskkill /PID /F`. Или запустить мок на другом порту: - `python mock_server.py --port 9999` и поменять `apiBase` в `config.ini`. -- **`curl` нет** → использовать `Invoke-WebRequest` (PowerShell) или - установить curl через `winget install curl`. -- **Firewall блокирует** → разрешить `python.exe` в Windows Firewall - (или отключить на время теста). Мок слушает только `127.0.0.1` — внешний - доступ не нужен. -- **ARC не видит устройства** → проверить USB-драйверы, включить - «USB debugging» на телефоне, подтвердить RSA-ключ. -- **Кодировка в консоли** → `chcp 65001` перед запуском мока (UTF-8), - иначе русские символы в логах могут быть кракозябрами. -- **SSL ошибки при сборке ARC** → убедиться, что OpenSSL DLL скопированы - в директорию сборки (`cmake --build` делает это автоматически через - post-build commands в CMakeLists.txt). +| Проблема | Решение | +|---|---| +| `python` не найден | Запустите `setup.ps1` — установит автоматически. Или используйте скрипты `gen_transactions.ps1` / `run_server.ps1` — в них путь уже зашит | +| Порт 8888 занят | `setup.ps1` убивает старый процесс автоматически. Вручную: `netstat -ano \| findstr :8888` -> `taskkill /PID /F` | +| `curl` нет | `Invoke-WebRequest` (PowerShell) или `winget install curl` | +| Firewall блокирует | Разрешить `python.exe` в Windows Firewall. Мок слушает только `127.0.0.1` | +| ARC не видит устройства | Проверить USB-драйверы, включить USB debugging, подтвердить RSA-ключ | +| Кракозябры в консоли | `chcp 65001` перед запуском (UTF-8). Скрипты `.ps1` делают это автоматически | +| BOM в config.ini | `setup.ps1` пишет без BOM. Если правили вручную — пересохраните в UTF-8 без BOM (VS Code) | +| No active materials | БД пустая. Запустите ARC с боевым сервером, залогиньтесь, добавьте устройства | +| Пул транзакций пуст | Сгенерируйте: [раздел 3](#3-генерация-fetch_transactions-задач) | +| `No ADB devices match` | Устройства не подключены или serial не совпадает с БД. Проверьте `adb devices` | +| SSL ошибки при сборке | OpenSSL DLL копируются автоматически через CMake post-build | --- ## 9. Быстрый чеклист ``` -[ ] Python 3.9+ установлен, pip install flask -[ ] config.ini: apiBase=http://127.0.0.1:8888 -[ ] Устройства в adb devices → device -[ ] Терминал 1: python mock_server.py --port 8888 -[ ] Терминал 2: ARC.exe +[ ] powershell -ExecutionPolicy Bypass -File setup.ps1 +[ ] Устройства в adb devices -> device +[ ] (опционально) powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 +[ ] ARC.exe запущен (отдельный терминал) [ ] Браузер: http://127.0.0.1:8888/_admin/report/html -[ ] Дождаться completed = 12 в отчёте +[ ] Дождаться completed = всего в отчёте [ ] Вернуть config.ini: apiBase=http://93.183.75.134:8005 ``` diff --git a/tests/mock_server/gen_fetch_transactions.py b/tests/mock_server/gen_fetch_transactions.py index 4bfcc2d..5259d06 100755 --- a/tests/mock_server/gen_fetch_transactions.py +++ b/tests/mock_server/gen_fetch_transactions.py @@ -95,6 +95,28 @@ def adb(serial, *args): return subprocess.run(cmd, check=True, capture_output=True) +PACKAGE_TO_BANK = { + "ru.ozon.fintech.finance": "ozon", + "tj.dc.next1": "dushanbe_city_bank", + "app.blackwallet.ar": "black", +} + + +def get_foreground_package(serial) -> str | None: + """Return the package name of the currently focused app.""" + try: + r = adb(serial, "shell", "dumpsys", "window", "windows") + for line in r.stdout.decode("utf-8", errors="replace").splitlines(): + if "mCurrentFocus" in line or "mFocusedApp" in line: + # e.g. mCurrentFocus=Window{... u0 tj.dc.next1/tj.dc.next1.MainActivity} + m = re.search(r"u\d+\s+(\S+)/", line) + if m: + return m.group(1) + except Exception: + pass + return None + + def dump_xml(serial) -> str: adb(serial, "shell", "uiautomator", "dump", "/sdcard/_gen_ft.xml") r = adb(serial, "exec-out", "cat", "/sdcard/_gen_ft.xml") @@ -219,8 +241,13 @@ def short_label(amount, time_str): def collect_tasks(serial, bank, mid, device_tz, kopecks, prefix): xml = dump_xml(serial) - is_ozon = bank == "ozon" or is_ozon_list(xml) - is_dush = "dushanbe" in bank or is_dushanbe_list(xml) + # Detect bank by foreground app package first, fall back to XML heuristics + fg_pkg = get_foreground_package(serial) + fg_bank = PACKAGE_TO_BANK.get(fg_pkg) if fg_pkg else None + if fg_bank: + print(f"Foreground app: {fg_pkg} -> {fg_bank}") + is_dush = fg_bank == "dushanbe_city_bank" or "dushanbe" in bank or is_dushanbe_list(xml) + is_ozon = not is_dush and (fg_bank == "ozon" or bank == "ozon" or is_ozon_list(xml)) if not is_ozon and not is_dush: print("ERROR: not on a recognized transactions list", file=sys.stderr) diff --git a/tests/mock_server/gen_mock_data.py b/tests/mock_server/gen_mock_data.py index 614b100..75ccdb5 100755 --- a/tests/mock_server/gen_mock_data.py +++ b/tests/mock_server/gen_mock_data.py @@ -15,7 +15,7 @@ Or specify config explicitly: python tests\\mock_server\\gen_mock_data.py --config C:\\path\\to\\config.ini On Windows: - python tests\mock_server\gen_mock_data.py + python tests\\mock_server\\gen_mock_data.py """ import argparse @@ -30,7 +30,7 @@ from pathlib import Path def resolve_db_path(config_path: Path) -> Path: config = configparser.ConfigParser() - config.read(str(config_path), encoding="utf-8") + config.read(str(config_path), encoding="utf-8-sig") raw = config.get("db", "dbPath", fallback="") if not raw: print(f"ERROR: [db] dbPath not found in {config_path}", file=sys.stderr) @@ -169,18 +169,6 @@ def generate(materials: list, out_dir: Path): ) print(f" wrote scenarios/{fname}") - # Also write a fetch_transactions scenario (empty body, for manual use) - ft_fname = f"{dev_short}_{bank}_fetch_transactions.json" - ft_task = { - "bank_name": bank, - "material_id": row["material_id"], - "type": "FETCH_TRANSACTIONS", - "body": {}, - } - (scenarios_dir / ft_fname).write_text( - json.dumps(ft_task, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - devices_seen.setdefault(dev_short, []).append( f"{bank} ({row['material_id'][:8]}…)" ) diff --git a/tests/mock_server/gen_transactions.ps1 b/tests/mock_server/gen_transactions.ps1 new file mode 100644 index 0000000..acffc1e --- /dev/null +++ b/tests/mock_server/gen_transactions.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$_configCandidates = @( + (Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"), + (Join-Path $PSScriptRoot "..\..\config.ini"), + (Join-Path $PSScriptRoot "..\config.ini"), + (Join-Path $PSScriptRoot "config.ini") +) +$_configPath = $null +foreach ($_c in $_configCandidates) { + $_resolved = [System.IO.Path]::GetFullPath($_c) + if (Test-Path $_resolved) { $_configPath = $_resolved; break } +} +if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 } +$_configArg = @("--config", $_configPath) +Write-Host "Config: $_configPath" -ForegroundColor Gray +& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args \ No newline at end of file diff --git a/tests/mock_server/mock_server.py b/tests/mock_server/mock_server.py index 31db30f..e0286d2 100644 --- a/tests/mock_server/mock_server.py +++ b/tests/mock_server/mock_server.py @@ -93,7 +93,7 @@ def load_default_profile(): if not p.exists(): print(f"[mock] no {p.name} found — profile starts empty") return - data = json.loads(p.read_text()) + data = json.loads(p.read_text(encoding="utf-8")) _bank_profiles = data.get("bank_profiles", []) mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) @@ -117,7 +117,7 @@ def load_pool(): count = 0 for f in sorted(pool_dir.glob("*.json")): try: - task = json.loads(f.read_text()) + task = json.loads(f.read_text(encoding="utf-8")) except Exception as e: print(f"[mock] skipping {f.name}: {e}") continue @@ -559,6 +559,97 @@ def render_entry_html(e) -> str: """ +def render_profile_section() -> str: + """Render loaded bank profiles / materials as an HTML info block.""" + import html as _html + if not _bank_profiles: + return '
Профиль не загружен
' + + # Group by device + devices: dict = {} + for bp in _bank_profiles: + dev_id = bp.get("device_id", "?") + label = bp.get("device_label") or (dev_id[:8] + "…") + if dev_id not in devices: + devices[dev_id] = {"label": label, "profiles": []} + devices[dev_id]["profiles"].append(bp) + + device_blocks = [] + for dev_id, dev in devices.items(): + label = _html.escape(dev["label"]) + dev_mats = sum(len(bp.get("materials", [])) for bp in dev["profiles"]) + bp_cards = [] + for bp in dev["profiles"]: + bank = _html.escape(bp.get("bank_name", "?")) + first = bp.get("first_name") or "" + last = bp.get("last_name") or "" + name = _html.escape(f"{first} {last}".strip() or "—") + phone = _html.escape(bp.get("phone_number") or "—") + mats = bp.get("materials", []) + mat_rows = [] + for m in mats: + mid = _html.escape(m.get("id", "?")[:12]) + card = _html.escape(m.get("card_number") or "—") + cur = m.get("currency", "") + state = _html.escape(m.get("state", "?")) + # Count pool tasks for this material + pool_n = len(_pool_by_material.get(m.get("id", ""), [])) + pool_badge = (f'{pool_n}' + if pool_n > 0 else '0') + mat_rows.append( + f'{mid}…' + f'{card}{cur}' + f'{state}' + f'{pool_badge}' + ) + mat_table = "" + if mat_rows: + mat_table = ( + '' + '' + '' + + "".join(mat_rows) + + '
Material IDКартаВалютаСтатусПул
' + ) + bp_cards.append( + f'
' + f'
' + f'{bank}' + f'{name}' + f'{phone}' + f'
' + f'{mat_table}' + f'
' + ) + + device_blocks.append( + f'
' + f'
' + f'{label}' + f'{len(dev["profiles"])} профилей, {dev_mats} материалов' + f'
' + f'
{"".join(bp_cards)}
' + f'
' + ) + + pool_count = sum(len(v) for v in _pool_by_material.values()) + pool_info = (f'{pool_count} задач в пуле' + if pool_count > 0 + else 'пул пуст') + total_mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) + + return ( + '
' + 'Загруженные данные ' + f'({len(devices)} устройств, ' + f'{len(_bank_profiles)} профилей, ' + f'{total_mats} материалов, ' + f'{pool_info})' + + "".join(device_blocks) + + '
' + ) + + def render_report_html(entries, summary) -> str: import html as _html @@ -697,6 +788,82 @@ def render_report_html(entries, summary) -> str: .group h2 .g-done {{ color: var(--completed); font-weight: 700; }} .group h2 .g-total {{ color: var(--muted); }} .updating {{ opacity: 0.6; }} + /* ─── Profile section ─── */ + .profile-section {{ + margin-bottom: 16px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel); + padding: 10px 14px; + }} + .profile-section summary {{ + cursor: pointer; + font-weight: 600; + font-size: 14px; + padding: 4px 0; + user-select: none; + }} + .bp-grid {{ + display: flex; flex-wrap: wrap; gap: 10px; + margin-top: 10px; + }} + .bp-card {{ + background: #161921; + border: 1px solid var(--border); + border-radius: 6px; + padding: 10px 12px; + min-width: 320px; + flex: 1; + }} + .bp-header {{ + display: flex; gap: 10px; align-items: center; + margin-bottom: 8px; flex-wrap: wrap; + }} + .dev-label {{ color: #ffb86c; font-weight: 700; }} + .bp-bank {{ color: #9cdcfe; font-weight: 600; }} + .bp-name {{ color: var(--text); }} + .bp-phone {{ color: var(--muted); font-family: monospace; font-size: 12px; }} + .mat-table {{ + width: 100%; + border-collapse: collapse; + font-size: 12px; + }} + .mat-table th {{ + text-align: left; + color: var(--muted); + font-weight: 500; + border-bottom: 1px solid var(--border); + padding: 3px 6px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.04em; + }} + .mat-table td {{ + padding: 4px 6px; + border-bottom: 1px solid #1f232d; + }} + .mono {{ font-family: "SF Mono", Menlo, Consolas, monospace; }} + .badge-sm {{ + padding: 1px 6px; border-radius: 8px; + font-size: 10px; font-weight: 600; text-transform: uppercase; + }} + .badge-sm.enabled {{ background: var(--completed); color: #0a1a0a; }} + .badge-sm.disabled {{ background: var(--muted); color: #1a1d25; }} + .pool-ok {{ color: var(--completed); }} + .pool-empty {{ color: var(--pending); }} + .dev-group {{ + margin-top: 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: #13161d; + padding: 10px 14px; + }} + .dev-group-header {{ + display: flex; align-items: baseline; gap: 10px; + padding-bottom: 8px; margin-bottom: 10px; + border-bottom: 1px solid var(--border); + }} + .dev-group-header .dev-label {{ font-size: 14px; }} @@ -707,6 +874,7 @@ def render_report_html(entries, summary) -> str:
{summary['pending']}
в очереди
{summary['unsolicited']}
незапрошенные
+ {render_profile_section()}
{body_rows}
diff --git a/tests/mock_server/run_server.ps1 b/tests/mock_server/run_server.ps1 new file mode 100644 index 0000000..aa4dff6 --- /dev/null +++ b/tests/mock_server/run_server.ps1 @@ -0,0 +1,2 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\mock_server.py" --port 8888 @args \ No newline at end of file diff --git a/tests/mock_server/scenarios/pixel7_dushanbe_fetch_transactions.json b/tests/mock_server/scenarios/pixel7_dushanbe_fetch_transactions.json deleted file mode 100644 index 1fc8e2b..0000000 --- a/tests/mock_server/scenarios/pixel7_dushanbe_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/scenarios/pixel7_ozon_fetch_transactions.json b/tests/mock_server/scenarios/pixel7_ozon_fetch_transactions.json deleted file mode 100644 index b0e1c9f..0000000 --- a/tests/mock_server/scenarios/pixel7_ozon_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_profile.json b/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_profile.json deleted file mode 100644 index 8c93290..0000000 --- a/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_profile.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_PROFILE", - "body": {} -} diff --git a/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_transactions.json b/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_transactions.json deleted file mode 100644 index 1fc8e2b..0000000 --- a/tests/mock_server/scenarios/pixel_7_dushanbe_city_bank_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/scenarios/pixel_7_ozon_fetch_profile.json b/tests/mock_server/scenarios/pixel_7_ozon_fetch_profile.json deleted file mode 100644 index 6d2cca9..0000000 --- a/tests/mock_server/scenarios/pixel_7_ozon_fetch_profile.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_PROFILE", - "body": {} -} diff --git a/tests/mock_server/scenarios/pixel_7_ozon_fetch_transactions.json b/tests/mock_server/scenarios/pixel_7_ozon_fetch_transactions.json deleted file mode 100644 index b0e1c9f..0000000 --- a/tests/mock_server/scenarios/pixel_7_ozon_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_01_embedded_minus_1.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_01_embedded_minus_1.json deleted file mode 100644 index 0566385..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_01_embedded_minus_1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -100, - "created_at": "2026-04-10T09:39:15Z" - }, - "_tag": "pixel7_dushanbe_01_embedded_minus_1" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_02_minus_1_2131.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_02_minus_1_2131.json deleted file mode 100644 index 26e54d0..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_02_minus_1_2131.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -100, - "created_at": "2026-04-05T14:31:21Z" - }, - "_tag": "pixel7_dushanbe_02_minus_1_2131" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_03_minus_1_2125.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_03_minus_1_2125.json deleted file mode 100644 index f74ba7e..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_03_minus_1_2125.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -100, - "created_at": "2026-04-05T14:25:48Z" - }, - "_tag": "pixel7_dushanbe_03_minus_1_2125" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_04_minus_1_1836.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_04_minus_1_1836.json deleted file mode 100644 index 112d97b..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_04_minus_1_1836.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -100, - "created_at": "2026-04-05T11:36:06Z" - }, - "_tag": "pixel7_dushanbe_04_minus_1_1836" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_05_minus_1_1819.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_05_minus_1_1819.json deleted file mode 100644 index 7ec7da9..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_05_minus_1_1819.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -100, - "created_at": "2026-04-05T11:19:43Z" - }, - "_tag": "pixel7_dushanbe_05_minus_1_1819" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_06_plus_1_2052.json b/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_06_plus_1_2052.json deleted file mode 100644 index 62a1bdf..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_dushanbe_ft_06_plus_1_2052.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "dushanbe_city_bank", - "material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 100, - "created_at": "2026-04-03T13:52:06Z" - }, - "_tag": "pixel7_dushanbe_06_plus_1_2052" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_01_evgeny_plus_100.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_01_evgeny_plus_100.json deleted file mode 100644 index 8391848..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_01_evgeny_plus_100.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 10000, - "created_at": "2026-04-11T10:39:00Z" - }, - "_tag": "pixel7_ozon_01_evgeny_plus_100" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_02_vlad_minus_100.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_02_vlad_minus_100.json deleted file mode 100644 index 40612bb..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_02_vlad_minus_100.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -10000, - "created_at": "2026-04-10T09:40:00Z" - }, - "_tag": "pixel7_ozon_02_vlad_minus_100" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_03_vlad_minus_300.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_03_vlad_minus_300.json deleted file mode 100644 index f753eef..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_03_vlad_minus_300.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -30000, - "created_at": "2026-04-08T08:34:00Z" - }, - "_tag": "pixel7_ozon_03_vlad_minus_300" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_04_vlad_plus_100.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_04_vlad_plus_100.json deleted file mode 100644 index 6619834..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_04_vlad_plus_100.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 10000, - "created_at": "2026-04-08T08:14:00Z" - }, - "_tag": "pixel7_ozon_04_vlad_plus_100" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_05_vlad_plus_100_b.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_05_vlad_plus_100_b.json deleted file mode 100644 index cb38b8b..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_05_vlad_plus_100_b.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 10000, - "created_at": "2026-04-08T08:02:00Z" - }, - "_tag": "pixel7_ozon_05_vlad_plus_100_b" -} diff --git a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_06_evgeny_plus_100_b.json b/tests/mock_server/scenarios/pool/pixel7_ozon_ft_06_evgeny_plus_100_b.json deleted file mode 100644 index ca93087..0000000 --- a/tests/mock_server/scenarios/pool/pixel7_ozon_ft_06_evgeny_plus_100_b.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 10000, - "created_at": "2026-04-06T13:54:00Z" - }, - "_tag": "pixel7_ozon_06_evgeny_plus_100_b" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_01_galina_plus_100.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_01_galina_plus_100.json deleted file mode 100644 index 26988ec..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_01_galina_plus_100.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 10000, - "created_at": "2026-04-10T09:40:00Z" - }, - "_tag": "tecno_ozon_01_galina_plus_100" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_02_galina_plus_300.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_02_galina_plus_300.json deleted file mode 100644 index 31c0474..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_02_galina_plus_300.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 30000, - "created_at": "2026-04-08T08:34:00Z" - }, - "_tag": "tecno_ozon_02_galina_plus_300" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_03_vlad_minus_100_a.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_03_vlad_minus_100_a.json deleted file mode 100644 index 71857e0..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_03_vlad_minus_100_a.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -10000, - "created_at": "2026-04-08T08:32:00Z" - }, - "_tag": "tecno_ozon_03_vlad_minus_100_a" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_04_vlad_minus_100_b.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_04_vlad_minus_100_b.json deleted file mode 100644 index f9fb7cb..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_04_vlad_minus_100_b.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -10000, - "created_at": "2026-04-08T08:23:00Z" - }, - "_tag": "tecno_ozon_04_vlad_minus_100_b" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_05_galina_minus_100_a.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_05_galina_minus_100_a.json deleted file mode 100644 index 5455a40..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_05_galina_minus_100_a.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -10000, - "created_at": "2026-04-08T08:14:00Z" - }, - "_tag": "tecno_ozon_05_galina_minus_100_a" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_06_galina_minus_100_b.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_06_galina_minus_100_b.json deleted file mode 100644 index 37b938b..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_06_galina_minus_100_b.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": -10000, - "created_at": "2026-04-08T08:02:00Z" - }, - "_tag": "tecno_ozon_06_galina_minus_100_b" -} diff --git a/tests/mock_server/scenarios/pool/tecno_ozon_ft_07_vlad_plus_550.json b/tests/mock_server/scenarios/pool/tecno_ozon_ft_07_vlad_plus_550.json deleted file mode 100644 index 4d08d38..0000000 --- a/tests/mock_server/scenarios/pool/tecno_ozon_ft_07_vlad_plus_550.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": { - "amount": 55000, - "created_at": "2026-03-20T14:36:00Z" - }, - "_tag": "tecno_ozon_07_vlad_plus_550" -} diff --git a/tests/mock_server/scenarios/tecno_km4_ozon_fetch_profile.json b/tests/mock_server/scenarios/tecno_km4_ozon_fetch_profile.json deleted file mode 100644 index c5858fb..0000000 --- a/tests/mock_server/scenarios/tecno_km4_ozon_fetch_profile.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_PROFILE", - "body": {} -} diff --git a/tests/mock_server/scenarios/tecno_km4_ozon_fetch_transactions.json b/tests/mock_server/scenarios/tecno_km4_ozon_fetch_transactions.json deleted file mode 100644 index 23b91ee..0000000 --- a/tests/mock_server/scenarios/tecno_km4_ozon_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/scenarios/tecno_ozon_fetch_transactions.json b/tests/mock_server/scenarios/tecno_ozon_fetch_transactions.json deleted file mode 100644 index 23b91ee..0000000 --- a/tests/mock_server/scenarios/tecno_ozon_fetch_transactions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "bank_name": "ozon", - "material_id": "49001bb5-3b24-478d-b913-c2242aea0732", - "type": "FETCH_TRANSACTIONS", - "body": {} -} diff --git a/tests/mock_server/setup.ps1 b/tests/mock_server/setup.ps1 new file mode 100644 index 0000000..25cb83c --- /dev/null +++ b/tests/mock_server/setup.ps1 @@ -0,0 +1,552 @@ +<# +.SYNOPSIS + Установка зависимостей, генерация тестовых данных и запуск mock-сервера. + +.DESCRIPTION + Скрипт выполняет: + 1. Проверяет/устанавливает Python 3.12+ + 2. Устанавливает Flask (pip install flask) + 3. Находит config.ini и БД + 4. Проверяет наличие БД и активных данных (devices, bank_profiles, materials) + 5. Генерирует default_profile.json и scenarios из БД + 6. Переключает config.ini на mock (apiBase=http://127.0.0.1:PORT) + 7. Показывает сводку по данным из БД + 8. Запускает mock-сервер и открывает отчёт в браузере + +.PARAMETER Port + Порт mock-сервера (default: 8888) + +.PARAMETER FtPerMaterial + Сколько FETCH_TRANSACTIONS на материал при бутстрапе (default: 3) + +.PARAMETER SkipInstall + Пропустить установку Python/Flask (если уже установлены) + +.PARAMETER GenerateOnly + Только сгенерировать данные, не запускать сервер + +.EXAMPLE + .\setup.ps1 + .\setup.ps1 -Port 9999 -FtPerMaterial 5 + .\setup.ps1 -SkipInstall + .\setup.ps1 -GenerateOnly +#> + +param( + [int]$Port = 8888, + [int]$FtPerMaterial = 3, + [switch]$SkipInstall, + [switch]$GenerateOnly, + [switch]$Clean +) + +$ErrorActionPreference = "Continue" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$Host.UI.RawUI.WindowTitle = "ARC Mock Server Setup" + +# ─── Helpers ─────────────────────────────────────────────────────────────── + +function Write-Step([string]$n, [string]$msg) { + Write-Host "`n=== [$n] $msg ===" -ForegroundColor Cyan +} + +function Write-Ok([string]$msg) { + Write-Host " OK: $msg" -ForegroundColor Green +} + +function Write-Warn([string]$msg) { + Write-Host " WARN: $msg" -ForegroundColor Yellow +} + +function Write-Err([string]$msg) { + Write-Host " ERROR: $msg" -ForegroundColor Red +} + +function Exit-WithError([string]$msg) { + Write-Err $msg + Write-Host "`nPress any key to exit..." -ForegroundColor Gray + $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + exit 1 +} + +# ─── 1. Find script and project directories ─────────────────────────────── + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition + +$configPath = $null +$configCandidates = @( + (Join-Path $scriptDir "..\..\cmake-build-debug\config.ini"), + (Join-Path $scriptDir "..\..\config.ini"), + (Join-Path $scriptDir "..\config.ini"), + (Join-Path $scriptDir "config.ini") +) +foreach ($c in $configCandidates) { + $resolved = [System.IO.Path]::GetFullPath($c) + if (Test-Path $resolved) { + $configPath = $resolved + $projectRoot = Split-Path -Parent $resolved + break + } +} + +if (-not $configPath) { + Exit-WithError "config.ini not found. Run from project root or Release/tests/mock_server/" +} + +Write-Host "Project root : $projectRoot" -ForegroundColor Gray +Write-Host "Config : $configPath" -ForegroundColor Gray +Write-Host "Script dir : $scriptDir" -ForegroundColor Gray + +# ─── 2. Python ───────────────────────────────────────────────────────────── + +Write-Step "1/6" "Python" + +$pythonExe = $null + +function Find-Python { + foreach ($cmd in @("python", "python3", "py")) { + try { + $ver = & $cmd --version 2>&1 + if ($ver -match "Python\s+3\.(\d+)") { + if ([int]$Matches[1] -ge 9) { + return (Get-Command $cmd).Source + } + } + } catch {} + } + $paths = @( + "$env:LOCALAPPDATA\Programs\Python\Python3*\python.exe", + "$env:LOCALAPPDATA\Programs\Python\Python31*\python.exe", + "C:\Python3*\python.exe" + ) + foreach ($pattern in $paths) { + $found = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | Select-Object -First 1 + if ($found) { + $ver = & $found.FullName --version 2>&1 + if ($ver -match "Python\s+3\.(\d+)" -and [int]$Matches[1] -ge 9) { + return $found.FullName + } + } + } + return $null +} + +if (-not $SkipInstall) { + $pythonExe = Find-Python + if ($pythonExe) { + $ver = & $pythonExe --version 2>&1 + Write-Ok "Found: $ver ($pythonExe)" + } else { + Write-Warn "Python not found - installing via winget..." + try { + winget install Python.Python.3.12 --accept-source-agreements --accept-package-agreements --silent 2>&1 | Out-Null + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + + [System.Environment]::GetEnvironmentVariable("Path", "User") + $pythonExe = Find-Python + if ($pythonExe) { + $ver = & $pythonExe --version 2>&1 + Write-Ok "Installed: $ver" + } else { + Exit-WithError "Python installed but not in PATH. Restart terminal and try again." + } + } catch { + Exit-WithError "Failed to install Python. Install manually: https://python.org" + } + } +} else { + $pythonExe = Find-Python + if (-not $pythonExe) { + Exit-WithError "Python not found. Remove -SkipInstall or install manually." + } + Write-Ok "Python: $pythonExe" +} + +# ─── Generate helper scripts ────────────────────────────────────────────── + +$genFtPs1 = Join-Path $scriptDir "gen_transactions.ps1" +$runServerPs1 = Join-Path $scriptDir "run_server.ps1" + +# Shared config-finder block for helper scripts +$configFinderBlock = @' +$_configCandidates = @( + (Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"), + (Join-Path $PSScriptRoot "..\..\config.ini"), + (Join-Path $PSScriptRoot "..\config.ini"), + (Join-Path $PSScriptRoot "config.ini") +) +$_configPath = $null +foreach ($_c in $_configCandidates) { + $_resolved = [System.IO.Path]::GetFullPath($_c) + if (Test-Path $_resolved) { $_configPath = $_resolved; break } +} +if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 } +$_configArg = @("--config", $_configPath) +'@ + +# gen_transactions.ps1 +$genFtContent = @" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$configFinderBlock +Write-Host "Config: `$_configPath" -ForegroundColor Gray +& "$pythonExe" "`$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args +"@ +[System.IO.File]::WriteAllText($genFtPs1, $genFtContent, [System.Text.UTF8Encoding]::new($false)) + +# run_server.ps1 +$runServerContent = @" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +& "$pythonExe" "`$PSScriptRoot\mock_server.py" --port 8888 @args +"@ +[System.IO.File]::WriteAllText($runServerPs1, $runServerContent, [System.Text.UTF8Encoding]::new($false)) + +Write-Ok "Created gen_transactions.ps1, run_server.ps1" + +# ─── 3. Flask ────────────────────────────────────────────────────────────── + +Write-Step "2/6" "Flask" + +$flaskInstalled = $false +try { + $pipOut = & $pythonExe -m pip show flask 2>&1 + if ($pipOut -match "Name:\s*Flask") { + $flaskVer = ($pipOut | Select-String "Version:\s*(.+)").Matches.Groups[1].Value + Write-Ok "Flask $flaskVer already installed" + $flaskInstalled = $true + } +} catch {} + +if (-not $flaskInstalled) { + Write-Host " Installing Flask..." -ForegroundColor Gray + & $pythonExe -m pip install flask --quiet 2>&1 + if ($LASTEXITCODE -ne 0) { + Exit-WithError "Failed to install Flask. Try: $pythonExe -m pip install flask" + } + Write-Ok "Flask installed" +} + +# ─── 4. Database check ──────────────────────────────────────────────────── + +Write-Step "3/6" "Database" + +$configContent = Get-Content $configPath -Encoding UTF8 +$dbMatch = $configContent | Select-String "dbPath\s*=\s*(.+)" +if (-not $dbMatch) { + Exit-WithError "[db] dbPath not found in config.ini" +} +$dbPathRaw = $dbMatch.Matches.Groups[1].Value.Trim() + +if ([System.IO.Path]::IsPathRooted($dbPathRaw)) { + $dbPath = $dbPathRaw +} else { + $dbPath = [System.IO.Path]::GetFullPath((Join-Path $projectRoot $dbPathRaw)) +} + +Write-Host " DB path: $dbPath" -ForegroundColor Gray + +if (-not (Test-Path $dbPath)) { + Exit-WithError ("DB not found: $dbPath`n`n" + + "To create the DB:`n" + + " 1. Set apiBase to production server in config.ini`n" + + " 2. Run ARC.exe and log in`n" + + " 3. Devices, profiles and materials will appear in the DB`n" + + " 4. Re-run this script") +} + +Write-Ok "DB found: $dbPath" + +# Write a temp Python helper that queries DB and outputs structured data +$dbHelperPy = Join-Path $env:TEMP "arc_db_check.py" +@' +import sqlite3, sys, os + +sys.stdout.reconfigure(encoding='utf-8') +db_path = sys.argv[1] +mode = sys.argv[2] if len(sys.argv) > 2 else "count" +pool_dir = sys.argv[3] if len(sys.argv) > 3 else "" + +conn = sqlite3.connect(db_path) +conn.row_factory = sqlite3.Row + +if mode == "count": + cnt = conn.execute(""" + SELECT COUNT(*) FROM devices d + JOIN bank_profiles bp ON bp.device_id = d.id + JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code + WHERE bp.status = 'active' AND m.status = 'active' + AND m.material_id != '' AND bp.bank_profile_id != '' + """).fetchone()[0] + conn.close() + print(cnt) + sys.exit(0 if cnt > 0 else 1) + +elif mode == "clean_events": + try: + cnt = conn.execute("SELECT COUNT(*) FROM events").fetchone()[0] + conn.execute("DELETE FROM events") + conn.commit() + print(cnt) + except Exception: + print(0) + conn.close() + sys.exit(0) + +elif mode == "summary": + devices = conn.execute("SELECT id, name, api_id, adb_serial FROM devices").fetchall() + profiles = conn.execute(""" + SELECT bp.code, bp.full_name, bp.phone, bp.status, d.name AS device + FROM bank_profiles bp JOIN devices d ON d.id = bp.device_id + WHERE bp.status = 'active' AND bp.bank_profile_id != '' + ORDER BY d.name, bp.code + """).fetchall() + materials = conn.execute(""" + SELECT m.material_id, m.card_number, m.last_numbers, m.currency, m.status, + m.app_code, d.name AS device + FROM materials m JOIN devices d ON d.id = m.device_id + WHERE m.status = 'active' AND m.material_id != '' + ORDER BY d.name, m.app_code + """).fetchall() + conn.close() + + print("DEVICES|" + str(len(devices))) + for d in devices: + serial = d['adb_serial'] or '-' + api = d['api_id'] or '-' + print(f" DEV|{d['name']}|{serial}|{api}") + + print("PROFILES|" + str(len(profiles))) + for p in profiles: + name = p['full_name'] or '-' + phone = p['phone'] or '-' + print(f" PRF|{p['device']}|{p['code']}|{name}|{phone}") + + print("MATERIALS|" + str(len(materials))) + for m in materials: + card = m['card_number'] or ('****' + m['last_numbers'] if m['last_numbers'] else '-') + print(f" MAT|{m['device']}|{m['app_code']}|{card}|{m['currency']}|{m['material_id'][:12]}") + + pool_n = 0 + if pool_dir and os.path.isdir(pool_dir): + pool_n = len([f for f in os.listdir(pool_dir) if f.endswith('.json')]) + print(f"POOL|{pool_n}") +'@ | Set-Content -Path $dbHelperPy -Encoding UTF8 + +# Check active materials count +$matCount = & $pythonExe $dbHelperPy $dbPath "count" 2>&1 +if ($LASTEXITCODE -ne 0) { + Exit-WithError ("DB exists but no active materials found.`n`n" + + "To populate:`n" + + " 1. Set apiBase to production server`n" + + " 2. Run ARC.exe, log in, add devices and profiles`n" + + " 3. Re-run this script") +} + +Write-Ok "Active materials in DB: $matCount" + +# Clean events table +$eventsCleared = & $pythonExe $dbHelperPy $dbPath "clean_events" 2>&1 +if ($eventsCleared -and [int]$eventsCleared -gt 0) { + Write-Ok "Cleared $eventsCleared events from DB" +} else { + Write-Ok "Events table already empty" +} + +# ─── 5. Generate mock data ──────────────────────────────────────────────── + +Write-Step "4/6" "Generate test data" + +# Clean old generated data +$oldProfile = Join-Path $scriptDir "default_profile.json" +if (Test-Path $oldProfile) { + Remove-Item $oldProfile -Force + Write-Host " Cleaned: default_profile.json" -ForegroundColor Gray +} +$scenariosDir = Join-Path $scriptDir "scenarios" +if (Test-Path $scenariosDir) { + $oldScenarios = Get-ChildItem "$scenariosDir\*.json" -ErrorAction SilentlyContinue + if ($oldScenarios) { + $oldScenarios | Remove-Item -Force + Write-Host " Cleaned: scenarios/ ($($oldScenarios.Count) files)" -ForegroundColor Gray + } +} +# -Clean: also wipe the transaction pool +$poolDir = Join-Path $scriptDir "scenarios\pool" +if ($Clean -and (Test-Path $poolDir)) { + $oldPool = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue + if ($oldPool) { + $oldPool | Remove-Item -Force + Write-Host " Cleaned: scenarios/pool/ ($($oldPool.Count) files)" -ForegroundColor Gray + } +} + +$genScript = Join-Path $scriptDir "gen_mock_data.py" +if (-not (Test-Path $genScript)) { + Exit-WithError "gen_mock_data.py not found in $scriptDir" +} + +$genOutput = & $pythonExe $genScript --config $configPath 2>&1 +$genOutput | ForEach-Object { Write-Host " $_" } +if ($LASTEXITCODE -ne 0) { + Exit-WithError "Data generation failed" +} + +$profileJson = Join-Path $scriptDir "default_profile.json" +if (Test-Path $profileJson) { + Write-Ok "default_profile.json created" +} else { + Exit-WithError "default_profile.json was not created" +} + +$poolDir = Join-Path $scriptDir "scenarios\pool" +$poolCount = 0 +if (Test-Path $poolDir) { + $poolFiles = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue + if ($poolFiles) { $poolCount = $poolFiles.Count } +} +if ($poolCount -gt 0) { + Write-Ok "Transaction pool: $poolCount files" +} else { + Write-Warn "Transaction pool is empty - bootstrap will only create FETCH_PROFILE tasks" + Write-Host " To generate pool run: $pythonExe gen_fetch_transactions.py" -ForegroundColor Gray +} + +# ─── 6. Switch config to mock ───────────────────────────────────────────── + +Write-Step "5/6" "Switch config.ini to mock" + +$mockBase = "http://127.0.0.1:$Port" +$configText = Get-Content $configPath -Raw -Encoding UTF8 + +# Find the active (non-commented) apiBase line +$configLines = Get-Content $configPath -Encoding UTF8 +$activeIdx = -1 +$currentBase = "" +for ($i = 0; $i -lt $configLines.Count; $i++) { + if ($configLines[$i] -match "^\s*apiBase\s*=\s*(.+)") { + $activeIdx = $i + $currentBase = $Matches[1].Trim() + break + } +} + +if ($activeIdx -eq -1) { + Exit-WithError "apiBase not found in config.ini" +} + +if ($currentBase -eq $mockBase) { + Write-Ok "apiBase already points to mock: $mockBase" +} else { + $backupPath = "$configPath.prod_backup" + if (-not (Test-Path $backupPath)) { + Copy-Item $configPath $backupPath + Write-Host " Backup saved: $backupPath" -ForegroundColor Gray + } + # Comment out old line, insert mock line after it + $configLines[$activeIdx] = "#$($configLines[$activeIdx])" + $before = $configLines[0..$activeIdx] + $after = if ($activeIdx + 1 -lt $configLines.Count) { $configLines[($activeIdx + 1)..($configLines.Count - 1)] } else { @() } + $configLines = $before + @("apiBase=$mockBase") + $after + $newContent = ($configLines -join "`r`n") + "`r`n" + [System.IO.File]::WriteAllText($configPath, $newContent, [System.Text.UTF8Encoding]::new($false)) + Write-Ok "apiBase switched: $currentBase -> $mockBase" +} + +if ($GenerateOnly) { + Write-Host "`n=== Done (generate only) ===" -ForegroundColor Green + Write-Host "To start server: $pythonExe mock_server.py --port $Port" -ForegroundColor Gray + exit 0 +} + +# ─── 7. Start mock server ───────────────────────────────────────────────── + +Write-Step "6/6" "Start mock server" + +# Kill old process on this port if any +$portCheck = netstat -ano 2>$null | Select-String ":${Port}\s.*LISTENING" +if ($portCheck) { + $oldPid = ($portCheck -split '\s+')[-1] + if ($oldPid -and $oldPid -ne "0") { + Write-Warn "Port $Port occupied by PID $oldPid - killing..." + taskkill /PID $oldPid /F 2>$null | Out-Null + Start-Sleep -Seconds 1 + Write-Ok "Old process killed" + } +} + +$mockScript = Join-Path $scriptDir "mock_server.py" + +# ─── Summary banner with DB data ────────────────────────────────────────── + +$poolDirArg = Join-Path $scriptDir "scenarios\pool" +$bannerLines = & $pythonExe $dbHelperPy $dbPath "summary" $poolDirArg 2>&1 + +Write-Host "" +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host " ARC Mock Server - Summary " -ForegroundColor Cyan +Write-Host "================================================================" -ForegroundColor Cyan + +foreach ($line in $bannerLines) { + $l = $line.ToString().Trim() + + if ($l -match "^DEVICES\|(\d+)") { + Write-Host "" + Write-Host " Devices ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-30} {1,-20} {2}" -f "Name", "ADB Serial", "API ID") -ForegroundColor DarkGray + } + elseif ($l -match "^PROFILES\|(\d+)") { + Write-Host "" + Write-Host " Bank profiles ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f "Device", "Bank", "Name", "Phone") -ForegroundColor DarkGray + } + elseif ($l -match "^MATERIALS\|(\d+)") { + Write-Host "" + Write-Host " Materials ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f "Device", "Bank", "Card", "Cur.", "Material ID") -ForegroundColor DarkGray + } + elseif ($l -match "^POOL\|(\d+)") { + Write-Host "" + if ([int]$Matches[1] -gt 0) { + Write-Host " Transaction pool: $($Matches[1]) tasks" -ForegroundColor Green + } else { + Write-Host " Transaction pool: empty (only FETCH_PROFILE at bootstrap)" -ForegroundColor Yellow + } + } + elseif ($l -match "^\s*DEV\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-30} {1,-20} {2}" -f $Matches[1], $Matches[2], $Matches[3]) -ForegroundColor Gray + } + elseif ($l -match "^\s*PRF\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4]) -ForegroundColor Gray + } + elseif ($l -match "^\s*MAT\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4], $Matches[5]) -ForegroundColor Gray + } +} + +Write-Host "" +Write-Host " ==========================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host " Mock server: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:$Port" -ForegroundColor Green +Write-Host " HTML report: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:${Port}/_admin/report/html" -ForegroundColor Green +Write-Host " State: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:${Port}/_admin/state" -ForegroundColor Green +Write-Host "" +Write-Host " FT/material: $FtPerMaterial" -ForegroundColor Gray +Write-Host " Config: $configPath" -ForegroundColor Gray +Write-Host "" +Write-Host " Ctrl+C to stop the server" -ForegroundColor Yellow +Write-Host " After testing restore: apiBase=http://93.183.75.134:8005" -ForegroundColor Yellow +Write-Host "" + +# Cleanup temp helper +Remove-Item $dbHelperPy -ErrorAction SilentlyContinue + +# Open report in browser +Start-Process "http://127.0.0.1:${Port}/_admin/report/html" + +# Launch server (blocks until Ctrl+C) +& $pythonExe $mockScript --host 127.0.0.1 --port $Port --ft-per-material $FtPerMaterial