# Design: API до сводной ОСВ — пробелы по макету и комментариям фронтенда ## Context Срезы 4 и 3b закрыли API пути пользователя до сводной ОСВ по `10-user-journey.md`. Аудит «экраны макета ↔ API» (07.10.2026, находки П-1…П-14) и комментарии фронтенд-разработчика показали три места, где экран собирается только цепочкой вызовов или не собирается вовсе: - модалка [42] «Новая консолидация» — в коде макета (`docs/mockup/dkb_24.09.26/dkb_24.09.26.html`, блок «ЮЛ консолидации») после выбора ГК и ГСК показывает все неархивные ЮЛ ГК, отмечены члены ГСК, отметки меняются, минимум два; создаётся консолидация с отмеченным списком. Снимок `42-new-consolidation.png` снят до выбора ГК, поэтому `10-user-journey.md` шаг 2.1 и `mockup/INDEX.md` этот блок не описывают (П-17); - панель «Организации» [34] — ячейки чек-листа участника позиционные (П-15); нет «Тип» и перечня счетов ОСВ (П-9); - комментарий фронта «нет ручки для файлов ЮЛ» — эндпоинт есть (`GET /legal-entities/{id}/files`, Срез 2c), но не описан в контракте для фронта (П-5). Модель данных не меняется: всё вычисляется из существующих таблиц DBML v5.16. ## Goals / Non-Goals **Goals:** экран [42] создаёт консолидацию одним запросом с выбранным периметром; экран [34] строится из одной карточки версии; обратная совместимость ответов (поля только добавляются, вход без `legal_entity_ids` ведёт себя как прежде). **Non-Goals:** см. `proposal.md` «Non-goals» — хранение ГСК версии, поля Среза 7, `period_check` (OQ-120), фильтр файлов по версии, П-4/П-6/П-11/П-12/П-13, DBML и контракт распознавания. ## Decisions ### D-1. Периметр при создании — необязательное `legal_entity_ids` (решение владельца, П-17) `ConsolidationCreate.legal_entity_ids: list[uuid] | None = Field(default=None, max_length=MAX_MEMBERS)`. Без поля — прежнее поведение (члены ГСК, `PERIMETER_TOO_SMALL` по ГСК). С полем — ровно выбранные ЮЛ ГК. ГСК остаётся обязательной: модалка требует её всегда, по ней предзаполняются отметки, проверка `RELATED_GROUP_MISMATCH` сохраняется. ГСК в версии не хранится (П-2): `consolidation.related_group_id` — NULL до верификации. Проверка списка — та же, что у `PUT …/members`: дубли схлопываются в порядке первого вхождения, `repo.entities_in_group` → 422 `ENTITY_NOT_IN_GROUP` с перечнем, затем `PERIMETER_TOO_SMALL`. Выношу её в функцию сервиса `_validate_perimeter(company_group_id, ids) -> list[uuid]`, которую вызывают `create` и `set_members`: правила не разойдутся. В `create` проверка стоит на месте проверки числа членов ГСК — после ГСК, до проверки периода (порядок ошибок не меняется для запросов без списка). `_insert` получает список ЮЛ (сейчас — сущности ГСК); подхват документов и аудит — как сейчас, по фактическому периметру. Архивность ЮЛ отдельно не проверяется: принадлежность ГК определяется `legal_entity.company_group_id`, как в `PUT …/members` (выведенное из ГК ЮЛ ГК уже не принадлежит). Альтернатива «фронт делает create → lock → PUT members» отвергнута: не атомарна, два события аудита, не позволяет ГСК из одного ЮЛ. ### D-2. Самоописывающие ячейки чек-листа (подход B) `ChecklistCellOut` получает `document_type: DocumentType` и `account: str | None` правила. `_cell_out(rule, cell)` заполняет их в обоих местах — карточка версии и `GET …/checklist`; порядок ячеек прежний. Доменный `checklist.build` не меняется; `checklist_csv.render_checklist_csv` читает `ChecklistOut` (подписи столбцов — из `rules`, статусы — из ячеек), новые поля ячейки не использует — вывод CSV не меняется, меняются только конструкторы ячеек в его тестах. Альтернатива A (`rules[]` на уровне карточки) отвергнута: сохраняет позиционную связь, на которой споткнулся фронт. Повтор двух коротких полей в каждой ячейке дешевле ошибки сопоставления по индексу. ### D-3. Участник периметра: `entity_type` и `osv_accounts` (вариант B) `MemberOut.entity_type: EntityType` — из `legal_entity`. `MemberOut.osv_accounts: list[str]` — новый запрос репозитория `osv_accounts(session, entity_ids, year, quarters) -> dict[uuid, list[str]]` (`quarters` — `quarters_through(quarter версии)`) одним `SELECT DISTINCT legal_entity_id, osv_account_code` по `document ⋈ stored_file`: `document_type = OSV`, `period_year = year`, `period_quarter IN quarters`, `document.status != ERROR`, `stored_file.status NOT IN (SUPERSEDED, ERROR)`, `osv_account_code IS NOT NULL`. Сортировка — в Python `account_sort_key` (та же натуральная, что у столбцов чек-листа); ЮЛ без документов — пустой список. Смысл колонки — «какие счета загружены к периоду версии», а не «что подойдёт своду»: подходящесть по §3 (нарастающая vs квартальная, Q1-эквивалентность) здесь не применяется, готовность счетов комплекта видна по ячейкам чек-листа. Квартал «не позже квартала версии», потому что свод склеивает квартальные ОСВ Q1…Qn. ### D-4. Документы — bounded-PR в docs после реализации Контракт для фронта (файлы и документы, справочники, новые поля, правило «Ответственный» = `lock.locked_by_name` при активной блокировке, иначе `initiator.name`), OQ-43 (П-3), формулировка баннера (П-1), шаг 2.1 пути и [42] в `mockup/INDEX.md`, устаревшее «CSV в Срезе 4 нет». Сквозного change в docs нет: модель и контракт обмена не меняются. ## Risks / Trade-offs - **Шире ответ карточки** — `osv_accounts` добавляет один запрос на карточку (индексы `document(legal_entity_id)` есть); на реальных группах — десятки ЮЛ, нагрузка несущественна. - **Фронт может спутать `osv_accounts` с «готово для свода»** — в контракте прямо сказать, что готовность — в ячейках чек-листа. - **Выбранный периметр без членов ГСК** допустим (отметки можно снять все, кроме двух любых ЮЛ ГК) — так в макете; ГСК версии в Срезе 7 будет определяться периметром, а не выбранной ГСК (П-2). ## Migration Plan Миграций нет. Ответы только расширяются; запросы без `legal_entity_ids` работают как прежде. Откат — revert PR. ## Open Questions Новых нет. Решения П-1/П-2/П-3 владельца — в docs-PR (OQ-43 переписывается). ## Decisions made without discussion | № | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | 1 | 2026-10-07 | Явный периметр не обязан включать членов выбранной ГСК | так в макете (отметки ГСК снимаются); ГСК версии не хранится (П-2) | версия без ЮЛ ГСК — исправляется `PUT …/members` | | 2 | 2026-10-07 | Архивность ЮЛ при создании не проверяется отдельно — только `company_group_id`, как в `PUT …/members` | одно правило периметра для создания и правки | фронт может отправить ЮЛ, которое в макете скрыто как архивное; ядро примет, если ЮЛ ещё в ГК | | 3 | 2026-10-07 | `legal_entity_ids: null` равнозначно отсутствию поля | pydantic `None` по умолчанию; фронту проще | — | | 4 | 2026-10-07 | `osv_accounts` не учитывает документы без квартала и без кода счёта | их не к чему отнести в колонке; пустой код — дефект распознавания, виден в drawer | ОСВ без периода не видна в колонке «ОСВ» до правки периода | | 5 | 2026-10-07 | Порядок проверок создания: ГК → ГСК → периметр → период | периметр зависит от ГК и ГСК, конфликт периода — последним, как сейчас | при одновременной ошибке периметра и занятом периоде клиент увидит 422, а не 409 | | 6 | 2026-10-07 | Фильтр файлов ЮЛ по версии не делается (решение владельца, вопрос 1, вариант A) | drawer [39] показывает все документы ЮЛ; OQ-120 | — | | 7 | 2026-10-07 | `osv_accounts` — документы за год версии с кварталом ≤ версии в любом статусе, кроме `ERROR` (решение владельца, вопрос 2, вариант B) | «что загружено к периоду»; свод склеивает кварталы | — | | 8 | 2026-10-07 | Ячейки чек-листа самоописывающие, без `rules[]` в карточке (решение владельца, подход B) | фронт не сопоставляет по индексу | — | | 9 | 2026-10-07 | «Неархивные ЮЛ ГК» модалки [42] в контракте — `legal-entities?closed=false` (без `close_date`) | в API архив ЮЛ выражен только `close_date`; вывод из ГК и так снимает `company_group_id` | фронт покажет/скроет не те ЮЛ — правка строки контракта | | 10 | 2026-10-07 | Интервал опроса файлов 3–5 с — рекомендация в контракте | спека молчит, фронту нужен ориентир | лишняя нагрузка или медленный отклик — правится в контракте | | 11 | 2026-10-07 | `MemberOut.entity_type` nullable (I-1 финального ревью) | `legal_entity.entity_type` nullable в DBML; не-nullable схема давала бы 500 на карточке и создании | фронт обязан показать «—» для `null` | | 12 | 2026-10-07 | Подписи статусов drawer [39] в контракте — как в чек-листе: `RECOGNIZED`/`NEEDS_CHECK` — «Требуется проверка», `CONFIRMED` — «Загружен» (I-4) | одна подпись на один статус документа во всех экранах | если владелец хочет «Загружен» для `RECOGNIZED` — правка контракта |