Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
152 lines
14 KiB
Markdown
152 lines
14 KiB
Markdown
# consolidations-api Specification
|
||
|
||
## Purpose
|
||
Консолидация ГК за период: создание, дерево версий, карточка версии, периметр, блокировка «взять в работу», статусы
|
||
Среза 4 и чек-лист комплекта документов (§9.1, §10 `05-domain-rules.md`; DBML `consolidation*`, `required_document_rule`;
|
||
макет `34`, `35`, `36`, `37`, `42`). Префикс всех маршрутов — `/api/v1`.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Создание консолидации
|
||
`POST /consolidations` (`CONSOLIDATION_EDIT`, policy по ГК) с телом `{company_group_id, related_group_id, year, quarter}`
|
||
SHALL в одной транзакции создать `consolidation` (`reporting_date`: Q1 → 01.04.year, Q2 → 01.07.year, Q3 → 01.10.year,
|
||
Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATED`, `scenario = CREATE`, `approach = BASIC`,
|
||
`goodwill_enabled = false`, `initiator_id` и `initiator_office_id` из пользователя, `is_in_work = true`),
|
||
`consolidation_member` из текущих членов ГСК (`is_head_entity = false`), первую запись `consolidation_status_history`
|
||
(`from_status` NULL → `INITIATED`), подхват подтверждённых документов (см. `documents-api` «Подхват документов») и
|
||
`audit_event(CONSOLIDATION_CREATE)`; ответ 201 с карточкой версии. ГСК SHALL принадлежать ГК и иметь `kind = gsk`,
|
||
иначе 422 `RELATED_GROUP_MISMATCH`; членов меньше двух → 422 `PERIMETER_TOO_SMALL`; пара (ГК, год, квартал) уже есть
|
||
→ 409 `CONSOLIDATION_EXISTS` (в том числе при гонке двух запросов — UNIQUE `(company_group_id, year, quarter)`);
|
||
`year` вне 2000..2100 → 422. Блокировка на создателя MUST NOT ставиться. `related_group_id` в `consolidation` SHALL
|
||
оставаться NULL (фиксируется при верификации, Срез 7).
|
||
|
||
#### Scenario: Консолидация РБ за Q3 2024
|
||
- **WHEN** `ka_rf` Санкт-Петербургского РФ создаёт консолидацию ГК «Русский бисквит», ГСК из двух ЮЛ, 2024, Q3
|
||
- **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью,
|
||
`audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет
|
||
|
||
#### Scenario: ГСК из одного ЮЛ
|
||
- **WHEN** в ГСК один текущий член
|
||
- **THEN** 422 `PERIMETER_TOO_SMALL`; `consolidation` и версия не созданы
|
||
|
||
#### Scenario: Повтор периода
|
||
- **WHEN** консолидация ГК за 2024 Q3 уже есть, создаётся ещё одна
|
||
- **THEN** 409 `CONSOLIDATION_EXISTS`
|
||
|
||
### Requirement: Список консолидаций ГК и карточка версии
|
||
`GET /company-groups/{id}/consolidations` (`CONSOLIDATION_VIEW`, policy) SHALL возвращать периоды ГК (`consolidation_id`,
|
||
`year`, `quarter`, `reporting_date`) по убыванию года и квартала, в каждом — версии по убыванию `version_no`
|
||
(`id`, `version_no`, `status`, `scenario`, `approach`, `lock` — активная блокировка или null, `created_at`,
|
||
`members_count`); параметр `in_work_only=true` SHALL оставлять только версии со статусом не из
|
||
{`VERIFIED`, `CANCELLED`, `ARCHIVED`, `REJECTED`}. `GET /consolidations/versions/{id}` SHALL отдавать карточку: поля
|
||
версии и консолидации, `company_group {id, name}`, инициатора и его офис, `lock`, `members[]` (ЮЛ: `id`, `name`, `inn`,
|
||
`is_head_entity`, `checklist[]` — ячейки по правилам подхода, см. «Чек-лист комплекта»), `history[]` (`from_status`,
|
||
`to_status`, `changed_by`, `changed_at`, `comment`). Консолидация ГК вне видимости и несуществующий идентификатор —
|
||
404 `NOT_FOUND` с сообщением `CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND` и одинаковым телом (так же во всех
|
||
спецификациях среза: идентификаторы `…_NOT_FOUND` — текст сообщения при фиксированном коде `NOT_FOUND`).
|
||
|
||
#### Scenario: Дерево версий
|
||
- **WHEN** у ГК консолидации 2024 Q1 (версии 1 и 2) и 2024 Q3 (версия 1), запрошен список
|
||
- **THEN** первым период 2024 Q3, затем 2024 Q1 с версиями `[2, 1]`; `members_count` у каждой версии
|
||
|
||
#### Scenario: Чужой филиал
|
||
- **WHEN** `ka_rf` другого филиала запрашивает карточку версии
|
||
- **THEN** 404 `NOT_FOUND` (сообщение `VERSION_NOT_FOUND`), тело совпадает с ответом на случайный `uuid`
|
||
|
||
### Requirement: Периметр версии
|
||
`PUT /consolidations/versions/{id}/members` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя активная блокировка)
|
||
с телом `{legal_entity_ids: [...]}` SHALL заменить периметр версии: все ЮЛ SHALL принадлежать ГК консолидации
|
||
(`legal_entity.company_group_id`), иначе 422 `ENTITY_NOT_IN_GROUP` с перечнем; меньше двух → 422
|
||
`PERIMETER_TOO_SMALL`; дубли в списке схлопываются. Для добавленных ЮЛ SHALL выполняться подхват подтверждённых
|
||
документов, для удалённых — отвязка их `consolidation_document`; `audit_event(MEMBERSHIP_CHANGE, section =
|
||
CONSOLIDATION, object = consolidation_version)` с `before_value`/`after_value` — полные списки ЮЛ. `related_group_member`
|
||
MUST NOT изменяться. Ответ — карточка версии.
|
||
|
||
#### Scenario: Добавление ЮЛ с подтверждёнными документами
|
||
- **WHEN** в периметр версии 2024 Q3 добавлено ЮЛ ГК, у которого подтверждена ОСВ 60 за 2024 Q3 (нарастающая)
|
||
- **THEN** 200; в `members` три ЮЛ; у нового ЮЛ ячейка ОСВ 60 `CONFIRMED`; `consolidation_document` на этот документ есть
|
||
|
||
#### Scenario: ЮЛ другой ГК
|
||
- **WHEN** в список попало ЮЛ, `company_group_id` которого не равен ГК консолидации
|
||
- **THEN** 422 `ENTITY_NOT_IN_GROUP`, периметр не изменился
|
||
|
||
#### Scenario: Без блокировки
|
||
- **WHEN** у версии нет активной блокировки, пользователь с `CONSOLIDATION_EDIT` меняет периметр
|
||
- **THEN** 409 `LOCK_REQUIRED`
|
||
|
||
### Requirement: Взять в работу, перехват, снятие
|
||
`POST /consolidations/versions/{id}/lock` (`CONSOLIDATION_EDIT`, policy; версия в любом статусе, кроме `CANCELLED` —
|
||
409 `VERSION_NOT_EDITABLE`) SHALL: при отсутствии активной блокировки создать `consolidation_lock(locked_by, locked_at)`
|
||
и `audit_event(CONSOLIDATION_LOCK)` → 201; при своей активной — вернуть её без изменений → 200; при чужой без
|
||
`takeover_reason` → 409 `LOCKED_BY_OTHER` с `locked_by`, `locked_by_name`, `locked_at` в `detail`; при чужой с
|
||
непустым `takeover_reason` (≤ 1000 символов) — закрыть прежнюю (`released_at = now`), создать новую с
|
||
`taken_over_from` и `takeover_reason`, `audit_event(CONSOLIDATION_LOCK_TAKEOVER, before = прежний владелец, after =
|
||
новый, comment = причина)` → 201. Два одновременных `POST` без активной блокировки → у одного 201, у другого 409
|
||
`LOCKED_BY_OTHER` (частичный UNIQUE). `DELETE /consolidations/versions/{id}/lock` SHALL снять свою активную
|
||
блокировку (`released_at = now`, `audit_event(CONSOLIDATION_LOCK, after_value.released = true)`) → 204; чужую — 409
|
||
`NOT_LOCK_OWNER`; без активной — 204. Блокировка MUST NOT истекать по времени (OQ-119).
|
||
|
||
#### Scenario: Перехват с причиной
|
||
- **WHEN** версию держит `ka_rf` А, `ka_rf` Б того же филиала вызывает `POST …/lock {takeover_reason: "Отпуск"}`
|
||
- **THEN** 201; у Б активная блокировка с `taken_over_from = A`; у А `released_at` заполнен;
|
||
`audit_event(CONSOLIDATION_LOCK_TAKEOVER)` с `comment = "Отпуск"`
|
||
|
||
#### Scenario: Перехват без причины
|
||
- **WHEN** Б вызывает `POST …/lock` без тела при активной блокировке А
|
||
- **THEN** 409 `LOCKED_BY_OTHER`, `detail.locked_by = A`
|
||
|
||
#### Scenario: Чужое снятие
|
||
- **WHEN** Б вызывает `DELETE …/lock` при активной блокировке А
|
||
- **THEN** 409 `NOT_LOCK_OWNER`, блокировка А активна
|
||
|
||
### Requirement: Статусы Среза 4
|
||
Версия SHALL менять статус только через сервис статусов, который пишет `consolidation_status_history(from, to,
|
||
changed_by, changed_at, comment)` и `audit_event(CONSOLIDATION_STATUS_CHANGE, before = from, after = to, comment)`.
|
||
Автоматические переходы: загрузка файла с `consolidation_version_id` (см. `primary-files`) — `INITIATED →
|
||
DOCUMENTS_PROCESSING`; маппинг документа `NEEDS_CHECK`, подходящего версии (см. `osv-result-mapping`) —
|
||
`DOCUMENTS_PROCESSING → DATA_CORRECTION`; подтверждение документа, привязанного к версии (см. `documents-api`) —
|
||
`INITIATED` или `DOCUMENTS_PROCESSING → DATA_CORRECTION`. Ручной `POST /consolidations/versions/{id}/status`
|
||
(`CONSOLIDATION_EDIT`, policy, своя блокировка) с телом `{to_status, comment}` SHALL допускать только: `CANCELLED` из
|
||
любого статуса «в работе» (`comment` обязателен, иначе 422 `COMMENT_REQUIRED`; `cancelled_reason = comment`,
|
||
`is_in_work = false`, активная блокировка снимается) и `INITIATED` из `DOCUMENTS_PROCESSING` / `DATA_CORRECTION`
|
||
(`comment` необязателен; `consolidation_document` сохраняются). Любой другой `to_status` → 409 `TRANSITION_NOT_ALLOWED`
|
||
с текущим и запрошенным статусами. Повтор того же статуса → 409 `TRANSITION_NOT_ALLOWED`. Статус «в работе» =
|
||
{`INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`}.
|
||
|
||
#### Scenario: Аннулирование
|
||
- **WHEN** владелец блокировки переводит версию `DATA_CORRECTION` в `CANCELLED` с комментарием
|
||
- **THEN** 200; `status = CANCELLED`, `cancelled_reason` = комментарий, `is_in_work = false`, блокировка снята; запись
|
||
истории `DATA_CORRECTION → CANCELLED` с комментарием; `audit_event(CONSOLIDATION_STATUS_CHANGE)`
|
||
|
||
#### Scenario: Переход Среза 7
|
||
- **WHEN** запрошен `to_status = CONSOLIDATION_RF`
|
||
- **THEN** 409 `TRANSITION_NOT_ALLOWED`, статус не изменился
|
||
|
||
#### Scenario: Аннулирование без комментария
|
||
- **WHEN** `to_status = CANCELLED`, `comment` пуст
|
||
- **THEN** 422 `COMMENT_REQUIRED`
|
||
|
||
### Requirement: Чек-лист комплекта
|
||
`GET /consolidations/versions/{id}/checklist` (`CONSOLIDATION_VIEW`, policy) SHALL вернуть `rules[]` (правила
|
||
`required_document_rule` подхода версии: `document_type`, `account`, `is_mandatory`; порядок — `BALANCE`, `INCOME`,
|
||
затем ОСВ по коду счёта) и `rows[]` по ЮЛ периметра (по имени) с `cells[]` в порядке `rules`. Ячейка `{status,
|
||
missing_quarters, document_ids}`: `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными документами (ОСВ: по
|
||
правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии); `NEEDS_CHECK` / `RECOGNIZED` —
|
||
есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии (приоритет `NEEDS_CHECK`);
|
||
`PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по `uploaded_at` файл ЮЛ в `ERROR`;
|
||
иначе `MISSING`. `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при частичном покрытии. Правило ОСВ
|
||
SHALL закрываться только документом с `osv_account_code`, равным `account` правила (OQ-04). Те же ячейки SHALL
|
||
отдаваться в карточке версии у каждого члена.
|
||
|
||
#### Scenario: Пробел Q2
|
||
- **WHEN** у ЮЛ привязаны квартальные ОСВ 60 за Q1 и Q3 2024, версия за 2024 Q3
|
||
- **THEN** ячейка ОСВ 60: `status = MISSING`, `missing_quarters = ["Q2"]`, `document_ids` — два документа
|
||
|
||
#### Scenario: Субсчёт не закрывает правило
|
||
- **WHEN** у ЮЛ подтверждена только ОСВ 58.03 за квартал версии
|
||
- **THEN** ячейка ОСВ 58 — `MISSING`
|
||
|
||
#### Scenario: Файл в обработке
|
||
- **WHEN** у ЮЛ загружен файл, задание ещё не завершено, других документов нет
|
||
- **THEN** все ячейки ЮЛ — `PROCESSING`
|