## MODIFIED 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 включает балансовые счета (`is_off_balance = false`) вида `A`/`AP` для дебета и `P`/`AP` для кредита; забалансовые счета (001–012) в ALL MUST NOT входить. MANUAL принимает обязательные непустые `debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета по иерархии и оставляет подходящий вид; забалансовые счета в MANUAL допускаются. Неизвестный счёт 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 тогда и только тогда, когда ровно одна из сумм `amount_a`, `amount_b` равна нулю (знак ненулевой суммы не важен); наличие строк ОСВ с нулевым сальдо на признак не влияет. Пара подсвечивается, если `exceeds_threshold` или `one_sided`. `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** у A по B сальдо 100 000 000, а у B по A есть строка ОСВ с нулевым сальдо (одни обороты) - **THEN** `one_sided = true`, `exceeds_threshold = false`, пара подсвечивается #### Scenario: Обе стороны нулевые - **WHEN** у A и у B есть строки ОСВ друг на друга, но обе суммы равны 0 - **THEN** `one_sided = false`, `exceeds_threshold = false`, пара не подсвечивается #### Scenario: Отрицательное сальдо - **WHEN** у A по B сальдо −50, у B по A — 0 - **THEN** `one_sided = true` #### Scenario: Забалансовые счета - **WHEN** у A по B есть сальдо на счёте 001 (`is_off_balance = true`) - **THEN** в режиме ALL оно в суммы не входит; в MANUAL с выбранным счётом 001 — входит #### 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`, шапкой (ГК, период, версия, единицы, режим, выбранные счета; для ALL — «Все балансовые счета») и строками разницы, дебета, кредита и процента расхождения для каждой пары. Пара SHALL заливаться цветом порога, если `exceeds_threshold` или `one_sided` — так же, как в API. #### Scenario: Экспорт выбранных счетов - **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1 - **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования #### Scenario: Подсветка пары без встречной стороны - **WHEN** выгружается ALL, у пары `amount_a = 100000000`, `amount_b = 0` - **THEN** все строки пары залиты цветом порога, в шапке «Режим» и «Счета дебета/кредита» — «Все балансовые счета»