Решение владельца после показа на стенде: «Указать» во «Встречных записях» открывает список из одного участника пары; «Показать все организации» — обычный список. Чистая pairCandidateRows + vitest, спека и design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
14 KiB
Markdown
101 lines
14 KiB
Markdown
# Дизайн: пары участников без встречных записей (AURORA-1746, arm-core #195)
|
||
|
||
## Контекст и зачем
|
||
|
||
Перед шахматкой аналитику важно одно: все ли внутригрупповые связи видны с обеих сторон. Шахматка строится по строкам
|
||
свода с признаком «ГСК». Если у компании Б контрагент А лежит строкой без ИНН и без сопоставления, он «Внешний»,
|
||
и пара А–Б в шахматке выглядит однобокой.
|
||
|
||
Сейчас это подменено счётчиком всех строк без ИНН. Баннер требует «сопоставьте все», модалка [31-00] всплывает при каждом
|
||
уходе с «ОСВ», хотя переход не блокируется (AURORA-1388). Заказчик (Циликов, 23.10.2026) прочитал это как обязанность
|
||
прокликать каждую строку, а счётчик на реальных данных до нуля не доходит.
|
||
|
||
Успех: аналитик видит только существенное — пары участников без встречных записей — и разбирает их в одном месте, видя
|
||
обе стороны. Строки без ИНН, не влияющие на пары, ему не мешают.
|
||
|
||
Антицель: экран, который снова заставляет «прокликать всё», или модалка, которая всплывает при каждом клике.
|
||
|
||
## Решения владельца (27.10.2026, обсуждение в чате)
|
||
|
||
1. Проверка — пары участников периметра: у А есть записи с контрагентом Б, у Б нет записей с контрагентом А. Уровень —
|
||
компании, не счета.
|
||
2. У Б может быть настоящий пропуск в учёте, поэтому переход никогда не блокируется.
|
||
3. Модалка — вариант «короткая сводка»: только цифры, разбор — в отдельном представлении («Показать пары»).
|
||
4. Представление — отдельный режим вкладки «ОСВ» с таблицами («они любят таблицы»): список пар, строки А с Б, строки Б
|
||
без ИНН и без сопоставления.
|
||
5. Подсветка кандидата по совпадению суммы — отдельная задача.
|
||
6. В БД ничего не хранить. Модалка — один раз за сессию или после исправлений в сводной ОСВ.
|
||
7. (после показа на стенде) «Указать» в режиме пар открывает модалку сопоставления, сразу суженную до участника пары, с флажком «Показать все организации» — вариант 1 из двух показанных; кнопку «Связать с <А>» прямо в строке не делаем (легко промахнуться).
|
||
|
||
## Устройство
|
||
|
||
### Ядро
|
||
|
||
- `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`: список пар + две таблицы (antd `Table`), колонки — счёт,
|
||
контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт; суммы — через существующий `useNumberDisplay` (единицы и знаки
|
||
из шапки). Вторая (нижняя) таблица — постранично, `OSV_PAGE_SIZE`, «Указать» → `CounterpartyMapModal` (существующая).
|
||
- `ConsolidationReportPage` / `OsvTabs`: вместо `unmappedTotal` получают сводку (пары и `unmapped_total`); правило показа
|
||
модалки — чистая функция в `utils/consolidations.ts` (`counterPairsKey(pairs)`, `shouldWarnCounterPairs(versionId,
|
||
status, 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 | Колонки таблиц режима — счёт, контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт (без сальдо на начало) | Колонки должны уместиться на всю ширину при 1440; для поиска пары важнее обороты и итог | Низкая: добавить колонки |
|
||
| 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 | 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 проверяются финальным ревью всей ветки | Задача не содержит кода; отдельный ревьюер дублировал бы финальный | Низкая |
|
||
| 27.10.2026 | R-4 (design #12): подпись под парой в списке — «записей: N · без ИНН у <Б>: M» вместо служебных «держатель»/«Б» | Аналитик не знает ролей «держатель» и «Б»; название организации понятнее | Низкая |
|
||
| 27.10.2026 | R-5 (design #13): раскладка режима «Встречные записи» — пары одной строкой «таблеток» с переносом, под ними две таблицы на всю ширину одна под другой (вместо трёх колонок) | В трёх колонках таблицам не хватало ширины: суммы обрезались, видны были только «Обороты Дт» (находка H-1) | Низкая |
|
||
| 27.10.2026 | R-6 (design #14): уход с «ОСВ» (продолжение: пока сводки режима нет — Spin и подпись «Встречные записи» без «(N)»; ошибка запроса сводки — Alert с «Повторить»; сводка, догруженная при уходе, не кешируется и живёт только на время модалки) — закрытая версия (FINISHED_STATUSES) переходит без запроса сводки; при `summary = null` запрашивается сводка, при ошибке запроса переход идёт без модалки и без своего сообщения; на время догрузки переключатель вкладок блокируется; выбранная пара хранится в адресе и в режиме «Свод» (`?holder=&missing=` без `view`) | Модалка нужна только открытой версии; ошибку сводки показывает общий слой; блокировка исключает двойные клики; адрес — единственное хранилище состояния режима | Низкая |
|
||
|
||
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.
|