2026-10-07 18:23:09 +04:00

13 KiB
Raw Permalink Blame History

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

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 — правка контракта