- 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.
385 lines
16 KiB
Markdown
385 lines
16 KiB
Markdown
# 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
|