## MODIFIED 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` и версия не созданы ## ADDED Requirements ### 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` ## REMOVED Requirements ### Requirement: Правила элиминации версии **Reason**: с DBML v5.23 `elimination_rules_version` задан у каждой версии консолидации; ветка «версия правил не зафиксирована — последние открытые правила» больше не существует. **Migration**: требование «Правила элиминации зафиксированной версии»; клиенту `is_version_fixed` всегда `true`.