# 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 автоматически записать в `consolidation_version.elimination_rules_version` номер самой актуальной версии правил: максимальный `version_no` среди правил, действующих на московскую календарную дату создания версии (`valid_from <= дата` и `valid_to IS NULL` либо `valid_to >= дата`; OQ-174, docs PR #146). Пользователь не выбирает версию правил. Отчётные год, квартал и `reporting_date` не влияют на определение номера. Если на дату создания нет действующих правил, поле остаётся NULL. Периметр 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 изменяться. #### 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** в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде - **THEN** `consolidation_version.elimination_rules_version = 2`; номер не меняется при последующей публикации правил #### 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` ### 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`. `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** запрошена карточка версии подхода `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}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК) SHALL возвращать объект `{version_no, is_version_fixed, rules}`. Если у версии задан `consolidation_version.elimination_rules_version`, `version_no` SHALL равняться ему, `is_version_fixed = true`, а `rules` — правилами этой версии. Если он не задан, `is_version_fixed = false`, а `rules` — открытые правила (`valid_to IS NULL`) только с максимальным `version_no`; ответ SHALL содержать этот фактический `version_no`. При отсутствии открытых правил `version_no = null`, `rules = []`. Каждое правило 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` пуст - **THEN** ответ содержит `is_version_fixed = false`, фактический `version_no` и открытые правила только этой версии #### Scenario: Открыты две версии правил - **WHEN** открыты правила с `version_no = 1` и `version_no = 2`, в том числе с одинаковым `rule_no` - **THEN** ответ содержит только правила версии 2 и `version_no = 2` ### Requirement: Сопоставленные строки сводной ОСВ по правилу и паре компаний `GET /api/v1/consolidations/versions/{version_id}/elimination-pairs` SHALL требовать `CONSOLIDATION_VIEW` и проверять видимость версии по ГК. Запрос SHALL принимать обязательные query-параметры `rule_id`, `company_a_id`, `company_b_id`. Компании SHALL быть разными участниками периметра версии, а правило — пресетом ББ её версии правил. Для этой тройки SHALL возвращаться только её результат; строки других правил и направленных пар компаний в расчёт не входят. Отбор счетов, раскрытие счетов-предков и их исключений SHALL совпадать с расчётом шахматки PRESET. Ответ SHALL содержать `version_no`, `rule_id`, `rule_no`, `company_a_id`, `company_b_id`, `matched[]` (точные группы), `with_discrepancy[]` (группы с допустимым ненулевым расхождением) и `unpaired[]` (строки без группы), без полного перечня допустимых пар счетов. На уровне тройки ответ SHALL содержать `amount_a`, `amount_b` (суммы всех выбранных строк, включая оставшиеся без пары), `elimination_amount` (абсолютное расхождение), `deviation_percent` (от меньшей суммы, `null` при нуле), `eliminated_amount` (сумма фактической элиминации) и `exceeds_threshold`. При расхождении не более 5 % `eliminated_amount` SHALL равняться `min(amount_a, amount_b)`; при превышении порога, нулевой стороне или `elimination_mode = manual` SHALL равняться нулю. Упаковка строк SHALL не менять этот итог. Каждая сопоставленная группа SHALL содержать `rule_id`, `company_a_id`, `company_b_id`, `amount_a`, `amount_b`, `elimination_amount`, `deviation_percent` и исходные `osv_entry_ids_a/b`. Суммы `amount_a/b` SHALL рассчитываться по ОСВ; `elimination_amount` SHALL равняться `abs(amount_a - amount_b)`. Невидимая и несуществующая версия или отсутствующее правило SHALL отвечать 404 `NOT_FOUND`; недопустимая пара компаний — 422 `INVALID_COMPANY_PAIR`. Пары и группы не сохраняются. Строка сводной ОСВ неделима, даже если содержит несколько `entry_ids`. Внутри выбранной тройки строки с распознанным встречным ЮЛ и допустимыми счетами группируются как 1:1, 1:N или N:1 при N ≤ 5; N:M не допускается. Комбинации строятся из всех строк выбранных счетов соответствующей стороны правила, без дополнительного требования совпадения конкретных счетов внутри группы. Сравниваются модули остатков по соответствующей стороне. Нулевые строки остаются в `unpaired` с причиной `ZERO_AMOUNT`. Сначала выбираются группы с наименьшим общим числом строк: 1:1, затем 1:2 и 2:1, и так до 1:5 и 5:1. Внутри размера сначала точные, затем с расхождением не более 5 % относительно меньшей из двух сумм, по возрастанию расхождения. Строка входит не более чем в одну группу данного правила. Если группы одного размера с одинаковым расхождением пересекаются, все строки конкурирующих групп исключаются из автоматического поиска и возвращаются в `unpaired` с причиной `AMBIGUOUS` для ручного разбора. Остальные строки без группы возвращаются в `unpaired` с причиной `NO_MATCH` независимо от того, отсутствуют ли встречные суммы, превышен ли порог или подходящие строки уже заняты другой группой. Упаковка не меняет сумму элиминации. #### Scenario: Исключённый счёт не входит в пары - **WHEN** правило включает родительский счёт 76 и исключает 76.07.5 - **THEN** строки счёта 76.07.5 и его потомков не входят в сопоставленные группы и `unpaired` #### Scenario: Две строки свода упакованы с одной встречной - **WHEN** по одному правилу есть дебетовые строки 40 и 60 и встречная кредитовая строка 100 - **THEN** `matched[]` содержит пару с суммами 100 и 100, нулевым расхождением и исходными `osv_entry_ids_a/b` #### Scenario: Пара с допустимым расхождением - **WHEN** группа имеет суммы 103 и 100 при пороге 5 % - **THEN** она входит в `with_discrepancy[]`, а не в `matched[]` #### Scenario: Другая пара компаний не смешивается с выбранной - **WHEN** по одному правилу есть встречные строки для A→Б и A→В, а запрошены `company_a_id=A`, `company_b_id=Б` - **THEN** ответ содержит только строки A→Б и Б→A ### 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.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 с тем же телом, что у несуществующей версии