- Add toggleMaterial logic to NetworkService for enabling/disabling materials via PATCH request.
- Implement `toggleMaterialStatus` in `AccountSettingsWindow` for UI control and threading integration. - Enhance `MaterialDAO` with numeric validation for `lastNumbers` field to ensure data integrity. - Update `PayByCardScript` and `PayByPhoneScript` to handle PIN entry if required after payment confirmation. - Introduce dynamic UI adjustments for material toggling, including a new toggle button and layout refinements. - Add documentation for task logic in `ozon_tasks.md` and `black_tasks.md`. - Switch default currency in `GetCardInfoScript` (Black app) from USD to ARS.
This commit is contained in:
parent
bceceb516b
commit
de8c2e84d0
@ -110,7 +110,7 @@ void GetCardInfoScript::doStart() {
|
|||||||
account.lastNumbers = lastNumbers;
|
account.lastNumbers = lastNumbers;
|
||||||
account.cardNumber = cvuNumber;
|
account.cardNumber = cvuNumber;
|
||||||
account.amount = balance;
|
account.amount = balance;
|
||||||
account.currency = "USD";
|
account.currency = "ARS";
|
||||||
account.description = "CVU";
|
account.description = "CVU";
|
||||||
account.status = MaterialStatus::Active;
|
account.status = MaterialStatus::Active;
|
||||||
account.updateTime = QDateTime::currentDateTimeUtc();
|
account.updateTime = QDateTime::currentDateTimeUtc();
|
||||||
|
|||||||
@ -81,6 +81,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (enviarBtn.isEmpty()) {
|
if (enviarBtn.isEmpty()) {
|
||||||
m_error = "Button 'Enviar a CBU o alias' not found: " + deviceId;
|
m_error = "Button 'Enviar a CBU o alias' not found: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +105,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (!onInputScreen) {
|
if (!onInputScreen) {
|
||||||
m_error = "Input card number screen not loaded: " + deviceId;
|
m_error = "Input card number screen not loaded: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +157,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (!onTransferConfirm) {
|
if (!onTransferConfirm) {
|
||||||
m_error = "Transfer confirm screen not loaded: " + deviceId;
|
m_error = "Transfer confirm screen not loaded: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +192,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (!onInputSum) {
|
if (!onInputSum) {
|
||||||
m_error = "Input sum screen not loaded: " + deviceId;
|
m_error = "Input sum screen not loaded: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +286,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (!onPayConfirm) {
|
if (!onPayConfirm) {
|
||||||
m_error = "Pay confirm screen not loaded: " + deviceId;
|
m_error = "Pay confirm screen not loaded: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,6 +340,8 @@ void PayByCardScript::makePayment(
|
|||||||
if (!onSuccess) {
|
if (!onSuccess) {
|
||||||
m_error = "Pay success screen not loaded: " + deviceId;
|
m_error = "Pay success screen not loaded: " + deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("black/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -281,10 +281,28 @@ void PayByCardScript::makePayment(
|
|||||||
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
|
AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y());
|
||||||
QThread::msleep(3000);
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
// 8.5. Проверяем экран ввода PIN-кода
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||||
|
!pinNodes.isEmpty()) {
|
||||||
|
qDebug() << "[PayByCard] PIN code screen detected after confirm, entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinNodes);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
// 9. Ждём результат: "Успешно" или "Не удалось"
|
// 9. Ждём результат: "Успешно" или "Не удалось"
|
||||||
for (int attempt = 0; attempt < 15; ++attempt) {
|
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
// Если PIN-код появился позже — вводим
|
||||||
|
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||||
|
!pinNodes.isEmpty()) {
|
||||||
|
qDebug() << "[PayByCard] PIN code screen detected (retry), entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinNodes);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
|
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
|
||||||
qDebug() << "[PayByCard] Payment successful!";
|
qDebug() << "[PayByCard] Payment successful!";
|
||||||
m_error.clear();
|
m_error.clear();
|
||||||
|
|||||||
@ -386,10 +386,28 @@ void PayByPhoneScript::makePayment(
|
|||||||
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
|
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
|
||||||
QThread::msleep(3000);
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
// 8.5. Проверяем экран ввода PIN-кода
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||||
|
!pinNodes.isEmpty()) {
|
||||||
|
qDebug() << "[PayByPhone] PIN code screen detected after confirm, entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinNodes);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
// 9. Ждём результат: "Успешно" или "Не удалось"
|
// 9. Ждём результат: "Успешно" или "Не удалось"
|
||||||
for (int attempt = 0; attempt < 15; ++attempt) {
|
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
// Если PIN-код появился позже — вводим
|
||||||
|
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||||
|
!pinNodes.isEmpty()) {
|
||||||
|
qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinNodes);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
|
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
|
||||||
qDebug() << "[PayByPhone] Payment successful!";
|
qDebug() << "[PayByPhone] Payment successful!";
|
||||||
m_error.clear();
|
m_error.clear();
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include "MaterialDAO.h"
|
#include "MaterialDAO.h"
|
||||||
|
|
||||||
#include "db/MaterialInfo.h"
|
#include "db/MaterialInfo.h"
|
||||||
@ -7,6 +8,14 @@
|
|||||||
#include "time/DateUtils.h"
|
#include "time/DateUtils.h"
|
||||||
|
|
||||||
bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
|
bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
|
||||||
|
// Валидация: lastNumbers должен содержать только цифры
|
||||||
|
static const QRegularExpression digitsOnly(R"(^\d+$)");
|
||||||
|
if (!info.lastNumbers.isEmpty() && !digitsOnly.match(info.lastNumbers).hasMatch()) {
|
||||||
|
qWarning() << "[MaterialDAO] upsertAccount rejected: lastNumbers is not numeric:"
|
||||||
|
<< info.lastNumbers << "appCode:" << info.appCode;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
|
|||||||
368
docs/black_tasks.md
Normal file
368
docs/black_tasks.md
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
# Black - Логика задач
|
||||||
|
|
||||||
|
[← Главная](index.md) | [События](events.md)
|
||||||
|
|
||||||
|
Описание логики всех типов задач для банка **Black** (appCode: `black`).
|
||||||
|
|
||||||
|
> Black — аргентинское банковское приложение. Интерфейс на испанском. Валюта: ARS (код 32). Таймзона: America/Argentina/Buenos_Aires (UTC-3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Общие механизмы (CommonScript)
|
||||||
|
|
||||||
|
### Запуск приложения (`runAppAndGoToHomeScreen`)
|
||||||
|
|
||||||
|
1. Убивает приложение если запущено
|
||||||
|
2. Запускает приложение
|
||||||
|
3. Ждёт 4.5 секунды
|
||||||
|
4. Цикл до 10 попыток:
|
||||||
|
- Получает XML-дамп экрана
|
||||||
|
- Проверяет `isHomeScreen()` (наличие "Saldo disponible" + content-desc "Inicio")
|
||||||
|
- Если экран с PIN-кодом (`isPinCodeScreen()` — "¿Olvidaste tu PIN?" или "Ingresá tu PIN"):
|
||||||
|
- Находит EditText с password=true
|
||||||
|
- Тапает для фокуса → ожидание 500мс
|
||||||
|
- Вводит PIN через `AdbUtils::inputText()`
|
||||||
|
- Ожидание 3 сек
|
||||||
|
- Ожидание 1 сек между попытками
|
||||||
|
5. При неудаче — скриншот
|
||||||
|
|
||||||
|
### Прокрутка к кнопке (`swipeToButton`)
|
||||||
|
|
||||||
|
- До 10 свайпов вверх
|
||||||
|
- После каждого свайпа парсит XML и ищет кнопку
|
||||||
|
- Аналогично Ozon
|
||||||
|
|
||||||
|
### Создание банковского профиля (`createBankProfileIfNeeded`)
|
||||||
|
|
||||||
|
- Аналогично Ozon (общий код в CommonScript)
|
||||||
|
- Валюта по умолчанию: ARS→32
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FETCH_PROFILE
|
||||||
|
|
||||||
|
**Скрипты:** `Black::LoginAndCheckAccountsScript` → `Black::GetProfileInfoScript` → `Black::GetCardInfoScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Авторизация, сбор профиля (ФИО, телефон, email), получение CVU и баланса.
|
||||||
|
|
||||||
|
### Шаги LoginAndCheckAccountsScript
|
||||||
|
|
||||||
|
1. **Валидация** — получение DeviceInfo и BankProfileInfo из БД
|
||||||
|
2. **Запуск приложения** — `runAppAndGoToHomeScreen()` с PIN-кодом
|
||||||
|
3. **Запуск GetProfileInfoScript** — синхронно, для сбора профиля
|
||||||
|
4. При ошибке: скриншот + лог + обновление комментария в `BankProfileDAO`
|
||||||
|
|
||||||
|
### Шаги GetProfileInfoScript
|
||||||
|
|
||||||
|
1. **Домашний экран** — проверка/перезапуск
|
||||||
|
|
||||||
|
2. **Парсинг с домашнего экрана**:
|
||||||
|
- fullName из `parseUserNameFromHomeScreen()` — формат "¡Hola!\nИМЯ\ninitials"
|
||||||
|
- Баланс из `parseBalanceFromHomeScreen()` — формат "$1.857,00" → 1857.00
|
||||||
|
- Обновление баланса в `BankProfileDAO`
|
||||||
|
|
||||||
|
3. **Навигация в боковое меню**:
|
||||||
|
- Поиск кнопки "¡Hola!" (аватар)
|
||||||
|
- Тап → ожидание 2 сек
|
||||||
|
- Ожидание `isSideMenu()` (5 × 1 сек) — маркеры: "Tu Información" + "Cerrar sesión"
|
||||||
|
|
||||||
|
4. **Навигация к профилю**:
|
||||||
|
- Тап "Tu Información" (content-desc)
|
||||||
|
- Ожидание 2.5 сек
|
||||||
|
- Ожидание `isProfilePage()` (5 × 1 сек) — маркер: content-desc "Tu información"
|
||||||
|
|
||||||
|
5. **Парсинг профиля**:
|
||||||
|
- Nombre, Apellido, Email — по hint-атрибутам EditText
|
||||||
|
- Свайп вниз → ожидание 1.5 сек
|
||||||
|
- Телефон — из hint-атрибута
|
||||||
|
- fullName = "nombre apellido" (или fallback на имя с домашнего экрана)
|
||||||
|
|
||||||
|
6. **Сохранение**: `BankProfileDAO::updateProfileInfo(id, fullName, phone, email)`
|
||||||
|
|
||||||
|
7. **Синхронизация с сервером**: `createBankProfileIfNeeded()`
|
||||||
|
|
||||||
|
8. **Отправка аккаунтов**: `postAccountsData()` для аккаунтов без materialId
|
||||||
|
|
||||||
|
9. **Завершение** — убийство приложения
|
||||||
|
|
||||||
|
### Шаги GetCardInfoScript (CVU)
|
||||||
|
|
||||||
|
1. **Домашний экран** — проверка/перезапуск
|
||||||
|
|
||||||
|
2. **Парсинг баланса** → обновление в БД
|
||||||
|
|
||||||
|
3. **Навигация к CVU**:
|
||||||
|
- Поиск кнопки "Tu CVU" (exact match)
|
||||||
|
- Тап → ожидание 2 сек
|
||||||
|
- Ожидание `isCVUScreen()` (5 × 1 сек)
|
||||||
|
|
||||||
|
4. **Парсинг CVU**:
|
||||||
|
- `parseCVUNumber()` — 22-значный номер после маркера "CVU"
|
||||||
|
- lastNumbers = последние 4 цифры CVU
|
||||||
|
|
||||||
|
5. **Сохранение в БД**:
|
||||||
|
- `MaterialDAO::upsertAccount()` с:
|
||||||
|
- cardNumber = полный CVU
|
||||||
|
- lastNumbers = CVU[-4:]
|
||||||
|
- currency = "USD" (хардкод)
|
||||||
|
- description = "CVU"
|
||||||
|
|
||||||
|
6. **Отправка материала на сервер**: `ns.postMaterial()` (если есть bankProfileId и нет materialId)
|
||||||
|
|
||||||
|
7. **Завершение** — убийство приложения
|
||||||
|
|
||||||
|
### Ответ серверу (при успехе)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "FETCH_PROFILE",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "black",
|
||||||
|
"data": {
|
||||||
|
"bank_account_id": "",
|
||||||
|
"amount": 185700,
|
||||||
|
"currency": 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FETCH_TRANSACTIONS
|
||||||
|
|
||||||
|
**Скрипт:** `Black::GetLastTransactionsScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Получение списка последних транзакций из истории операций.
|
||||||
|
|
||||||
|
### Шаги
|
||||||
|
|
||||||
|
1. **Домашний экран** — проверка/перезапуск
|
||||||
|
|
||||||
|
2. **Навигация к истории транзакций**:
|
||||||
|
- Поиск кнопки "Ver más" после секции "Tu actividad" через `findVerMasAfterTuActividad()`
|
||||||
|
- Если не найдена — до 5 свайпов вниз (1 сек между свайпами)
|
||||||
|
- Тап "Ver más" → ожидание 2 сек
|
||||||
|
|
||||||
|
3. **Ожидание загрузки** (15 × 1.5 сек):
|
||||||
|
- `isTransactionLoadingScreen()` = "Tu Actividad" есть, но транзакций нет (загрузка)
|
||||||
|
- `isTransactionListScreen()` = "Tu Actividad" + видимые транзакции
|
||||||
|
- Если после 15 попыток всё ещё loading → пустой аккаунт (нормально), возврат без ошибки
|
||||||
|
|
||||||
|
4. **Парсинг транзакций** (`parseTransactionItems()`):
|
||||||
|
- Источник: ImageView ноды с текстом формата:
|
||||||
|
```
|
||||||
|
Transferencia
|
||||||
|
DD de MES de YYYY - HH:MM
|
||||||
|
$amount,cents
|
||||||
|
```
|
||||||
|
или (входящие):
|
||||||
|
```
|
||||||
|
Transferencia externa de ИМЯ
|
||||||
|
DD de MES de YYYY - HH:MM
|
||||||
|
$amount,cents
|
||||||
|
```
|
||||||
|
- Отрицательные суммы: `$-amount` (исходящие переводы)
|
||||||
|
- Парсинг испанских дат (`parseSpanishDate()`): enero, febrero, ..., diciembre
|
||||||
|
|
||||||
|
5. **Сохранение в БД** (для каждой транзакции):
|
||||||
|
- bankName = "black"
|
||||||
|
- status = Complete
|
||||||
|
- timestamp = currentDateTimeUtc()
|
||||||
|
- Конвертация времени: парсинг испанской даты → таймзона Buenos Aires
|
||||||
|
- `TransactionDAO::insertTransactionIfNotExists()`
|
||||||
|
|
||||||
|
6. **Возврат** — `goBack()` → ожидание 1 сек
|
||||||
|
|
||||||
|
### Retry-логика (EventHandler)
|
||||||
|
|
||||||
|
Аналогична Ozon:
|
||||||
|
```
|
||||||
|
"Transaction not found" + < 10 мин → повтор через 15 сек
|
||||||
|
"Transaction not found" + ≥ 10 мин → ошибка
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ответ серверу
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "FETCH_TRANSACTIONS",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "black",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"bank_transaction_id": null,
|
||||||
|
"amount": 185700,
|
||||||
|
"currency": 32,
|
||||||
|
"transaction_type": "bank",
|
||||||
|
"bank_participator_id": "+5491112345678",
|
||||||
|
"status": "completed",
|
||||||
|
"extra": { "fee": 0 },
|
||||||
|
"created_at": "2025-01-15T14:30:00.000-03:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CREATE_TRANSACTION (bank — по CBU/CVU/Alias)
|
||||||
|
|
||||||
|
**Скрипт:** `Black::PayByCardScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Перевод на CBU, CVU или Alias получателя.
|
||||||
|
|
||||||
|
### Входные параметры
|
||||||
|
|
||||||
|
- `event.phone` — CBU/CVU/Alias получателя (поле `m_cardNumber`)
|
||||||
|
- `event.amount` — сумма перевода
|
||||||
|
- `m_account` — аккаунт-отправитель
|
||||||
|
|
||||||
|
### Шаги
|
||||||
|
|
||||||
|
1. **Домашний экран** — проверка/перезапуск
|
||||||
|
|
||||||
|
2. **Поиск кнопки оплаты**:
|
||||||
|
- `findNodeByContentDesc("Enviar a\nCBU o alias", contains=true)`
|
||||||
|
- Если не найдена — до 5 свайпов вниз
|
||||||
|
- Тап → ожидание 2 сек
|
||||||
|
|
||||||
|
3. **Ввод реквизитов** (10 × 1.5 сек ожидание экрана):
|
||||||
|
- Экран `isPayInputCardNumberScreen()` — hint "Ingresar CBU, CVU o Alias"
|
||||||
|
- Тап на EditText → ожидание 500мс
|
||||||
|
- Ввод CBU/CVU/Alias
|
||||||
|
- Тап "Continuar" → ожидание 2 сек
|
||||||
|
|
||||||
|
4. **Подтверждение получателя** (10 × 1.5 сек):
|
||||||
|
- Экран `isPayTransferConfirmScreen()` — кнопка "Transferir" + поле "Alias:"
|
||||||
|
- Тап "Transferir" → ожидание 2 сек
|
||||||
|
|
||||||
|
5. **Ввод суммы** (10 × 1.5 сек):
|
||||||
|
- Экран `isPayInputSumScreen()` — "Cuánto vas a transferír"
|
||||||
|
- Тап на EditText → ожидание 500мс
|
||||||
|
- **Очистка поля**: 20 × keyevent 67 (backspace)
|
||||||
|
- **Ввод суммы в сентаво**: `int(amount * 100)` → строка
|
||||||
|
- Пример: 500.00 → "50000"
|
||||||
|
- Проверка: перечитывает поле, если не совпадает — повторяет ввод
|
||||||
|
- Тап "Continuar" → ожидание 2 сек
|
||||||
|
|
||||||
|
6. **Финальное подтверждение** (10 × 1.5 сек):
|
||||||
|
- Экран `isPayConfirmScreen()` — "Vas a transferir \na ИМЯ_ПОЛУЧАТЕЛЯ"
|
||||||
|
- Парсинг имени получателя (`parseRecipientNameFromConfirm()`)
|
||||||
|
- Тап "Transferir" → ожидание 3 сек
|
||||||
|
|
||||||
|
7. **Ввод PIN** (если запрошен):
|
||||||
|
- `isPinCodeScreen()` → `inputPinCode()`
|
||||||
|
- Ожидание 3 сек
|
||||||
|
|
||||||
|
8. **Проверка результата** (15 × 2 сек):
|
||||||
|
- **"Dinero enviado!"** → успех:
|
||||||
|
- Парсинг bankTime: `parsePaySuccessDateTime()` → "DD/MM/YY HH:MM"
|
||||||
|
- Парсинг ID: `parsePaySuccessCoelsaId()` → Coelsa transaction ID
|
||||||
|
- Скриншот как чек
|
||||||
|
- Если снова PIN-экран → повторный ввод PIN
|
||||||
|
- Таймаут → ошибка
|
||||||
|
|
||||||
|
9. **Сохранение транзакции**:
|
||||||
|
- amount = -m_amount (отрицательная)
|
||||||
|
- fee = 0
|
||||||
|
- type = Card, status = Complete
|
||||||
|
- name = имя получателя
|
||||||
|
- description = "Transferencia a ИМЯ"
|
||||||
|
- phone = CBU/CVU/Alias
|
||||||
|
- bankTrExternalId = Coelsa ID
|
||||||
|
- receiptImage = скриншот
|
||||||
|
- `TransactionDAO::insertTransaction()`
|
||||||
|
|
||||||
|
### Ответ серверу
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "CREATE_TRANSACTION",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "black",
|
||||||
|
"data": {
|
||||||
|
"amount": -50000,
|
||||||
|
"currency": 32,
|
||||||
|
"transaction_type": "bank",
|
||||||
|
"bank_participator_id": "0000003100012345678901",
|
||||||
|
"status": "completed",
|
||||||
|
"bank_transaction_id": "COELSA-12345",
|
||||||
|
"extra": { "fee": 0 },
|
||||||
|
"created_at": "2025-01-15T14:30:00.000-03:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ScreenXmlParser (Black)
|
||||||
|
|
||||||
|
### Определение экранов
|
||||||
|
|
||||||
|
| Метод | Маркеры |
|
||||||
|
|-------|---------|
|
||||||
|
| `isHomeScreen()` | "Saldo disponible" + content-desc "Inicio" |
|
||||||
|
| `isPinCodeScreen()` | "¿Olvidaste tu PIN?" или "Ingresá tu PIN" |
|
||||||
|
| `isSideMenu()` | "Tu Información" + "Cerrar sesión" |
|
||||||
|
| `isProfilePage()` | content-desc "Tu información" |
|
||||||
|
| `isCVUScreen()` | маркер "CVU" |
|
||||||
|
| `isTransactionLoadingScreen()` | "Tu Actividad" без "Transferencia" |
|
||||||
|
| `isTransactionListScreen()` | "Tu Actividad" + транзакции |
|
||||||
|
| `isPayInputCardNumberScreen()` | EditText с hint "Ingresar CBU, CVU o Alias" |
|
||||||
|
| `isPayTransferConfirmScreen()` | "Transferir" + "Alias:" без "Vas a transferir" |
|
||||||
|
| `isPayInputSumScreen()` | "Cuánto vas a transferír" |
|
||||||
|
| `isPayConfirmScreen()` | "Vas a transferir" |
|
||||||
|
| `isPaySuccessScreen()` | "Dinero enviado!" |
|
||||||
|
|
||||||
|
### Парсинг данных
|
||||||
|
|
||||||
|
| Метод | Формат / Логика |
|
||||||
|
|-------|-----------------|
|
||||||
|
| `parseUserNameFromHomeScreen()` | "¡Hola!\nИМЯ\ninitials" → ИМЯ |
|
||||||
|
| `parseBalanceFromHomeScreen()` | "$1.857,00" → 1857.00 (аргентинский формат) |
|
||||||
|
| `parseProfileNombre/Apellido/Email/Phone()` | По hint-атрибутам EditText |
|
||||||
|
| `parseCVUNumber()` | 22-значный номер после маркера "CVU" |
|
||||||
|
| `parseTransactionItems()` | ImageView ноды с "Transferencia" |
|
||||||
|
| `parseSpanishDate()` | "DD de MES de YYYY - HH:MM" → QDateTime |
|
||||||
|
| `parseRecipientNameFromConfirm()` | "Vas a transferir \na ИМЯ" → ИМЯ |
|
||||||
|
| `parsePaySuccessDateTime()` | "DD/MM/YY HH:MM" |
|
||||||
|
| `parsePaySuccessCoelsaId()` | Текст после "Id Coelsa" |
|
||||||
|
|
||||||
|
### Парсинг транзакций
|
||||||
|
|
||||||
|
Формат элемента списка (ImageView text):
|
||||||
|
```
|
||||||
|
Transferencia // тип
|
||||||
|
15 de enero de 2025 - 14:30 // дата (испанский)
|
||||||
|
$1.500,00 // сумма (положительная = входящая)
|
||||||
|
```
|
||||||
|
|
||||||
|
или:
|
||||||
|
```
|
||||||
|
Transferencia externa de Juan Perez // тип + имя отправителя
|
||||||
|
15 de enero de 2025 - 14:30
|
||||||
|
$-1.500,00 // отрицательная = исходящая
|
||||||
|
```
|
||||||
|
|
||||||
|
### Испанские месяцы
|
||||||
|
|
||||||
|
| Месяц | Перевод |
|
||||||
|
|-------|---------|
|
||||||
|
| enero | январь |
|
||||||
|
| febrero | февраль |
|
||||||
|
| marzo | март |
|
||||||
|
| abril | апрель |
|
||||||
|
| mayo | май |
|
||||||
|
| junio | июнь |
|
||||||
|
| julio | июль |
|
||||||
|
| agosto | август |
|
||||||
|
| septiembre | сентябрь |
|
||||||
|
| octubre | октябрь |
|
||||||
|
| noviembre | ноябрь |
|
||||||
|
| diciembre | декабрь |
|
||||||
384
docs/ozon_tasks.md
Normal file
384
docs/ozon_tasks.md
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
# Ozon - Логика задач
|
||||||
|
|
||||||
|
[← Главная](index.md) | [События](events.md)
|
||||||
|
|
||||||
|
Описание логики всех типов задач для банка **Ozon** (appCode: `ozon`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Общие механизмы (CommonScript)
|
||||||
|
|
||||||
|
### Запуск приложения (`runAppAndGoToHomeScreen`)
|
||||||
|
|
||||||
|
1. Убивает приложение если запущено (`AdbUtils::tryToKillApplication`)
|
||||||
|
2. Запускает приложение (`AdbUtils::tryToStartApplication`)
|
||||||
|
3. Ждёт 4.5 секунды на старт
|
||||||
|
4. Цикл до 10 попыток:
|
||||||
|
- Получает XML-дамп экрана
|
||||||
|
- Проверяет `isHomeScreen()` (наличие кнопок "Перевести" + "Пополнить")
|
||||||
|
- Закрывает баннеры: диалоги, баннер безопасности, Google Play, рекламный bottom-sheet
|
||||||
|
- Если экран с PIN-кодом — вводит PIN (100–900мс между цифрами)
|
||||||
|
- Ожидание 1 сек между попытками
|
||||||
|
5. При неудаче — скриншот + лог ошибки
|
||||||
|
|
||||||
|
### Навигация к продуктам (`goToAllProducts`)
|
||||||
|
|
||||||
|
1. Закрывает баннеры на домашнем экране
|
||||||
|
2. Ищет вкладку "Финансы"
|
||||||
|
3. Тапает до 3 раз с ожиданием 1 сек
|
||||||
|
|
||||||
|
### Прокрутка к кнопке (`swipeToButton`)
|
||||||
|
|
||||||
|
- До 10 свайпов вверх (от 3/4 высоты к 1/4 высоты экрана)
|
||||||
|
- После каждого свайпа парсит XML и ищет кнопку
|
||||||
|
- Ожидание 1 сек между свайпами
|
||||||
|
|
||||||
|
### Создание банковского профиля (`createBankProfileIfNeeded`)
|
||||||
|
|
||||||
|
- Читает fullName и phone из `BankProfileDAO`
|
||||||
|
- Нормализует телефон: убирает нецифровые символы, добавляет код страны из `config.ini`
|
||||||
|
- Маппинг валют: RUB→643, USD→840, KRW→410, AZN→944, ARS→32
|
||||||
|
- POST (новый) или PATCH (существующий) на сервер
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FETCH_PROFILE
|
||||||
|
|
||||||
|
**Скрипт:** `Ozon::LoginAndCheckAccountsScript` + `Ozon::GetCardInfoScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Авторизация в приложении, получение баланса, полных номеров карт и синхронизация с сервером.
|
||||||
|
|
||||||
|
### Шаги LoginAndCheckAccountsScript
|
||||||
|
|
||||||
|
1. **Валидация** — получение DeviceInfo и BankProfileInfo из БД
|
||||||
|
2. **Запуск приложения** — `runAppAndGoToHomeScreen()` с PIN-кодом
|
||||||
|
3. **Создание профиля** — `createBankProfileIfNeeded()` (синхронизация fullName/phone)
|
||||||
|
4. **Сбор аккаунтов** (если не режим fetchProfileOnly):
|
||||||
|
- Навигация к "Финансы" через `goToAllProducts()`
|
||||||
|
- Парсинг видимых аккаунтов из XML
|
||||||
|
- Upsert в `MaterialDAO`
|
||||||
|
- Отправка на сервер
|
||||||
|
5. **Запуск GetCardInfoScript** — синхронно, для получения полных номеров карт
|
||||||
|
|
||||||
|
### Шаги GetCardInfoScript
|
||||||
|
|
||||||
|
1. **Навигация на домашний экран**
|
||||||
|
2. **Парсинг профиля** (если нет в БД):
|
||||||
|
- Тап на имя пользователя вверху экрана
|
||||||
|
- Парсинг fullName и phone
|
||||||
|
- Обновление `BankProfileDAO`
|
||||||
|
3. **Загрузка карт** — `parseCardsOnHomeScreen()` (до 5 попыток, 1.5 сек)
|
||||||
|
4. **Upsert всех карт** в `MaterialDAO`
|
||||||
|
5. **Синхронизация с сервером**:
|
||||||
|
- Создание устройства (POST) если нет `apiId`
|
||||||
|
- POST/PATCH банковского профиля с балансом, ФИО, телефоном
|
||||||
|
6. **Получение полных номеров карт** (для каждой карты):
|
||||||
|
- Тап на карту → ожидание экрана карты (10 × 2 сек)
|
||||||
|
- Тап "Показать" → ожидание появления номера (10 × 2 сек)
|
||||||
|
- Regex: `\d{4}[\s]\d{4}[\s]\d{4}[\s]\d{4}` (с unicode-пробелами)
|
||||||
|
- Обработка баннера "Понятно"
|
||||||
|
- Сохранение в `MaterialDAO::updateCardNumber()`
|
||||||
|
- При неудаче: статус карты → Off, лог + скриншот
|
||||||
|
7. **Отправка материалов** на сервер (`ns.postMaterial()`)
|
||||||
|
8. **Завершение** — убийство приложения
|
||||||
|
|
||||||
|
### Ответ серверу (при успехе)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "FETCH_PROFILE",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "ozon",
|
||||||
|
"data": {
|
||||||
|
"bank_account_id": "",
|
||||||
|
"amount": 1500000,
|
||||||
|
"currency": 643
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `amount` — в копейках (баланс × 100)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FETCH_TRANSACTIONS
|
||||||
|
|
||||||
|
**Скрипт:** `Ozon::GetLastTransactionsScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Поиск конкретной транзакции по сумме и времени в истории операций банка.
|
||||||
|
|
||||||
|
### Входные параметры (из JSON задачи)
|
||||||
|
|
||||||
|
- `body.amount` — сумма в копейках (делится на 100)
|
||||||
|
- `body.created_at` — время транзакции (ISO формат)
|
||||||
|
|
||||||
|
### Два режима работы
|
||||||
|
|
||||||
|
| Режим | Условие | Поведение |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| **Поиск** | `searchAmount > 0` | Ищет конкретную транзакцию |
|
||||||
|
| **Сканирование** | `searchAmount == 0` | Собирает все транзакции за 24 часа |
|
||||||
|
|
||||||
|
### Шаги (режим поиска)
|
||||||
|
|
||||||
|
1. **Навигация к списку операций** (`navigateToTransactionList`):
|
||||||
|
- Закрытие рекламного баннера
|
||||||
|
- Проверка домашнего экрана (перезапуск если нужно)
|
||||||
|
- Свайп до кнопки "Все" (до 10 свайпов)
|
||||||
|
- Ожидание экрана "Операции" (10 × 1.5 сек)
|
||||||
|
|
||||||
|
2. **Поиск транзакции** (`searchTransaction`):
|
||||||
|
- Цикл до 15 прокруток
|
||||||
|
- Для каждой видимой кнопки-транзакции:
|
||||||
|
- Парсинг суммы из текста кнопки
|
||||||
|
- Проверка совпадения: `|amount - searchAmount| ≤ 0.01`
|
||||||
|
- Парсинг даты из текста, проверка: `|time - searchTime| ≤ 1 час`
|
||||||
|
- При совпадении:
|
||||||
|
- Тап на транзакцию → парсинг деталей (телефон, имя, время)
|
||||||
|
- Вставка в `TransactionDAO`
|
||||||
|
- Возврат
|
||||||
|
|
||||||
|
3. **Если не найдена** — возвращает `"Transaction not found"`
|
||||||
|
|
||||||
|
### Шаги (режим сканирования)
|
||||||
|
|
||||||
|
1. Навигация к списку операций (аналогично)
|
||||||
|
2. **Сканирование** (`scanRecentTransactions`):
|
||||||
|
- Для каждой транзакции:
|
||||||
|
- Тап → ожидание деталей (10 × 1.5 сек)
|
||||||
|
- Парсинг кнопки и деталей
|
||||||
|
- Фильтр по времени: пропускает транзакции старше 24 часов
|
||||||
|
- **Получение чека:**
|
||||||
|
- Тап "Документы"
|
||||||
|
- Ожидание "Поделиться" (20 × 1.5 сек)
|
||||||
|
- Скриншот → OCR (Tesseract, rus+eng)
|
||||||
|
- Извлечение ID операции через regex:
|
||||||
|
- `ID операции = ...` (буквенно-цифровой)
|
||||||
|
- `№ Ф-YYYY-XXXXXXX`
|
||||||
|
- Вставка в `TransactionDAO`
|
||||||
|
- Возврат назад → ожидание списка
|
||||||
|
|
||||||
|
### Retry-логика (EventHandler)
|
||||||
|
|
||||||
|
```
|
||||||
|
Результат "Transaction not found"
|
||||||
|
└─ Прошло < 10 мин? → статус Wait, повтор через 15 сек
|
||||||
|
└─ Прошло ≥ 10 мин? → статус Error
|
||||||
|
```
|
||||||
|
|
||||||
|
> Между повторами другие задачи могут выполниться (см. приоритеты в events.md)
|
||||||
|
|
||||||
|
### Ответ серверу (при успехе)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "FETCH_TRANSACTIONS",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "ozon",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"bank_transaction_id": "Ф-2025-1234567",
|
||||||
|
"amount": 50000,
|
||||||
|
"currency": 643,
|
||||||
|
"transaction_type": "top-up",
|
||||||
|
"bank_participator_id": "+79001234567",
|
||||||
|
"status": "completed",
|
||||||
|
"extra": { "fee": 0 },
|
||||||
|
"created_at": "2025-01-15T14:30:00.000+03:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Возвращаются **все** транзакции аккаунта за последние 24 часа (не только найденная).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CREATE_TRANSACTION (bank — по номеру карты)
|
||||||
|
|
||||||
|
**Скрипт:** `Ozon::PayByCardScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Перевод по номеру карты получателя.
|
||||||
|
|
||||||
|
### Входные параметры
|
||||||
|
|
||||||
|
- `event.phone` — номер карты получателя
|
||||||
|
- `event.amount` — сумма перевода
|
||||||
|
- `event.comment` — имя банка-получателя
|
||||||
|
|
||||||
|
### Шаги
|
||||||
|
|
||||||
|
1. **Подготовка**:
|
||||||
|
- Закрытие рекламных баннеров
|
||||||
|
- Проверка домашнего экрана (перезапуск если нужно)
|
||||||
|
- Закрытие: баннеры, Google Play, "Заказать карту"
|
||||||
|
|
||||||
|
2. **Тап "Перевести"** → ожидание 2 сек
|
||||||
|
|
||||||
|
3. **Ожидание "Платежи и переводы"** (10 × 1.5 сек)
|
||||||
|
|
||||||
|
4. **Тап "По номеру карты"** (секция OTHER_WAYS) → ожидание 2 сек
|
||||||
|
|
||||||
|
5. **Ожидание "Перевод по номеру карты"** (10 × 1.5 сек)
|
||||||
|
|
||||||
|
6. **Ввод номера карты**:
|
||||||
|
- Поиск EditText с hint "Номер карты получателя"
|
||||||
|
- `AdbUtils::inputText(cardNumber)`
|
||||||
|
- Ожидание 2 сек
|
||||||
|
|
||||||
|
7. **Ввод суммы**:
|
||||||
|
- Поиск EditText с hint "Сумма от"
|
||||||
|
- Ввод суммы (целое число)
|
||||||
|
- Ожидание 1 сек
|
||||||
|
|
||||||
|
8. **Ожидание расчёта комиссии** (15 × 1.5 сек):
|
||||||
|
- Поиск текста "Комиссия" + парсинг суммы после него
|
||||||
|
- Извлечение цифр из формата "45 ₽"
|
||||||
|
- Fallback: комиссия = 0
|
||||||
|
|
||||||
|
9. **Тап "Перевести"** (contains-match) → ожидание 3 сек
|
||||||
|
|
||||||
|
10. **Проверка результата** (15 × 2 сек):
|
||||||
|
- **"Успешно"** → сохранение транзакции:
|
||||||
|
- amount = -сумма (отрицательная)
|
||||||
|
- fee = рассчитанная комиссия
|
||||||
|
- type = Card, status = Complete
|
||||||
|
- timestamp = Moscow timezone
|
||||||
|
- **"Не удалось"** → ошибка
|
||||||
|
- **Таймаут** → "Не дождались результата"
|
||||||
|
|
||||||
|
11. **Получение чека** (при успехе):
|
||||||
|
- Тап "Документы"
|
||||||
|
- Ожидание "Поделиться" (20 × 1.5 сек)
|
||||||
|
- Скриншот → OCR → извлечение ID операции
|
||||||
|
- Обновление БД: receipt image + external ID
|
||||||
|
|
||||||
|
### Ответ серверу
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "CREATE_TRANSACTION",
|
||||||
|
"material_id": "...",
|
||||||
|
"bank_name": "ozon",
|
||||||
|
"data": {
|
||||||
|
"amount": -50000,
|
||||||
|
"currency": 643,
|
||||||
|
"transaction_type": "bank",
|
||||||
|
"bank_participator_id": "2200123456789012",
|
||||||
|
"status": "completed",
|
||||||
|
"bank_transaction_id": "Ф-2025-1234567",
|
||||||
|
"extra": { "fee": 4500 },
|
||||||
|
"created_at": "2025-01-15T14:30:00.000+03:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CREATE_TRANSACTION (top-up — по номеру телефона)
|
||||||
|
|
||||||
|
**Скрипт:** `Ozon::PayByPhoneScript`
|
||||||
|
|
||||||
|
### Цель
|
||||||
|
|
||||||
|
Перевод по номеру телефона с выбором банка-получателя.
|
||||||
|
|
||||||
|
### Входные параметры
|
||||||
|
|
||||||
|
- `event.phone` — номер телефона получателя
|
||||||
|
- `event.amount` — сумма перевода
|
||||||
|
- `event.comment` — имя банка-получателя (для СБП)
|
||||||
|
|
||||||
|
### Шаги
|
||||||
|
|
||||||
|
1. **Подготовка** — аналогично PayByCard
|
||||||
|
|
||||||
|
2. **Тап "Перевести"** → ожидание 2 сек
|
||||||
|
|
||||||
|
3. **Ожидание страницы платежей** → поиск EditText (10 × 1.5 сек)
|
||||||
|
|
||||||
|
4. **Навигация к "Кому перевести"**:
|
||||||
|
- Тап на первый EditText → ожидание 2 сек
|
||||||
|
- Ожидание экрана "Кому перевести" (10 × 1.5 сек)
|
||||||
|
|
||||||
|
5. **Ввод номера телефона**:
|
||||||
|
- Поиск первого EditText
|
||||||
|
- `AdbUtils::inputText(phone)`
|
||||||
|
- Ожидание 2 сек
|
||||||
|
|
||||||
|
6. **Выбор получателя** (цикл 20 попыток):
|
||||||
|
- **Вариант A**: Найдена кнопка с номером телефона → тап
|
||||||
|
- **Вариант B**: Выбор банка:
|
||||||
|
- Тап "Другой банк" (content-desc="other-bank-item")
|
||||||
|
- Включение ADB IME клавиатуры
|
||||||
|
- Ввод названия банка через `AdbUtils::inputAdbIMEText()`
|
||||||
|
- Отключение ADB IME
|
||||||
|
- Поиск банка в списке (10 × 1.5 сек)
|
||||||
|
- Если банк не найден → ошибка "Банк '...' не найден"
|
||||||
|
- **Условие выхода**: текст "По номеру телефона" вверху экрана (y < height/7)
|
||||||
|
|
||||||
|
7. **Ввод суммы**:
|
||||||
|
- Первый EditText → ввод суммы (целое число)
|
||||||
|
- Ожидание 1 сек
|
||||||
|
|
||||||
|
8. **Тап "Продолжить"** → ожидание 2 сек
|
||||||
|
|
||||||
|
9. **Ожидание "Подтвердите перевод"** (10 × 1.5 сек)
|
||||||
|
|
||||||
|
10. **Тап "Перевести ..."** (contains-match) → ожидание 3 сек
|
||||||
|
|
||||||
|
11. **Проверка результата** — аналогично PayByCard:
|
||||||
|
- "Успешно" → транзакция (type = Phone)
|
||||||
|
- "Не удалось" → ошибка
|
||||||
|
- Получение чека через OCR
|
||||||
|
|
||||||
|
### Ответ серверу
|
||||||
|
|
||||||
|
Аналогичен PayByCard, но:
|
||||||
|
- `transaction_type`: `"top-up"`
|
||||||
|
- `bank_participator_id`: номер телефона
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ScreenXmlParser (Ozon)
|
||||||
|
|
||||||
|
### Определение экранов
|
||||||
|
|
||||||
|
| Метод | Маркеры |
|
||||||
|
|-------|---------|
|
||||||
|
| `isHomeScreen()` | Кнопки "Перевести" + "Пополнить" |
|
||||||
|
| `isProfileScreen()` | Заголовок "Аккаунт" + кнопка "Выйти" |
|
||||||
|
| `isTransactionListScreen()` | WebView с text="Операции" |
|
||||||
|
|
||||||
|
### Баннеры и диалоги
|
||||||
|
|
||||||
|
| Тип | Обнаружение | Действие |
|
||||||
|
|-----|-------------|----------|
|
||||||
|
| Диалог/пуш | Кнопки закрытия, "Позже" | Тап на кнопку закрытия |
|
||||||
|
| Безопасность | "Мы заботимся о вашей безопасности" | Тап "Отложить" |
|
||||||
|
| Биометрия | "Быстрый вход" | Тап "Не сейчас" |
|
||||||
|
| Реклама | resource-id "design_bottom_sheet" | Тап "touch_outside" или back |
|
||||||
|
| Google Play | "Закрыть диалоговое окно" / "Доступно обновление" | Тап закрытия |
|
||||||
|
|
||||||
|
### PIN-код (`tryToFindPinCode`)
|
||||||
|
|
||||||
|
**Метод 1**: Поиск кликабельных нод с текстом `^(?:key\s+)?(\d)$` → нужны все 10 цифр
|
||||||
|
|
||||||
|
**Метод 2** (fallback): Поиск сетки 4×3 из кликабельных элементов одного размера (±10px)
|
||||||
|
|
||||||
|
### Парсинг карт (`parseCardsOnHomeScreen`)
|
||||||
|
|
||||||
|
- Ищет "Основной счёт X ₽" для общего баланса
|
||||||
|
- Парсит кнопки с текстом, оканчивающимся на 4 цифры
|
||||||
|
- Regex для суммы: `([\d\s]+(?:[.,]\d+)?)\s*₽`
|
||||||
|
|
||||||
|
### Парсинг транзакций
|
||||||
|
|
||||||
|
- `parseTransactionButtonText()` — извлекает сумму и описание из текста кнопки в списке
|
||||||
|
- `parseTransactionDetail()` — парсит детали: bankTime (DD.MM.YYYY, HH:mm), phone (+...), name
|
||||||
@ -929,6 +929,26 @@ void NetworkService::patchBankProfile() {
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkService::toggleMaterial() {
|
||||||
|
if (!ensureValidToken()) { emit finished(); return; }
|
||||||
|
|
||||||
|
const QString materialId = m_extraData.value("material_id").toString();
|
||||||
|
const bool enable = m_extraData.value("enable").toBool();
|
||||||
|
|
||||||
|
if (materialId.isEmpty()) {
|
||||||
|
qWarning() << "[NetworkService] toggleMaterial: material_id is empty";
|
||||||
|
emit finished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString enableStr = enable ? "true" : "false";
|
||||||
|
const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr;
|
||||||
|
patchJson(path, QJsonObject(), true);
|
||||||
|
qDebug() << "[NetworkService] toggleMaterial done:" << materialId << "enable=" << enable;
|
||||||
|
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkService::toggleBankProfile() {
|
void NetworkService::toggleBankProfile() {
|
||||||
if (!ensureValidToken()) { emit finished(); return; }
|
if (!ensureValidToken()) { emit finished(); return; }
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,8 @@ public slots:
|
|||||||
|
|
||||||
void postMaterial();
|
void postMaterial();
|
||||||
|
|
||||||
|
void toggleMaterial();
|
||||||
|
|
||||||
void postDeviceScreenshot();
|
void postDeviceScreenshot();
|
||||||
|
|
||||||
void getPayments();
|
void getPayments();
|
||||||
|
|||||||
@ -495,19 +495,45 @@ void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) {
|
|||||||
paymentThread->start();
|
paymentThread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account, QTableWidget *tableWidget) {
|
||||||
|
const bool isActive = account.status == MaterialStatus::Active;
|
||||||
|
const QString newStatus = isActive ? "off" : "active";
|
||||||
|
|
||||||
|
if (!MaterialDAO::updateAccountById(account.id, newStatus, account.description, account.currency)) {
|
||||||
|
qWarning() << "[AccountSettings] Failed to update material status:" << account.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем на сервер PATCH /api/v1/profile/material/{id}/{true|false}
|
||||||
|
if (!account.materialId.isEmpty()) {
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *net = new NetworkService;
|
||||||
|
net->moveToThread(thread);
|
||||||
|
net->addExtra("material_id", account.materialId);
|
||||||
|
net->addExtra("enable", !isActive);
|
||||||
|
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||||||
|
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadContent(tableWidget);
|
||||||
|
tableWidget->resizeColumnsToContents();
|
||||||
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
||||||
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
|
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
|
||||||
|
|
||||||
tableWidget->clearContents();
|
tableWidget->clearContents();
|
||||||
tableWidget->setRowCount(accounts.length());
|
tableWidget->setRowCount(accounts.length());
|
||||||
tableWidget->setColumnCount(9);
|
tableWidget->setColumnCount(8);
|
||||||
tableWidget->setHorizontalHeaderLabels({
|
tableWidget->setHorizontalHeaderLabels({
|
||||||
"Время обновления",
|
"Время обновления",
|
||||||
"Номер",
|
"Номер",
|
||||||
"",
|
|
||||||
"Сумма",
|
"Сумма",
|
||||||
"Валюта",
|
"Валюта",
|
||||||
"Статус",
|
"",
|
||||||
"Сервер",
|
"Сервер",
|
||||||
"",
|
"",
|
||||||
""
|
""
|
||||||
@ -516,7 +542,6 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
|||||||
tableWidget->setWordWrap(true);
|
tableWidget->setWordWrap(true);
|
||||||
tableWidget->setTextElideMode(Qt::ElideNone);
|
tableWidget->setTextElideMode(Qt::ElideNone);
|
||||||
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
|
|
||||||
tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||||
|
|
||||||
|
|
||||||
@ -534,44 +559,42 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
|||||||
desc->setFlags(desc->flags() & ~Qt::ItemIsEditable);
|
desc->setFlags(desc->flags() & ~Qt::ItemIsEditable);
|
||||||
tableWidget->setItem(k, 1, desc);
|
tableWidget->setItem(k, 1, desc);
|
||||||
|
|
||||||
QTableWidgetItem *lastNumbers = new QTableWidgetItem(account.lastNumbers);
|
|
||||||
lastNumbers->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
|
||||||
lastNumbers->setFlags(lastNumbers->flags() & ~Qt::ItemIsEditable);
|
|
||||||
tableWidget->setItem(k, 2, lastNumbers);
|
|
||||||
|
|
||||||
QTableWidgetItem *amount = new QTableWidgetItem(QString::number(account.amount));
|
QTableWidgetItem *amount = new QTableWidgetItem(QString::number(account.amount));
|
||||||
amount->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
amount->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
amount->setFlags(amount->flags() & ~Qt::ItemIsEditable);
|
amount->setFlags(amount->flags() & ~Qt::ItemIsEditable);
|
||||||
tableWidget->setItem(k, 3, amount);
|
tableWidget->setItem(k, 2, amount);
|
||||||
|
|
||||||
QTableWidgetItem *currency = new QTableWidgetItem(account.currency);
|
QTableWidgetItem *currency = new QTableWidgetItem(account.currency);
|
||||||
currency->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
currency->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
|
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
|
||||||
tableWidget->setItem(k, 4, currency);
|
tableWidget->setItem(k, 3, currency);
|
||||||
|
|
||||||
QTableWidgetItem *status = new QTableWidgetItem(materialStatusToUiString(account.status));
|
// Кнопка вкл/выкл карты
|
||||||
status->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
const bool isActive = account.status == MaterialStatus::Active;
|
||||||
status->setFlags(status->flags() & ~Qt::ItemIsEditable);
|
auto *toggleBtn = new QPushButton(isActive ? "Выкл" : "Вкл", tableWidget);
|
||||||
if (account.status != MaterialStatus::Active) {
|
toggleBtn->setStyleSheet(isActive
|
||||||
status->setForeground(QBrush(Qt::red));
|
? "background-color: orange; color: white;"
|
||||||
}
|
: "background-color: green; color: white;");
|
||||||
tableWidget->setItem(k, 5, status);
|
tableWidget->setCellWidget(k, 4, toggleBtn);
|
||||||
|
|
||||||
|
connect(toggleBtn, &QPushButton::clicked, this, [this, account, tableWidget]() {
|
||||||
|
toggleMaterialStatus(account, tableWidget);
|
||||||
|
});
|
||||||
|
|
||||||
auto *syncItem = new QTableWidgetItem(account.materialId.isEmpty() ? "Не синхр." : "Синхр.");
|
auto *syncItem = new QTableWidgetItem(account.materialId.isEmpty() ? "Не синхр." : "Синхр.");
|
||||||
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
syncItem->setForeground(account.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
|
syncItem->setForeground(account.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
|
||||||
syncItem->setFlags(syncItem->flags() & ~Qt::ItemIsEditable);
|
syncItem->setFlags(syncItem->flags() & ~Qt::ItemIsEditable);
|
||||||
tableWidget->setItem(k, 6, syncItem);
|
tableWidget->setItem(k, 5, syncItem);
|
||||||
|
|
||||||
|
|
||||||
auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
|
auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
|
||||||
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;");
|
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;");
|
||||||
tableWidget->setCellWidget(k, 7, deleteBankAccountBtn);
|
tableWidget->setCellWidget(k, 6, deleteBankAccountBtn);
|
||||||
|
|
||||||
connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
|
connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
|
||||||
QMessageBox msgBox(this);
|
QMessageBox msgBox(this);
|
||||||
msgBox.setWindowTitle("Удалить аккаунт?");
|
msgBox.setWindowTitle("Удалить аккаунт?");
|
||||||
msgBox.setText("Вы уверены, что хотите удалить аккаунт " + account.lastNumbers + "?");
|
msgBox.setText("Вы уверены, что хотите удалить аккаунт?");
|
||||||
QPushButton *deleteBtn = msgBox.addButton("Удалить", QMessageBox::AcceptRole);
|
QPushButton *deleteBtn = msgBox.addButton("Удалить", QMessageBox::AcceptRole);
|
||||||
QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole);
|
QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole);
|
||||||
msgBox.setDefaultButton(cancelBtn);
|
msgBox.setDefaultButton(cancelBtn);
|
||||||
@ -588,7 +611,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
|||||||
|
|
||||||
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
|
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
|
||||||
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
|
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
|
||||||
tableWidget->setCellWidget(k, 8, transactionsBtn);
|
tableWidget->setCellWidget(k, 7, transactionsBtn);
|
||||||
|
|
||||||
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
|
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
|
||||||
auto *txWindow = new TransactionWindow(account.id, this);
|
auto *txWindow = new TransactionWindow(account.id, this);
|
||||||
|
|||||||
@ -33,5 +33,7 @@ private:
|
|||||||
|
|
||||||
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);
|
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);
|
||||||
|
|
||||||
|
void toggleMaterialStatus(const MaterialInfo &account, QTableWidget *tableWidget);
|
||||||
|
|
||||||
void updateAccountData(const MaterialInfo &account);
|
void updateAccountData(const MaterialInfo &account);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user