# Obmenka — архитектура аналитической системы **Версия:** 0.3 (зафиксированы ключевые решения, ожидает данные для старта E1) **Дата:** 2026-05-13 **Цель:** прозрачная аналитика и двусторонняя сверка данных для крипто-обменника (наличные ↔ крипта + онлайн), с заделом под CRM, мониторинг качества работы менеджеров и встроенную базу знаний. ## История версий - **0.3 (2026-05-13)** — зафиксированы решения: рабочие выделенные TG-аккаунты для мониторинга, snapshot deployments под отдельные subdomain, MVP-дашборд показывает оборот/остаток наличных/остаток крипты/маржу. - **0.2 (2026-05-13)** — добавлены: версионирование релизов, Telegram-мониторинг менеджеров, knowledge base в админке; этапы перестроены под приоритеты пользователя (сверка+дашборд — первый шаг). - **0.1 (2026-05-13)** — первый draft. --- ## 1. Принципы 1. **Сырое отдельно от чистого.** Любая загрузка сначала пишется в `raw_*` без изменений (источник правды для аудита). Нормализация — отдельным шагом. 2. **Идемпотентность.** Повторный запуск ETL не создаёт дублей. Ключи: `(chain, tx_hash, log_index)` для блокчейна, `(sheet_id, row_hash)` для Sheets. 3. **Аудит-трейл.** Любое изменение в `operations`/`matches` пишется в `audit_log` с user_id, before/after, timestamp. 4. **Прозрачность сверки.** У каждой матч-пары есть статус, разница и причина. Никакой "магии". 5. **Минимум кастома, максимум стандартного.** Postgres + Python + FastAPI + React — без экзотики. --- ## 2. Слои ``` ┌─────────────────────────────────────────────────────────────────┐ │ Sources (внешние) │ │ TRON nodes/API Ethereum nodes/API Google Sheets (later: CRM forms) │ └──────────┬──────────────┬──────────────────┬─────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ ETL collectors (Python) │ │ tron_collector eth_collector sheets_collector │ │ (poll каждые N мин, идемпотентная запись в raw_*) │ └──────────────────────────┬──────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Postgres (БД obmenka) │ │ raw_blockchain_tx raw_sheet_rows operations matches │ │ wallets cash_points users roles balances_snapshot audit_log│ └──────────┬──────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Reconciliation engine │ │ (нормализация raw → operations, авто-матчинг, расхождения) │ └──────────┬──────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ FastAPI (REST, JWT, RBAC) │ └──────────┬──────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ React + Vite UI (дашборд, сверка, CRM) │ └──────────┬──────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Caddy → obmenka.. (TLS, reverse proxy) │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Стек | Слой | Технология | Обоснование | |---|---|---| | БД | **Postgres** (shared instance на сервере, отдельная БД `obmenka`) | Средний объём, нужны транзакции и FK для сверки | | ETL / Backend | **Python 3.12 + FastAPI** | Один проект для collectors и API, богатые либы для блокчейн API | | Планировщик | **APScheduler** в процессе FastAPI (для старта); миграция на systemd timers если разрастётся | Просто, без отдельного Celery/Redis | | Frontend | **React + Vite + TypeScript**, UI: shadcn/ui, роутинг: TanStack Router | Быстрый старт, удобные таблицы для сверки | | Auth | На старте — локальные users + JWT (`passlib`/`python-jose`). После — Keycloak (forge infra) | Минимум зависимостей на MVP | | Деплой | systemd --user units + Caddy reverse_proxy | По правилам сервера | | Секреты | `pass` под `obmenka/` | Глобальное правило сервера | --- ## 4. Модель данных (черновик) ### 4.1. Справочники ```sql chains (code PK, name) -- 'tron', 'ethereum' assets (code PK, chain_code FK, contract, decimals, symbol) -- 'usdt-trc20', 'usdc-erc20', ... wallets (id PK, chain_code FK, address, label, owner_user_id, is_internal, created_at) cash_points (id PK, name, location, base_currency, manager_user_id, is_active, created_at) roles (code PK, name) -- admin, manager, cashier, accountant, viewer users (id PK, email, password_hash, role_code FK, cash_point_id FK NULL, is_active, created_at) ``` ### 4.2. Сырые данные ```sql raw_blockchain_tx ( id PK, chain_code FK, tx_hash, log_index, block_number, block_time, from_addr, to_addr, asset_code FK, amount NUMERIC(38,8), payload JSONB, ingested_at, UNIQUE(chain_code, tx_hash, log_index) ) raw_sheet_rows ( id PK, sheet_id, sheet_name, row_index, row_hash, payload JSONB, ingested_at, UNIQUE(sheet_id, row_hash) ) ``` ### 4.3. Бизнес-данные ```sql operations ( id PK, op_type ENUM('cash_in','cash_out','crypto_in','crypto_out','internal_transfer'), occurred_at TIMESTAMPTZ, cash_point_id FK NULL, -- для cash_* wallet_id FK NULL, -- для crypto_* asset_code FK, amount NUMERIC(38,8), counterparty_label TEXT, source ENUM('blockchain','sheet','manual'), source_ref_id BIGINT, -- FK на raw_* created_by_user_id, created_at, notes TEXT ) matches ( id PK, cash_op_id FK, -- операция кассы (наличные) crypto_op_id FK, -- операция блокчейна status ENUM('auto_matched','manual_matched','disputed','unmatched'), amount_diff NUMERIC(38,8), time_diff_seconds INT, fx_rate NUMERIC(20,8), matched_by_user_id, matched_at, notes TEXT ) balances_snapshot ( id PK, snapshot_at TIMESTAMPTZ, scope ENUM('wallet','cash_point'), scope_id BIGINT, asset_code FK, amount NUMERIC(38,8), computed_from ENUM('chain','operations','manual_count') ) audit_log ( id PK, occurred_at, user_id, entity_type, entity_id, action ENUM('create','update','delete','match','unmatch'), before JSONB, after JSONB ) ``` --- ## 5. Reconciliation (сверка) **Шаг 1: Нормализация.** Каждые N минут engine забирает свежие `raw_*` и превращает в `operations` (idempotent upsert по source_ref). **Шаг 2: Авто-матчинг.** Для каждой пары (cash_op, crypto_op) считаем score: - по сумме (с учётом FX, в нативной валюте крипты): exact / в пределах X% - по времени: разница <= Y минут - по адресу/контрагенту: явная привязка через `wallets.label` или счёт кассы Если score выше порога → `auto_matched`. Иначе остаётся `unmatched` и попадает на ручной разбор. **Шаг 3: Ручная сверка.** В UI — таблица расхождений с двух сторон, оператор тыкает "это одна операция" / "пометить как одиночную" / "это внутренний перевод". **Шаг 4: Балансы.** Каждый день в полночь — снимок `balances_snapshot` по всем кошелькам и кассам. Источник: on-chain getBalance + сумма операций кассы с начала дня. --- ## 6. Роли и доступ | Роль | Может | |---|---| | `admin` | Всё, включая создание пользователей и редактирование справочников | | `manager` | Видит всё, может матчить, не может создавать пользователей | | `accountant` | Read-only по всем кассам и кошелькам, экспорт | | `cashier` | Видит только свою кассу (`users.cash_point_id`), может вводить операции | | `viewer` | Read-only по всем dashboards без деталей операций | RBAC проверяется в FastAPI middleware и дополнительно в SQL через row-level security (опционально на v2). --- ## 7. Этапы реализации (от MVP к CRM) Этапы перестроены под приоритеты: сначала сверка + дашборд по отделениям, потом блокчейн с точными данными, потом мониторинг менеджеров, потом knowledge base. | Этап | Что делаем | Видимый результат | |---|---|---| | **E0. Скелет** ✅ | git + структура проекта; docs viewer; Caddy + TLS | https://obmenka.felix.nohumaninside.me работает | | **E1. Сверка + дашборд (MVP)** | БД `obmenka`; ручной ввод операций касс через простую админку; CSV-импорт смен; дашборд с 4 виджетами: **(1) оборот по отделениям** за день/неделю/месяц, **(2) остаток наличных по отделениям**, **(3) остаток крипты по кошелькам** (заглушка до E2, потом on-chain), **(4) маржа/P&L по сделкам**; ручной матчинг кассы↔крипта | Руководитель открывает дашборд и за 10 секунд видит: оборот, остатки, маржу, расхождения | | **E2. Блокчейн ETL** | TRC20 + ERC20 collectors; `raw_blockchain_tx`; автоматическая привязка крипто-стороны к операциям кассы; полные данные транзакции у каждой записи (hash, block, время, комиссия) | По любой операции из кассы видно соответствующую on-chain транзакцию с полным контекстом | | **E3. Sheets ETL (опционально)** | Если кассиры ведут в Sheets — авто-забор и нормализация в `operations`. Альтернатива — продолжать ручной ввод/CSV | Sheets-операции попадают в БД без ручного импорта | | **E4. Telegram-мониторинг менеджеров** | MTProto клиенты для аккаунтов менеджеров; pipeline сообщений → анализ; флаги «расхождение условий», «леваки»; сверка обсуждённых сумм с реальными операциями | На каждую сделку видно цепочку диалога, отклонения от регламента подсвечены | | **E5. RBAC + Knowledge base + полировка UI** | Роли (кассир/менеджер/бух/руководитель); каждой роли — раздел с инструкциями (markdown с версионированием через git); кассирский режим | Менеджер сам читает свои инструкции в админке; данные видны только по своему scope | | **E6. CRM** | Клиенты, заявки на обмен, привязка к operations и Telegram-диалогам, история | Полная карточка клиента: сделки + переписка + соответствие условий | Каждый этап — отдельный git tag (`v0.1`, `v0.2`...), отдельный snapshot деплоя (см. раздел 8), отдельный отчёт «что сделано». --- ## 8. Версионирование релизов **Цель:** в любой момент посмотреть «как было неделю назад», сравнить две версии админки/документации, при необходимости откатиться. ### 8.1. Уровни версионирования | Что версионируем | Механизм | Где смотреть | |---|---|---| | Документация (`docs/*.md`) | git история (auto-commit при изменениях + ручные коммиты) | Docs viewer покажет историю файла + diff между версиями (TODO: добавить кнопку «история») | | Код backend + frontend | git + теги релизов `v0.1`, `v0.2`... | GitLab UI + локальный `git log` | | UI/админка (собранный артефакт) | Snapshot deployments — каждый релиз отдельным subdomain | `https://v0.1.obmenka.felix.nohumaninside.me`, `https://v0.2.obmenka...`, основной `https://obmenka...` указывает на последний | | Данные (БД) | Ежедневные `pg_dump` в зашифрованный S3/локально + retention 90 дней | Восстановление через скрипт | ### 8.1.1. Решение **Snapshot deployments включены** — каждая старая версия UI доступна по своему subdomain (`v0.X.obmenka.felix.nohumaninside.me`). Все версии смотрят в одну БД (read-only режим для не-текущих). Основной домен всегда указывает на текущую. ### 8.2. Snapshot deployments — как работает 1. При релизе делаем `git tag v0.X`, билдим UI, сохраняем артефакт в `/srv/obmenka/releases/v0.X/`. 2. Скрипт деплоя добавляет в Caddyfile блок `v0.X.obmenka.felix.nohumaninside.me { root * /srv/obmenka/releases/v0.X; reverse_proxy /api/* 127.0.0.1:8781 }`. 3. Backend старых версий может смотреть в ту же БД (read-only режим) — для просмотра, не для редактирования. 4. Основной домен `obmenka.felix.nohumaninside.me` всегда указывает на текущую версию. ### 8.3. Сравнение версий - **Документы:** docs viewer получит кнопку «история» рядом с каждым документом → список коммитов → diff между двумя. - **UI:** открой два subdomain в соседних вкладках. - **Поведение:** API двух версий смотрят в одну БД, так что данные одинаковые, отличается только UI/логика. --- ## 9. Telegram-мониторинг менеджеров (этап E4) **Цель:** контроль качества работы менеджеров, защита от «леваков» (договорённостей в обход компании), проверка что условия в диалоге соответствуют операции в учёте. ### 9.1. Технология - **MTProto через Telethon** (Python) — подключение к аккаунту менеджера от его имени. Видит все диалоги (личные + группы) этого аккаунта. Это user-API, не bot — поэтому видит ретроспективу и личные сообщения. - **НЕ Bot API** — бот видит только то, куда его добавили, не работает для личных диалогов клиент↔менеджер. - Сессии Telethon хранятся зашифрованно в `pass` (`obmenka/telegram-sessions/`). ### 9.1.1. Аккаунты — РЕШЕНО Используем **рабочие выделенные аккаунты Telegram** (отдельные номера/SIM на компанию). - Перевод существующих клиентов на рабочие аккаунты — операционная задача перед стартом E4. - Аккаунты юридически принадлежат компании → проще модель доступа, нет вопросов с личной перепиской менеджеров. - API ID/Hash регистрируем под корпоративный аккаунт. ### 9.2. Pipeline ``` Telegram (MTProto) → ingest worker (по 1 на менеджера) → raw_telegram_messages (Postgres) ↓ analyzer (LLM + правила) → events таблица (флаги: discussed_amount, fx_rate, agreement, suspicion) ↓ reconcile с operations → match_telegram_to_operation → пометки расхождений ↓ UI: страница «диалоги менеджера X» + страница «подозрительные события» ``` ### 9.3. Что анализируем Базовые правила: - Извлечь обсуждённую сумму и валюту → сравнить с `operations.amount` - Извлечь обсуждённый курс → сравнить с применённым (с допуском) - Триггеры: «лично», «не оформляй», «без чека», «личная карта», «вне обменника» → флаг - Договорённость на сумму без последующей операции в БД за N часов → флаг «возможный леваль» Глубокий анализ через LLM (опционально, дороже): - Контекстная оценка тона диалога - Распознавание неформальных договорённостей - Связывание клиента в нескольких диалогах ### 9.4. Юридический и operational контекст (требует проработки) - **Согласие менеджеров обязательно** — фиксируется в трудовом/служебном договоре. Без подписи — не подключаем. - **Хранение** — зашифрованно, доступ только у руководителя (роль `manager` или `admin` не имеют этого скоупа). - **Retention** — определить срок хранения (обычно 90–180 дней для операционных целей). - **Аудит доступа** — каждый просмотр диалога руководителем пишется в `audit_log`. --- ## 10. Knowledge base в админке (этап E5) **Цель:** менеджеры и другие роли видят свои инструкции прямо в админке, без отдельных wiki/notion. ### 10.1. Источник Markdown-файлы в репозитории, директория `docs/handbook/`: - `docs/handbook/manager.md` - `docs/handbook/cashier.md` - `docs/handbook/accountant.md` - `docs/handbook/_shared.md` ### 10.2. Доступ по ролям | Роль | Видит | |---|---| | cashier | `cashier.md` + `_shared.md` | | manager | `manager.md` + `_shared.md` | | accountant | `accountant.md` + `_shared.md` | | admin | всё | ### 10.3. Версионирование Те же файлы что лежат в git → автоматически версионируются. В админке на странице инструкции — кнопка «история» → виден список изменений с автором и датой. ### 10.4. Редактирование - На MVP — правка через git/PR (только admin). - На v2 — встроенный markdown-редактор в админке для роли `admin`, который коммитит изменения в git. --- ## 11. Открытые вопросы (требуют ответа до старта E1) ### Зафиксировано - ✅ Subdomain docs viewer: `obmenka.felix.nohumaninside.me` - ✅ Telegram-аккаунты: рабочие выделенные (отдельные номера на компанию) - ✅ Snapshot deployments: да, под каждый релиз свой subdomain - ✅ Состав MVP-дашборда: оборот по отделениям + остаток наличных + остаток крипты + маржа ### Ещё требует ответа до старта E1 1. **Отделения / кассы** — сколько их сейчас, какие названия, в каких валютах работают? 2. **Курс** — кто и где сейчас фиксирует обменный курс по сделке? Excel, бот, голова кассира? 3. **Формат ручного ввода** на старте E1 — CSV-импорт раз в день / форма в админке / прямо Google Sheets? 4. **Маржа** — считаем как (курс клиенту − рыночный курс) × объём? Откуда брать рыночный (Binance API)? ### До старта E2 (блокчейн) 5. **Список кошельков** с лейблами (отделение / тип) — на время можно фейковые. ### До старта E4 (Telegram) 6. **Глубина анализа диалогов** — хватит правил («сумма», «вне обменника», «личная карта») или сразу подключаем LLM для распознавания смысла? --- ## 12. Что НЕ делаем сейчас - Никаких микросервисов на старте — один монорепо, один процесс backend + cron внутри. - Никаких message queue (Kafka, RabbitMQ) — Postgres LISTEN/NOTIFY достаточно. - Никакой Web3-кастоди (хранение приватных ключей) — мы только читаем on-chain. - Никакого ML-скоринга — все правила сверки явные и проверяемые.