## ADDED Requirements ### Requirement: Пары участников без встречных записей Ядро SHALL вычислять по своду версии пары участников без встречных записей (§3 п. 9 `05-domain-rules.md`, AURORA-1746): для участников периметра А ≠ Б (`consolidation_member` версии) пара (А, Б) входит в список, если в своде есть хотя бы одна строка ЮЛ А с `counterparty_id = Б` (сохранённым или дорезолвленным на чтение) и нет ни одной строки ЮЛ Б с `counterparty_id = А`. А — сторона, у которой записи есть (`holder`), Б — сторона без встречных записей (`missing`). Пара, где записей нет ни у А, ни у Б, в список MUST NOT входить. Строки ЮЛ с контрагентом — самим собой не учитываются. Счета, суммы и тип строки (`data_type`) на признак не влияют: учитываются все строки свода. Расчёт — чистая функция в `domain/` от строк свода и периметра; результат MUST NOT сохраняться. Порядок списка — по имени Б, затем по имени А. У каждой пары SHALL быть: `holder_id`, `holder_name`, `missing_id`, `missing_name`, `holder_rows` (число строк А с контрагентом Б), `missing_unmapped_rows` (число строк Б с `unmapped = true`), `missing_rows_total` (число всех строк Б в своде; 0 — у Б нет строк, например не привязано ни одной ОСВ или все её пары счетов — пробелы). #### Scenario: Русский бисквит до сопоставления - **WHEN** версия ГК «Русский бисквит» 2024 Q3 с периметром {АО «Русский бисквит», ООО «Пекарня Счастливочная»}, 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}` подтверждены; у Счастливочной контрагент «Русский бисквит» с ИНН 3528052718 сопоставлен по ИНН; у РБ контрагент «Пекарня Счастливочная ООО» без ИНН не сопоставлен - **THEN** список содержит одну пару: `holder` — ООО «Пекарня Счастливочная», `missing` — АО «Русский бисквит», `holder_rows` ≥ 1, `missing_unmapped_rows` ≥ 1 #### Scenario: Пара закрыта ручным сопоставлением - **WHEN** в той же версии строка РБ «Пекарня Счастливочная ООО» связана с ООО «Пекарня Счастливочная» (`POST …/osv/entries/{entry_id}/counterparty`) - **THEN** список пар пуст #### Scenario: Компании не связаны - **WHEN** в периметре три участника; у А и Б нет записей друг с другом ни в одну сторону - **THEN** пары (А, Б) и (Б, А) в списке нет #### Scenario: У стороны нет строк - **WHEN** у А есть строки с контрагентом Б, а у Б в своде нет ни одной строки (ОСВ не привязаны) - **THEN** пара (А, Б) в списке, `missing_rows_total = 0`, `missing_unmapped_rows = 0` ## MODIFIED Requirements ### Requirement: Страница свода и сводка `GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`, `row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`, `counterparty_type`, `counterparty_id` (строка с этим `counterparty_id` после дорезолва; AURORA-1746), `unmapped=true` (строка требует сопоставления: `counterparty_id` пуст после дорезолва **и** `counterparty_inn` пуст или из пробелов; строка с ИНН из файла, которого нет в справочнике, сопоставления не требует — решение владельца 08.10.2026, отмена П-1; `unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q` (подстрока без регистра в `counterparty_name` или префикс `counterparty_inn`). Те же фильтры SHALL принимать `GET …/osv/export`. `GET …/osv/summary` SHALL отдавать `rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} независимо от `match_stored`), `documents_total` (привязанные ОСВ-документы), `gaps[]` (`legal_entity_id`, `legal_entity_name`, `account_code`, `missing_quarters`) и `counter_pairs[]` — пары участников без встречных записей (требование «Пары участников без встречных записей»). Версия в любом статусе, включая `CANCELLED`, SHALL читаться. #### Scenario: Только несопоставленные - **WHEN** запрошено `GET …/osv?unmapped=true` - **THEN** все строки ответа с `counterparty_id = null` и без `counterparty_inn`, `total` = `summary.unmapped_total` #### Scenario: Строка с ИНН вне справочника не требует сопоставления - **WHEN** у строки `counterparty_inn = "7701000001"`, такого ИНН и имени в справочнике нет (`counterparty_id = null`) - **THEN** строки нет в `GET …/osv?unmapped=true`, она не входит в `summary.unmapped_total` #### Scenario: ИНН сопоставленного ЮЛ - **WHEN** строка без ИНН в файле сопоставлена с ЮЛ по алиасу (на лету или хранимо) - **THEN** `counterparty_inn = null`, `counterparty_entity_inn` — ИНН этого ЮЛ из справочника #### Scenario: Фильтр по способу - **WHEN** запрошено `GET …/osv?match_kind=NAME&match_kind=ALIAS` - **THEN** в ответе только строки с этими `match_kind`, `total` = `summary.auto_matched_total` #### Scenario: Строки одной стороны пары - **WHEN** в `summary.counter_pairs` есть пара `holder_id = А`, `missing_id = Б` и запрошено `GET …/osv?legal_entity_id=А&counterparty_id=Б` - **THEN** `total` = `holder_rows` пары, у всех строк ответа ЮЛ А и `counterparty_id = Б` #### Scenario: Кандидаты другой стороны пары - **WHEN** для той же пары запрошено `GET …/osv?legal_entity_id=Б&unmapped=true` - **THEN** `total` = `missing_unmapped_rows` пары