2026-10-10 10:04:07 +04:00

10 KiB
Raw Blame History

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

  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. Цена ошибки: низкая.