12 KiB
Дизайн: пары участников без встречных записей (AURORA-1746, arm-core #195)
Контекст и зачем
Перед шахматкой аналитику важно одно: все ли внутригрупповые связи видны с обеих сторон. Шахматка строится по строкам свода с признаком «ГСК». Если у компании Б контрагент А лежит строкой без ИНН и без сопоставления, он «Внешний», и пара А–Б в шахматке выглядит однобокой.
Сейчас это подменено счётчиком всех строк без ИНН. Баннер требует «сопоставьте все», модалка [31-00] всплывает при каждом уходе с «ОСВ», хотя переход не блокируется (AURORA-1388). Заказчик (Циликов, 23.10.2026) прочитал это как обязанность прокликать каждую строку, а счётчик на реальных данных до нуля не доходит.
Успех: аналитик видит только существенное — пары участников без встречных записей — и разбирает их в одном месте, видя обе стороны. Строки без ИНН, не влияющие на пары, ему не мешают.
Антицель: экран, который снова заставляет «прокликать всё», или модалка, которая всплывает при каждом клике.
Решения владельца (27.10.2026, обсуждение в чате)
- Проверка — пары участников периметра: у А есть записи с контрагентом Б, у Б нет записей с контрагентом А. Уровень — компании, не счета.
- У Б может быть настоящий пропуск в учёте, поэтому переход никогда не блокируется.
- Модалка — вариант «короткая сводка»: только цифры, разбор — в отдельном представлении («Показать пары»).
- Представление — отдельный режим вкладки «ОСВ» с таблицами («они любят таблицы»): список пар, строки А с Б, строки Б без ИНН и без сопоставления.
- Подсветка кандидата по совпадению суммы — отдельная задача.
- В БД ничего не хранить. Модалка — один раз за сессию или после исправлений в сводной ОСВ.
Устройство
Ядро
domain/consolidated_osv.py:counter_pairs(rows, members) -> list[CounterPair]— чистая функция по строкамConsolidated.rows(после дорезолва) и периметру. Множество «кто кого видит» — пары(row.legal_entity_id, row.counterparty_id), где оба в периметре и различны. Пара (А, Б) в списке ⇔ (А, Б) есть, (Б, А) нет. Счётчики:holder_rows— строки А с контрагентом Б;missing_unmapped_rows— строки Б сneeds_mapping;missing_rows_total— все строки Б. Сортировка — имя Б, имя А (затем id для стабильности).OsvFilter.counterparty_id+_matches;OsvFilters.counterparty_idв схеме; экспорт принимает тот же фильтр автоматически (общая модель фильтров).OsvSummaryOut.counter_pairs: list[CounterPairOut];ConsolidatedOsvService.summaryсчитает их из того же_build, второго прохода по БД нет.- Аудит не пишется (чтение).
Фронт
useConsolidatedOsv/ConsolidatedOsvContent: режим из?view=pairs(useSearchParams,replace).- Новый виджет
widgets/ConsolidatedOsv/CounterPairs*.tsx: список пар + две таблицы (antdTable), колонки — счёт, контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт; суммы — через существующийuseNumberDisplay(единицы и знаки из шапки). Правая таблица — постранично,OSV_PAGE_SIZE, «Указать» →CounterpartyMapModal(существующая). ConsolidationReportPage/OsvTabs: вместоunmappedTotalполучают сводку (пары иunmapped_total); правило показа модалки — чистая функция вutils/consolidations.ts(counterPairsKey(pairs),shouldWarnCounterPairs(versionId, pairs, storage)), с тестами vitest. КлючsessionStorage—armka:counter-pairs:<version_id>, значение — ключ списка пар (holder_id>missing_id, отсортированные, через запятую). Чтение и запись — вtry/catch.UnmappedInnModalзаменяется модалкой «Проверьте встречные записи участников»; склонение «пара/пары/пар», «участника/участников» — существующаяpluralRu(web/src/utils/labels.ts).- Над правой таблицей — поле поиска по контрагенту (
qсуществующего фильтраGET …/osv, пауза ввода как в своде): у РБ на фикстуре около тысячи строк без ИНН, листать страницы в поисках одной строки — та же «прокликай всё». - Модалка не показывается для версий в завершённых статусах (
VERIFIED,CANCELLED,ARCHIVED,REJECTED—FINISHED_STATUSES): править там нечего. Режим «Встречные записи» доступен в любом статусе, «Указать» ведёт себя как в своде (ошибки 409 — от сервера). - Баннер — новый текст (спека
consolidated-osv-ui).
Документы (отдельный PR в docs)
05-domain-rules.md§3 п. 9 «Пары участников без встречных записей» — определение из спекиconsolidated-osv.10-user-journey.mdстрока 3.1 — новый текст баннера, режим «Встречные записи», модалка вместо [31-00].dev/api-contract-slice-4.md—counter_pairs[]в сводке, фильтрcounterparty_id.
Что оставлено исполнителю
Раскладка трёх колонок и ширины, тексты пустых состояний в пределах спеки, имена компонентов, вынос общих колонок сумм между сводом и новым режимом.
Риски
- Объём:
GET …/osv/summaryтеперь считает пары — линейный проход по уже собранным строкам, без новых запросов к БД. - Дорезолв на чтение влияет на пары: появление алиаса может закрыть пару без «Связать». Это корректно — признак считается от того же свода, что видит аналитик.
Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 27.10.2026 | «После исправлений» = список пар изменился (ключ списка пар в sessionStorage), а не любое «Связать» |
Повтор модалки нужен, когда есть что сказать нового; «Связать» вне пар модалку не меняет | Низкая: правило в одной функции фронта |
| 27.10.2026 | Строки А и Б берутся существующим GET …/osv с фильтрами (counterparty_id — новый), в сводке — только пары и счётчики |
Без нового эндпоинта; пагинация и экспорт работают как в своде | Низкая |
| 27.10.2026 | Учитываются все строки свода, без ограничения счетами правил элиминации и без порога по суммам | Владелец согласовал уровень компаний; строка с нулями тоже говорит, что контрагент виден | Средняя: возможны «лишние» пары из-за случайных счетов — правится фильтром в функции |
| 27.10.2026 | Колонки таблиц режима — счёт, контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт (без сальдо на начало) | Три колонки на экране; для поиска пары важнее обороты и итог | Низкая: добавить колонки |
| 27.10.2026 | Режим — параметр адреса ?view=pairs (с holder/missing), переключение без записи в историю |
Как вкладки отчёта (AURORA-1851); ссылку на пару можно переслать | Низкая |
| 27.10.2026 | Тексты баннера и модалки — из спек этого change | Владелец согласовал смысл, формулировки мои | Низкая |
| 27.10.2026 | Поиск q над правой таблицей режима (находка builder check) |
У РБ ~1000 строк без ИНН; без поиска разбор пары — листание страниц | Низкая: существующий фильтр API |
| 27.10.2026 | Модалка не показывается для версий в FINISHED_STATUSES (находка builder check) |
Замороженную версию не исправить — предупреждение было бы шумом | Низкая |
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.
| 27.10.2026 | R-1 (design #9): needsUnmappedWarning удаляется в Task 4 вместе с его использованием в OsvTabs, а не в Task 3 | Иначе между коммитами Task 3 и 4 сборка (tsc) красная | Нулевая |
| 27.10.2026 | R-2 (design #10): в тексте модалки «Ещё K строк без ИНН…» слово «строк» не склоняется, M — число разных missing_id (по спеке) | Спека дословна; склонение K=1 даёт всё равно неграмматичную фразу («учтены») | Низкая |
| 27.10.2026 | R-3 (design #11): отдельное ревью Task 6 не проводится — её diff в arm-core (одна фраза в CLAUDE.md) и docs-PR #176 проверяются финальным ревью всей ветки | Задача не содержит кода; отдельный ревьюер дублировал бы финальный | Низкая |