10 KiB
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 по объявлению):
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
- 09.10.2026 — R-1: права маршрутов консолидации —
DOCUMENT_EDIT/DOCUMENT_VIEW, а неCONSOLIDATION_*: первичку загружает и смотрит тот же пользователь, что и на странице ЮЛ. Цена ошибки: низкая — одна константа вdependenciesмаршрута. - 09.10.2026 — R-2: 404 загрузки/стола по консолидации — код
NOT_FOUNDс текстом «консолидация не найдена» (константаCONSOLIDATION_NOT_FOUNDвservices/consolidation_guard.py), в духе правила (7) README. Цена ошибки: низкая. - 09.10.2026 — R-3: стол не проверяет принадлежность ЮЛ к ГК консолидации (D-6). Цена ошибки: низкая — пустой список вместо 422.
- 09.10.2026 — R-4: advisory-lock ГК при переходе версий после загрузки не берётся (D-4). Цена ошибки: низкая —
версия, созданная параллельно с загрузкой, останется
INITIATEDдо следующего события, как и сейчас. - 09.10.2026 — R-5: общий приёмный цикл
accept_uploadsживёт вendpoints/files/upload.pyи импортируетсяconsolidation_files.py(а не в сервисе): он работает сUploadFile/ResponseFastAPI. Цена ошибки: низкая. - 09.10.2026 — R-6: backfill миграции тестируется сценарием «файл с двумя привязками к версиям разных периодов →
самый ранний»; файл без привязок остаётся
NULL. Цена ошибки: низкая.