## Context Сквозной дизайн — `../docs/openspec/changes/console-files/design.md` (модель, алгоритмы, API, сравнение вариантов привязки). Здесь — только решения уровня ядра: где лежит код, как считается стол, как переводятся версии, миграция. Точки опоры в коде: единственная точка создания `stored_file` — `services/primary_files._store`; переход версии после загрузки — `after_upload_to_version` (отдельная транзакция `session_transaction`, `lock_version FOR UPDATE`, затем `consolidation_status.transition`); автозамена — `services/result_mapping.supersede_previous` (старые файлы под `FOR UPDATE ORDER BY id`); карточка файла — `services/documents.file_out`; видимость консолидации — `repositories/consolidations._visible_consolidations` (ГК в области пользователя). ## Goals / Non-Goals Goals: см. proposal. Non-goals: см. proposal. ## Decisions **D-1. Колонка последней.** `StoredFile.consolidation_id` объявляется после `updated_at` в ORM и в DBML v5.18: `ALTER TABLE … ADD COLUMN` ставит её в конец, а `check_dbml_vs_orm` сверяет порядок (как `kpp` в v5.17). FK — `fk_stored_file_consolidation_id_consolidation` по соглашению имён 0001; индекс `ix_stored_file_legal_entity_id_consolidation_id`. **D-2. Backfill в миграции 0006 — одним UPDATE.** Для каждого файла с документом в `consolidation_document` берётся консолидация версии; при нескольких — самый ранний период (`ORDER BY c.year, c.quarter, c.id`, enum `period_quarter` упорядочен Q1 < … < Q4 по объявлению): ```sql UPDATE {S}.stored_file AS sf SET consolidation_id = pick.consolidation_id FROM ( SELECT DISTINCT ON (d.stored_file_id) d.stored_file_id, c.id AS consolidation_id FROM {S}.document d JOIN {S}.consolidation_document cd ON cd.document_id = d.id JOIN {S}.consolidation_version v ON v.id = cd.consolidation_version_id JOIN {S}.consolidation c ON c.id = v.consolidation_id ORDER BY d.stored_file_id, c.year, c.quarter, c.id ) AS pick WHERE sf.id = pick.stored_file_id ``` Откат — `DROP INDEX`, `DROP COLUMN` (FK уходит вместе с колонкой). **D-3. Проверки загрузки в консолидацию — до приёма, без блокировок.** `PrimaryFilesService.check_consolidation_for_upload (consolidation_id, entity) -> Consolidation`: `consolidations_repo.get_consolidation(session, scope, id)` (видимость по ГК, `None` → 404 `NOT_FOUND` «консолидация не найдена»); `consolidations_repo.versions_of(session, consolidation_id)`; версии со статусом из `IN_WORK_STATUSES` — пусто → 409 `CONSOLIDATION_NOT_EDITABLE`; `consolidations_repo.members` по каждой из них — ЮЛ ни в одной → 422 `ENTITY_NOT_IN_PERIMETER`. Как и раньше, `FOR UPDATE` не нужен: статус перечитывается под блокировкой при переходе. **D-4. Переход N версий одной отдельной транзакцией.** `after_upload_to_consolidation(consolidation_id, entity_id)`: в `session_transaction` новый репозиторий `consolidations_repo.lock_versions_of_consolidation(session, consolidation_id, entity_id, statuses={INITIATED})` — `SELECT … FOR UPDATE ORDER BY id` версий этой консолидации с ЮЛ в периметре; по каждой `transition(INITIATED → DOCUMENTS_PROCESSING, changed_by = загрузивший)`. Любое исключение логируется и не меняет ответ (R-10 Среза 4 сохраняется). Advisory-lock ГК не берётся: версии создаются только через `POST /consolidations`, которое берёт его само, а новая версия не видна до commit — она просто не попадёт в выборку, и её статус `INITIATED` переведёт следующая загрузка или подтверждение (то же, что сейчас для одной версии). **D-5. Один цикл приёма для двух маршрутов.** Логика приёма файлов (чтение с лимитом, `upload_one`, сбор `accepted`/`rejected`, 422 при пустом `accepted`) выносится из `upload.py` в функцию `accept_uploads(service, entity, files, response, *, consolidation_id) -> UploadResult`; маршрут ЮЛ вызывает её с `None`, маршрут консолидации — с id после проверок D-3 и затем `after_upload_to_consolidation`. Новый модуль `api/endpoints/files/consolidation_files.py` (оба маршрута консолидации), подключается в `files/__init__.py`. Проверка права — `DOCUMENT_EDIT`/`DOCUMENT_VIEW`, как у маршрутов ЮЛ (загружает и смотрит первичку тот же аналитик; `CONSOLIDATION_*` права не требуются). **D-6. Рабочий стол — один запрос с `OR EXISTS`.** `files_repo.list_desk_files(session, scope, entity_id, consolidation_id, *, include_superseded, limit, offset)` повторяет `list_files` с условием `StoredFile.consolidation_id == cid OR EXISTS (document d JOIN consolidation_document cd JOIN consolidation_version v WHERE d.stored_file_id = StoredFile.id AND v.consolidation_id = cid)`; `count` тем же фильтром; порядок прежний. Сервис `page_desk(consolidation_id, entity_id, *, include_superseded, page)`: консолидация видима (404), ЮЛ видимо (404), затем `_cards`. Привязка ЮЛ к ГК консолидации не проверяется: стол ЮЛ чужой ГК просто пуст. **D-7. `FileOut.consolidation_id`** — из `file.consolidation_id` в `file_out`; `DocumentBrief` не меняется. **D-8. Наследование при автозамене** — в `supersede_previous` сразу после `new_file.version_no = …`: `if new_file.consolidation_id is None: new_file.consolidation_id = next((old.consolidation_id for old in locked if old.consolidation_id is not None), None)`; `locked` уже упорядочен по `id`. **D-9. Удаление старого контекста версии.** `check_version_for_upload`, `after_upload_to_version` и query `consolidation_version_id` удаляются вместе с тестами на них (переписываются под консолидацию); код `VERSION_NOT_EDITABLE` остаётся для правок версии, `VERSION_NOT_FOUND` — для маршрутов версии. **D-10. Аудит.** `FILE_UPLOAD.after_value` получает `consolidation_id` (строка uuid или `null`); отдельного события «загрузка в консолидацию» нет; переходы версий пишут `CONSOLIDATION_STATUS_CHANGE` как раньше. ## Risks / Trade-offs - `OR EXISTS` по `consolidation_document` на большом ЮЛ — подзапрос по индексу `(consolidation_version_id, document_id)` и `document (stored_file_id)`; объёмы — сотни файлов на ЮЛ, не тысячи. - Переход нескольких версий в одной транзакции — пока версия одна; порядок `ORDER BY id` совпадает с R-2 Среза 4. - Backfill «самый ранний период» — эвристика для тестовых стендов; на проде данных до миграции нет. ## Decisions made without discussion 1. 09.10.2026 — **R-1:** права маршрутов консолидации — `DOCUMENT_EDIT`/`DOCUMENT_VIEW`, а не `CONSOLIDATION_*`: первичку загружает и смотрит тот же пользователь, что и на странице ЮЛ. Цена ошибки: низкая — одна константа в `dependencies` маршрута. 2. 09.10.2026 — **R-2:** 404 загрузки/стола по консолидации — код `NOT_FOUND` с текстом «консолидация не найдена» (константа `CONSOLIDATION_NOT_FOUND` в `services/consolidation_guard.py`), в духе правила (7) README. Цена ошибки: низкая. 3. 09.10.2026 — **R-3:** стол не проверяет принадлежность ЮЛ к ГК консолидации (D-6). Цена ошибки: низкая — пустой список вместо 422. 4. 09.10.2026 — **R-4:** advisory-lock ГК при переходе версий после загрузки не берётся (D-4). Цена ошибки: низкая — версия, созданная параллельно с загрузкой, останется `INITIATED` до следующего события, как и сейчас. 5. 09.10.2026 — **R-5:** общий приёмный цикл `accept_uploads` живёт в `endpoints/files/upload.py` и импортируется `consolidation_files.py` (а не в сервисе): он работает с `UploadFile`/`Response` FastAPI. Цена ошибки: низкая. 6. 09.10.2026 — **R-6:** backfill миграции тестируется сценарием «файл с двумя привязками к версиям разных периодов → самый ранний»; файл без привязок остаётся `NULL`. Цена ошибки: низкая.