Кандидаты «Требуется проверка» / «Распознан» и состояние файлов («Обработка» / «Ошибка загрузки») чек-лист берёт только со стола консолидации (§9.5 п. 11) — тем же фильтром, что панель «Первичные документы» (`files.on_desk`). Документы организации из других консолидаций и из карточки ЮЛ ячейку больше не меняют: без документов — «Отсутствует». В `on_desk` документ подзапроса — псевдоним: в запросе по `Document` он иначе коррелирует с внешним. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
398 lines
39 KiB
Markdown
398 lines
39 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,
|
||
legal_entity_ids?}` 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)`, у которого
|
||
`after_value.members` — отсортированный список ЮЛ периметра; ответ 201 с карточкой версии. Периметр SHALL быть: без
|
||
`legal_entity_ids` (поле отсутствует или `null`) — текущие члены ГСК; с `legal_entity_ids` — ровно эти ЮЛ после
|
||
схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации
|
||
(`legal_entity.company_group_id`), иначе 422 `ENTITY_NOT_IN_GROUP` с перечнем `legal_entity_ids` чужих в порядке
|
||
запроса; состав ГСК при явном списке не проверяется и не меняется. ГСК SHALL быть обязательной, принадлежать ГК и иметь
|
||
`kind = gsk`, иначе 422 `RELATED_GROUP_MISMATCH`. Периметр меньше двух ЮЛ (членов ГСК без списка либо выбранных ЮЛ после
|
||
схлопывания) → 422 `PERIMETER_TOO_SMALL`; пара (ГК, год, квартал) уже есть → 409 `CONSOLIDATION_EXISTS` (в том числе при
|
||
гонке двух запросов — UNIQUE `(company_group_id, year, quarter)`); `year` вне 2000..2100 → 422. Порядок проверок: ГК
|
||
видима (404) → ГСК видима (404) и соответствует ГК (422) → периметр (422) → период свободен (409). Блокировка на
|
||
создателя MUST NOT ставиться; `MEMBERSHIP_CHANGE` при создании MUST NOT писаться. `related_group_id` в `consolidation`
|
||
SHALL оставаться NULL (фиксируется при верификации, Срез 7); `related_group_member` MUST NOT изменяться.
|
||
В `consolidation_version.elimination_rules_version` SHALL записываться номер версии правил элиминации, действующей на
|
||
московскую календарную дату создания: максимальный `version_no` среди правил с `valid_from <= дата` и `valid_to IS NULL`
|
||
либо `valid_to >= дата` (OQ-174; DBML v5.23, поле NOT NULL). Пользователь версию правил не выбирает; год, квартал и
|
||
`reporting_date` на номер не влияют; позже номер не меняется. Нет правил, действующих на дату, → 409
|
||
`ELIMINATION_RULES_MISSING`, консолидация не создаётся.
|
||
|
||
#### Scenario: Консолидация РБ за Q3 2024
|
||
- **WHEN** `ka_rf` Санкт-Петербургского РФ создаёт консолидацию ГК «Русский бисквит», ГСК из двух ЮЛ, 2024, Q3, без
|
||
`legal_entity_ids`
|
||
- **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью,
|
||
`audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет
|
||
|
||
#### Scenario: Выбранный периметр шире ГСК
|
||
- **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК
|
||
- **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился
|
||
|
||
#### Scenario: ГСК из одного ЮЛ, второе отмечено вручную
|
||
- **WHEN** в ГСК один текущий член, в `legal_entity_ids` он и ещё одно ЮЛ той же ГК
|
||
- **THEN** 201; в `members` два ЮЛ
|
||
|
||
#### Scenario: ГСК из одного ЮЛ
|
||
- **WHEN** в ГСК один текущий член, `legal_entity_ids` не передан
|
||
- **THEN** 422 `PERIMETER_TOO_SMALL`; `consolidation` и версия не созданы
|
||
|
||
#### Scenario: ЮЛ другой ГК в списке
|
||
- **WHEN** в `legal_entity_ids` попало ЮЛ, `company_group_id` которого не равен ГК консолидации
|
||
- **THEN** 422 `ENTITY_NOT_IN_GROUP` с этим ЮЛ в `legal_entity_ids`; `consolidation` не создана
|
||
|
||
#### Scenario: Дубли схлопываются до одного ЮЛ
|
||
- **WHEN** `legal_entity_ids` — один и тот же идентификатор дважды
|
||
- **THEN** 422 `PERIMETER_TOO_SMALL`
|
||
|
||
#### Scenario: Повтор периода
|
||
- **WHEN** консолидация ГК за 2024 Q3 уже есть, создаётся ещё одна
|
||
- **THEN** 409 `CONSOLIDATION_EXISTS`
|
||
|
||
#### Scenario: Правила действуют на дату создания версии
|
||
- **WHEN** в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде
|
||
- **THEN** `consolidation_version.elimination_rules_version = 2`; номер не меняется при последующей публикации правил
|
||
|
||
#### Scenario: Нет действующих правил
|
||
- **WHEN** у всех правил `valid_from` позже даты создания
|
||
- **THEN** 409 `ELIMINATION_RULES_MISSING`; `consolidation` и версия не созданы
|
||
|
||
### 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[]`, `history[]`
|
||
(`from_status`, `to_status`, `changed_by`, `changed_at`, `comment`). Член периметра SHALL содержать `id`, `name`, `inn`,
|
||
`entity_type` (`ЮЛ`/`ИП`, из `legal_entity`), `is_head_entity`, `checklist[]` (ячейки по правилам подхода, см.
|
||
«Чек-лист комплекта») и `osv_accounts[]` — уникальные `osv_account_code` документов ЮЛ, у которых `document_type = OSV`,
|
||
`period_year` равен году версии, `period_quarter` не позже квартала версии, статус документа не `ERROR`, файл не в
|
||
`SUPERSEDED` и не в `ERROR`; документы без квартала или без кода счёта не учитываются; статус подтверждения и привязка к
|
||
версии не важны; порядок — натуральная сортировка кода счёта (как столбцы чек-листа: `01 < 51 < 58.03 < 60`). Консолидация
|
||
ГК вне видимости и несуществующий идентификатор — 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`
|
||
|
||
#### Scenario: Счета ОСВ участника
|
||
- **WHEN** версия за 2024 Q3; у ЮЛ есть ОСВ 60 за Q1 2024 (`RECOGNIZED`), ОСВ 51 за Q3 2024 (`CONFIRMED`), ОСВ 58.03 за
|
||
Q3 2024 (`NEEDS_CHECK`), ОСВ 62 за Q4 2024, ОСВ 01 за 2023, ОСВ 76 в заменённом (`SUPERSEDED`) файле и ОСВ 90 в
|
||
документе `ERROR`
|
||
- **THEN** `osv_accounts = ["51", "58.03", "60"]`
|
||
|
||
#### Scenario: Тип ЮЛ
|
||
- **WHEN** в периметре индивидуальный предприниматель
|
||
- **THEN** у него `entity_type = "ИП"`, у остальных — `"ЮЛ"`
|
||
|
||
### 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)`.
|
||
Автоматические переходы: загрузка в консолидацию (`POST /consolidations/{id}/legal-entities/{le}/files`, см. `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`. Ячейка `{document_type,
|
||
account, status, missing_quarters, document_ids}`: `document_type` и `account` — правила ячейки (ячейка
|
||
самоописывающая, фронт не опирается на позицию); `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными
|
||
документами (ОСВ: по правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии);
|
||
`NEEDS_CHECK` / `RECOGNIZED` — есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии
|
||
(приоритет `NEEDS_CHECK`); `PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по
|
||
`uploaded_at` файл ЮЛ в `ERROR`; иначе `MISSING`. Неподтверждённые документы и файлы SHALL браться только со стола
|
||
консолидации версии (§9.5 п. 11: файлы, загруженные в эту консолидацию, и файлы с документом, привязанным к любой её
|
||
версии) — тем же фильтром, что панель «Первичные документы»; документы и файлы ЮЛ из других консолидаций и из карточки
|
||
ЮЛ на ячейки MUST NOT влиять (AURORA-1646). `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при
|
||
частичном покрытии. Правило ОСВ SHALL закрываться только документом с `osv_account_code`, равным `account` правила
|
||
(OQ-04). Те же ячейки (с `document_type` и `account`, в том же порядке) SHALL отдаваться в карточке версии у каждого
|
||
члена. С `?format=csv` SHALL отдаваться тот же чек-лист файлом `text/csv; charset=utf-8` с BOM и разделителем `;`:
|
||
заголовок «Организация;ИНН;<правило>…» (правила — «Бух. баланс», «ОФР», «ОСВ <счёт>»), ячейки — подписи макета:
|
||
`CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED` «Требуется проверка», `ERROR` «Ошибка
|
||
загрузки», `MISSING` «Отсутствует» (с « (нет: Q1, Q2)» при `missing_quarters`); `Content-Disposition` — `attachment` с
|
||
именем `checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987.
|
||
|
||
#### Scenario: Пробел Q2
|
||
- **WHEN** у ЮЛ привязаны квартальные ОСВ 60 за Q1 и Q3 2024, версия за 2024 Q3
|
||
- **THEN** ячейка ОСВ 60: `document_type = OSV`, `account = "60"`, `status = MISSING`, `missing_quarters = ["Q2"]`,
|
||
`document_ids` — два документа
|
||
|
||
#### Scenario: Субсчёт не закрывает правило
|
||
- **WHEN** у ЮЛ подтверждена только ОСВ 58.03 за квартал версии
|
||
- **THEN** ячейка ОСВ 58 — `MISSING`
|
||
|
||
#### Scenario: Файл в обработке
|
||
- **WHEN** у ЮЛ в консолидацию загружен файл, задание ещё не завершено, других документов нет
|
||
- **THEN** все ячейки ЮЛ — `PROCESSING`
|
||
|
||
#### Scenario: Документы другой консолидации
|
||
- **WHEN** создана консолидация 2024 Q3; у ЮЛ «А» в файле, загруженном в консолидацию 2024 Q2, — ББ нарастающий за Q3
|
||
(`NEEDS_CHECK`) и ОСВ 60 за Q3 (`RECOGNIZED`); у ЮЛ «Б» — файл консолидации 2024 Q2 в `ERROR` и файл в `PROCESSING`,
|
||
загруженный из карточки ЮЛ; в консолидацию 2024 Q3 ничего не загружено
|
||
- **THEN** все ячейки обоих ЮЛ — `MISSING` с пустыми `document_ids`
|
||
|
||
#### Scenario: Файл, подхваченный привязкой
|
||
- **WHEN** файл ЮЛ загружен из карточки ЮЛ, один его документ подтверждён и привязан к версии консолидации, другой —
|
||
`NEEDS_CHECK` и подходит версии
|
||
- **THEN** файл на столе консолидации, ячейка второго документа — `NEEDS_CHECK`
|
||
|
||
#### Scenario: Ячейки карточки подписаны
|
||
- **WHEN** запрошена карточка версии подхода `BASIC`
|
||
- **THEN** у каждого члена `checklist[i].document_type` и `checklist[i].account` равны `rules[i]` чек-листа той же
|
||
версии; ячейка ББ — `document_type = BALANCE`, `account = null`
|
||
|
||
#### Scenario: Чек-лист в CSV
|
||
- **WHEN** запрошен чек-лист версии с `format=csv`, у ЮЛ подтверждён ББ, ОСВ 60 отсутствует за Q2
|
||
- **THEN** файл начинается с BOM, первая строка — «Организация;ИНН;Бух. баланс;ОФР;ОСВ 60…», в строке ЮЛ — «Загружен» и «Отсутствует (нет: Q2)»
|
||
|
||
### Requirement: Границы входных данных
|
||
Входы консолидаций SHALL иметь верхние границы: `legal_entity_ids` тела `PUT …/members` и тела `POST /consolidations` —
|
||
не больше 500 идентификаторов, `comment` тела `POST …/status` — не больше 1000 символов, `q` фильтров `GET …/osv` и
|
||
экспорта — не больше 500 символов. Превышение SHALL давать 422 со списком ошибок валидации (`detail[].loc` указывает
|
||
поле) до обращения к БД. Версия консолидации SHALL проверяться на видимость до блокировки `FOR UPDATE`: невидимая
|
||
версия отвечает 404 сразу, не ожидая транзакцию, которая держит её строку.
|
||
|
||
#### Scenario: Периметр длиннее предела
|
||
- **WHEN** в `PUT …/members` передано 501 ЮЛ
|
||
- **THEN** 422, `detail[0].loc = ["body", "legal_entity_ids"]`, периметр не изменился
|
||
|
||
#### Scenario: Периметр создания длиннее предела
|
||
- **WHEN** в `POST /consolidations` передано 501 ЮЛ в `legal_entity_ids`
|
||
- **THEN** 422, `detail[0].loc = ["body", "legal_entity_ids"]`, консолидация не создана
|
||
|
||
#### Scenario: Невидимая версия при занятой строке
|
||
- **WHEN** версию чужого филиала держит `FOR UPDATE` другая транзакция, пользователь без видимости вызывает `POST …/lock`
|
||
- **THEN** 404 `VERSION_NOT_FOUND` без ожидания
|
||
|
||
### Requirement: Результирующая таблица ВГО
|
||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table`
|
||
(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{companies[], articles[]}`. Компании — все члены периметра
|
||
по имени; статья содержит `code`, `name`, `amounts[] {company_id, amount}` и `group_total`.
|
||
Пять фиксированных статей §6: дебиторская и кредиторская задолженность, финансовые вложения,
|
||
долгосрочные и краткосрочные займы. Сумма компании SHALL считаться по всем остальным компаниям периметра
|
||
из конечного сальдо нужной стороны и выбранных счетов статьи; `group_total` — сумма по компаниям.
|
||
Отсутствие любого необходимого кода счёта SHALL давать 422 `RESULT_ACCOUNTS_MISSING` с перечнем кодов.
|
||
|
||
#### Scenario: Компания без ОСВ
|
||
- **WHEN** в периметре есть компания без подходящих строк ОСВ
|
||
- **THEN** она есть в `companies`, её сумма по каждой статье — строка `"0"`
|
||
|
||
#### Scenario: Нет обязательного счёта
|
||
- **WHEN** в плане счетов отсутствует счёт 58.03
|
||
- **THEN** ответ 422 `RESULT_ACCOUNTS_MISSING`, сообщение содержит `58.03`
|
||
|
||
### Requirement: XLSX результирующей таблицы ВГО
|
||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table/export` (`CONSOLIDATION_VIEW`,
|
||
policy) SHALL возвращать XLSX с теми же статьями, компаниями и итогами, что `GET …/intercompany-matrix/result-table`,
|
||
из того же расчёта сводной ОСВ версии. Книга — лист «Результирующая таблица» с шапкой (ГК, период, версия, единицы
|
||
«руб.») и таблицей «Статья ВГО» × компании периметра + колонка «Итого по группе»; суммы — числа с двумя знаками. Имя
|
||
файла — `intercompany-result-table_<ГК>_<год>-<квартал>_v<n>.xlsx`. Ошибки — как у JSON-маршрута: невидимая или
|
||
несуществующая версия — 404, отсутствие необходимого кода счёта — 422 `RESULT_ACCOUNTS_MISSING`.
|
||
|
||
#### Scenario: Совпадение с JSON
|
||
- **WHEN** пользователь выгружает результирующую таблицу ГК «Русский бисквит» за 2024 Q1, версия 1
|
||
- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, порядок компаний, названия статей, суммы по компаниям и
|
||
«Итого по группе» совпадают с ответом `GET …/result-table` той же версии
|
||
|
||
#### Scenario: Нет счёта статьи
|
||
- **WHEN** в плане счетов нет кода 58.03
|
||
- **THEN** выгрузка отвечает 422 `RESULT_ACCOUNTS_MISSING` с кодом в сообщении, файл не формируется
|
||
|
||
#### Scenario: Версия вне видимости
|
||
- **WHEN** пользователь филиала выгружает таблицу версии чужой ГК
|
||
- **THEN** ответ 404 с тем же телом, что у несуществующей версии
|
||
|
||
### Requirement: Правила элиминации зафиксированной версии
|
||
`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК)
|
||
SHALL возвращать объект `{version_no, is_version_fixed, rules}`: `version_no` — `consolidation_version.elimination_rules_version`
|
||
(с DBML v5.23 задан у каждой версии), `is_version_fixed = true` (поле оставлено для совместимости клиента), `rules` —
|
||
правила этой версии; правила других версий, в том числе более новых, MUST NOT попадать в ответ. Каждое правило SHALL
|
||
содержать `id`, `rule_no`, `version_no`, `statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`),
|
||
`company_1`, `company_2`; каждая сторона SHALL содержать `accounts: UUID[]` и `excluded: UUID[]` до раскрытия иерархии
|
||
счетов. Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`.
|
||
|
||
#### Scenario: Правило с исключением
|
||
- **WHEN** у правила первая сторона включает счёт 76 и исключает 76.07.5
|
||
- **THEN** ID 76 находится в `company_1.accounts`, ID 76.07.5 — в `company_1.excluded`
|
||
|
||
#### Scenario: Версия правил зафиксирована
|
||
- **WHEN** у версии консолидации `elimination_rules_version = 1`
|
||
- **THEN** ответ содержит `is_version_fixed = true`, `version_no = 1` и только правила с `version_no = 1`
|
||
|
||
#### Scenario: Опубликована новая версия правил
|
||
- **WHEN** после создания версии консолидации с `elimination_rules_version = 1` открыта версия правил 2
|
||
- **THEN** ответ по-прежнему содержит `version_no = 1` и правила версии 1; пресет правила версии 2 для этой версии — 404
|
||
|
||
### Requirement: Пары ВГО тройки
|
||
`GET /api/v1/consolidations/versions/{version_id}/elimination-pairs?rule_id=&company_a_id=&company_b_id=` SHALL требовать
|
||
`CONSOLIDATION_VIEW` и видимость версии по ГК; три параметра обязательны. Компании SHALL быть разными участниками
|
||
периметра версии (иначе 422 `INVALID_COMPANY_PAIR`), правило — правилом ББ версии правил версии консолидации с
|
||
`measure = closing` (иначе 404 `NOT_FOUND` или 422 `INVALID_RULE`). Отбор счетов, раскрытие счетов-предков и исключения
|
||
SHALL совпадать с шахматкой PRESET. Расчёт — по `05-domain-rules.md` §5, на лету, без сохранения.
|
||
|
||
Итог тройки: `amount_a` — сумма строк A с контрагентом B по счетам стороны 1, `amount_b` — строк B с контрагентом A по
|
||
счетам стороны 2, **со знаком** (как в шахматке); `deviation_amount = |amount_a − amount_b|`; `deviation_percent` — к
|
||
`min(amount_a, amount_b)`, `null`, если он ≤ 0; `exceeds_threshold` — процент > 5 % или меньшая сторона ≤ 0 при ненулевом
|
||
расхождении; `eliminated_amount = min(amount_a, amount_b)` при расхождении ≤ 5 %, иначе, при `elimination_mode = manual` и
|
||
при меньшей стороне ≤ 0 — 0. Упаковка итог не меняет. Каждое правило считается отдельно: строка общего счёта может войти
|
||
в тройки нескольких правил (каскад отменён, AURORA-1751).
|
||
|
||
Упаковка: строка свода неделима; в группы идут только строки с суммой > 0; группы 1:1, 1:N, N:1 при N ≤ 5, N:M нет;
|
||
сначала меньшее число строк, внутри размера — точные, затем с расхождением ≤ 5 % к меньшей сумме по возрастанию
|
||
**процента** расхождения; строка — не более чем в одной группе правила; группы одного размера с одинаковым процентом,
|
||
делящие строку, не сопоставляются. Ответ SHALL содержать `version_no`, `rule_id`, `rule_no`, `company_a_id`,
|
||
`company_b_id`, итог тройки, `matched[]` (точные группы), `with_discrepancy[]` (расхождение ≤ 5 %) и `unpaired[]`. Группа —
|
||
`{rule_id, company_a_id, company_b_id, amount_a, amount_b, deviation_amount, deviation_percent, osv_entry_ids_a[],
|
||
osv_entry_ids_b[]}`; строка без пары — `{row, reason}`, `reason`: `NO_MATCH`, `AMBIGUOUS` (ничья), `ZERO_AMOUNT`,
|
||
`NEGATIVE_AMOUNT` (сумма < 0, в упаковку не идёт). Суммы и проценты в JSON SHALL быть строками с двумя знаками.
|
||
|
||
#### Scenario: Точная строка среди несопоставленных остатков
|
||
- **WHEN** правило № 6 для Счастливочной и АО «Русский бисквит» даёт суммы 110 429 113,15 и 106 311 116,46 руб.,
|
||
а точная группа 62.01 ↔ 60.01 составляет 106 311 116,46 руб.
|
||
- **THEN** точная группа в `matched`, три остальные строки стороны A — в `unpaired`, `eliminated_amount = 106311116.46`,
|
||
`deviation_percent = 3.87`
|
||
|
||
#### Scenario: Отрицательная строка
|
||
- **WHEN** у A по B строки 150 и −50, у B по A строка 100
|
||
- **THEN** `amount_a = 100.00`, `amount_b = 100.00`, `eliminated_amount = 100.00`; строка −50 — в `unpaired` с причиной
|
||
`NEGATIVE_AMOUNT`
|
||
|
||
#### Scenario: Две строки свода упакованы с одной встречной
|
||
- **WHEN** по одному правилу есть дебетовые строки 40 и 60 и встречная кредитовая строка 100
|
||
- **THEN** `matched[]` содержит группу с суммами 100.00 и 100.00, `deviation_amount = 0.00` и исходными `osv_entry_ids_a/b`
|
||
|
||
#### Scenario: Ручное правило и превышение порога
|
||
- **WHEN** правило ручное либо расхождение больше 5 %
|
||
- **THEN** `eliminated_amount = 0.00` независимо от упаковки
|
||
|
||
#### Scenario: Исключённый счёт не входит в пары
|
||
- **WHEN** правило включает родительский счёт 76 и исключает 76.07.5
|
||
- **THEN** строки счёта 76.07.5 и его потомков не входят ни в группы, ни в `unpaired`
|
||
|
||
#### Scenario: Другая пара компаний не смешивается с выбранной
|
||
- **WHEN** по одному правилу есть встречные строки для A→Б и A→В, а запрошены `company_a_id=A`, `company_b_id=Б`
|
||
- **THEN** ответ содержит только строки A→Б и Б→A
|
||
|
||
#### Scenario: Расхождение сравнивается в процентах
|
||
- **WHEN** со строкой 100 стороны A могут сойтись строки B 95,50 (4,50 руб., 4,71 %) и 104,60 (4,60 руб., 4,60 %)
|
||
- **THEN** группа — 100 ↔ 104,60; строка 95,50 — в `unpaired` с причиной `NO_MATCH`
|
||
|
||
#### Scenario: Правило по оборотам
|
||
- **WHEN** запрошена тройка по правилу ББ с `measure = turnover`
|
||
- **THEN** 422 `INVALID_RULE`
|
||
|
||
### Requirement: Пары ВГО версии
|
||
`GET /api/v1/consolidations/versions/{version_id}/elimination-totals` (`CONSOLIDATION_VIEW`, policy по ГК) SHALL
|
||
возвращать `{version_no, items[]}` — итоги всех троек версии: правила ББ с `measure = closing` версии правил ×
|
||
упорядоченные пары разных компаний периметра, у которых есть строки хотя бы на одной стороне. Сводная ОСВ SHALL строиться
|
||
один раз на запрос. Элемент — `{rule_id, rule_no, elimination_mode, company_a_id, company_b_id, amount_a, amount_b,
|
||
deviation_amount, deviation_percent, eliminated_amount, exceeds_threshold, status, osv_entry_ids_a[], osv_entry_ids_b[]}`
|
||
с теми же суммами, что у тройки («Пары ВГО тройки»); `status`: `MANUAL` — `elimination_mode = manual`; `ONE_SIDED` — одна из
|
||
сумм нулевая; `AUTO` — `eliminated_amount > 0`; иначе `DISCREPANCY` (расхождение > 5 % или меньшая сторона < 0). Порядок — `rule_no`, затем порядок компаний периметра.
|
||
Суммы и проценты — строки с двумя знаками. Невидимая и несуществующая версия — 404 `NOT_FOUND`.
|
||
|
||
#### Scenario: Список совпадает с тройкой
|
||
- **WHEN** по правилу № 6 у A на B 300, у B на A 100, у B на A дебиторка 70 и у A на B кредиторка 70
|
||
- **THEN** в `items` тройка (6, A, B) — `DISCREPANCY`, `eliminated_amount = 0.00`; тройка (6, B, A) — `AUTO`,
|
||
`eliminated_amount = 70.00`; те же суммы возвращает `elimination-pairs` для каждой тройки
|
||
|
||
#### Scenario: Пересечение правил
|
||
- **WHEN** дебиторка A на B по счетам № 6 есть, а займов B у A (67) нет
|
||
- **THEN** в `items` есть и тройка (6, A, B), и тройка (7, A, B) со статусом `ONE_SIDED` — двойной учёт до разнесения
|
||
строк (AURORA-1751)
|
||
|
||
#### Scenario: Правило без строк
|
||
- **WHEN** по счетам правила № 3 строк ни у A, ни у B нет
|
||
- **THEN** тройки (3, A, B) в `items` нет
|
||
|
||
#### Scenario: Обе стороны отрицательные
|
||
- **WHEN** у A по B −50, у B по A −50
|
||
- **THEN** тройка `DISCREPANCY`, `deviation_percent = null`, `eliminated_amount = 0.00`
|