## ADDED Requirements ### Requirement: Шахматка по сводной ОСВ версии `GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/all` и `POST /api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual` (`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать матрицу без сохранения в БД, из строк сводной ОСВ версии и её периметра. ALL включает все счета вида `A`/`AP` для дебета и `P`/`AP` для кредита. MANUAL принимает обязательные непустые `debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета по иерархии и оставляет подходящий вид. Неизвестный счёт SHALL давать 422 `ACCOUNT_NOT_FOUND`, неверный вид — 422 `ACCOUNT_KIND_MISMATCH`; пустой массив — 422 валидации тела. Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`. Ответ SHALL содержать `companies[] {id, name}` для всего периметра и `cells[]` для каждой упорядоченной пары разных компаний: `company_a_id`, `company_b_id`, `amount_a`, `amount_b`, `variance_amount`, `variance_percent`, `exceeds_threshold`, `one_sided`, `skipped_osv_entry_count`, `osv_entry_ids_debit`, `osv_entry_ids_credit`. Для пары `(A, B)` `amount_a` — сумма `closing_debit` строк A с контрагентом B по дебетовым счетам, `amount_b` — сумма `closing_credit` строк B с контрагентом A по кредитовым. Расхождение — модуль разности; процент — расхождение / меньшую сумму × 100 или null при нуле. `exceeds_threshold` SHALL быть true при точном проценте > 5, до округления; суммы и процент в JSON SHALL быть десятичными строками с двумя знаками после запятой (`ROUND_HALF_UP`). `one_sided` SHALL быть true при наличии исходных записей ОСВ только с одной стороны, в том числе при нулевой сумме. `skipped_osv_entry_count` SHALL считать исходные строки ОСВ пары с `account_id = null` на обеих сторонах; они не входят в суммы. Массивы `osv_entry_ids_debit/credit` SHALL содержать ID исходных записей соответствующей стороны. #### Scenario: Пустой периметр и отсутствие ОСВ - **WHEN** у версии нет членов периметра - **THEN** `companies = []`, `cells = []`; если компании есть, но ОСВ по выбранным счетам нет, ячейки присутствуют с нулевыми суммами #### Scenario: Только одна сторона пары - **WHEN** у A есть строка ОСВ с контрагентом B, а у B встречной строки нет - **THEN** для `(A, B)` `one_sided = true`, `variance_percent = null`, соответствующий массив `osv_entry_ids_*` пуст #### Scenario: Счёт не сопоставлен - **WHEN** у пары есть строка свода с `account_id = null` и двумя `entry_ids` - **THEN** она не входит в суммы, `skipped_osv_entry_count` обеих направленных ячеек пары увеличивается на 2 #### Scenario: Эталон «Русский бисквит» - **WHEN** матрица строится по ОСВ РБ и Пекарни Счастливочной из эталонных фикстур - **THEN** суммы сравниваются с эталоном только для точных пар счетов и компаний; общая сумма элиминации эталона не приравнивается к минимуму по паре в API (решение владельца 08.10.2026) ### Requirement: XLSX шахматки `GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/export` SHALL использовать те же режимы, тело и права, что и соответствующие JSON-маршруты. Ответ — XLSX с именем `intercompany-matrix_<ГК>_<год>-<квартал>_v.xlsx`, шапкой (ГК, период, версия, единицы, режим, выбранные счета) и строками разницы, дебета, кредита и процента расхождения для каждой пары. #### Scenario: Экспорт выбранных счетов - **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1 - **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования