# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.13) ## Компоненты (= репозитории, см. `00-workspace-layout.md`) ``` ┌──────────────┐ JWT (email) ┌──────────────────────────────┐ браузер ──────▶│ React SPA │ ───────────────▶│ arm-core (FastAPI) │ └──────────────┘ │ REST /api/v1, policy-слой, │ │ единственный писатель в БД │ └───────┬──────────┬───────────┘ │ │ PostgreSQL │ │ S3 (uploads/ results/ exports/ archive/) │ │ NATS JetStream ◀─────────────────┘ outbox │ recognition.tabular.request │ recognition.image.request │ recognition.result ───────────────────▶ arm-core (inbox, дедуп по msg_id) │ ┌───────────────┴───────────────┐ ┌────────▼────────┐ ┌─────────▼────────┐ ┌──────────────────┐ │ tabular-parser │ │ scan-gateway │ ◀─────▶│ ИС Сканирование │ (внешняя, горячие папки) │ XLS/XLSX/XLSB │ │ PDF/PNG/JPG │ └──────────────────┘ │ stateless, без БД│ │ stateless, без БД│ └─────────────────┘ └──────────────────┘ ``` - **arm-core** — монолитное ядро: справочники, файлы/документы, маппинг результатов распознавания, консолидация (вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема. - **tabular-parser** — читает файл из S3 `uploads/`, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) **по содержимому**, пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Контракт результата — `docs/contracts/recognition-result.schema.json` (черновик; финализировать в срезе 2). Правила распознавания заголовков/группировок — `rules/*.yaml`. - **scan-gateway** — адаптер ИС Сканирование: переименовать файл `UUID_имя.pdf`, положить в «Входящие», опросить «Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), нормализовать в тот же контракт результата, опубликовать `recognition.result`. Таймаут 5 мин → `timeout`. - **Frontend** — React + TS по шаблону команды; экраны по макету `dkb_24.09.26` (справочник ГК, консолидации, первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли). ## Контракт обмена через NATS (DECISION-4) | Поток | Subject | Кто пишет | Полезная нагрузка | |---|---|---|---| | запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `job_id`, `file_id`, `s3` (bucket/key исходника), `file_type`, `schema_version`, `reply_deadline` | | результат | `recognition.result` | tabular-parser / scan-gateway | `job_id`, `status` (done/error), `error_code`, `service_name/version`, `result_s3` (bucket/key JSON), `result_schema_version`, `metrics`, `group_schema` (только tabular) | Правила: - `recognition_job` создаётся в статусе `queued` **в одной транзакции** со `stored_file`; фоновый публикатор переводит в `published` (`request_msg_id` = `Nats-Msg-Id`). Повтор публикации — по `queued` старше N секунд. - Результат принимается через durable consumer; `result_msg_id UNIQUE` = дедуп. После `max_deliver` → `dlq`; replay = сброс в `queued` с `attempt + 1`. - Результат одного файла содержит **N документов**; ядро на каждый создаёт `document` и маппит данные. - Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв контрагентов, проверки — в ядре. ## Схема ключей S3 `{bucket_role}/{file_id}/{job_id}/{name}`; роли: `uploads` (исходники, срок — весь срок хранения), `results` (нормализованные JSON; ретеншн — ARCH-010, открыт), `exports` (7 дней), `archive` (data lake). Все объекты регистрируются в `s3_object` (sha256, size, expires_at). Сырой ответ OCR `raw/{job_id}.json` — по конвенции, без записи в БД. ## Аутентификация и авторизация 1. SPA получает JWT от РАИСА; каждый запрос несёт `Authorization: Bearer`. 2. arm-core валидирует подпись/срок, извлекает email, находит `app_user` (`is_active`); нет пользователя — 403. 3. Права: `effective_permissions = ∪ role_permission(roles(user))`. Проверка в зависимости FastAPI `require(permission_code)`. 4. **Policy-слой** поверх прав (отдельный модуль, чистые функции): видимость по филиалу (`regional_office`), по ЗО (`business_segment`), допустимость действия по статусу версии (`consolidation_status`) и по активной блокировке (`consolidation_lock`). Право говорит «может», policy — «эту ли и сейчас ли». 5. Локальная разработка: заглушка-эмитент JWT с теми же claims (feature-flag `AUTH_DEV_ISSUER`). ## Слои arm-core (предложение; подстроить под шаблон команды) Ниже — структура репозитория `arm-core`; `tabular-parser` и `scan-gateway` — отдельные репозитории по тому же шаблону без `frontend/`. ``` backend/app/ api/ # роутеры FastAPI по контекстам: directories, files, documents, consolidation, kfo, ex, audit, users domain/ # чистая предметная логика без I/O: osv parsing rules, period normalize, matching, elimination, # checks, thresholds, status machine, policy services/ # use-cases: оркестрация domain + repositories + outbox repositories/ # SQLAlchemy, запросы-вычисления (сводная ОСВ, шахматка) — SQL + pandas там, где уместно models/ # ORM 1:1 с DBML v5.13; имена таблиц/колонок — как в DBML integrations/ # nats (outbox publisher, result consumer), s3, raisa migrations/ # alembic frontend/ openspec/ tests/ # фикстуры — из ../fixtures (ARM_KA_FIXTURES) ``` Принципы: `domain/` тестируется без БД на фикстурах из `../fixtures`; вычисляемые объекты консолидации возвращаются как pydantic-модели и **не** имеют ORM-таблиц; все мутации идут через `services/` с записью `audit_event`. ## Статусы и машины состояний Три независимые машины: `file_status` (техника файла), `document_status` (проверка аналитиком), `consolidation_status` (маршрут КФО) + `recognition_status` (жизненный цикл задания). Переходы — `05-domain-rules.md` §9. Реализовать как явные таблицы переходов в `domain/`, а не `if`-ы по роутерам. ## Что ожидаем от платформы (для запроса ресурсов) PostgreSQL 15+ (одна БД, схема `public`), NATS JetStream (один stream `RECOGNITION`, 3 subject), S3-совместимое хранилище (4 бакета по ролям или один бакет с префиксами + lifecycle), Gitea + CI (lint, pytest, миграции с нуля, сборка образов), реестр образов. Секреты — через переменные окружения.