arm-ka-docs/dev/api-contract-slice-4.md

29 KiB
Raw Permalink Blame History

Контракт API Среза 4 (консолидация) для разработчика фронта

Источник — реальная схема GET /openapi.json arm-core (ветка feat/18-slice-4-consolidation, Swagger — /docs); коды ошибок и нюансы сверены с кодом и спекой arm-core/openspec/changes/archive/2026-10-06-slice-4-consolidation/ (design D-1…D-16, решения №1–№81+). Правила предметной области — context/05-domain-rules.md §2, §3, §9.1, §10; экраны — context/10-user-journey.md (пути 2 и 3), номера в […] — снимки макета (mockup/INDEX.md). Здесь — только то, что нужно, чтобы писать клиент.

1. Общее

  • Префикс /api/v1, заголовок Authorization: Bearer <JWT> (в dev — POST /dev/token). Права: CONSOLIDATION_VIEW (чтение), CONSOLIDATION_EDIT (правки); загрузка файлов — DOCUMENT_EDIT. Нет права — 403 строкой {"detail": "недостаточно прав: требуется <КОД>"}; нет/плохой токен — 401. Скрывать кнопки по праву можно по GET /me.
  • Доменные ошибки: {"detail": {"code": "...", "message": "на русском", ...доп. поля}}; по code работает клиент. 422 валидации тела/параметров — массив pydantic (detail: [{loc, msg, type}]). Каталог кодов — arm-core/README.md, «Ошибки API».
  • Видимость: ГК и версии вне области видимости пользователя (ka_rf/rka/client_manager — только свой филиал) неотличимы от несуществующих: 404 с кодом NOT_FOUND. Различие — только по тексту message (см. §4); клиент не должен ветвиться по тексту.
  • Деньги — строки JSON с двумя знаками ("100.00", "0.00"), могут быть null (нарастающий документ отдаётся «как в файле»). Не парсить во float: суммы numeric(18,2).
  • Идентификаторы — uuid-строки; даты — ISO; quarter — Q1…Q4.
  • Страницы — {items, total, limit, offset}, offset ≥ 0. Лимиты у списков разные: сводная ОСВ и строки ОСВ документа — limit ≤ 2000 (по умолчанию 500); файлы ЮЛ и справочники — limit ≤ 200 (по умолчанию 50). Больше предела — 422. Файлы, документы и справочники — dev/api-contract-files-directories.md.
  • Перечисления: ConsolidationStatus — INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION, CONSOLIDATION_RF, ADJUSTMENT, VERIFICATION_RF, CONSOLIDATION_GO, VERIFICATION_GO, VERIFIED, CANCELLED, REJECTED, ARCHIVED (в Срезе 4 достижимы первые три и CANCELLED); CounterpartyType — GSK, GK, EXTERNAL; CounterpartyMatchKind — INN, NAME, ALIAS, MANUAL, PROPAGATED; CellStatus — CONFIRMED, NEEDS_CHECK, RECOGNIZED, PROCESSING, ERROR, MISSING; ConsolidationScenario — CREATE, CONFIRM; ConsolidationApproach — BASIC, EXTENDED.
  • «В работе» — INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION (VersionCard.is_in_work).

2. Эндпоинты

Порядок проверок на каждом маршруте: право (403) → видимость (404) → для правок статус «в работе» и своя блокировка (409) → операция. В таблице «Ошибки» — доменные коды и HTTP; 401/403 везде и ниже не повторяются.

Метод и путь Право Запрос Ответ Ошибки Экран
POST /consolidations EDIT ConsolidationCreate {company_group_id, related_group_id, year (2000–2100), quarter, legal_entity_ids? (≤ 500)} 201 VersionCard (версия №1, INITIATED, lock = null) 404 NOT_FOUND (ГК/ГСК не видна), 409 CONSOLIDATION_EXISTS, 422 RELATED_GROUP_MISMATCH, 422 ENTITY_NOT_IN_GROUP (legal_entity_ids), 422 PERIMETER_TOO_SMALL, 422 (pydantic-массив) при более чем 500 legal_entity_ids [42]
GET /company-groups/{group_id}/consolidations VIEW query in_work_only (bool, по умолчанию false) ConsolidationPeriodOut[] (период + versions[] — VersionBrief) 404 NOT_FOUND [34]
GET /consolidations/versions/{version_id} VIEW — VersionCard 404 NOT_FOUND [34] панель «Организации», [36], [37]
POST /consolidations/versions/{version_id}/lock EDIT тело необязательно: LockIn {takeover_reason?} (≤ 1000) 201 LockOut (новая блокировка или перехват) / 200 LockOut (уже ваша — идемпотентно) 404 NOT_FOUND, 409 VERSION_NOT_EDITABLE (версия CANCELLED), 409 LOCKED_BY_OTHER, 422 (pydantic-массив) при takeover_reason длиннее 1000 символов [35]
DELETE /consolidations/versions/{version_id}/lock EDIT — 204 (в т. ч. если активной блокировки нет) 404 NOT_FOUND, 409 NOT_LOCK_OWNER [34]
POST /consolidations/versions/{version_id}/status EDIT StatusIn {to_status, comment? (≤ 1000)} 200 VersionCard 404, 409 VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER / TRANSITION_NOT_ALLOWED, 422 COMMENT_REQUIRED, 422 (pydantic-массив) при comment длиннее 1000 символов [33-01], меню «…» [34]
PUT /consolidations/versions/{version_id}/members EDIT MembersIn {legal_entity_ids[] (≤ 500)} 200 VersionCard 404, 409 VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER, 422 ENTITY_NOT_IN_GROUP (legal_entity_ids), 422 PERIMETER_TOO_SMALL, 422 (pydantic-массив) при более чем 500 legal_entity_ids [36]
GET /consolidations/versions/{version_id}/checklist VIEW query format (json по умолчанию / csv) ChecklistOut {rules[], rows[]}; с format=csv — файл CSV (Срез 3b) 404 [37]
GET /consolidations/versions/{version_id}/osv VIEW query: limit, offset, legal_entity_id, account_root, counterparty_type, unmapped (bool), match_kind (повторяемый), q (≤ 500) 200 Page[ConsolidatedRowOut] 404, 422 (pydantic-массив) при q длиннее 500 символов [30]
GET /consolidations/versions/{version_id}/osv/summary VIEW — OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]} 404 [30], баннер «Осталось: N», [31-00]
GET /consolidations/versions/{version_id}/osv/export VIEW те же фильтры, что у …/osv, без limit/offset файл xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) 404 «Экспорт» [30]
GET /consolidations/versions/{version_id}/counterparty-candidates VIEW query: q (≤ 500), scope (group по умолчанию / all), entry_id CandidateOut[] (до 100) 404, 422 QUERY_TOO_SHORT (scope=all и q короче 3 символов после обрезки) [32-osv-map-counterparty]
POST /consolidations/versions/{version_id}/osv/entries/{entry_id}/counterparty EDIT MapCounterpartyIn {legal_entity_id, move_alias?=false} 200 MapCounterpartyOut 404, 409 VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER, 409 ALIAS_TAKEN (owners[] {legal_entity_id, name}) [32-osv-map-counterparty] «Связать»
POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files DOCUMENT_EDIT multipart files (v5.18, docs #90; прежний query consolidation_version_id у POST /legal-entities/{id}/files удалён) как в Срезе 2c: 201/422 {accepted[], rejected[]} 404 (консолидация или ЮЛ), 409 CONSOLIDATION_NOT_EDITABLE, 422 ENTITY_NOT_IN_PERIMETER [39] / [40]

Остальные маршруты, влияющие на версию: POST /documents/{id}/confirm (Срез 2c) привязывает подтверждённый документ ко всем версиям «в работе» своей ГК и может перевести версию в DATA_CORRECTION; DELETE /files/{id} и автозамена — снимают привязки (см. context/05-domain-rules.md §9.5).

3. Схемы

Ref {id, name}. Поля без ? присутствуют всегда.

ConsolidationPeriodOut {consolidation_id, year, quarter, reporting_date, versions[]}. VersionBrief {id, version_no, status, scenario, approach, lock (LockOut | null), created_at (datetime | null), members_count}. LockOut {locked_by, locked_by_name, locked_at, taken_over_from (uuid | null), takeover_reason (string | null)}. VersionCard = VersionBrief + {consolidation_id, year, quarter, reporting_date, company_group (Ref), initiator (Ref), initiator_office (Ref), goodwill_enabled, is_in_work, previous_version_id, members[], history[]}:

  • members[] — {id, name, inn, entity_type ("ЮЛ" | "ИП" | null — поле legal_entity nullable в DBML), is_head_entity, checklist[], osv_accounts[]} (arm-core #42):
    • checklist[] — ячейки ChecklistCellOut по правилам комплекта (тот же порядок, что ChecklistOut.rules); ячейка самоописывающая — ищите нужную по document_type/account («Бух. баланс» — BALANCE, «ОФР» — INCOME, «ОСВ 60» — OSV + "60"), а не по индексу;
    • osv_accounts[] — коды счетов ОСВ ЮЛ, загруженных к периоду версии: документы ОСВ за год версии с кварталом не позже квартала версии, в любом статусе, кроме ошибки, из незаменённых файлов; натуральный порядок (["01", "51", "58.03", "60"]). Это колонка «ОСВ» панели «Организации» [34] — «что загружено», а не «что готово»: готовность счетов комплекта — в ячейках checklist. Пока файл распознаётся, его счетов в списке ещё нет (ячейки показывают PROCESSING);
  • history[] — {from_status (null у первой записи), to_status, changed_by, changed_at, comment}.

ChecklistOut {rules[], rows[]}; rules[] — {document_type (BALANCE/INCOME/OSV…), account (string | null — счёт ОСВ), is_mandatory} в порядке: ББ, ОФР, ОСВ по коду счёта (натурально: 58 < 58.03 < 60); rows[] — {legal_entity_id, legal_entity_name, legal_entity_inn, cells[]}, порядок cells = порядок rules. ChecklistCellOut {document_type, account (string | null), status (CellStatus), missing_quarters[], document_ids[]} — document_type/account — правило ячейки (arm-core #42): CONFIRMED — пара полностью покрыта привязанными подтверждёнными документами; NEEDS_CHECK / RECOGNIZED — есть неподтверждённый подходящий документ; PROCESSING — у ЮЛ есть файл в обработке (по всем недостающим столбцам ЮЛ, пока файл не смаплен — это известная неточность, OQ-122); ERROR — последний файл ЮЛ в ERROR; MISSING — нет ничего. missing_quarters и document_ids — при частичном покрытии квартальными ОСВ (document_ids — только привязанные документы). ОСВ по субсчёту (58.03) закрывает только своё правило, не «ОСВ 58».

ConsolidatedRowOut — строка свода: {entry_ids[], legal_entity_id, legal_entity_name, legal_entity_inn, account_code?, account_root?, period ("<год> Q<n>"), is_cumulative, row_number?, counterparty_name?, counterparty_inn?, counterparty_id?, counterparty_entity_name?, counterparty_entity_inn?, counterparty_type, match_kind?, match_stored, opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?, warnings[]} — поля с ? могут быть null (ключ присутствует всегда). counterparty_name/inn — как в файле ОСВ; counterparty_id/counterparty_entity_name/counterparty_entity_inn — сопоставленное ЮЛ справочника (id, имя, ИНН) или null; match_stored = true — сопоставление хранится в БД, false — вычислено на лету (в БД не пишется); match_kind — способ (null, если ЮЛ не найдено). Для склеенных строк entry_ids — записи по кварталам (порядок — от раннего к позднему). OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}: rows_total — число строк свода (без фильтров); unmapped_total — число строк свода с пустым counterparty_id после дорезолва на лету и без counterparty_inn (то же, что GET …/osv?unmapped=true даёт в total; unmapped=false — все остальные строки); auto_matched_total — число строк с match_kind NAME или ALIAS независимо от match_stored (INN, MANUAL, PROPAGATED не входят); documents_total — число ОСВ-документов, привязанных к версии; gaps[] — {legal_entity_id, legal_entity_name, account_code, missing_quarters[]} (пара ЮЛ × счёт, у которой нет ОСВ за какие-то кварталы; строк такой пары в своде нет). Баннер «Требуется указать соответствия… Осталось: N», кнопка «Показать ЮЛ без ИНН» [32-osv-no-inn] и предупреждение [31-00] опираются на unmapped_total / …/osv?unmapped=true: строки свода без сопоставленного ЮЛ и без ИНН из файла; строка с ИНН, которого нет в справочнике, сопоставления не требует (решение владельца 08.10.2026, arm-core #74 — отмена П-1 от 07.10.2026). CandidateOut {id, inn, name, company_group (Ref | null), gsk (Ref | null), gsz (Ref | null), counterparty_type, recommended}. MapCounterpartyOut {row (ConsolidatedRowOut | null), propagated, alias_created, alias_moved_from (uuid | null)}.

4. Нюансы, которые легко пропустить

  1. Блокировка — не «мягкая». Любая правка версии (PUT …/members, POST …/status, ручное сопоставление) требует статуса «в работе» и вашей активной блокировки; нет блокировки — 409 LOCK_REQUIRED, чужая — 409 LOCKED_BY_OTHER. Загрузка файлов и подтверждение документов блокировкой не ограничиваются. Блокировка не истекает по времени; CANCELLED её снимает.
  2. LOCKED_BY_OTHER приходит в двух формах. Из POST …/lock — с locked_by, locked_by_name, locked_at. Из правок (PUT …/members, POST …/status, сопоставление, т. е. из ensure_version_editable) — только locked_by, locked_at, без locked_by_name (доменная функция имени не знает; имя — из lock.locked_by_name карточки версии). NOT_LOCK_OWNER отдаёт locked_by и locked_at владельца.
  3. Перехват. POST …/lock с непустым takeover_reason при чужой блокировке снимает её и создаёт вашу (201, taken_over_from, takeover_reason). Пустая причина или из одних пробелов считается отсутствующей (→ 409 LOCKED_BY_OTHER); при свободной версии причина игнорируется. Повторный POST по своей блокировке — 200, а не 201.
  4. 404 — всегда код NOT_FOUND; различие по тексту message: «версия консолидации не найдена», «ГК не найдена», «ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено». Отдельных кодов CONSOLIDATION_NOT_FOUND / VERSION_NOT_FOUND / ENTRY_NOT_FOUND нет (design №17, №62) — это обозначения в спеке. ГСК из чужой ГК или с видом gsz — 422 RELATED_GROUP_MISMATCH, а невидимая/несуществующая ГСК — 404.
  5. MapCounterpartyOut.row может быть null: строка привязанного документа, которая в свод не попадает (пара с пробелом, квартальный документ при нарастающем), сопоставляется, но строки свода для ответа нет (design №69). Если row == null, перечитайте GET …/osv и …/summary; обязательно проверяйте на null.
  6. match_kind склеенной строки — от последнего квартала (D-3, design №8). Строка свода из нескольких кварталов несёт entry_ids[]; ручное сопоставление пишется на записи, чью entry_id вы отправили. Чтобы в ответе увидеть MANUAL, отправляйте последнюю entry_id строки (entry_ids[entry_ids.length - 1]); для записи более раннего квартала в строке будет PROPAGATED (в БД при этом одна запись MANUAL). Если в записях разных кварталов разные counterparty_id, расхождение — в warnings.
  7. Ручное сопоставление — только строки свода (R-11, design №62/№63): group_type = COUNTERPARTY, data_type БУ/СУММ привязанного ОСВ-документа версии. Строки NU/ACCOUNT, чужие и несуществующие — 404 NOT_FOUND. Автоприменение (PROPAGATED) затрагивает строки с тем же counterparty_name.strip(), пустым counterparty_id и ИНН контрагента пустым или равным ИНН выбранного ЮЛ — только среди строк свода (COUNTERPARTY, БУ/СУММ) ОСВ-документов, привязанных именно к ЭТОЙ версии; ранее сопоставленные строки не перевязываются; propagated — число записей osv_entry, а не строк свода: unmapped_total падает на число склеенных строк (R-15).
  8. ALIAS_TAKEN (owners[]): алиас закреплён за другим ЮЛ; повторите с move_alias: true (право DIRECTORY_DELETE не нужно), в ответе alias_moved_from — прежний владелец.
  9. Экспорт. Заголовок Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<Qn>_v<n>.xlsx (имя percent-кодировано целиком). Забирайте файл как blob с Authorization (прямая ссылка <a href> не подойдёт). Фильтры — те же, что у GET …/osv, пагинации нет, порядок строк тот же. Лист «Пробелы» строится по своду целиком и от фильтров не зависит (при unmapped=true без совпадений лист «Сводная ОСВ» — только заголовок, «Пробелы» заполнен). Суммы — числовые ячейки #,##0.00, текст не вычисляется как формула.
  10. warnings — не контракт. Тексты диагностики строки (расхождение counterparty_id между кварталами и т. п.) могут меняться; показывать как подсказку, не разбирать.
  11. Q1. ОСВ за Q1 (парсер помечает её нарастающей) подходит версии любого квартала года, в версиях Q2+ служит квартальным источником Q1; ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14/R-14a, OQ-124). В ConsolidatedRowOut.is_cumulative — признак строки свода: true только для строк нарастающего документа за квартал версии.
  12. Чтение не требует блокировки и доступно для версии любого статуса, включая CANCELLED.
  13. Все деньги — строки с двумя знаками; склеенные квартальные строки не содержат null (пустое = "0.00"), строки нарастающего документа — как в файле (могут быть null).
  14. period_check документа остаётся UNKNOWN: подходящесть документа версии — свойство пары (документ, версия), показывайте её через чек-лист версии, а не через поле документа (OQ-120).
  15. Создание не ставит блокировку. После POST /consolidations версия свободна; «Взять в работу» — отдельное действие [35]. ГСК в запросе не сохраняется (related_group_id в версии пока пуст); члены берутся из текущего состава ГСК на момент создания.
  16. Границы входов (arm-core #26). MembersIn.legal_entity_ids ≤ 500 элементов, StatusIn.comment ≤ 1000 символов, q в GET …/osv и …/osv/export ≤ 500 символов (у counterparty-candidates так было и раньше). Превышение — 422 в формате pydantic (detail[] {loc, msg, type}), без доменного code: показывайте как ошибку валидации формы, не ищите code. Ограничивайте ввод на клиенте теми же числами.
  17. ОСВ со скользящим периодом («Октябрь 2024 г. – Сентябрь 2025 г.», arm-tabular-parser #13, OQ-131): у документа period_year = год конца диапазона, а period_quarter и is_cumulative — null. Такой документ не подходит ни одной версии (привязка идёт по кварталу), в чек-листе и своде не участвует, подтверждать его можно. В списке документов показывайте год и пометку «период не определён», не подставляйте квартал сами. Границы периода появятся в контракте v1.2 (Срез 2b).
  18. Удаление алиаса (DELETE /directories/legal-entities/{id}/aliases/{alias_id}) во время переноса этого же алиаса ручным сопоставлением (move_alias: true) теперь ждёт блокировку и отвечает 404 NOT_FOUND, если алиас уже переехал (arm-core #26, T11.3) — это не ошибка клиента, перечитайте список алиасов.
  19. Периметр при создании (модалка [42], arm-core #42). После выбора ГК и ГСК модалка показывает все неархивные ЮЛ ГК (GET /directories/legal-entities?company_group_id=<ГК>&closed=false), отмечены члены выбранной ГСК (GET /directories/related-groups/{id} → members[]); отметки меняются, «Создать» активна при двух и более. Отправляйте отмеченные в legal_entity_ids. Без поля (или null) ядро берёт текущий состав ГСК; пустой список [] — это явный пустой периметр → 422 PERIMETER_TOO_SMALL. ЮЛ не из ГК (или несуществующее) → 422 ENTITY_NOT_IN_GROUP с перечнем. ГСК обязательна и проверяется (RELATED_GROUP_MISMATCH), но в версии не хранится (П-2): периметр живёт в consolidation_member, в справочник переезжает при верификации (Срез 7). Колонку «ГСК» таблицы [34] до верификации заполнить нечем — оставляйте пустой.
  20. «Ответственный» на панели «Организации» [34]: lock.locked_by_name, если у версии есть активная блокировка, иначе initiator.name. «Утверждён в РФ» и «Верифицирован в ГО» — маршрут согласования, Срез 7 (в API пока нет; показывайте «—»).
  21. «К отчетности» [34] доступна во всех статусах, начиная с INITIATED (OQ-43 пересмотрен владельцем 07.10.2026, П-3): свод читается в любом статусе; правки свода (сопоставление контрагента) — только «в работе» со своей блокировкой.

5. Сквозные сценарии экранов

Экран Вызовы
[42] Новая консолидация справочники: ГК → её ГСК (…/company-groups/{id}/related-groups, kind = gsk) → ЮЛ ГК и члены ГСК (нюанс 19) → POST /consolidations с legal_entity_ids
[34] Консолидации ГК GET /company-groups/{id}/consolidations (+ in_work_only), клик по строке — GET …/versions/{id} (панель «Организации»: members[] — тип, ячейки ББ/ОФР, osv_accounts; «Ответственный» — нюанс 20); «Взять в работу» — POST …/lock; меню «…» → «Отмена КФО» — POST …/status {CANCELLED, comment}
[36] Состав GET …/versions/{id} (члены), все ЮЛ ГК — справочники, «Сохранить» — PUT …/members
[39]/[40] Документы организации GET /consolidations/{cid}/legal-entities/{id}/files (рабочий стол консоли; «Все документы организации» — GET /legal-entities/{id}/files; опрос во время распознавания — им же или GET /files/{id}; схема — dev/api-contract-files-directories.md), POST /consolidations/{cid}/legal-entities/{id}/files
[37] Чек-лист GET …/checklist; «Скачать CSV» — GET …/checklist?format=csv (Срез 3b)
[30] ОСВ GET …/osv с фильтрами, GET …/osv/summary, «Экспорт» — GET …/osv/export
[32-osv-no-inn], [32-osv-map-counterparty] GET …/osv?unmapped=true, GET …/counterparty-candidates, «Связать» — POST …/osv/entries/{entry_id}/counterparty
[31-00] Предупреждение при уходе с вкладки GET …/osv/summary (unmapped_total)