diff --git a/openspec/changes/console-files/.openspec.yaml b/openspec/changes/console-files/.openspec.yaml new file mode 100644 index 0000000..ea7acf6 --- /dev/null +++ b/openspec/changes/console-files/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-10-09 +skip_specs: true diff --git a/openspec/changes/console-files/design.md b/openspec/changes/console-files/design.md new file mode 100644 index 0000000..210ea42 --- /dev/null +++ b/openspec/changes/console-files/design.md @@ -0,0 +1,150 @@ +## Context + +Панель «Первичные документы» [39] открывается из строки ЮЛ в карточке версии консолидации и показывает +`GET /legal-entities/{id}/files` — все файлы ЮЛ без фильтра. У одной ГК несколько консолидаций за разные периоды, их +файлы лежат в одном списке ЮЛ. Хранится только привязка подтверждённых документов к версии (`consolidation_document`, +§3 п. 1); факт «файл загружен в эту консоль» не хранится: `consolidation_version_id` при загрузке используется для +проверки версии и перехода `INITIATED → DOCUMENTS_PROCESSING` и забывается. + +Целевая картина (владелец, 09.10.2026): + +- рабочие версии одного периода делят первичку; различаются периметром; +- верифицированная версия заморожена, первичка зафиксирована в ЕХ (`ex_snapshot_item.stored_file_id`, Срез 7); +- новая версия после `VERIFIED` может менять документы, периметр сохраняется; +- загрузка в консоль — маршрутом от консолидации. + +Текущее состояние ядра: одна консолидация — одна версия (`POST /consolidations` создаёт версию 1; «новая версия» и +параллельные версии не реализованы). Единственная точка создания `stored_file` — `services/primary_files._store` +(загрузка); горячие папки scan-gateway файлов в ядре не создают. Автозамена (`services/result_mapping`) создаёт +`SUPERSEDED` у старых файлов по ключу документа (тип, счёт, год, квартал), не по консоли. + +## Goals / Non-Goals + +Goals: панель ЮЛ в версии показывает рабочий стол консоли этого ЮЛ; файл помнит консоль загрузки; маршруты загрузки и +списка в контексте консоли — от консолидации; модель совместима с целевой картиной версий без копирования связей. +Non-goals: см. proposal. + +## Модель + +`stored_file.consolidation_id uuid [ref: > consolidation.id]`, nullable. Семантика: консолидация (ГК + год + квартал), +в контексте которой файл загружен; `NULL` — без контекста. Пишется один раз при загрузке. Индекс +`(legal_entity_id, consolidation_id)` — выборка рабочего стола. + +Почему период, а не версия: + +| Вариант | Плюс | Минус | +|---|---|---| +| `consolidation_file(version_id, file_id)` | буквально «загружено в версию» | при второй рабочей версии и новой версии после `VERIFIED` связи надо копировать; вторая связь рядом с `consolidation_document`; верифицированная версия и так заморожена привязками и ЕХ | +| `stored_file.uploaded_in_version_id` | дешевле таблицы | те же проблемы разделения между рабочими версиями | +| **`stored_file.consolidation_id`** | рабочие версии периода делят стол автоматически; новая версия после `VERIFIED` видит стол и подхватывает подтверждённое | нет истории «в какой версии загружено» — она в `FILE_UPLOAD.after_value` | + +Заморозка остаётся на `consolidation_document` (после `VERIFIED` привязки не снимаются) и на `ex_snapshot` (Срез 7). + +## Алгоритмы (§9.5) + +**Загрузка в консоль.** `POST /consolidations/{cid}/legal-entities/{le}/files` (`DOCUMENT_EDIT`): + +``` +консолидация cid видима (policy по ГК)? нет → 404 NOT_FOUND («консолидация не найдена») +ЮЛ le видимо? нет → 404 NOT_FOUND +есть версия cid в работе (IN_WORK_STATUSES)? нет → 409 CONSOLIDATION_NOT_EDITABLE +le в периметре хотя бы одной такой версии? нет → 422 ENTITY_NOT_IN_PERIMETER +приём файлов как в POST /legal-entities/{le}/files; file.consolidation_id = cid +после приёма ≥ 1 файла, отдельной транзакцией: + для каждой версии cid в работе с le в периметре и статусом INITIATED: + FOR UPDATE → перечитать статус → transition(INITIATED → DOCUMENTS_PROCESSING, changed_by = загрузивший) +``` + +Аудит `FILE_UPLOAD.after_value` дополняется `consolidation_id`; версии, переведённые в `DOCUMENTS_PROCESSING`, пишут +`CONSOLIDATION_STATUS_CHANGE` как сейчас. + +**Загрузка без контекста.** `POST /legal-entities/{le}/files` — как сейчас, без query; `consolidation_id = NULL`. + +**Рабочий стол.** `GET /consolidations/{cid}/legal-entities/{le}/files` (`DOCUMENT_VIEW`; 404 на невидимую +консолидацию или ЮЛ; `limit`, `offset`, `include_superseded`): + +``` +DESK = файлы le с consolidation_id = cid +BOUND = файлы le, у которых есть документ с consolidation_document к любой версии cid +результат = DESK ∪ BOUND, новые первыми (uploaded_at desc, id) +SUPERSEDED — только при include_superseded = true (и для DESK, и для BOUND) +``` + +`BOUND` нужен для файлов, загруженных без контекста или в другой период и подхваченных версией при подтверждении +(ОСВ за `Q1`, подходящая версиям `Q2`–`Q4`, R-14). Файл отдаётся той же карточкой `FileOut`, что и список ЮЛ. + +**Автозамена.** Новый файл после маппинга: если `consolidation_id IS NULL`, берётся `consolidation_id` первого +заменённого файла (по порядку `id`), у которого он не `NULL`. При загрузке в контексте своё значение сохраняется. + +**Удаление, подтверждение, привязки** — без изменений. + +**Backfill (миграция 0006):** `stored_file.consolidation_id` = консолидация версии, к которой привязан любой документ +файла; при привязках к версиям разных консолидаций — самый ранний период (год, затем квартал). Файлы без привязок — +`NULL`. Откат — удаление колонки. + +## API + +| Маршрут | Право | Вход | Ответ | Ошибки | +|---|---|---|---|---| +| `POST /consolidations/{cid}/legal-entities/{le}/files` | `DOCUMENT_EDIT` | multipart `files` (повторяется) | 201 `UploadResult`; 422 `UploadResult`, если не принят ни один | 404; 409 `CONSOLIDATION_NOT_EDITABLE`; 422 `ENTITY_NOT_IN_PERIMETER`; 503 `STORAGE_UNAVAILABLE` | +| `GET /consolidations/{cid}/legal-entities/{le}/files` | `DOCUMENT_VIEW` | `limit`, `offset`, `include_superseded` | `Page[FileOut]` | 404 | +| `POST /legal-entities/{le}/files` | `DOCUMENT_EDIT` | как сейчас, **без** `consolidation_version_id` | как сейчас | как сейчас | +| `GET /legal-entities/{le}/files`, `GET /files/{id}` | `DOCUMENT_VIEW` | как сейчас | `FileOut` + `consolidation_id: uuid \| null` | как сейчас | + +## Фронт (arm-web) + +- Стор `consolidations`: `current_consolidation_id` рядом с `current_version_id`; ставится там же (строка списка — + `con_id`, карточка версии — `consolidation_id`). +- `services/api/files.ts`: `uploadFiles(legalEntityId, files, consolidationId?)` и + `listLegalEntityFiles(legalEntityId, consolidationId?)` — с консолидацией маршруты `/consolidations/{cid}/…`, без неё + маршруты ЮЛ. +- Панель [39]: список рабочего стола; переключатель «Все документы организации» внизу переключает на список ЮЛ; у файла + с `consolidation_id`, отличным от текущего, подпись «2025 К1» (год и квартал из списка консолидаций ГК в сторе), у + файла без консолидации — «вне консолидации». Опрос раз в 5 с — как сейчас, по активному списку. +- Кнопка «Загрузить документы» недоступна, если открытая версия не «в работе» (статус из строки списка). +- Экран [60] «Монозаёмщики» (когда появится) — панель без консолидации. + +## Документы + +`model/data-model-v5.18.dbml` (+ changelog в шапке), `model/entities-v5.18.md`, `04-data-model-guide.md` (v5.18 имена +не меняет), `05-domain-rules.md` §9.5 п. 1, 3 и новый пункт «Рабочий стол консоли», `10-user-journey.md` п. 2.5, +`dev/frontend-api-guide.md`, `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, +`07-open-questions.md` (новый вопрос о целевой модели версий), ссылки на v5.17 в `CLAUDE.md` workspace и +`repo-templates/*.CLAUDE.md`. + +## Risks / Trade-offs + +- Файл с неподходящим периодом, загруженный в консоль, остаётся в её столе без пометки: «не подходит версии» видно + только по чек-листу. Приемлемо: владелец выбрал семантику «загружено сюда». +- Два списка (стол и все файлы ЮЛ) с одинаковыми карточками — пользователь может не заметить переключатель. Подпись + консолидации у файла и заголовок панели это смягчают. +- Backfill по «самому раннему периоду» для файла, привязанного к версиям разных периодов, — эвристика; таких файлов + на стенде единицы (ОСВ `Q1`, подхваченные версией `Q2`). +- Переход `INITIATED → DOCUMENTS_PROCESSING` у нескольких версий сразу — сейчас версия одна; порядок блокировок (по + возрастанию `id`, R-2 Среза 4) сохраняется. + +## Decisions made without discussion + +1. 09.10.2026 — **R-1:** `consolidation_file` (связь файл–версия) не вводится; поле `stored_file.consolidation_id` на + уровне периода. Обсуждено с владельцем на brainstorming; здесь зафиксировано как итог. Цена ошибки: средняя — + при переходе к «файлы по версиям» потребуется таблица связей и миграция данных. +2. 09.10.2026 — **R-2:** коды ошибок загрузки в консоль: 404 `NOT_FOUND` (консолидация/ЮЛ вне видимости, как везде), + 409 `CONSOLIDATION_NOT_EDITABLE` (нет версии «в работе»), 422 `ENTITY_NOT_IN_PERIMETER` (существующий код). Цена + ошибки: низкая — коды в README и гайде фронта. +3. 09.10.2026 — **R-3:** backfill — самый ранний период среди привязок; файлы без привязок остаются `NULL`. Цена ошибки: + низкая — стенд разработки, данные тестовые. +4. 09.10.2026 — **R-4:** при автозамене новый файл без контекста наследует консоль заменённого; с контекстом — своё. + Цена ошибки: низкая — влияет на то, в каком столе виден файл; привязки документов не затрагиваются. +5. 09.10.2026 — **R-5:** рабочий стол = `DESK ∪ BOUND` (файлы консоли плюс файлы с документами, привязанными к любой + версии консоли). Без `BOUND` файл, загруженный без контекста и подхваченный версией, отсутствовал бы в панели при + заполненной ячейке чек-листа. Цена ошибки: низкая — лишние строки в панели. +6. 09.10.2026 — **R-6:** целевые правила версий («вторая рабочая версия = другой периметр», «новая версия после + `VERIFIED` с тем же периметром и правом менять документы») в этом change не реализуются и не специфицируются — + фиксируются вопросом в `07-open-questions.md` для среза «версии консолидации». Цена ошибки: низкая — модель файлов + с ними совместима. +7. 09.10.2026 — **R-7:** замороженный список первички верифицированной версии в панели не строится (ЕХ, Срез 7); + панель из карточки версии не «в работе» показывает стол консоли без кнопки загрузки. Цена ошибки: средняя — если + Срез 7 задержится, у верифицированной версии не будет экрана «что именно использовано»; чек-лист версии частично + закрывает (`document_ids` в ячейках). +8. 09.10.2026 — **R-8:** query `consolidation_version_id` у `POST /legal-entities/{id}/files` удаляется, а не + сохраняется для совместимости: единственный клиент — arm-web, меняется в том же выпуске. Цена ошибки: низкая. diff --git a/openspec/changes/console-files/proposal.md b/openspec/changes/console-files/proposal.md new file mode 100644 index 0000000..3477481 --- /dev/null +++ b/openspec/changes/console-files/proposal.md @@ -0,0 +1,74 @@ +## Why + +Панель «Первичные документы» ЮЛ [39], открытая из версии консолидации, показывает все файлы ЮЛ, в том числе +загруженные в другие консолидации той же ГК. Причина в модели: файл принадлежит ЮЛ (`stored_file.legal_entity_id`), +контекста консолидации у него нет, `GET /legal-entities/{id}/files` ничем не фильтруется. Связь с версией есть только у +подтверждённых документов (`consolidation_document`), поэтому «что версия использует» и «что аналитик загрузил в эту +консоль» сегодня разные множества, и второе нигде не хранится (`consolidation_version_id` при загрузке используется лишь +для перехода статуса версии). + +Решения владельца 09.10.2026 (RSHB/arm-ka-docs#90), целевая картина: + +- файл загружается **в консолидацию** (ГК + год + квартал) и становится частью её «рабочего стола», общего для всех + рабочих версий периода; рабочие версии одного периода различаются периметром; +- верифицированная версия заморожена: её первичка фиксируется в ЕХ (`ex_snapshot_item.stored_file_id`, Срез 7); + новая версия после `VERIFIED` может менять документы при том же периметре; +- что использует конкретная версия, по-прежнему определяют `consolidation_document` и чек-лист версии; +- загрузка и список файлов в контексте консоли идут по маршрутам от консолидации, а не query-параметром у ЮЛ. + +Срез: вне плана `09-delivery-plan.md` — доработка Среза 2d/4 (UI первичных документов в карточке версии). + +## What Changes + +- **Модель, DBML v5.18:** `stored_file.consolidation_id uuid` (nullable, FK → `consolidation`) — консолидация, в + контексте которой загружен файл; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Индекс + `(legal_entity_id, consolidation_id)`. Пишется один раз при загрузке; при автозамене наследуется заменённым файлом, + если у нового файла контекста нет. +- **API ядра:** новые маршруты `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` + (загрузка в консоль) и `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (рабочий стол + консоли для ЮЛ). У `POST /legal-entities/{id}/files` query `consolidation_version_id` убирается — маршрут остаётся + для загрузки без контекста. `FileOut.consolidation_id` в списках и карточке файла. Переход `INITIATED → + DOCUMENTS_PROCESSING` после загрузки — у всех версий консоли «в работе», где ЮЛ в периметре. +- **Правила домена, §9.5:** загрузка в консоль (проверки, коды ошибок, переход версий), состав рабочего стола, + наследование при автозамене. +- **Фронт arm-web:** панель [39] и загрузка [40] ходят на маршруты консолидации; переключатель «Все документы + организации» с подписью консолидации у файла; загрузка недоступна, если открытая версия не «в работе». +- **Документы:** `model/data-model-v5.18.dbml`, `model/entities-v5.18.md`, `04-data-model-guide.md`, + `05-domain-rules.md` §9.5, `10-user-journey.md` п. 2.5, `dev/frontend-api-guide.md`, + `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, `07-open-questions.md`. + +Дочерние change'и `console-files`: RSHB/arm-core (миграция `0006_dbml_v5_18`, сервис, маршруты, спеки `primary-files` +и `documents-api`), RSHB/arm-web (спека `primary-files-ui`). Issue этого репозитория — RSHB/arm-ka-docs#90. +Контракт обмена с сервисами распознавания не меняется, `schema_version` прежняя. + +## Non-goals + +- Несколько рабочих версий одного периода с разным периметром и «новая версия после `VERIFIED`» ядро пока не создаёт + (`POST /consolidations` создаёт только версию 1); эти правила — отдельный срез (OQ, design «Decisions» №6). Этот + change лишь делает модель файлов совместимой с ними. +- Замороженный список первички верифицированной версии в панели не строится: заморозка — `ex_snapshot` (Срез 7). + Панель из карточки версии не «в работе» показывает рабочий стол консоли как есть, без кнопки загрузки. +- Признак «документ подходит / привязан к версии» в списке файлов не вводится: это чек-лист версии (OQ-120). +- Привязка файла к версии (`consolidation_file`) не вводится: отвергнута на brainstorming 09.10.2026 (design, №1). +- `consolidation_document`, правило `fits`, подхват при создании версии и добавлении ЮЛ в периметр — без изменений. +- Горячие папки scan-gateway файлов в ядре не создают; их контекст не рассматривается. +- Backfill касается только файлов с документами в `consolidation_document`; прочие остаются `NULL`. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- Спецификации этого репозитория (`dev-environment`, `quality-checks`, `recognition-contract`) не меняются; требования + к API и UI — в дочерних change'ах arm-core (`primary-files`, `documents-api`) и arm-web (`primary-files-ui`). + +## Impact + +- `arm-core`: миграция с backfill; `services/primary_files.py` (загрузка, `page_files`, автозамена в + `services/result_mapping.py`), `repositories/files.py`, `api/endpoints/files/upload.py`, `read.py`, новый роутер + под `/consolidations/{id}/legal-entities/{le}/files`, `schemas/files.py`; тесты; README «Ошибки API». +- `arm-web`: `services/api/files.ts`, стор `consolidations` (`current_consolidation_id`), `widgets/FilesDrawer/*`, + типы `FileOut`. +- Совместимость: query `consolidation_version_id` у `POST /legal-entities/{id}/files` исчезает — фронт меняется + одновременно с ядром (стенд обновляется после слияния обоих PR). Файлы, загруженные до миграции без привязок, + остаются «вне консолидации» и видны только по переключателю «Все документы организации». diff --git a/openspec/changes/console-files/tasks.md b/openspec/changes/console-files/tasks.md new file mode 100644 index 0000000..af0fa87 --- /dev/null +++ b/openspec/changes/console-files/tasks.md @@ -0,0 +1,19 @@ +## Порядок + +Подробный план с шагами и тестами — superpowers writing-plans (ledger SDD); здесь — крупные задачи по репозиториям +в порядке зависимостей. + +- [ ] 1. **docs** (этот change): `model/data-model-v5.18.dbml` + changelog, `model/entities-v5.18.md`, `04-data-model-guide.md`, + `05-domain-rules.md` §9.5, `10-user-journey.md` п. 2.5, `dev/frontend-api-guide.md`, + `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, `07-open-questions.md` + (целевая модель версий), ссылки на версию DBML в `CLAUDE.md` workspace и `repo-templates/*.CLAUDE.md`. +- [ ] 2. **arm-core** (change `console-files`, спеки `primary-files`, `documents-api`): модель `StoredFile.consolidation_id`, + миграция `0006_dbml_v5_18` с backfill и откатом, `make dbml-check`; `repositories/files.py` (рабочий стол); + `services/primary_files.py` (загрузка в консоль, проверки, переход версий, `page_desk`); `services/result_mapping.py` + (наследование при автозамене); маршруты `/consolidations/{cid}/legal-entities/{le}/files`; удаление query у + `POST /legal-entities/{id}/files`; `schemas/files.py`; README «Ошибки API»; тесты; `make check-full`. +- [ ] 3. **arm-web** (change `console-files`, спека `primary-files-ui`): стор `current_consolidation_id`, `services/api/files.ts`, + `widgets/FilesDrawer/*` (стол, переключатель, подпись консолидации, недоступная загрузка вне «в работе»), типы; + `make check`; ручной прогон сценария из design на стенде в Playwright со сверкой с макетом [39]/[40]. +- [ ] 4. **Закрытие:** обновить `03-architecture.md` (если затронуты схемы), `07-open-questions.md`, `09-delivery-plan.md` + (строка про доработку 2d/4); архив change'ей в трёх репозиториях.