docs(openspec): change demo-prototype — прототип пути до сводной ОСВ (#3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
This commit is contained in:
Andrey Epifancev 2026-10-08 00:24:23 +04:00
parent 47df65fd6c
commit 9b03b0f51c
7 changed files with 363 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-08

View File

@ -0,0 +1,124 @@
## Context
arm-web выделен из `arm-core/web` командой фронта (3 коммита 06–07.10.2026) и дополнен экранами групп, консолидаций и
загрузки. Обзор 08.10.2026: список консолидаций и карточка версии работают с arm-core; загрузка теряет ошибки (нет
`await`, лишние поля формы); проверка документов (`PDFResultViewer`, `FileViewer`, `FileUploaderModel`) скопирована из
старого АРМ и ходит в `/fsl/...`, `/osv/by-file-id`, `POST /files` мимо клиента API; сводной ОСВ нет; `tsc` — 185 ошибок,
biome — 276.
Демо — 09.10.2026 на локальном стенде; затем прототип уходит в контур на тестирование заказчику и переезжает в
`arm-core/web`, где развивается дальше. Поэтому код держится соглашений шаблона `arm-core/web`.
Источник контракта — `../docs/dev/frontend-api-guide.md` (§5.5–§5.7) и схемы `arm-core/api/src/schemas/`.
## Goals / Non-Goals
**Goals:** аналитик на стенде проходит путь «создание консолидации → загрузка ОСВ и PDF → проверка и подтверждение →
сводная ОСВ» без сбоев и консоли разработчика; `make check` зелёный; все запросы — к API arm-core.
**Non-Goals:** см. proposal.md («Non-goals»).
## Decisions
### D-1. Чинить на месте, новое — по образцу arm-core/web (согласовано 08.10.2026)
Оставляем `api/client.ts`, `auth/*`, список консолидаций, карточку версии, модалку создания (с правками), Redux-слайсы
(типизируем). Новые экраны держат состояние в локальных хуках (`useStatement`, `useDocumentEntries`,
`useConsolidatedOsv`) как `arm-core/web/src/widgets/Admin/hooks.ts`. Альтернативы — генерация типов из OpenAPI (новая
dev-зависимость, доступность в Nexus не проверена) и переписывание с нуля (не успеть) — отклонены владельцем.
### D-2. Типы DTO руками
`src/types/` повторяют схемы arm-core только для используемых эндпоинтов: `Page<T>`, `UploadResult`, `FileOut`,
`DocumentBrief`, `DocumentOut`, `StatementOut` (`HeaderOut`, `ColumnOut`, `LineOut`, `ValueOut`, `ChecksOut`),
`EntryOut`, `ConsolidatedRowOut`, `OsvSummaryOut`, `VersionCard` (исправления: `is_head_entity: boolean`,
`entity_type: 'ЮЛ' | 'ИП' | null`, `osv_accounts: string[]`, `changed_by` — UUID). `Decimal` и даты приходят строками —
в типах `string`.
### D-3. Деньги — строки
Суммы не превращаются в `number`. Чистые функции `src/utils/money.ts`: `formatAmount(s)` — разделители разрядов и два
знака без арифметики над float; `parseAmountInput(s)` — нормализация ввода (пробелы, запятая → точка) и проверка
`^-?\d+(\.\d{1,2})?$`, пустое — `null`. Единицы ББ/ОФР — из `header.units`, ОСВ — рубли.
### D-4. Бинарные ответы — `apiFetchBlob`
`src/api/client.ts` получает `apiFetchBlob(path) → {blob, filename | null}` с той же обработкой токена, 401 и ошибок
(тело ошибки — JSON). Используется для скана (`blob.arrayBuffer()` → pdf.js) и экспорта (сохранение через
`URL.createObjectURL` + `<a download>`). Имя экспорта — из `Content-Disposition` (`filename*=UTF-8''…` или `filename=`),
иначе `exportFilename(group, year, quarter)`.
### D-5. Dev-proxy вместо CORS
`rsbuild.config.ts`: `server.proxy['/api'] → process.env.ARM_CORE_URL ?? 'http://localhost:8000'`; в dev
`REACT_APP_API_URL` пустой. Запросы идут с того же источника: нет CORS и читается `Content-Disposition` (follow-up
docs#58 O-1 на фронте обходится). В контуре SPA отдаёт сам arm-core — тот же источник, код не меняется. Гайд фронта §2
(где `REACT_APP_API_URL=http://localhost:8000`) остаётся верным для варианта без proxy.
### D-6. Просмотр PDF
Из `widgets/PDFResultViewer/IFrameBlock.tsx` сохраняется только постраничный рендер pdf.js (canvas, масштаб, листание);
загрузка — `apiFetchBlob('/files/{id}/content?disposition=inline')` → `arrayBuffer`. Компонент переименовывается в
`widgets/DocumentReview/PdfPages.tsx`. Worker pdf.js подключается из пакета `pdfjs-dist` сборкой (без CDN — в контуре
внешнего интернета нет). `PDFDataViewer.tsx`, `types.ts`, `styles.module.css` старого АРМ удаляются.
### D-7. Экран документа
`layouts/DocumentPage.tsx` (`/documents/:document_id`) → `GET /documents/{id}` → `widgets/DocumentReview/StatementReview`
(BALANCE/INCOME) или `widgets/DocumentReview/OsvDocument` (OSV). Чистые функции: `buildStatementGrid(statement)` —
матрица строк × колонок по `column_id` (не по индексу), `buildEntryTree(entries)` — дерево по `parent_entry_id` с
сохранением `row_number`. Правка ячейки — локальный черновик ячеек и «Сохранить» одним `PATCH` со всеми изменёнными
ячейками; ответ сервера заменяет состояние (сервер пересчитывает проверки и сбрасывает акцепт).
### D-8. Панель файлов
`widgets/FilesDrawer` переписывается: `UploadDocumentsModal` → один запрос со всеми файлами, итог из
`summarizeUpload(result)`; список — `FilesList` с вложенными документами; подписи — `fileStatusLabel`,
`documentTypeLabel`, `periodLabel`, `documentStatusLabel` (чистые функции). Опрос — `useDocumentsPolling` с остановкой,
когда нет `UPLOADED`/`PROCESSING`. Слайс `files` типизируется или заменяется локальным хуком (решает исполнитель по
месту, без смены поведения).
### D-9. Сводная ОСВ
`layouts/ConsolidatedOsvPage.tsx` + `widgets/ConsolidatedOsv/` (`useConsolidatedOsv`, `OsvFilters`, `OsvTable`,
`OsvGaps`). Таблица — antd `Table` с серверной пагинацией (`limit` 500). Фильтры — query-параметры как в API; ЮЛ —
из `VersionCard.members`.
### D-10. Тесты — vitest на чистых функциях
`vitest` (devDependency), `npm test` → `vitest run`, цель `make test` входит в `make check`. Покрытие по TDD: money,
`summarizeUpload`, подписи статусов/типов/периодов, `normalizeData` карточки версии, `exportFilename` и разбор
`Content-Disposition`, `buildStatementGrid`, `buildEntryTree`. Компоненты — ручной демо-сценарий.
## Демо-сценарий (приёмка)
Стенд по `../docs/dev/qa-guide.md` §4 и §6.8 (профиль `services`: tabular-parser, scan-gateway, scan-emulator),
`make seed-dev`, вход `ka_rf@armka.local` (или пользователь из гайда с правами EDIT на ГК «Русский бисквит»).
1. Группы → «Русский бисквит» → «Новая консолидация»: ГСК, 2024, Q3 → члены ГСК отмечены → «Создать» → открыта версия №1.
2. Панель «Организации» → РБ → «Файлы»: загрузить `fixtures/osv/rb/2024-Q{1,2,3}/*`; то же для Счастливочной
(`fixtures/osv/schastl/2024-Q{1,2,3}/*`). Дождаться «Требуется проверка», подтвердить ОСВ-документы (экран ОСВ).
3. Загрузить `fixtures/ocr/…/aep_6.pdf` одному из ЮЛ → ББ и ОФР «Требуется проверка» → открыть ББ: скан слева, форма
справа, исправить ячейку, принять нарушения с комментарием (если есть), подтвердить.
4. «К отчётности» → сводная ОСВ: 1385 строк, 22 документа, 7 пробелов; фильтр Счастливочная + «бисквит» → строка `60.01`
по `qa-guide` §7.2; «Скачать xlsx».
Шаги API (2–4) перед браузерным прогоном проверяются curl-скриптом; браузерный прогон — владелец.
## Risks / Trade-offs
- [Круг «ядро + scan-gateway + эмулятор» на стенде целиком не прогонялся (`qa-guide` §6.8: «сервис проверяется
отдельно»)] → Задача 1 плана — прогон до кода. Если сломан: issue в arm-core/scan-gateway, а форма ББ/ОФР на демо —
из xlsx (`fixtures/statements-xlsx/mezheninovskaya`, `qa-guide` §6.9), скан — заглушка «превью недоступно».
- [Объём к завтра] → порядок задач от критичного к желательному; экспорт и пробелы — последними.
- [Ручные типы расходятся с API] → типы только по используемым полям, сверка со схемами при ревью.
- [Подтверждение 22 ОСВ кликами долго на демо] → в README — скрипт подтверждения из `qa-guide` §7.1; на демо можно
показать подтверждение одного документа, остальные подготовить заранее.
## Migration Plan
Один PR `feat/3-demo-prototype` → `test`. Откат — revert PR. Перенос в `arm-core/web` — отдельная задача после демо.
## Open Questions
- Какой ЮЛ «владеет» `aep_6.pdf` в фикстурах (ИНН в шапке) — влияет только на `counterparty_check` (не блокирует
подтверждение); выясняется в задаче 1.
## Decisions made without discussion
- 08.10.2026 — D-5 dev-proxy `/api` вместо `REACT_APP_API_URL=http://localhost:8000`. Обоснование: снимает CORS и
нечитаемый `Content-Disposition` без правок arm-core. Цена ошибки: низкая — переменная `REACT_APP_API_URL` продолжает
работать, proxy только в dev.
- 08.10.2026 — D-6 worker pdf.js из пакета, без CDN. Обоснование: в контуре нет внешнего интернета. Цена ошибки: низкая.
- 08.10.2026 — D-7 правка ячеек пакетом по «Сохранить», а не по каждой ячейке. Обоснование: меньше запросов, сервер
принимает до 5000 ячеек и пересчитывает проверки один раз. Цена ошибки: низкая — поведение меняется в одном хуке.

View File

@ -0,0 +1,57 @@
## Why
В пятницу 09.10.2026 — демо владельцу и бизнесу, затем прототип уходит в контур банка на тестирование заказчику
(и переезжает внутрь `arm-core/web`). Сейчас arm-web не проводит аналитика по пути «консолидация → первичка →
проверка → сводная ОСВ»: загрузка теряет ошибки, проверка документов ходит в эндпоинты старого АРМ, экрана сводной ОСВ
нет, `make check` красный (tsc — 185 ошибок, biome — 276).
## What Changes
- Модалка «Новая консолидация» [42]: предотметка членов ГСК, догрузка всех ЮЛ ГК, ошибки API, переход к новой версии.
- Панель «Файлы ЮЛ» [39]/[40] переписана: загрузка в контексте версии с итогом «принято / отклонено», файлы со
вложенными документами, переход к документу, опрос до окончания распознавания.
- Новый экран документа `/documents/:document_id`: для ББ/ОФР [41]/[41-02] — скан PDF рядом с формой, правка ячеек,
проверки и их акцепт, подтверждение; для ОСВ — дерево строк только для чтения и подтверждение.
- Новый экран «Сводная ОСВ» [30] `/groups/:group_id/versions/:version_id/osv`: таблица, фильтры, пробелы, счётчик
несопоставленных, экспорт xlsx.
- `api/client.ts`: `apiFetchBlob` для бинарных ответов (скан, xlsx); dev-proxy `/api` в `rsbuild.config.ts`.
- **BREAKING (внутреннее):** удалены `layouts/FileViewer.tsx`, `components/FileUploaderModel.tsx`,
`widgets/PDFResultViewer/PDFDataViewer.tsx`, `services/api/mocks.ts`; маршрут `/files/:file_id` заменён на
`/documents/:document_id`.
- `make check` зелёный: biome + tsc + vitest + build; добавлен `vitest` (dev) для чистых функций.
## Capabilities
### New Capabilities
- `consolidation-setup`: создание консолидации ГК за период из модалки и переход к её версии.
- `primary-files-ui`: загрузка первичных файлов ЮЛ в контексте версии и список файлов/документов со статусами.
- `document-review-ui`: проверка и подтверждение документа — форма ББ/ОФР со сканом PDF, ОСВ-документ.
- `consolidated-osv-ui`: сводная ОСВ версии — просмотр, фильтры, пробелы, экспорт.
### Modified Capabilities
(нет — `openspec/specs/` пуст)
## Impact
- Экраны макета: [03], [34], [39], [40], [41], [41-02], [42], [30].
- Эндпоинты arm-core (`/api/v1`), только существующие:
`GET /directories/company-groups`, `GET /directories/company-groups/{id}/related-groups`,
`GET /directories/related-groups/{id}`, `GET /directories/legal-entities`, `POST /consolidations`,
`GET /company-groups/{id}/consolidations`, `GET /consolidations/versions/{id}`,
`POST /legal-entities/{id}/files`, `GET /legal-entities/{id}/files`, `GET /files/{id}/content`,
`GET /documents/{id}`, `GET /documents/{id}/entries`, `POST /documents/{id}/confirm`,
`GET|PATCH /documents/{id}/statement`, `POST /documents/{id}/statement/checks/accept`,
`GET /consolidations/versions/{id}/osv`, `GET …/osv/summary`, `GET …/osv/export`.
- Зависимости: `vitest` (devDependency).
- Код: `src/api/`, `src/services/api/`, `src/types/`, `src/widgets/{NewConsoleModal,FilesDrawer,ConsoleTable,…}`,
новые `src/widgets/DocumentReview/`, `src/widgets/ConsolidatedOsv/`, `src/layouts/`, `src/App.tsx`, `rsbuild.config.ts`,
`Makefile`, `README.md`.
## Non-goals
- Блокировка версии («Взять в работу»), сопоставление контрагентов [32-*], экран чек-листа [37], комментарии,
удаление файлов, смена периметра и ручные переходы статусов.
- Правка строк ОСВ (в API нет), картинки страниц и координаты ячеек в скане (в API нет).
- Генерация типов из OpenAPI; автотесты компонентов.
- Перенос в `arm-core/web` и сборка под контур — отдельной задачей после демо.
- Изменения arm-core / scan-gateway: найденный пробел — отдельная issue в том репозитории.

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Сводка версии
Экран «Сводная ОСВ» [30] SHALL показывать шапку версии (ГК, период, номер, статус) и сводку
`GET /consolidations/versions/{id}/osv/summary`: строк всего, документов, несопоставленных (`unmapped_total` — счётчик
«Осталось N» без перехода к сопоставлению), автоматически сопоставленных.
#### Scenario: Контрольные цифры РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3 после подтверждения 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}`
- **THEN** «строк» = 1385, «документов» = 22, «несопоставленных» = 1381
### Requirement: Таблица свода и фильтры
Экран SHALL показывать строки `GET /consolidations/versions/{id}/osv` постранично (размер страницы ≤ 2000): ЮЛ, счёт,
контрагент, ИНН контрагента, признак контрагента (ГСК/ГК/Внешний), способ сопоставления, сальдо на начало Дт/Кт,
обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с разделителями разрядов). Фильтры: ЮЛ (`legal_entity_id`),
корень счёта (`account_root`), признак контрагента (`counterparty_type`), только несопоставленные (`unmapped`), поиск по
контрагенту (`q`, ≤ 500 символов). Смена фильтра SHALL перечитывать первую страницу.
#### Scenario: Строка из контрольных цифр
- **WHEN** фильтр ЮЛ «Пекарня Счастливочная» и поиск «бисквит»
- **THEN** строка `60.01` показывает сальдо нач. Кт 4 454 151.93, обороты Дт 128 058 143.65, Кт 125 950 450.77,
сальдо кон. Кт 2 346 459.05, признак «ГСК»
### Requirement: Пробелы
Экран SHALL показывать `gaps[]` сводки списком «ЮЛ — счёт — недостающие кварталы» с пояснением, что строки этих пар в
своде отсутствуют.
#### Scenario: Пробелы РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3
- **THEN** показано 7 пробелов, у каждого недостающие кварталы «Q1, Q2»
### Requirement: Экспорт
Кнопка «Скачать xlsx» SHALL получать `GET /consolidations/versions/{id}/osv/export` с текущими фильтрами через клиент API
с токеном и сохранять файл; имя — из `Content-Disposition`, а если заголовок недоступен —
`Сводная_ОСВ_<ГК>_<год>_<квартал>.xlsx`.
#### Scenario: Экспорт с фильтром
- **WHEN** аналитик скачивает свод с фильтром по ЮЛ
- **THEN** сохраняется xlsx, а ошибка сервера показывается текстом, а не скачивается

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Состав модалки «Новая консолидация»
Модалка [42] SHALL предлагать выбор ГК (`GET /directories/company-groups`), ГСК этой ГК с `kind = gsk`
(`GET /directories/company-groups/{id}/related-groups`), года и квартала, и показывать **все** незакрытые ЮЛ ГК
(`GET /directories/legal-entities?company_group_id=&closed=false`, страницы дочитываются по `offset`, пока не получены
все `total`). Члены выбранной ГСК (`GET /directories/related-groups/{id}` → `members[]`) SHALL быть отмечены
автоматически; пользователь может снять или добавить отметки.
#### Scenario: Выбор ГСК отмечает её членов
- **WHEN** аналитик выбирает ГК «Русский бисквит» и её ГСК
- **THEN** в списке ЮЛ видны все ЮЛ ГК, а члены ГСК («АО "Русский бисквит"», «ООО "Пекарня Счастливочная"») отмечены
#### Scenario: ЮЛ больше одной страницы
- **WHEN** у ГК больше ЮЛ, чем `limit` одной страницы
- **THEN** модалка показывает все ЮЛ ГК, а не первую страницу
### Requirement: Создание и переход к версии
По «Создать» модалка SHALL отправить `POST /consolidations {company_group_id, related_group_id, year, quarter,
legal_entity_ids}` с отмеченными ЮЛ. При 201 модалка SHALL закрыться, список консолидаций ГК — перечитаться, а созданная
версия (`VersionCard.id`) — открыться в карточке. При ошибке модалка SHALL остаться открытой и показать `message` из
`ApiError` (коды `CONSOLIDATION_EXISTS`, `RELATED_GROUP_MISMATCH`, `ENTITY_NOT_IN_GROUP`, `PERIMETER_TOO_SMALL` и 422
валидации).
#### Scenario: Успешное создание
- **WHEN** аналитик создаёт консолидацию 2024 Q3 с периметром из ГСК
- **THEN** открыта карточка версии №1 со статусом «Инициация» и двумя ЮЛ в панели «Организации»
#### Scenario: Консолидация за период уже есть
- **WHEN** сервер отвечает 409 `CONSOLIDATION_EXISTS`
- **THEN** модалка не закрывается и показывает текст ошибки сервера
### Requirement: Переход к сводной ОСВ из карточки
Кнопка «К отчётности» карточки версии SHALL вести на `/groups/:group_id/versions/:version_id/osv` и быть доступна в
любом статусе версии.
#### Scenario: Переход к своду
- **WHEN** аналитик нажимает «К отчётности» у версии
- **THEN** открывается экран сводной ОСВ этой версии

View File

@ -0,0 +1,64 @@
## ADDED Requirements
### Requirement: Экран документа
Маршрут `/documents/:document_id` SHALL загрузить `GET /documents/{id}` и показать вид по `document_type`: `BALANCE` и
`INCOME` — проверка формы, `OSV` — просмотр ОСВ; для прочих типов — карточку документа без данных. Шапка экрана SHALL
показывать тип, ЮЛ-владельца, период и статус документа, кнопку «Назад».
#### Scenario: Открытие формы баланса
- **WHEN** аналитик открывает документ `BALANCE`
- **THEN** показан экран проверки формы
### Requirement: Скан рядом с формой
Для ББ/ОФР экран SHALL показывать исходный файл документа: PDF — постранично через pdf.js из байт
`GET /files/{file_id}/content?disposition=inline`, полученных клиентом API с токеном (не через `<iframe>` и не прямой
ссылкой); изображение (png/jpg) — тегом `<img>` из blob; xlsx — без превью, с кнопкой «Скачать исходник».
#### Scenario: PDF под CSP sandbox
- **WHEN** сервер отдаёт PDF с `Content-Security-Policy: sandbox`
- **THEN** страницы PDF отрисованы на canvas и листаются
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`. Ячейки с
`confidence < 100` или `is_recognized = false` SHALL подсвечиваться как «требует внимания», с `is_modified = true` — как
«исправлено» с исходным значением `original_amount` в подсказке. Единицы (`header.units`) SHALL выводиться над таблицей.
Пока документ не `CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после
точки, знак минус допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values:
[{line_id, column_id, amount}]}` суммой-строкой, ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта,
единицы) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется.
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Проверки и их акцепт
Экран SHALL показывать `checks`: жёсткие (`failed`), мягкие (`soft_failed`), межформенную (`cross_form`) и акцепт
(`accepted`: кто, когда, комментарий). При непустом `failed` и `accepted = null` SHALL быть доступно «Принять с
комментарием»: комментарий 1–2000 символов → `POST /documents/{id}/statement/checks/accept {comment}`.
#### Scenario: Акцепт нарушений
- **WHEN** есть жёсткие нарушения и аналитик принимает их с комментарием
- **THEN** блок проверок показывает акцепт, нарушения остаются видимыми
### Requirement: Подтверждение документа
Кнопка «Подтвердить» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`. При 409
`CHECKS_NOT_PASSED` экран SHALL показать `detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать
исправить дату отчёта; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
### Requirement: ОСВ-документ
Для `OSV` экран SHALL показывать строки `GET /documents/{id}/entries` (все страницы, `limit` ≤ 2000) деревом по
`parent_entry_id` с колонками: счёт/субсчёт, контрагент, ИНН, сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец
Дт/Кт (суммы — строками с разделителями разрядов, рубли), только чтение. Кнопка «Подтвердить» — как для форм.
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`

View File

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /legal-entities/{id}/files?consolidation_version_id={version_id}` (multipart, поле `files` повторяется), без
других полей формы. Тип и период документа не спрашиваются. После ответа панель SHALL показать итог: принятые файлы
(`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом `UploadResult` SHALL
трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail` (`ENTITY_NOT_IN_PERIMETER`)
и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
### Requirement: Список файлов и документов ЮЛ
Панель SHALL показывать файлы ЮЛ из `GET /legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов есть `UPLOADED` или `PROCESSING`, панель SHALL перечитывать список раз в 5 секунд и
SHALL прекратить опрос, когда таких файлов нет или панель закрыта. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы ЮЛ перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются