frontend-auth-guide §8 и qa-guide §4.5: кнопка Authorize есть только со схемой bearerAuth (arm-core #29), публичные ручки без замка; на стенде старше — curl. Исправлен путь DELETE /directories/legal-entities/{id}/aliases/{alias_id} в api-contract-slice-4 §4 п. 18 и qa-guide N8 (неточность PR #31). Closes #32 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
24 KiB
Контракт API Среза 4 (консолидация) для разработчика фронта
Источник — реальная схема GET /openapi.json arm-core (ветка feat/18-slice-4-consolidation, Swagger — /docs); коды ошибок и
нюансы сверены с кодом и спекой arm-core/openspec/changes/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};limit ≤ 2000(по умолчанию 500),offset ≥ 0; больше 2000 — 422. - Перечисления:
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} |
201 VersionCard (версия №1, INITIATED, lock = null) |
404 NOT_FOUND (ГК/ГСК не видна), 409 CONSOLIDATION_EXISTS, 422 RELATED_GROUP_MISMATCH, 422 PERIMETER_TOO_SMALL |
[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 | — | ChecklistOut {rules[], rows[]} |
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 /legal-entities/{legal_entity_id}/files?consolidation_version_id= |
DOCUMENT_EDIT |
multipart files; параметр версии необязателен |
как в Срезе 2c: 201/422 {accepted[], rejected[]} |
404 (ЮЛ или версия), 409 VERSION_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, 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, is_head_entity, checklist[]}, гдеchecklist[]— ячейки по правилам комплекта (тот же порядок, чтоChecklistOut.rules);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 {status (CellStatus), missing_quarters[], document_ids[]}:
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_type, match_kind, match_stored, opening_debit, opening_credit, turnover_debit, turnover_credit, closing_debit, closing_credit, warnings[]}.
counterparty_name/inn — как в файле ОСВ; counterparty_id/counterparty_entity_name — сопоставленное ЮЛ справочника или 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 после дорезолва на лету (то же, что 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[]} (пара ЮЛ × счёт, у которой нет ОСВ за какие-то кварталы;
строк такой пары в своде нет). 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. Нюансы, которые легко пропустить
- Блокировка — не «мягкая». Любая правка версии (
PUT …/members,POST …/status, ручное сопоставление) требует статуса «в работе» и вашей активной блокировки; нет блокировки — 409LOCK_REQUIRED, чужая — 409LOCKED_BY_OTHER. Загрузка файлов и подтверждение документов блокировкой не ограничиваются. Блокировка не истекает по времени;CANCELLEDеё снимает. 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владельца.- Перехват.
POST …/lockс непустымtakeover_reasonпри чужой блокировке снимает её и создаёт вашу (201,taken_over_from,takeover_reason). Пустая причина или из одних пробелов считается отсутствующей (→ 409LOCKED_BY_OTHER); при свободной версии причина игнорируется. ПовторныйPOSTпо своей блокировке — 200, а не 201. - 404 — всегда код
NOT_FOUND; различие по текстуmessage: «версия консолидации не найдена», «ГК не найдена», «ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено». Отдельных кодовCONSOLIDATION_NOT_FOUND/VERSION_NOT_FOUND/ENTRY_NOT_FOUNDнет (design №17, №62) — это обозначения в спеке. ГСК из чужой ГК или с видомgsz— 422RELATED_GROUP_MISMATCH, а невидимая/несуществующая ГСК — 404. MapCounterpartyOut.rowможет бытьnull: строка привязанного документа, которая в свод не попадает (пара с пробелом, квартальный документ при нарастающем), сопоставляется, но строки свода для ответа нет (design №69). Еслиrow == null, перечитайтеGET …/osvи…/summary; обязательно проверяйте наnull.match_kindсклеенной строки — от последнего квартала (D-3, design №8). Строка свода из нескольких кварталов несётentry_ids[]; ручное сопоставление пишется на записи, чьюentry_idвы отправили. Чтобы в ответе увидетьMANUAL, отправляйте последнююentry_idстроки (entry_ids[entry_ids.length - 1]); для записи более раннего квартала в строке будетPROPAGATED(в БД при этом одна записьMANUAL). Если в записях разных кварталов разныеcounterparty_id, расхождение — вwarnings.- Ручное сопоставление — только строки свода (R-11, design №62/№63):
group_type = COUNTERPARTY,data_typeБУ/СУММ привязанного ОСВ-документа версии. СтрокиNU/ACCOUNT, чужие и несуществующие — 404NOT_FOUND. Автоприменение (PROPAGATED) затрагивает строки с тем жеcounterparty_name.strip(), пустымcounterparty_idи ИНН контрагента пустым или равным ИНН выбранного ЮЛ — только среди строк свода (COUNTERPARTY, БУ/СУММ) ОСВ-документов, привязанных именно к ЭТОЙ версии; ранее сопоставленные строки не перевязываются;propagated— число записейosv_entry, а не строк свода:unmapped_totalпадает на число склеенных строк (R-15). ALIAS_TAKEN(owners[]): алиас закреплён за другим ЮЛ; повторите сmove_alias: true(правоDIRECTORY_DELETEне нужно), в ответеalias_moved_from— прежний владелец.- Экспорт. Заголовок
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, текст не вычисляется как формула. warnings— не контракт. Тексты диагностики строки (расхождениеcounterparty_idмежду кварталами и т. п.) могут меняться; показывать как подсказку, не разбирать.- Q1. ОСВ за
Q1(парсер помечает её нарастающей) подходит версии любого квартала года, в версиях Q2+ служит квартальным источником Q1; ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14/R-14a, OQ-124). ВConsolidatedRowOut.is_cumulative— признак строки свода:trueтолько для строк нарастающего документа за квартал версии. - Чтение не требует блокировки и доступно для версии любого статуса, включая
CANCELLED. - Все деньги — строки с двумя знаками; склеенные квартальные строки не содержат
null(пустое ="0.00"), строки нарастающего документа — как в файле (могут бытьnull). period_checkдокумента остаётсяUNKNOWN: подходящесть документа версии — свойство пары (документ, версия), показывайте её через чек-лист версии, а не через поле документа (OQ-120).- Создание не ставит блокировку. После
POST /consolidationsверсия свободна; «Взять в работу» — отдельное действие [35]. ГСК в запросе не сохраняется (related_group_idв версии пока пуст); члены берутся из текущего состава ГСК на момент создания. - Границы входов (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. Ограничивайте ввод на клиенте теми же числами. - ОСВ со скользящим периодом («Октябрь 2024 г. – Сентябрь 2025 г.», arm-tabular-parser #13, OQ-131): у документа
period_year= год конца диапазона, аperiod_quarterиis_cumulative—null. Такой документ не подходит ни одной версии (привязка идёт по кварталу), в чек-листе и своде не участвует, подтверждать его можно. В списке документов показывайте год и пометку «период не определён», не подставляйте квартал сами. Границы периода появятся в контракте v1.2 (Срез 2b). - Удаление алиаса (
DELETE /directories/legal-entities/{id}/aliases/{alias_id}) во время переноса этого же алиаса ручным сопоставлением (move_alias: true) теперь ждёт блокировку и отвечает 404NOT_FOUND, если алиас уже переехал (arm-core #26, T11.3) — это не ошибка клиента, перечитайте список алиасов.
5. Сквозные сценарии экранов
| Экран | Вызовы |
|---|---|
| [42] Новая консолидация | справочники (ГК → её ГСК) → POST /consolidations |
| [34] Консолидации ГК | GET /company-groups/{id}/consolidations (+ in_work_only), клик по строке — GET …/versions/{id}; «Взять в работу» — POST …/lock; меню «…» → «Отмена КФО» — POST …/status {CANCELLED, comment} |
| [36] Состав | GET …/versions/{id} (члены), все ЮЛ ГК — справочники, «Сохранить» — PUT …/members |
| [39]/[40] Документы организации | GET /legal-entities/{id}/files, POST /legal-entities/{id}/files?consolidation_version_id= |
| [37] Чек-лист | GET …/checklist (CSV в Срезе 4 нет) |
| [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) |