fix(counter-pairs): правки финального ревью (I-1, I-2, I-3, M-1, M-2, M-3, M-5)

Подпись пары без служебных слов, тест пары на фикстурах РБ, модалка при уходе с «ОСВ» на любую вкладку,
без «Экспорт» в режиме пар, README и ARCHITECTURE, таблица решений design.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-27 19:22:25 +04:00
parent 50f28b5672
commit 397a1a7aef
7 changed files with 22 additions and 13 deletions

View File

@ -786,8 +786,8 @@ advisory-блокировкой.
| `POST /consolidations/versions/{id}/status` | EDIT | ручной переход: `CANCELLED` (с комментарием) или возврат в `INITIATED` |
| `PUT /consolidations/versions/{id}/members` | EDIT | заменить периметр (ЮЛ должны принадлежать ГК, ≥ 2) |
| `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки; `?format=csv` — тот же чек-лист файлом CSV |
| `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры (`limit` ≤ 2000, по умолчанию 500) |
| `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]` |
| `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры, в т. ч. `counterparty_id` — строки с этим контрагентом (`limit` ≤ 2000, по умолчанию 500) |
| `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]`, `counter_pairs[]` (пары «участник не видит встречных записей») |
| `GET /consolidations/versions/{id}/osv/export` | VIEW | xlsx: лист «Сводная ОСВ» и лист «Пробелы» |
| `GET /consolidations/versions/{id}/counterparty-candidates` | VIEW | кандидаты для сопоставления: `scope=group\|all`, `q`, `entry_id` |
| `POST /consolidations/versions/{id}/osv/entries/{entry_id}/counterparty` | EDIT | ручное сопоставление контрагента строки свода |

View File

@ -162,6 +162,8 @@ async def test_consolidation_on_russian_biscuit_fixtures( # noqa: PLR0912, PLR0
# Числа сверены независимым подсчётом по сырым golden-JSON (склейка Q1–Q3, пробелы по наличию файлов).
assert summary["rows_total"] == len(rows) == ROWS_TOTAL
assert summary["unmapped_total"] == UNMAPPED_TOTAL
# РБ записал «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» без ИНН: Счастливочная видит РБ, а РБ — её нет (§3 п. 9)
assert [(p["holder_id"], p["missing_id"]) for p in summary["counter_pairs"]] == [(str(schastl.id), str(rb.id))]
# строки свода: 60 и 62 у обеих, 76 у Счастливочной
present = {(row["legal_entity_id"], row["account_root"]) for row in rows}
@ -219,6 +221,7 @@ async def test_consolidation_on_russian_biscuit_fixtures( # noqa: PLR0912, PLR0
assert body["propagated"] == PROPAGATED
after = await _get(client, f"{version_url}/osv/summary")
assert after["counter_pairs"] == [] # после сопоставления пара закрыта
rows_after = await _all_rows(client, version_url)
# `propagated` считает записи `osv_entry` (в том числе в парах-пробелах 66/76, строк в своде не дающих), а
# `unmapped_total` — строки свода: квартальные записи одного контрагента склеены (D-2). Поэтому `unmapped_total`

View File

@ -49,7 +49,7 @@
из шапки). Правая таблица — постранично, `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>`, значение — ключ списка
status, pairs, storage)`), с тестами vitest. Ключ `sessionStorage` — `armka:counter-pairs:<version_id>`, значение — ключ списка
пар (`holder_id>missing_id`, отсортированные, через запятую). Чтение и запись — в `try/catch`.
- `UnmappedInnModal` заменяется модалкой «Проверьте встречные записи участников»; склонение «пара/пары/пар»,
«участника/участников» — существующая `pluralRu` (`web/src/utils/labels.ts`).
@ -89,8 +89,9 @@
| 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 проверяются финальным ревью всей ветки | Задача не содержит кода; отдельный ревьюер дублировал бы финальный | Низкая |
| 27.10.2026 | R-4 (design #12): подпись под парой в списке — «записей: N · без ИНН у <Б>: M» вместо служебных «держатель»/«Б» | Аналитик не знает ролей «держатель» и «Б»; название организации понятнее | Низкая |
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.

View File

@ -30,7 +30,9 @@ SPA поверх API arm-core (`/api/v1`). Своих данных и бизне
| `ConsoleTable/LockModal.tsx` | [35] | «Взять в работу» (`POST …/lock`) и перехват с причиной (`takeover_reason`) |
| `../utils/consolidations.ts` | — | правила без React: подписи и пояснения статусов версии, ручные переходы (`MANUAL_TRANSITIONS` — зеркало бэка), своя блокировка, «Версия», `in_work_only`, причина ≤ 1000 |
| `ConsoleTable/consts.ts` | — | цвета бейджей статусов версии |
| `ConsolidatedOsv/OsvTabs.tsx` | [30], [31-00] | вкладки шагов отчёта и «Остались неуказанные ИНН» при уходе с «ОСВ» |
| `ConsolidatedOsv/OsvTabs.tsx` | [30] | вкладки шагов отчёта и модалка «Проверьте встречные записи участников» при уходе с «ОСВ» (список пар — `counter_pairs` сводки) |
| `ConsolidatedOsv/CounterPairsView.tsx` | [30] | режим «Встречные записи» вкладки «ОСВ»: пары, строки держателя, строки Б без ИНН с «Указать» |
| `ConsolidatedOsv/counterPairs.ts` | — | правила без React: подпись пары, параметры адреса режима (`?view=pairs&holder=&missing=`) |
| `ConsolidatedOsv/useNumberDisplay.tsx` | [30] | «Единицы» и «Знаков после запятой» сводной ОСВ (`formatScaledAmount` из `utils/money.ts`) |
| `../hooks/useBeforeUnload.ts` | — | предупреждение браузера о несохранённых правках в открытой модалке |
| `ConsoleLegalEntities/` | [34] | панель «Организации» выбранной версии и чек-лист |

View File

@ -55,9 +55,9 @@ function usePagedRows(versionId: string, filters: OsvFilters | null, reloadKey:
getConsolidatedOsv(versionId, filters, OSV_PAGE_SIZE, (page - 1) * OSV_PAGE_SIZE)
.then((r) => {
if (requestId !== requestRef.current) return;
// страница ушла за конец (последнюю строку сопоставили) — назад на допустимую
// страница ушла за конец (последнюю строку сопоставили) — сразу на последнюю существующую
if (r.items.length === 0 && page > 1) {
setPage(page - 1);
setPage(Math.max(1, Math.ceil(r.total / OSV_PAGE_SIZE)));
return;
}
setRows(r.items);
@ -171,7 +171,7 @@ export function CounterPairsView({ versionId, pairs, selected, numFormat, onSele
}}>
<div style={{ fontSize: 13, fontWeight: 600 }}>{pairLabel(p)}</div>
<div style={{ fontSize: 11, color: MUTED }}>
строк у держателя: {p.holder_rows} · без ИНН у Б: {p.missing_unmapped_rows}
записей: {p.holder_rows} · без ИНН у {p.missing_name}: {p.missing_unmapped_rows}
</div>
</button>
);

View File

@ -76,7 +76,7 @@ export function OsvTabs({ summary, versionId, status }: Props) {
const newKey = String(value);
if (newKey === activeKey) return;
const pairs = summary?.counter_pairs ?? [];
if (activeKey === 'osv' && newKey === 'chess' && shouldWarnCounterPairs(versionId, status, pairs, safeSessionStorage())) {
if (activeKey === 'osv' && shouldWarnCounterPairs(versionId, status, pairs, safeSessionStorage())) {
rememberCounterPairsShown(versionId, pairs, safeSessionStorage());
setPending(newKey);
} else {

View File

@ -101,9 +101,12 @@ export function OsvContent({ card, onSummaryChange }: Props) {
/>
</div>
</div>
<Button icon={<DownloadOutlined />} loading={exporting} onClick={download}>
Экспорт
</Button>
{/* выгружает весь свод, а не пару — в режиме пар не нужна */}
{!inPairs && (
<Button icon={<DownloadOutlined />} loading={exporting} onClick={download}>
Экспорт
</Button>
)}
</div>
<Segmented
value={pairsView.view}