docs(slice-4): правила §2/§3/§9/§10 по факту, OQ, контракт API для фронта, QA-гайд
- 05-domain-rules: §2 (выбор источников, склейка, пробелы, Q1-эквивалентность), §3 (явная привязка, дорезолв, match_kind, кандидаты, ручное сопоставление), §9.1 (переходы Среза 4), §9.3, §9.5 (отвязка при автозамене, OQ-110), §10 (политика «в работе + своя блокировка») - 03-architecture: консолидации, чистый домен свода, порядок блокировок - 07-open-questions: закрыты OQ-03, OQ-19, OQ-110 (Срез 4), OQ-40 принят; добавлены OQ-118…OQ-129 - 09-delivery-plan, 10-user-journey: Срез 4 по факту, эндпоинты у шагов 2.1–2.6 и 3.1 - dev/api-contract-slice-4.md: маршруты, схемы, коды ошибок, нюансы для фронта - dev/qa-guide: раздел 7 «Консолидация через API» Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
ab564c1ab0
commit
9246a61439
@ -1,4 +1,4 @@
|
||||
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.15)
|
||||
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.16)
|
||||
|
||||
## Компоненты (= репозитории, см. `00-workspace-layout.md`)
|
||||
|
||||
@ -113,6 +113,36 @@
|
||||
`pg_advisory_xact_lock` по ЮЛ (иначе два параллельно смапленных файла с одним документом не увидят друг друга), загрузка —
|
||||
advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскакивает дедуп).
|
||||
|
||||
### Консолидации и сводная ОСВ (Срез 4)
|
||||
|
||||
Версия консолидации — единица работы аналитика: периметр (`consolidation_member`), комплект документов
|
||||
(`consolidation_document`, явная привязка), статус, блокировка «взять в работу». Сводная ОСВ, чек-лист, признак контрагента и
|
||||
пробелы — **чистые функции** от версии (DECISION-5, ничего не кэшируется в БД и в процессе).
|
||||
|
||||
- **Чистый домен свода (D-1).** `repositories/consolidations.py:load_osv_rows` одним запросом отдаёт строки привязанных
|
||||
ОСВ-документов; `domain/consolidated_osv.py:build` и `domain/consolidation_periods.py:select_sources` (без БД и сессий,
|
||||
mypy strict) выбирают источники, склеивают кварталы, находят пробелы, резолвят контрагента, применяют фильтры.
|
||||
`services/consolidated_osv.py:_build` — единственная сборка, которую делят страница (`GET …/osv`), сводка, кандидаты,
|
||||
ручное сопоставление (ответ) и экспорт xlsx (`services/osv_export.py`). Весь свод версии строится в памяти на запрос
|
||||
(замер: ≈5,4 тыс. строк — 0,27 с, 8 запросов; порог для кэша — OQ-121).
|
||||
- **Пакет консолидаций.** `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export,
|
||||
mapping); сервисы `consolidations` (создание, список, карточка, периметр), `consolidation_guard` (общий пролог правок:
|
||||
версия `FOR UPDATE` → видимость → статус «в работе» → своя блокировка), `consolidation_locks`, `consolidation_status`,
|
||||
`consolidation_documents` (привязка/отвязка), `consolidated_osv`, `counterparty_mapping`, `osv_export`; домен — `policy`
|
||||
(`ensure_version_editable`), `checklist`, `consolidation_periods`, `consolidated_osv`.
|
||||
- **Единственное место смены статуса** — `consolidation_status.transition`: ручной `POST …/status` и автоматические
|
||||
переходы (загрузка с версией, маппинг, подтверждение документа) зовут её же, поэтому история и аудит одинаковы. Версия к
|
||||
моменту вызова уже под `FOR UPDATE` у вызывающего.
|
||||
- **Порядок блокировок: файл → документ → advisory-lock ГК → версии (по возрастанию `id`).** Подтверждение документа берёт файл,
|
||||
документ и затем все версии «в работе» ГК ЮЛ (`lock_versions_for_entity` блокирует надмножество — член ИЛИ та же ГК — и
|
||||
только потом отбирает версии, где ЮЛ член, иначе confirm ∥ `PUT …/members` теряет привязку: R-2); создание версии берёт
|
||||
`pg_advisory_xact_lock` по ГК; правка версии начинается с `consolidation_version FOR UPDATE`. Документы блокируются
|
||||
`FOR NO KEY UPDATE` (R-3): вставка `consolidation_document` берёт `FOR KEY SHARE` на документ и конфликтовала бы с
|
||||
`FOR UPDATE`. Ручное сопоставление: версия → advisory-lock по нормализованному алиасу (его берёт и `aliases.add`
|
||||
справочника) → алиасы → строки `osv_entry` с тем же именем.
|
||||
- **Нагрузка и транзакции.** Переход версии после загрузки выполняется в отдельной короткой транзакции
|
||||
(`session_transaction`), сбой не отменяет принятые файлы (R-10). Чтение (`GET …/osv`, сводка, экспорт) блокировок не берёт.
|
||||
|
||||
## Схема ключей S3
|
||||
|
||||
**Один бакет** (`OPENBAO__MINIO__MINIO_BUCKET` шаблона); роль объекта — первый сегмент ключа:
|
||||
@ -152,7 +182,7 @@ api/src/
|
||||
# машины статусов, policy — pydantic-модели на входе и выходе
|
||||
services/ # use-cases: оркестрация domain + repositories + outbox + audit_event
|
||||
repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка)
|
||||
models/ # ORM 1:1 с DBML v5.15; имена таблиц/колонок/enum — как в DBML
|
||||
models/ # ORM 1:1 с DBML v5.16; имена таблиц/колонок/enum — как в DBML
|
||||
schemas/ # pydantic DTO API /api/v1
|
||||
core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент),
|
||||
# logger, exception_handlers
|
||||
@ -237,7 +267,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
|
||||
`version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`.
|
||||
- Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное
|
||||
`<rev>_<slug>.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`.
|
||||
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.15.
|
||||
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.16.
|
||||
|
||||
### Инфраструктурные клиенты
|
||||
|
||||
|
||||
@ -235,18 +235,35 @@ openpyxl, после чтения — только диапазоны, кото
|
||||
|
||||
---
|
||||
|
||||
## §2. Приведение периодов (DOC-259006)
|
||||
## §2. Приведение периодов (DOC-259006; Срез 4, решения владельца 06.10.2026: OQ-03, OQ-19)
|
||||
|
||||
Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), перед
|
||||
консолидацией строится **расчётное представление** (не хранится):
|
||||
Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), при построении
|
||||
сводной ОСВ (§3) строится **расчётное представление** (не хранится, DECISION-5). Реализация —
|
||||
`domain/consolidation_periods.py:select_sources` (выбор документов) и `domain/consolidated_osv.py:build` (склейка).
|
||||
|
||||
**Выбор источников на пару (ЮЛ, счёт документа)** среди документов, привязанных к версии (§3 п. 1); `n` — квартал версии:
|
||||
|
||||
1. есть привязанный документ с `is_cumulative = true` и `period_quarter = n` — он один, строки берутся как есть;
|
||||
2. иначе нужны привязанные квартальные документы за **каждый** квартал `Q1..Qn`; любого нет — **пробел**
|
||||
(`Gap(missing_quarters)`), строк пары в своде нет, пробел показывается в сводке (`gaps[]`) и в листе «Пробелы» экспорта;
|
||||
3. нарастающий и квартальные одновременно — берётся нарастающий (он полнее по определению);
|
||||
4. **Q1-эквивалентность ОСВ (R-14, design №81; OQ-124):** документ ОСВ за `Q1` равнозначен и нарастающему, и квартальному
|
||||
(парсер помечает ОСВ за Q1 как нарастающую: период с начала года = Q1) — в версиях Q2+ он служит квартальным источником
|
||||
Q1. ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14a: баланс на конец Q1).
|
||||
|
||||
**Склейка квартальных строк** — ключ `(account_code строки, counterparty_name.strip(), normalize_inn(counterparty_inn))`;
|
||||
договоры (`group_type = CONTRACT`) в свод не входят — строка COUNTERPARTY уже содержит их сумму:
|
||||
|
||||
```
|
||||
opening_* = opening_* первой ОСВ года (Q1)
|
||||
opening_* = opening_* строки Q1
|
||||
turnover_* = Σ turnover_* по кварталам Q1..Qn
|
||||
closing_* = closing_* последней ОСВ (Qn)
|
||||
closing_* = closing_* строки Qn
|
||||
```
|
||||
Ключ соединения строк между кварталами — (счёт, контрагент, [договор]). **[открыто]**: поведение при отсутствии
|
||||
промежуточного квартала; агрегировать на уровне контрагента или договора.
|
||||
|
||||
Отсутствующая в квартале строка ключа = нули; `None` в ячейке = 0; две строки с одним ключом внутри квартала
|
||||
суммируются (design №1); `entry_ids` строки свода перечисляет все источники. Нарастающий документ отдаётся строка в
|
||||
строку (OQ-125: различие с квартальным Q1 версии Q1 — только на дублях ключа и `None`). Строки свода сортируются:
|
||||
имя ЮЛ, счёт (натуральный порядок: `9 < 60 < 60.2 < 60.10`), номер строки.
|
||||
|
||||
---
|
||||
|
||||
@ -254,8 +271,14 @@ closing_* = closing_* последней ОСВ (Qn)
|
||||
|
||||
Вход: `consolidation_version_id`. Периметр — `consolidation_member` версии (не текущий `related_group_member`!).
|
||||
|
||||
1. Для каждой компании периметра: `document` типа OSV со статусом CONFIRMED за `year/quarter` версии, входящие в
|
||||
`consolidation_document` версии (если версия ещё не зафиксировала документы — последние подтверждённые).
|
||||
1. Для каждой компании периметра: `document` типа OSV, входящие в `consolidation_document` версии (**явная привязка**, а не
|
||||
«последние подтверждённые»). Документ **подходит** версии, если ЮЛ — член периметра, `period_year` = год версии, и:
|
||||
нарастающий (ББ/ОФР — всегда) — `period_quarter` = квартал версии; квартальный — `period_quarter` ≤ квартал версии;
|
||||
ОСВ за `Q1` подходит версии любого квартала года (§2 п. 4, OQ-124). Привязываются только `CONFIRMED` документы
|
||||
незаменённых файлов. Точки привязки: подтверждение документа (ко всем версиям «в работе»), создание версии и добавление
|
||||
ЮЛ в периметр (подхват: на ключ «тип, счёт, квартал, признак нарастающего» — документ с самым поздним `confirmed_at`);
|
||||
при привязке документ с тем же ключом у того же ЮЛ заменяется. Точки отвязки: автозамена файла (§9.5 п. 3), удаление ЮЛ
|
||||
из периметра. Версии `CANCELLED` привязок не меняют. Выбор источников и склейка кварталов — §2.
|
||||
2. Строки `osv_entry` этих документов с `group_type = COUNTERPARTY`; `data_type IN (BU, SUM)`.
|
||||
3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются
|
||||
как в файле.
|
||||
@ -269,13 +292,35 @@ closing_* = closing_* последней ОСВ (Qn)
|
||||
проваливается к имени); каждый шаг считает кандидатов отдельно: единственное точное совпадение имени побеждает
|
||||
неоднозначность нормализованного; имя из одних ОПФ (пусто после `normalize`) не ищется.
|
||||
5. Признак контрагента: ГСК / ГК / Внешний (см. `04-data-model-guide.md`).
|
||||
6. Количество строк свода = количеству отобранных `osv_entry`.
|
||||
6. Количество строк свода = количеству отобранных `osv_entry` для нарастающей ОСВ; для квартальных документов строки
|
||||
склеиваются по ключу (§2), `entry_ids` строки свода перечисляет источники.
|
||||
7. **Дорезолв на чтение.** Строка без `counterparty_id` резолвится на лету тем же `CounterpartyIndex`; результат **не
|
||||
сохраняется** (производное, DECISION-5): в ответе `match_stored = false`, `match_kind` — шаг резолва. Сохранённое значение
|
||||
`osv_entry.counterparty_id` приоритетнее; при расхождении между кварталами берётся значение самого позднего квартала
|
||||
(расхождение — в `warnings` строки).
|
||||
8. **Способ сопоставления** `osv_entry.counterparty_match_kind` (DBML v5.16): `INN`/`NAME`/`ALIAS` — шаг, на котором при
|
||||
маппинге нашёлся единственный кандидат; `MANUAL` — ручное сопоставление; `PROPAGATED` — автоприменение; `NULL` ⇔
|
||||
`counterparty_id IS NULL`. Отдельное поле нужно, потому что после ручного сопоставления появляется алиас и ручное решение
|
||||
становится неотличимым от `ALIAS`.
|
||||
|
||||
**Ручное сопоставление** (алгоритм в DOC-221278): список кандидатов = компании ГСК и ГК; поиск по ИНН/имени/алиасам;
|
||||
выбор → проверка дубля `normalized_alias` у другой компании (предложить перенести или отменить) → создать
|
||||
`legal_entity_alias` → `osv_entry.counterparty_id = выбранное`; `counterparty_name/inn` не трогать →
|
||||
`audit_event(OSV_COUNTERPARTY_MAP)` → автоприменить к другим строкам версии с тем же именем, если `counterparty_id`
|
||||
пуст, алиас однозначен и ИНН строки не противоречит.
|
||||
**Ручное сопоставление** (алгоритм в DOC-221278; Срез 4, `POST …/osv/entries/{entry_id}/counterparty`): правка версии —
|
||||
право `CONSOLIDATION_EDIT`, статус «в работе» и своя блокировка (§10).
|
||||
|
||||
- **Кандидаты:** `scope=group` (по умолчанию) — ЮЛ периметра и ЮЛ той же ГК, `q` необязателен; `scope=all` — весь справочник
|
||||
без policy видимости, `q` не короче 3 символов (иначе 422 `QUERY_TOO_SHORT`); поиск — ИНН по префиксу, имя и алиасы —
|
||||
подстрока без регистра; не больше 100; `recommended` — нормализованное имя/алиас совпало с `normalize_alias` имени
|
||||
строки `entry_id` (точное совпадение, OQ-127); рекомендуемые первыми.
|
||||
- **Строка** — строка свода версии: `group_type = COUNTERPARTY`, `data_type ∈ {BU, SUM}` привязанного ОСВ-документа; строки
|
||||
`NU`/`ACCOUNT` — 404 и в автоприменение не входят (R-11, §3 п. 2; OQ-126).
|
||||
- **Шаги в одной транзакции:** выбор → проверка дубля `normalized_alias` у другой компании (`409 ALIAS_TAKEN` со списком
|
||||
владельцев; с `move_alias = true` алиасы владельцев удаляются и алиас переносится — право `DIRECTORY_DELETE` для переноса
|
||||
не требуется, решение владельца 06.10.2026) → создать `legal_entity_alias` (`LEGAL_ENTITY_CHANGE`) →
|
||||
`osv_entry.counterparty_id = выбранное`, `counterparty_match_kind = MANUAL`; `counterparty_name/inn` не трогать →
|
||||
автоприменить к строкам **всех** ОСВ-документов версии с тем же `counterparty_name.strip()`, пустым `counterparty_id` и
|
||||
ИНН пустым или равным ИНН выбранного (`PROPAGATED`; ранее связанные строки не перевязываются) →
|
||||
`audit_event(OSV_COUNTERPARTY_MAP)` с `before/after` и `propagated_entry_ids`.
|
||||
- Строка `osv_entry` общая у всех версий, куда входит документ: сопоставление в одной версии меняет её для всех (OQ-123).
|
||||
- Пустое имя (после нормализации) — алиас не создаётся и автоприменения нет. Неоднозначность дорезолва при тёзке — OQ-128.
|
||||
|
||||
Наблюдение по фикстурам: у «Счастливочной» контрагент «Русский бисквит» идёт **с ИНН 3528052718** в колонке E, а у
|
||||
«Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу.
|
||||
@ -436,6 +481,21 @@ NEEDS_CHECK`; пройдено/акцептовано + нажато «Подт
|
||||
|
||||
Каждый переход → `consolidation_status_history` + `audit_event(CONSOLIDATION_STATUS_CHANGE)`.
|
||||
|
||||
**Срез 4 реализует только переходы, не требующие маршрута согласования** (`consolidation_status.transition` — единственное
|
||||
место смены статуса; остальные переходы таблицы — Срез 7, вручную дают 409 `TRANSITION_NOT_ALLOWED`):
|
||||
|
||||
| Событие | Переход | Кто (`changed_by`) |
|
||||
|---|---|---|
|
||||
| загрузка файла с `consolidation_version_id`, принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING | загрузивший |
|
||||
| маппинг создал документ `NEEDS_CHECK`, подходящий версии | DOCUMENTS_PROCESSING → DATA_CORRECTION | `stored_file.uploaded_by` (нет — переход пропускается, лог) |
|
||||
| подтверждение документа, привязанного к версии | INITIATED / DOCUMENTS_PROCESSING → DATA_CORRECTION | подтвердивший |
|
||||
| `POST …/status {CANCELLED, comment}` (комментарий обязателен, 422 `COMMENT_REQUIRED`) | любой «в работе» → CANCELLED | пользователь с блокировкой |
|
||||
| `POST …/status {INITIATED}` | DOCUMENTS_PROCESSING / DATA_CORRECTION → INITIATED (привязки сохраняются) | пользователь с блокировкой |
|
||||
|
||||
«В работе» = INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION. `CANCELLED` снимает активную блокировку и `is_in_work`.
|
||||
Загрузка и подтверждение блокировкой не ограничиваются; запрет загрузки/подтверждения после DATA_CORRECTION (ТЗ 03_9 A1) —
|
||||
Срез 7 (OQ-118). Условие «все обязательные документы CONFIRMED» для DATA_CORRECTION → CONSOLIDATION_* — Срез 7.
|
||||
|
||||
### 9.2 Файл (`file_status`): UPLOADED → PROCESSING → PROCESSED | ERROR; любой → SUPERSEDED (замена файлом целиком,
|
||||
`superseded_by_file_id`, `version_no + 1`). Статус меняют только по **текущему** заданию файла
|
||||
(`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян);
|
||||
@ -450,7 +510,7 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
|
||||
(OQ-05 закрыт: `RECOGNIZED` нужен); «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро
|
||||
документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки
|
||||
равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`;
|
||||
иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN` до Среза 4.
|
||||
иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN`: подходящесть документа версии — функция (документ, версия), а один документ входит в несколько версий (§3 п. 1, Срез 4; OQ-120).
|
||||
|
||||
### 9.4 Задание (`recognition_status`): queued → published → processing → done | needs_check | error | timeout → dlq;
|
||||
replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 5 мин.
|
||||
@ -478,18 +538,27 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
заменённых файлов сохраняются, но по умолчанию не показываются. Если новый файл содержит лишь часть документов
|
||||
заменяемого, заменяемый уходит в `SUPERSEDED` целиком (OQ-06: замена файлом целиком; частный случай — OQ-115).
|
||||
Аудит: системное `FILE_REPLACE` (`user_id = NULL`, `object_id` — заменённый файл, `after_value` — новый, в `description`
|
||||
перечень документов и `confirmed=true`).
|
||||
перечень документов и `confirmed=true`). **Срез 4:** автозамена снимает привязки (`consolidation_document`) **всех**
|
||||
документов заменённого файла из всех версий «в работе» (`CANCELLED` не трогаются); версии, где привязка снята, —
|
||||
`FILE_REPLACE.after_value.unbound_version_ids`; новый документ привязывается только после подтверждения (§3 п. 1).
|
||||
4. **Подтверждение** (`POST /documents/{id}/confirm`, `DOCUMENT_EDIT`): `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`
|
||||
(`confirmed_by/at`, `audit_event(DOCUMENT_CONFIRM)`); повтор — 409 `ALREADY_CONFIRMED`; документ заменённого файла —
|
||||
409 `FILE_SUPERSEDED`; прочие статусы — 409 `DOCUMENT_NOT_CONFIRMABLE`. Правка строк и корректировки — подсрез 2d/Срез 3.
|
||||
5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено при `CONFIRMED` документе
|
||||
(`FILE_HAS_CONFIRMED_DOCUMENTS`) и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить
|
||||
можно. Удаляются строки, документы, задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не
|
||||
стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`.
|
||||
стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии
|
||||
привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в
|
||||
версии невозможно; расширять удаление не требуется.
|
||||
6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); смены статусов
|
||||
задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55).
|
||||
7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы; автозамена дополнительно берёт
|
||||
advisory-lock по ЮЛ. Подробности — `03-architecture.md`.
|
||||
7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы (`FOR NO KEY UPDATE`) → advisory-lock ГК →
|
||||
версии консолидации (`FOR UPDATE`, по возрастанию `id`); автозамена дополнительно берёт advisory-lock по ЮЛ. Подробности —
|
||||
`03-architecture.md`.
|
||||
8. **Загрузка в контексте версии** (`?consolidation_version_id=`, Срез 4): до приёма — версия видима (404), «в работе»
|
||||
(409 `VERSION_NOT_EDITABLE`), ЮЛ — член периметра (422 `ENTITY_NOT_IN_PERIMETER`); после приёма хотя бы одного файла
|
||||
INITIATED → DOCUMENTS_PROCESSING (§9.1); сбой этого перехода не отменяет принятые файлы. Блокировка версии для загрузки не
|
||||
нужна.
|
||||
|
||||
---
|
||||
|
||||
@ -507,6 +576,15 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
|
||||
Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят только ГК своего `regional_office`; редактирование версии — только в
|
||||
допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости.
|
||||
|
||||
**Срез 4 (`domain/policy.py:ensure_version_editable`, решения владельца 06.10.2026, OQ-19):** правка версии (замена
|
||||
периметра, ручной статус, ручное сопоставление контрагента) разрешена, когда версия «в работе» (INITIATED,
|
||||
DOCUMENTS_PROCESSING, DATA_CORRECTION; иначе 409 `VERSION_NOT_EDITABLE`) **и** у пользователя своя активная
|
||||
`consolidation_lock` (нет — 409 `LOCK_REQUIRED`, чужая — 409 `LOCKED_BY_OTHER`). Блокировка закрывает все правки версии, но не
|
||||
загрузку и подтверждение документов. Перехват чужой блокировки — любому с `CONSOLIDATION_EDIT`, с обязательной причиной;
|
||||
блокировка не истекает по времени (OQ-119). Чтение (свод, чек-лист, экспорт, кандидаты) требует `CONSOLIDATION_VIEW`,
|
||||
блокировки не нужно, версия любого статуса читается. Видимость версии — видимость её ГК; чужая версия — 404.
|
||||
Порядок проверок: право → видимость → статус/блокировка → операция.
|
||||
Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI.
|
||||
|
||||
### 10.1 Видимость справочников (Срез 1, без проверки кода роли)
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|---|---|---|---|---|
|
||||
| OQ-01 | Хранить ли все строки ОСВ или только контрагентские; нужен ли `parent_entry_id` (замечание 2 ревью v5.13) | DBML, «Парсинг ОСВ» п. 11 | срез 2 (парсер, `osv_entry`) | **закрыт решением владельца 05.10.2026:** хранить **все** строки ОСВ с `level` и `parent_entry_id`; строку «Итого» не хранить, использовать только для сверки (парсер отдаёт её как `group_type = TOTAL`, маппер Среза 2c отбрасывает) |
|
||||
| OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | **закрыт решением владельца 05.10.2026:** ОСВ «за N квартал» — **квартальная**; признак сверяется по данным: если есть ОСВ Q1 того же года и `opening` совпадает — нарастающая (парсер ставит `is_cumulative` по тексту, ядро переопределяет при маппинге/в консолидации); **допущение парсера (Срез 2a):** `period_quarter` и `is_cumulative` вычисляются только по тексту периода (`rules/periods.yaml`; Q2–Q4 «N квартал» → `false`), сверка по данным — в ядре, подсрез 2c (`ASSUMPTION(OQ-02)`) |
|
||||
| OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | допущение: ключ (счёт, контрагент); пропуск квартала → блокирующая ошибка чек-листа |
|
||||
| OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | **закрыт решением владельца 06.10.2026 (Срез 4):** ключ склейки — (счёт, контрагент по сырому имени и ИНН), договоры внутри контрагента (строка COUNTERPARTY уже содержит их сумму); пропуск квартала — **пробел** по паре (ЮЛ, счёт), а не блокирующая ошибка: строк пары в своде нет, пробел отдаётся в `gaps[]` сводки и в листе «Пробелы» экспорта; квартал без строки ключа = нули; дубль ключа внутри квартала суммируется. Правило — `05-domain-rules.md` §2; код — `domain/consolidation_periods.select_sources`, `domain/consolidated_osv.build` |
|
||||
| OQ-04 | ОСВ по субсчёту (58.03 вместо 58) закрывает ли обязательный документ «ОСВ 58» | фикстуры | чек-лист | **закрыт решением владельца 05.10.2026:** **нет**, ОСВ по субсчёту (58.03) — только субсчёт, обязательный документ «ОСВ 58» закрывает только полная ОСВ по счёту 58; чек-лист считает документ выполненным при `osv_account_code = код правила` |
|
||||
| OQ-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | **закрыт решением владельца 05.10.2026:** статус `RECOGNIZED` **нужен**: документ после успешного распознавания — `RECOGNIZED`, при нарушениях — `NEEDS_CHECK`, после подтверждения аналитиком — `CONFIRMED` |
|
||||
| OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком |
|
||||
@ -24,7 +24,7 @@
|
||||
| OQ-16 | Матрица прав (ТЗ сноска 29 «будет разработана»); роли макета (Аналитик ДКБ, Наблюдатель) ≠ ТЗ (ANL-034) | ТЗ 8.1, макет | срез 1 | допущение: seed по §10 правил |
|
||||
| OQ-17 | `user_role` M:N или одна роль на пользователя | DBML | срез 1 | **закрыт реализацией Среза 1 (05.10.2026): M:N** — `user_role`, `PUT /admin/users/{id}/roles` заменяет набор ролей |
|
||||
| OQ-18 | Статусы REJECTED / ARCHIVED из макета (ANL-035); `VERIFICATION_GO` (РКА ГО) — есть ли в маршруте | DBML, DOC-037734 | срез 7 | допущение: REJECTED не вводить (возврат = переход + комментарий); ARCHIVED — флаг фильтра; VERIFICATION_GO — есть |
|
||||
| OQ-19 | `consolidation_lock` (взять в работу / перехват, ANL-036) — подтвердить с заказчиком | макет | срез 7 | реализовать как в DBML |
|
||||
| OQ-19 | `consolidation_lock` (взять в работу / перехват, ANL-036) — подтвердить с заказчиком | макет | срез 7 | **закрыт решением владельца 06.10.2026 (Срез 4):** блокировка «взять в работу» закрывает **все** правки версии (периметр, статус, ручное сопоставление; загрузка и подтверждение документов блокировкой не ограничиваются); перехват — любому с `CONSOLIDATION_EDIT`, с обязательной причиной (`takeover_reason`), событие `CONSOLIDATION_LOCK_TAKEOVER`; истечения нет (OQ-119). `05-domain-rules.md` §10 |
|
||||
| OQ-20 | `company_group.crm_id` обязателен (модалка) или nullable (UC-02, DBML) | DOC-221280 | срез 1 | решено DBML: nullable + `code` |
|
||||
| OQ-21 | «Дата перевода» ЮЛ в другую ГК — помечена «??» | DOC-221280 | срез 1 | **закрыт решением владельца 05.10.2026:** дата перевода — `effective_date` ≥ сегодня, пишется в `legal_entity_history`; перевод применяется сразу; граница суток — по МСК (OQ-96, follow-up arm-core#8) |
|
||||
| OQ-22 | Ретеншн `results/` в S3 (ARCH-010) | DBML | срез 2 | допущение: как `uploads/` |
|
||||
@ -45,7 +45,7 @@
|
||||
| OQ-37 | Строка «Гудвилл 1105» (ФСБУ 4/2023) в распознанной форме (`41`) vs строка `kfo_result_line.line_code = 'GOODWILL'` в разделе I КФО: использовать код 1105 или служебный GOODWILL? Влияет на шаблон строк на фронте и на формулы §7 (1100 = … + 1105?) | макет [41], ФСБУ 4/2023 | срез 3, 6 | допущение: хранить код 1105 как пришёл; строка КФО «GOODWILL» — отдельная, уточнить у методологов |
|
||||
| OQ-38 | ЕХ в макете показывает и **неверифицированные** консолидации («Консолидация 2025 К4», без даты верификации, `62` [83-01]); ЕХ ТЗ 6.2 — срез пишется только после «Верифицирован». Показывать ли в ЛК ЕХ незавершённые консолидации (как «в работе») или только `ex_snapshot`? | макет [62] [83-01] vs ЕХ ТЗ 6.2 | срез 7 | допущение: только срезы `ex_snapshot`; незавершённые — в разделе «Отчётность» |
|
||||
| OQ-39 | Состав «сформированной отчётности» в ЕХ (`83-01`): КФО .xlsx, Шахматка .xlsx и «Консолидированная отчетность .pdf» с датой «Сформирован» и подписями Утвердил/Верифицировал. Нужен ли PDF-экспорт КФО и какие ещё артефакты (сводная ОСВ, корректировки, гудвилл — ЕХ ТЗ 5.2) показывать? | макет [83-01] vs ЕХ ТЗ 5.2 | срез 7 | допущение: xlsx из `payload` на лету для всех `item_type`; PDF — не делать до решения |
|
||||
| OQ-40 | «Квартальная дата» в журнале для консолидации 2024 К1 показана как 01.01.2024 (`43-02`); по правилам банка 1 квартал 2024 = отчётная дата 01.04.2024 (`consolidation.reporting_date`). Что выводить? | макет [43-02] | срез 4, 7 | допущение: `reporting_date` по правилам (01.04.2024); макет неточен |
|
||||
| OQ-40 | «Квартальная дата» в журнале для консолидации 2024 К1 показана как 01.01.2024 (`43-02`); по правилам банка 1 квартал 2024 = отчётная дата 01.04.2024 (`consolidation.reporting_date`). Что выводить? | макет [43-02] | срез 4, 7 | **допущение принято и реализовано в Срезе 4 (06.10.2026):** `consolidation.reporting_date` по правилу банка (Q1 → 01.04.Y, Q2 → 01.07.Y, Q3 → 01.10.Y, Q4 → 01.01.Y+1), `domain/consolidation_periods.reporting_date`; макет неточен |
|
||||
| OQ-41 | Экран «ЮЛ вне ГК» (`61`, заголовок «Юридические лица») показывает все ЮЛ с заполненной колонкой ГК, а не только монозаёмщиков. Это сводный реестр ЮЛ или фильтр `company_group_id IS NULL`? | макет [61] | срез 1 | допущение: сводный реестр всех ЮЛ с фильтром «вне ГК», по умолчанию выключенным |
|
||||
| OQ-42 | «Проверка распознавания» для ОСВ (xlsx) в макете отсутствует (строки `doc-row--readonly`, `39`). Нужен ли отдельный экран проверки ОСВ (дерево строк, подсветка контрагентов) или проверка ОСВ только через сводную ОСВ (`30`)? | макет [39] | срез 2 (UI) | **закрыт решением владельца 05.10.2026:** экран проверки ОСВ **нужен** (дерево строк, суммы, нераспознанные контрагенты); в макете его нет — проектируется в подсрезе 2d «UI первичных документов» |
|
||||
| OQ-43 | Кнопка «К отчетности» неактивна у консолидаций в статусах «Инициация» и «Верификация РФ», активна у «Консолидация РФ» и «Верифицирован» (`34`). Каково правило доступности отчёта по статусу / роли? | макет [34] | срез 4, 7 (policy) | допущение: отчёт доступен с `CONSOLIDATION_RF` и далее для всех ролей с `CONSOLIDATION_VIEW`; «Инициация» — только карточка |
|
||||
@ -115,7 +115,7 @@
|
||||
| OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) |
|
||||
| OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Остаточные риски после R-23/R-25: (1) бомба по объёму самих ячеек в пределах лимита распакованного zip — xlsx в 5 МБ с 2 млн обычных ячеек читается openpyxl 13 с и занимает 1,5 ГБ (замер ревьюера); (2) объём `sharedStrings` и стилей в тех же пределах; (3) entity expansion (от экспоненты «billion laughs» защищает только сам expat ≥ 2.4); (4) **известный обход предсканирования (R-26):** лист в части без `.xml` с ≥ 64 байтами ведущих пробелов и без XML-декларации не попадает в скан (файл 1,8 КБ → 27 с / 1,4 ГБ в openpyxl) — первый follow-up: убрать «sniff» и отдавать expat каждую некаталожную запись; (5) скан сам не ограничен размером чанка: «мусорная» XML-часть с атрибутом в 400 МБ → 1,3 ГБ / 1,2 с, 400 МБ тегов → 8,5 с CPU; (6) xls с очень многими крупными merged-диапазонами (> 10 000 ячеек каждый) — линейный просмотр индекса `Sheet` (≈ 4,4 мс на обращение), останавливает только `PARSE_TIMEOUT_SECONDS`: потолок на число таких диапазонов. Предскан — эшелон защиты, не граница безопасности; настоящая граница — лимит памяти контейнера / подпроцесс с RLIMIT. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `<dimension>` и потоковый счётчик ячеек `<c>`/строк `sharedStrings` (тем же expat-сканом, что объединения) с лимитом; и/или разбор книги в подпроцессе с `RLIMIT_AS`/таймаутом, который можно убить. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23, R-25 | tabular-parser (readers), платформа (зависимости и лимиты образа/пода) | допущение принято: действуют лимиты R-23/R-25 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000 (потоковый expat-скан всех XML-частей до openpyxl); объём ячеек, `sharedStrings`/стили и entity expansion не ограничены (README «Известные ограничения») |
|
||||
| OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | **закрыт реализацией Среза 2c (06.10.2026, `arm-core/openspec/changes/slice-2c-osv-ingest`):** ядро ограничивает размер файла на загрузке (`UPLOAD_MAX_BYTES` = 50 МиБ, как `PARSE_MAX_BYTES` парсера), поэтому в воркер большие объекты из ядра не попадают; проверка по `stat` до скачивания в `tabular-parser` остаётся как есть (дефенсивная, не требуется) |
|
||||
| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | открыт: в 2c сделана только проверка `CONFIRMED`/`superseded_by`; без расширения удаление файла с документом в версии даст IntegrityError (500) |
|
||||
| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | **закрыт для Среза 4 (06.10.2026):** привязываются к версии только `CONFIRMED` документы незаменённых файлов, а файл с `CONFIRMED` документом удалить нельзя (`FILE_HAS_CONFIRMED_DOCUMENTS`) — удаление файла с документом в версии невозможно, расширять `DELETE /files/{id}` не нужно; автозамена снимает привязки документов заменённого файла (`FILE_REPLACE.unbound_version_ids`). Остаток: FK из `financial_statement_meta` и `ex_snapshot_item` — Срезы 3 и 7 |
|
||||
| OQ-111 | Ретеншн S3-объектов при удалении файла: сейчас `DELETE` лишь ставит `s3_object.expires_at = now()`, объекты `uploads`/`results` физически не стираются (дополняет OQ-22). Кто и когда их удаляет — lifecycle-правило бакета по метаданным, фоновая задача ядра, ручная очистка? Объект, записанный в S3 до сбоя БД, остаётся сиротой | arm-core Срез 2c: design D-10, №36 | платформа (lifecycle), Срез 2 → эксплуатация | допущение принято: бакет растёт; очистка — вне среза |
|
||||
| OQ-112 | Остаточные объекты e2e-прогона: каждый запуск `make e2e` оставляет в MinIO dev-стенда ~2 объекта (`uploads/…`, `results/…` — следствие OQ-111) | arm-core Срез 2c: Task 11 (O-5) | dev-стенд | допущение принято: чистить бакет `arm-ka` вручную (раздел «Сброс стенда» `dev/qa-guide.md`) |
|
||||
| OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | открыт: допущение — `account_id = NULL` легален (§1.3), `account_code`/`account_root` заполнены |
|
||||
@ -123,6 +123,18 @@
|
||||
| OQ-115 | Автозамена: если новый файл содержит лишь часть документов прежнего (прежний — ОСВ 60 и 51, новый — только 60), прежний уходит в `SUPERSEDED` **целиком**, документ 51 пропадает из списка файлов; в `FILE_REPLACE.description` о нём ничего нет. Подтвердить «замена файлом целиком» (OQ-06) для этого случая или заменять по документам | arm-core Срез 2c: Task 8 (T8.4) | Срез 2d (список файлов), Срез 4 (комплект документов) | допущение принято: замена файлом целиком, документы заменённого файла сохраняются в БД |
|
||||
| OQ-116 | ОСВ Q2–Q4, признанная нарастающей по данным Q1 (сальдо на начало корня равно сальдо Q1, §1.5), получает предупреждение маппинга и поэтому остаётся `NEEDS_CHECK`, а не `RECOGNIZED` (буквально D-7/D-9 спеки: `RECOGNIZED` только без предупреждений). Верно ли, что аналитик подтверждает такие документы вручную, или нарастающая по данным — штатный случай без предупреждения (OQ-02, OQ-05) | arm-core Срез 2c: Task 7 (R-17, design №30) | Срез 2d (нагрузка на аналитика), §9.3 | допущение принято: `NEEDS_CHECK`; снять предупреждение — одна правка в `result_mapping.py` и правило статуса |
|
||||
| OQ-117 | Индексы `osv_entry(parent_entry_id)`, `osv_entry(recognition_job_id)`, `stored_file(current_recognition_job_id)`: `DELETE /files/{id}` (и очистка файла при повторном маппинге) делает seq scan на проверке self-FK — на временной таблице 200 тыс. строк удаление документа в 1000 строк 8,6 с без индекса и 16 мс с индексом (замер в psql на временной таблице, final review); при ~3 млн строк DELETE ≈ 2 мин → HTTP-таймаут и долгое удержание блокировок. Требуется DBML v5.16 + миграция 0004. Решает владелец (источник истины — DBML) | arm-core Срез 2c: final review (I-1, R-24, design №51) | `DELETE /files/{id}`, повторный маппинг; подсрез 2d (UI удаления), прод | **закрыт в v5.15 (06.10.2026):** индексы добавлены в DBML v5.15 (п. 4 changelog) и миграцию `0003_dbml_v5_15` arm-core — `ix_osv_entry_parent_entry_id`, `ix_osv_entry_recognition_job_id`, `ix_stored_file_current_recognition_job_id`; v5.16 и миграция 0004 не понадобились (v5.15 ещё не слит) |
|
||||
| OQ-118 | Запрет загрузки и подтверждения документов после перехода версии в `DATA_CORRECTION` (ТЗ 03_9 A1: «после перехода к исправлению данных загрузка и подтверждение заблокированы»). В Срезе 4 загрузка и подтверждение блокировкой не ограничиваются и в `DATA_CORRECTION` разрешены | решение владельца 06.10.2026; arm-core Срез 4 design D-6 | Срез 7 (маршрут) | допущение принято: в Срезе 4 запрета нет (только статус «в работе»); запрет вводится вместе с маршрутом |
|
||||
| OQ-119 | Истечение блокировки «взять в работу»: DBML не задаёт срока, перехват — только вручную с причиной. Нужен ли авто-сброс брошенной блокировки (таймаут, «сессия умерла») | arm-core Срез 4 design D-7 | Срез 4, 7 (эксплуатация) | допущение принято: блокировка не истекает; освободить — `DELETE …/lock` владельцем или перехват |
|
||||
| OQ-120 | `document.period_check` для документов, входящих в версии консолидации: подходящесть документа версии — функция (документ, версия), а один документ может входить в несколько версий (разные периметры и кварталы), поэтому записывать её в `document.period_check` нельзя. Показывать ли «подходит версиям: …» на карточке файла/документа | arm-core Срез 4 design D-12 | карточка файла (подсрез 2d), §9.3 | допущение принято: `period_check` остаётся `UNKNOWN`; подходящесть видна в чек-листе версии |
|
||||
| OQ-121 | Порог размера сводной ОСВ, после которого нужен кэш процесса: свод строится целиком в памяти на каждый запрос (страница, сводка, экспорт); на фикстурах РБ (≈5,4 тыс. строк) 0,27 с / 8 запросов. При десятках тысяч строк может потребоваться кэш (DECISION-5 запрещает кэш в БД) | arm-core Срез 4 design D-1, Risks | эксплуатация, Срез 5 (шахматка поверх свода) | допущение принято: без кэша; замер на реальной группе — до Среза 5 |
|
||||
| OQ-122 | Ячейка чек-листа `PROCESSING` по всем недостающим столбцам ЮЛ, пока файл не смаплен: до маппинга счёт файла неизвестен, поэтому «в обработке» показывается у всех незакрытых правил ЮЛ. Устраивает ли такая неточность или нужна эвристика по имени файла | arm-core Срез 4 design D-13, №6 | чек-лист, экраны `34`–`37` | допущение принято: временный шум в чек-листе |
|
||||
| OQ-123 | Общие строки `osv_entry` у нескольких версий: документ может входить в несколько версий одной ГК, ручное сопоставление контрагента хранится на строке `osv_entry` (DECISION-5) и поэтому меняет её для всех версий. Допустимо ли, или сопоставление должно быть «по версии» | arm-core Срез 4 design Risks, D-10 | Срез 4, 7 (замороженные версии не меняются — они в `ex_snapshot`) | допущение принято: сопоставление общее; `before_value` аудита хранит прежнее значение |
|
||||
| OQ-124 | Q1-эквивалентность ОСВ (R-14, design №81): парсер помечает ОСВ за Q1 как нарастающую (период с начала года = Q1), поэтому документ ОСВ за `Q1` в Срезе 4 равнозначен и нарастающему, и квартальному и подходит версиям любого квартала года (в версиях Q2+ он — квартальный источник Q1). ББ/ОФР за Q1 версиям Q2–Q4 **не** подходят (R-14a: баланс на конец Q1). Подтвердить; если ББ/ОФР за Q1 тоже должны подхватываться — снять условие в `consolidation_periods.effective_cumulative` | arm-core Срез 4: Task 13, R-14/R-14a, design №81 | `fits` (привязка), `select_sources` (свод), чек-лист | допущение принято и реализовано: без него свод на реальных ОСВ РБ за 2024 пуст (нарастающий Q1 не привязывался к версии Q3) |
|
||||
| OQ-125 | Версия за Q1 с квартальным ОСВ Q1: spec говорит «квартальный Q1 равнозначен нарастающему», но по design №50 квартальный Q1 идёт путём склейки (дубли ключа суммируются, `None` → 0, `is_cumulative = false`), а нарастающий Q1 отдаётся строка в строку (`None` сохраняется, дубли остаются раздельными). Различие видно только на дублях ключа и пустых ячейках. Приводить ли обе формы к одной | arm-core Срез 4: Task 9 (T9.b), design №50 | `consolidated_osv.build`, версии за Q1 | допущение принято: два пути; расхождение — только на дубле ключа внутри квартала и `None` в ячейках |
|
||||
| OQ-126 | Область ручного сопоставления контрагента (R-11, design №62/№63): «строка версии» сужена до строк свода — `group_type = COUNTERPARTY`, `data_type` BU/SUM привязанных ОСВ-документов; строки `NU`/`ACCOUNT` дают 404 `NOT_FOUND` («строка ОСВ не найдена в версии») и в автоприменение не входят (§3 п. 2, R-3 Среза 2c: НУ не участвует). Цена: NU-строки того же контрагента остаются без `counterparty_id`. Буквальное чтение spec шире. Подтвердить | arm-core Срез 4: Task 11, R-11 | `counterparty_mapping`, автоприменение | допущение принято и реализовано |
|
||||
| OQ-127 | Рекомендация кандидата (`recommended`) — только точное совпадение `normalize_alias` имени строки с нормализованным именем/алиасом ЮЛ. На реальных данных «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» (так записан контрагент в ОСВ РБ) рекомендации не даёт: аналитик находит ЮЛ поиском по `q`. Нужна ли нечёткая рекомендация (токены, порядок слов, префикс «Юг-Финанс /») или достаточно ручного выбора и накопления алиасов | arm-core Срез 4: Task 13 (O-3), design D-11 | `GET …/counterparty-candidates` | допущение принято: рекомендация точная; после первого ручного сопоставления алиас закрепляет имя |
|
||||
| OQ-128 | Неоднозначность дорезолва на лету при тёзке по имени: после ручного сопоставления создаётся алиас, но `CounterpartyIndex.resolve` на шаге «нормализованное имя/алиас» возвращает контрагента только при единственном кандидате — если ЮЛ-тёзка (по нормализованному `name`) есть рядом с владельцем нового алиаса, кандидатов двое, резолв неоднозначен и строки без сохранённого `counterparty_id` остаются несопоставленными. ТЗ DOC-221278: «алиас однозначен» — решает ли это, или алиас должен иметь приоритет над именем | arm-core Срез 4: Task 11 (T11.9) | дорезолв свода (D-3), `CounterpartyIndex` | открыт: допущение — резолв по Срезу 2c (неоднозначность → `NULL`); спасает автоприменение `PROPAGATED` (сохраняет `counterparty_id`) |
|
||||
| OQ-129 | Формулировка сценария «сопоставление и автоприменение» (spec consolidated-osv): «`unmapped_total` падает на 1 + propagated» неверна на реальных данных. `propagated` считает записи `osv_entry` (включая пары-пробелы и строки, не дающие строк свода), а свод склеивает кварталы, поэтому `unmapped_total` падает на число **склеенных** строк свода (R-15, design №80). Уточнить текст spec при архивации | arm-core Срез 4: Task 13 (T13.M1, R-15) | spec `consolidated-osv` при архивации change | допущение принято: тест сверяет обе величины отдельно; текст spec уточнён в change, `openspec/specs/` — при архивации |
|
||||
|
||||
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
|
||||
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.
|
||||
|
||||
@ -170,13 +170,41 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14,
|
||||
|
||||
## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ
|
||||
|
||||
- `consolidation` / `consolidation_version` / `consolidation_member` / `consolidation_document`; статусы INITIATED →
|
||||
DOCUMENTS_PROCESSING → DATA_CORRECTION; `consolidation_lock`.
|
||||
- Приведение квартальных ОСВ к нарастающим (§2) как вычисляемое представление.
|
||||
- Сводная ОСВ (§3) как запрос; признак контрагента; ручное сопоставление с алиасами и автоприменением.
|
||||
- UI: список консолидаций, карточка, состав, сводная ОСВ с фильтрами и экспортом xlsx.
|
||||
- Тест: сводная ОСВ группы РБ за 9 мес. 2024: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного
|
||||
сопоставления.
|
||||
**Статус: реализован в arm-core (бэкенд + API + экспорт xlsx), октябрь 2026; UI — отдельный исполнитель.** Объём по решению
|
||||
владельца 06.10.2026: бэкенд, API, экспорт xlsx. Issue arm-core#18, docs#19; спецификация —
|
||||
`arm-core/openspec/changes/slice-4-consolidation/` (design D-1…D-16, «Decisions made without discussion» №1–№81+),
|
||||
документный change — `docs/openspec/changes/slice-4-consolidation/`.
|
||||
|
||||
**Сделано (по факту):**
|
||||
- DBML v5.16: `osv_entry.counterparty_match_kind` + enum `counterparty_match_kind` (миграция `0004_dbml_v5_16`, backfill).
|
||||
- Версии консолидации (создание, список по ГК, карточка), периметр (`PUT …/members`), блокировка «взять в работу»
|
||||
(перехват с причиной, без истечения), статусы: автоматические по событиям документов (INITIATED → DOCUMENTS_PROCESSING →
|
||||
DATA_CORRECTION) и ручные (`CANCELLED`, возврат в `INITIATED`); остальные — Срез 7.
|
||||
- Явная привязка подтверждённых документов к версии (`consolidation_document`): подтверждение, подхват при создании версии и
|
||||
добавлении ЮЛ, отвязка при автозамене файла и удалении ЮЛ; Q1-эквивалентность ОСВ (R-14, OQ-124).
|
||||
- **Чек-лист** комплекта документов (перенесён из Среза 3: `required_document_rule × периметр`, статусы ячеек) — API; CSV не делали.
|
||||
- Сводная ОСВ как чистая функция от версии: выбор источников и склейка кварталов (§2), пробелы, дорезолв контрагента, признак
|
||||
`GSK/GK/EXTERNAL`, способ сопоставления; страница, сводка, фильтры.
|
||||
- Кандидаты и ручное сопоставление контрагента с алиасом, переносом алиаса и автоприменением (`PROPAGATED`).
|
||||
- Экспорт xlsx (лист «Сводная ОСВ» и лист «Пробелы»).
|
||||
- Сквозной тест на фикстурах РБ: ОСВ 60/62 РБ и 60/62/76 Счастливочной за Q1–Q3 2024 → версия Q3 → свод 1385 строк, 22
|
||||
документа привязано; пары с одним лишь Q3 (РБ: 01, 58.03, 66, 67, 76; Счастливочная: 01, 02) — в `gaps` с пропуском Q1–Q2;
|
||||
одно ручное сопоставление «Пекарня Счастливочная ООО» → ГСК.
|
||||
- Контракт API для фронта — `dev/api-contract-slice-4.md`; QA — `dev/qa-guide.md` §7.
|
||||
|
||||
**Закрыты OQ:** 03, 19, 40 (допущение реализовано), 110 (для Среза 4). **Открыты новые:** OQ-118…OQ-129 (запрет загрузки после
|
||||
DATA_CORRECTION, истечение блокировки, `period_check`, порог кэша свода, `PROCESSING` в чек-листе, общие строки `osv_entry`
|
||||
у версий, Q1-эквивалентность, склейка Q1 версии Q1, область ручного сопоставления, рекомендация кандидата, неоднозначность
|
||||
дорезолва, формулировка сценария «1 + propagated»).
|
||||
|
||||
**Остаётся (UI и последующие срезы):** экраны `34`–`37`, `42`, `30`, `32-*`, `31-00` (другой разработчик); CSV чек-листа;
|
||||
«Добавить версию», `CONFIRM`, `EXTENDED`, гудвилл, маршрут согласования (Срезы 6–7); ББ/ОФР в своде и чек-лист ББ/ОФР по
|
||||
реальным документам — Срез 3; шахматка и ВГО — Срез 5.
|
||||
|
||||
**Исходный план среза (для справки):** `consolidation` / `consolidation_version` / `consolidation_member` /
|
||||
`consolidation_document`; приведение квартальных ОСВ (§2); сводная ОСВ (§3) как запрос; признак контрагента; ручное
|
||||
сопоставление с алиасами и автоприменением; UI со списком, карточкой, составом, сводной ОСВ с фильтрами и экспортом;
|
||||
тест: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного сопоставления.
|
||||
|
||||
## Срез 5 — ВГО: автоматическое сопоставление, шахматка, корректировки
|
||||
|
||||
|
||||
@ -72,12 +72,12 @@
|
||||
|
||||
| Шаг | Экран | Действие | БД / статус |
|
||||
|---|---|---|---|
|
||||
| 2.1 | Сайдбар → **Новая консолидация** [42] | ГК → ГСК (активна после ГК) → Год → Квартал → **Создать**. `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false` по умолчанию (OQ-27) | `consolidation`, `consolidation_version(INITIATED)`, `consolidation_member` из текущего состава ГСК |
|
||||
| 2.2 | Отчётность → Группы компаний [03] → **Консолидации →** [34] | [03]: таблица ГК (Группа компаний, CRM ID, Основной РФ, Организаций), поиск по ГК, CRM ID или РФ. [34]: таблица консолидаций ГК: Год, Квартал, Версия (дерево версий под периодом), Статус (бейдж), ГСК, Дата создания, Дата верификации, Организаций; кнопки в строке **Взять в работу**, **К отчетности** (неактивна у статусов «Инициация» и «Верификация РФ» — по снимку, OQ-43), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип) | `consolidation_version`, `consolidation_lock`, `consolidation_member`, `document` |
|
||||
| 2.3 | [34] → **Взять в работу** [35] | «Консолидация … будет закреплена за вами для редактирования» | `consolidation_lock`, `audit_event(CONSOLIDATION_LOCK)` |
|
||||
| 2.4 | [34] панель Организации → **Редактировать состав** [36] | фильтр по ГСК, «Показать все ЮЛ в ГК», чекбоксы, Сохранить | `consolidation_member` |
|
||||
| 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании | `stored_file(UPLOADED)`, `s3_object`, `recognition_job(queued)`, `document.period_check/counterparty_check` → `DOCUMENTS_PROCESSING` |
|
||||
| 2.6 | [34] → **Скачать чек-лист** [37] | матрица Организация × (Бух. баланс, ОФР, ОСВ 01 58 60 62 66 67 76 90, Прочие); легенда: Загружен / Обработка / Требуется проверка / Ошибка загрузки / Отсутствует; Скачать CSV | `required_document_rule × consolidation_member − document(CONFIRMED)` |
|
||||
| 2.1 | Сайдбар → **Новая консолидация** [42] | ГК → ГСК (активна после ГК) → Год → Квартал → **Создать**. `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false` по умолчанию (OQ-27) **API (Срез 4):** `POST /consolidations` (`{company_group_id, related_group_id, year, quarter}`; ≥ 2 члена ГСК), ГСК для выбора — справочники | `consolidation`, `consolidation_version(INITIATED)`, `consolidation_member` из текущего состава ГСК |
|
||||
| 2.2 | Отчётность → Группы компаний [03] → **Консолидации →** [34] | [03]: таблица ГК (Группа компаний, CRM ID, Основной РФ, Организаций), поиск по ГК, CRM ID или РФ. [34]: таблица консолидаций ГК: Год, Квартал, Версия (дерево версий под периодом), Статус (бейдж), ГСК, Дата создания, Дата верификации, Организаций; кнопки в строке **Взять в работу**, **К отчетности** (неактивна у статусов «Инициация» и «Верификация РФ» — по снимку, OQ-43), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип) **API:** `GET /company-groups/{id}/consolidations?in_work_only=` (периоды и версии, блокировка), `GET /consolidations/versions/{id}` (карточка: члены с ячейками чек-листа, история) | `consolidation_version`, `consolidation_lock`, `consolidation_member`, `document` |
|
||||
| 2.3 | [34] → **Взять в работу** [35] | «Консолидация … будет закреплена за вами для редактирования» **API:** `POST …/versions/{id}/lock` (перехват — `takeover_reason`), `DELETE …/lock`; ручной статус — `POST …/status` | `consolidation_lock`, `audit_event(CONSOLIDATION_LOCK)` |
|
||||
| 2.4 | [34] панель Организации → **Редактировать состав** [36] | фильтр по ГСК, «Показать все ЮЛ в ГК», чекбоксы, Сохранить **API:** `PUT …/versions/{id}/members` (`{legal_entity_ids}`), нужны статус «в работе» и своя блокировка | `consolidation_member` |
|
||||
| 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании **API:** загрузка — `POST /legal-entities/{id}/files?consolidation_version_id=`; документы и подтверждение — `/files`, `/documents` (Срез 2c); подтверждённый документ привязывается к версиям «в работе» автоматически | `stored_file(UPLOADED)`, `s3_object`, `recognition_job(queued)`, `document.period_check/counterparty_check` → `DOCUMENTS_PROCESSING` |
|
||||
| 2.6 | [34] → **Скачать чек-лист** [37] | матрица Организация × (Бух. баланс, ОФР, ОСВ 01 58 60 62 66 67 76 90, Прочие); легенда: Загружен / Обработка / Требуется проверка / Ошибка загрузки / Отсутствует; Скачать CSV **API:** `GET …/versions/{id}/checklist` (`rules[]`, `rows[].cells[]`: `status`, `missing_quarters`, `document_ids`); CSV в Срезе 4 нет | `required_document_rule × consolidation_member − document(CONFIRMED)` |
|
||||
| 2.7 | drawer [39] → клик по **PDF**-документу → **Проверка распознавания** [41] | слева «Исходный документ» постранично с масштабом; справа «Результат распознавания»: шапка формы (организация, ИНН, ОКПО/ОКОПФ/ОКФС, адрес, аудит, ОКУД, отчётная дата, ОКЕИ, единица измерения) и таблица Пояснения / Наименование показателя / Код / колонки-даты (в таблице есть строки без кода «в том числе …» и строка «Гудвилл 1105» ФСБУ 4/2023 — OQ-36, OQ-37); выпадающий список **Документ** для перехода между документами ЮЛ; **Добавить комментарий**; **Редактировать** [41-02] → кнопки **Сохранить** / **Отменить** (значения таблицы редактируются на месте); **Подтвердить данные**. Блок логических ошибок — по ТЗ (в снимках не виден). Для ОСВ (xlsx) экран в макете не открывается — строки readonly, OQ-42 | `financial_statement_meta/column/line/value.is_modified`, `osv_entry.is_modified`, `document(CONFIRMED)`, `comment`, `audit_event(RECOGNIZED_DATA_EDIT, DOCUMENT_CONFIRM)` → `DATA_CORRECTION` |
|
||||
|
||||
Монозаёмщики [60] [84]: тот же drawer документов без консолидации — первичка в ЕХ напрямую.
|
||||
@ -90,7 +90,7 @@
|
||||
|
||||
| Шаг | Вкладка | Действие | БД |
|
||||
|---|---|---|---|
|
||||
| 3.1 | **ОСВ** [30] | сводная ОСВ: Компания ГК, Счёт, Период, Контрагенты, ИНН, 6 сумм, Тип контрагента. Баннер «Требуется указать соответствия… Осталось: N» → **Показать ЮЛ без ИНН** [32-osv-no-inn] → **Указать** → модалка «Выбор организации для связи с …» [32-osv-map-counterparty]: ИНН, Организация, ГК, ГСК, ГСЗ, блок «Рекомендуемые», **Связать**. При уходе с вкладки с несопоставленными — «Остались неуказанные ИНН… Остаться / Перейти» [31-00] | вычисляется; `osv_entry.counterparty_id`, `legal_entity_alias`, `audit_event(OSV_COUNTERPARTY_MAP)` |
|
||||
| 3.1 | **ОСВ** [30] | сводная ОСВ: Компания ГК, Счёт, Период, Контрагенты, ИНН, 6 сумм, Тип контрагента. Баннер «Требуется указать соответствия… Осталось: N» → **Показать ЮЛ без ИНН** [32-osv-no-inn] → **Указать** → модалка «Выбор организации для связи с …» [32-osv-map-counterparty]: ИНН, Организация, ГК, ГСК, ГСЗ, блок «Рекомендуемые», **Связать**. При уходе с вкладки с несопоставленными — «Остались неуказанные ИНН… Остаться / Перейти» [31-00] **API:** `GET …/versions/{id}/osv` (фильтры, `limit` ≤ 2000), `…/osv/summary` (баннер «Осталось: N» — `unmapped_total`, пробелы — `gaps[]`), `…/counterparty-candidates?scope=&q=&entry_id=` (блок «Рекомендуемые» — `recommended`), `POST …/osv/entries/{entry_id}/counterparty` (**Связать**), `GET …/osv/export` (**Экспорт**, xlsx) | вычисляется; `osv_entry.counterparty_id`, `legal_entity_alias`, `audit_event(OSV_COUNTERPARTY_MAP)` |
|
||||
| 3.2 | **Шахматка** [31-01] | фильтры Год, Период, Компании, Дт-счета, Кт-счета; матрица компания × компания: сумма A / сумма B / (расхождение) красным; Итого; ниже результирующая таблица «Статья × компания» | вычисляется (§5–6 правил) |
|
||||
| 3.3 | **Таблица корректировки** [31-02] [31-02-02] | фильтр «Отклонение» (Все); пары «Актив баланса ЮЛ-1 / Пассив баланса ЮЛ-2»: Наименование, Строка баланса, Счёт, Сумма; **Контроль**: % отклонения, Сумма отклонения; **Корректировка**: Строка баланса / Дт, Строка баланса / Кт, Комментарий; строка пары раскрывается в детализацию по субсчетам; Итого. В макете % отклонения считается к сумме стороны ЮЛ-1, а не к меньшей (OQ-35). **Редактировать** [31-02-01] → баннер «Режим редактирования включён…», ячейки пары и корректировки становятся полями ввода, **Отменить изменения** / **Сохранить** | `correction` (+ дочерние строки `parent_correction_id`), `audit_event(CORRECTION_*)`; ≥ 5 % → `ADJUSTMENT` |
|
||||
| 3.4 | **Гудвилл** [31-03] | здесь же включается/выключается расчёт гудвилла для консолидации (`goodwill_enabled`; переключателя в снимках нет — решение OQ-27); фильтр «Гудвилл: Все значения»; по головной организации (с «Выделить все» и строкой «Всего»): Учёт в отчёте ☐, Наименование дочерней организации, Доля владения, Фондовые вложения головной организации, Доля участия головной организации в уставном капитале дочерней организации, Уставный капитал, Гудвилл (+/−), Комментарий; **Редактировать** [31-03-01] → «Доступны для редактирования столбцы „Доля владения“ и „Комментарий“» | `consolidation_version.goodwill_enabled`, `goodwill(ownership_share, include_in_kfo, comment)` |
|
||||
|
||||
148
dev/api-contract-slice-4.md
Normal file
148
dev/api-contract-slice-4.md
Normal file
@ -0,0 +1,148 @@
|
||||
# Контракт API Среза 4 (консолидация) для разработчика фронта
|
||||
|
||||
Источник — реальная схема `GET /openapi.json` arm-core (ветка `feat/18-slice-4-consolidation`, Swagger — `/docs`); коды ошибок и
|
||||
нюансы сверены с кодом и спекой `arm-core/openspec/changes/slice-4-consolidation/` (design D-1…D-16, решения №1–№81+).
|
||||
Правила предметной области — `context/05-domain-rules.md` §2, §3, §9.1, §10; экраны — `context/10-user-journey.md`
|
||||
(пути 2 и 3), номера в `[…]` — снимки макета (`mockup/INDEX.md`). Здесь — только то, что нужно, чтобы писать клиент.
|
||||
|
||||
## 1. Общее
|
||||
|
||||
- Префикс `/api/v1`, заголовок `Authorization: Bearer <JWT>` (в dev — `POST /dev/token`). Права: `CONSOLIDATION_VIEW` (чтение),
|
||||
`CONSOLIDATION_EDIT` (правки); загрузка файлов — `DOCUMENT_EDIT`. Нет права — 403 строкой
|
||||
`{"detail": "недостаточно прав: требуется <КОД>"}`; нет/плохой токен — 401. Скрывать кнопки по праву можно по `GET /me`.
|
||||
- Доменные ошибки: `{"detail": {"code": "...", "message": "на русском", ...доп. поля}}`; по `code` работает клиент. 422 валидации
|
||||
тела/параметров — массив pydantic (`detail: [{loc, msg, type}]`). Каталог кодов — `arm-core/README.md`, «Ошибки API».
|
||||
- Видимость: ГК и версии вне области видимости пользователя (`ka_rf`/`rka`/`client_manager` — только свой филиал) неотличимы от
|
||||
несуществующих: **404 с кодом `NOT_FOUND`**. Различие — только по тексту `message` (см. §4); клиент не должен ветвиться по тексту.
|
||||
- **Деньги — строки JSON с двумя знаками** (`"100.00"`, `"0.00"`), могут быть `null` (нарастающий документ отдаётся «как в файле»).
|
||||
Не парсить во `float`: суммы `numeric(18,2)`.
|
||||
- Идентификаторы — uuid-строки; даты — ISO; `quarter` — `Q1`…`Q4`.
|
||||
- Страницы — `{items, total, limit, offset}`; `limit ≤ 2000` (по умолчанию 500), `offset ≥ 0`; больше 2000 — 422.
|
||||
- Перечисления: `ConsolidationStatus` — `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`, `CONSOLIDATION_RF`, `ADJUSTMENT`,
|
||||
`VERIFICATION_RF`, `CONSOLIDATION_GO`, `VERIFICATION_GO`, `VERIFIED`, `CANCELLED`, `REJECTED`, `ARCHIVED` (в Срезе 4 достижимы
|
||||
первые три и `CANCELLED`); `CounterpartyType` — `GSK`, `GK`, `EXTERNAL`; `CounterpartyMatchKind` — `INN`, `NAME`, `ALIAS`,
|
||||
`MANUAL`, `PROPAGATED`; `CellStatus` — `CONFIRMED`, `NEEDS_CHECK`, `RECOGNIZED`, `PROCESSING`, `ERROR`, `MISSING`;
|
||||
`ConsolidationScenario` — `CREATE`, `CONFIRM`; `ConsolidationApproach` — `BASIC`, `EXTENDED`.
|
||||
- «В работе» — `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION` (`VersionCard.is_in_work`).
|
||||
|
||||
## 2. Эндпоинты
|
||||
|
||||
Порядок проверок на каждом маршруте: право (403) → видимость (404) → для правок статус «в работе» и своя блокировка (409) → операция.
|
||||
В таблице «Ошибки» — доменные коды и HTTP; 401/403 везде и ниже не повторяются.
|
||||
|
||||
| Метод и путь | Право | Запрос | Ответ | Ошибки | Экран |
|
||||
|---|---|---|---|---|---|
|
||||
| `POST /consolidations` | EDIT | `ConsolidationCreate {company_group_id, related_group_id, year (2000–2100), quarter}` | 201 `VersionCard` (версия №1, `INITIATED`, `lock = null`) | 404 `NOT_FOUND` (ГК/ГСК не видна), 409 `CONSOLIDATION_EXISTS`, 422 `RELATED_GROUP_MISMATCH`, 422 `PERIMETER_TOO_SMALL` | [42] |
|
||||
| `GET /company-groups/{group_id}/consolidations` | VIEW | query `in_work_only` (bool, по умолчанию `false`) | `ConsolidationPeriodOut[]` (период + `versions[]` — `VersionBrief`) | 404 `NOT_FOUND` | [34] |
|
||||
| `GET /consolidations/versions/{version_id}` | VIEW | — | `VersionCard` | 404 `NOT_FOUND` | [34] панель «Организации», [36], [37] |
|
||||
| `POST /consolidations/versions/{version_id}/lock` | EDIT | тело необязательно: `LockIn {takeover_reason?}` (≤ 1000) | 201 `LockOut` (новая блокировка или перехват) / 200 `LockOut` (уже ваша — идемпотентно) | 404 `NOT_FOUND`, 409 `VERSION_NOT_EDITABLE` (версия `CANCELLED`), 409 `LOCKED_BY_OTHER` | [35] |
|
||||
| `DELETE /consolidations/versions/{version_id}/lock` | EDIT | — | 204 (в т. ч. если активной блокировки нет) | 404 `NOT_FOUND`, 409 `NOT_LOCK_OWNER` | [34] |
|
||||
| `POST /consolidations/versions/{version_id}/status` | EDIT | `StatusIn {to_status, comment?}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER` / `TRANSITION_NOT_ALLOWED`, 422 `COMMENT_REQUIRED` | [33-01], меню «…» [34] |
|
||||
| `PUT /consolidations/versions/{version_id}/members` | EDIT | `MembersIn {legal_entity_ids[]}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`), 422 `PERIMETER_TOO_SMALL` | [36] |
|
||||
| `GET /consolidations/versions/{version_id}/checklist` | VIEW | — | `ChecklistOut {rules[], rows[]}` | 404 | [37] |
|
||||
| `GET /consolidations/versions/{version_id}/osv` | VIEW | query: `limit`, `offset`, `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped` (bool), `match_kind` (повторяемый), `q` | 200 `Page[ConsolidatedRowOut]` | 404 | [30] |
|
||||
| `GET /consolidations/versions/{version_id}/osv/summary` | VIEW | — | `OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}` | 404 | [30], баннер «Осталось: N», [31-00] |
|
||||
| `GET /consolidations/versions/{version_id}/osv/export` | VIEW | те же фильтры, что у `…/osv`, без `limit`/`offset` | файл xlsx (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) | 404 | «Экспорт» [30] |
|
||||
| `GET /consolidations/versions/{version_id}/counterparty-candidates` | VIEW | query: `q` (≤ 500), `scope` (`group` по умолчанию / `all`), `entry_id` | `CandidateOut[]` (до 100) | 404, 422 `QUERY_TOO_SHORT` (`scope=all` и `q` короче 3 символов после обрезки) | [32-osv-map-counterparty] |
|
||||
| `POST /consolidations/versions/{version_id}/osv/entries/{entry_id}/counterparty` | EDIT | `MapCounterpartyIn {legal_entity_id, move_alias?=false}` | 200 `MapCounterpartyOut` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 409 `ALIAS_TAKEN` (`owners[] {legal_entity_id, name}`) | [32-osv-map-counterparty] «Связать» |
|
||||
| `POST /legal-entities/{legal_entity_id}/files?consolidation_version_id=` | `DOCUMENT_EDIT` | multipart `files`; параметр версии необязателен | как в Срезе 2c: 201/422 `{accepted[], rejected[]}` | 404 (ЮЛ или версия), 409 `VERSION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER` | [39] / [40] |
|
||||
|
||||
Остальные маршруты, влияющие на версию: `POST /documents/{id}/confirm` (Срез 2c) привязывает подтверждённый документ ко всем
|
||||
версиям «в работе» своей ГК и может перевести версию в `DATA_CORRECTION`; `DELETE /files/{id}` и автозамена — снимают привязки
|
||||
(см. `context/05-domain-rules.md` §9.5).
|
||||
|
||||
## 3. Схемы
|
||||
|
||||
`Ref {id, name}`. Поля без `?` присутствуют всегда.
|
||||
|
||||
**`ConsolidationPeriodOut`** `{consolidation_id, year, quarter, reporting_date, versions[]}`.
|
||||
**`VersionBrief`** `{id, version_no, status, scenario, approach, lock (LockOut | null), created_at, members_count}`.
|
||||
**`LockOut`** `{locked_by, locked_by_name, locked_at, taken_over_from (uuid | null), takeover_reason (string | null)}`.
|
||||
**`VersionCard`** = `VersionBrief` + `{consolidation_id, year, quarter, reporting_date, company_group (Ref), initiator (Ref),
|
||||
initiator_office (Ref), goodwill_enabled, is_in_work, previous_version_id, members[], history[]}`:
|
||||
- `members[]` — `{id, name, inn, is_head_entity, checklist[]}`, где `checklist[]` — ячейки по правилам комплекта (тот же порядок,
|
||||
что `ChecklistOut.rules`);
|
||||
- `history[]` — `{from_status (null у первой записи), to_status, changed_by, changed_at, comment}`.
|
||||
|
||||
**`ChecklistOut`** `{rules[], rows[]}`; `rules[]` — `{document_type (BALANCE/INCOME/OSV…), account (string | null — счёт ОСВ),
|
||||
is_mandatory}` в порядке: ББ, ОФР, ОСВ по коду счёта (натурально: `58 < 58.03 < 60`); `rows[]` — `{legal_entity_id,
|
||||
legal_entity_name, legal_entity_inn, cells[]}`, порядок `cells` = порядок `rules`. **`ChecklistCellOut`** `{status (CellStatus),
|
||||
missing_quarters[], document_ids[]}`:
|
||||
`CONFIRMED` — пара полностью покрыта привязанными подтверждёнными документами; `NEEDS_CHECK` / `RECOGNIZED` — есть
|
||||
неподтверждённый подходящий документ; `PROCESSING` — у ЮЛ есть файл в обработке (по **всем** недостающим столбцам ЮЛ, пока файл
|
||||
не смаплен — это известная неточность, OQ-122); `ERROR` — последний файл ЮЛ в `ERROR`; `MISSING` — нет ничего.
|
||||
`missing_quarters` и `document_ids` — при частичном покрытии квартальными ОСВ (`document_ids` — только привязанные документы).
|
||||
ОСВ по субсчёту (58.03) закрывает только своё правило, не «ОСВ 58».
|
||||
|
||||
**`ConsolidatedRowOut`** — строка свода:
|
||||
`{entry_ids[], legal_entity_id, legal_entity_name, legal_entity_inn, account_code, account_root, period ("<год> Q<n>"),
|
||||
is_cumulative, row_number, counterparty_name, counterparty_inn, counterparty_id, counterparty_entity_name, counterparty_type,
|
||||
match_kind, match_stored, opening_debit, opening_credit, turnover_debit, turnover_credit, closing_debit, closing_credit, warnings[]}`.
|
||||
`counterparty_name/inn` — как в файле ОСВ; `counterparty_id`/`counterparty_entity_name` — сопоставленное ЮЛ справочника или `null`;
|
||||
`match_stored = true` — сопоставление хранится в БД, `false` — вычислено на лету (в БД не пишется); `match_kind` — способ
|
||||
(`null`, если ЮЛ не найдено). Для склеенных строк `entry_ids` — записи по кварталам (порядок — от раннего к позднему).
|
||||
**`OsvSummaryOut`** `{rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}`; **`gaps[]`** —
|
||||
`{legal_entity_id, legal_entity_name, account_code, missing_quarters[]}` (пара ЮЛ × счёт, у которой нет ОСВ за какие-то кварталы;
|
||||
строк такой пары в своде нет). **`CandidateOut`** `{id, inn, name, company_group (Ref | null), gsk (Ref | null), gsz (Ref | null),
|
||||
counterparty_type, recommended}`. **`MapCounterpartyOut`** `{row (ConsolidatedRowOut | null), propagated, alias_created,
|
||||
alias_moved_from (uuid | null)}`.
|
||||
|
||||
## 4. Нюансы, которые легко пропустить
|
||||
|
||||
1. **Блокировка — не «мягкая».** Любая правка версии (`PUT …/members`, `POST …/status`, ручное сопоставление) требует статуса «в
|
||||
работе» **и** вашей активной блокировки; нет блокировки — 409 `LOCK_REQUIRED`, чужая — 409 `LOCKED_BY_OTHER`. Загрузка файлов и
|
||||
подтверждение документов блокировкой не ограничиваются. Блокировка **не истекает по времени**; `CANCELLED` её снимает.
|
||||
2. **`LOCKED_BY_OTHER` приходит в двух формах.** Из `POST …/lock` — с `locked_by`, `locked_by_name`, `locked_at`. Из правок
|
||||
(`PUT …/members`, `POST …/status`, сопоставление, т. е. из `ensure_version_editable`) — только `locked_by`, `locked_at`, **без
|
||||
`locked_by_name`** (доменная функция имени не знает; имя — из `lock.locked_by_name` карточки версии). `NOT_LOCK_OWNER` отдаёт
|
||||
`locked_by` и `locked_at` владельца.
|
||||
3. **Перехват.** `POST …/lock` с непустым `takeover_reason` при чужой блокировке снимает её и создаёт вашу (201,
|
||||
`taken_over_from`, `takeover_reason`). Пустая причина или из одних пробелов считается отсутствующей (→ 409 `LOCKED_BY_OTHER`);
|
||||
при свободной версии причина игнорируется. Повторный `POST` по своей блокировке — 200, а не 201.
|
||||
4. **404 — всегда код `NOT_FOUND`**; различие по тексту `message`: «версия консолидации не найдена», «ГК не найдена»,
|
||||
«ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено». Отдельных кодов `CONSOLIDATION_NOT_FOUND` /
|
||||
`VERSION_NOT_FOUND` / `ENTRY_NOT_FOUND` **нет** (design №17, №62) — это обозначения в спеке. ГСК из чужой ГК или с видом `gsz` —
|
||||
422 `RELATED_GROUP_MISMATCH`, а невидимая/несуществующая ГСК — 404.
|
||||
5. **`MapCounterpartyOut.row` может быть `null`:** строка привязанного документа, которая в свод не попадает (пара с пробелом,
|
||||
квартальный документ при нарастающем), сопоставляется, но строки свода для ответа нет (design №69). Если `row == null`, перечитайте
|
||||
`GET …/osv` и `…/summary`; обязательно проверяйте на `null`.
|
||||
6. **`match_kind` склеенной строки — от последнего квартала** (D-3, design №8). Строка свода из нескольких кварталов несёт
|
||||
`entry_ids[]`; ручное сопоставление пишется на записи, чью `entry_id` вы отправили. Чтобы в ответе увидеть `MANUAL`, отправляйте
|
||||
**последнюю** `entry_id` строки (`entry_ids[entry_ids.length - 1]`); для записи более раннего квартала в строке будет `PROPAGATED`
|
||||
(в БД при этом одна запись `MANUAL`). Если в записях разных кварталов разные `counterparty_id`, расхождение — в `warnings`.
|
||||
7. **Ручное сопоставление — только строки свода** (R-11, design №62/№63): `group_type = COUNTERPARTY`, `data_type` БУ/СУММ привязанного
|
||||
ОСВ-документа версии. Строки `NU`/`ACCOUNT`, чужие и несуществующие — 404 `NOT_FOUND`. Автоприменение (`PROPAGATED`) затрагивает
|
||||
строки с тем же именем и пустым `counterparty_id` во **всех** ОСВ версии; `propagated` — число записей `osv_entry`, а не строк
|
||||
свода: `unmapped_total` падает на число **склеенных** строк (R-15).
|
||||
8. **`ALIAS_TAKEN`** (`owners[]`): алиас закреплён за другим ЮЛ; повторите с `move_alias: true` (право `DIRECTORY_DELETE` не нужно),
|
||||
в ответе `alias_moved_from` — прежний владелец.
|
||||
9. **Экспорт.** Заголовок `Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<Qn>_v<n>.xlsx`
|
||||
(имя percent-кодировано целиком). Забирайте файл как blob с `Authorization` (прямая ссылка `<a href>` не подойдёт). Фильтры — те
|
||||
же, что у `GET …/osv`, пагинации нет, порядок строк тот же. **Лист «Пробелы» строится по своду целиком и от фильтров не зависит**
|
||||
(при `unmapped=true` без совпадений лист «Сводная ОСВ» — только заголовок, «Пробелы» заполнен). Суммы — числовые ячейки
|
||||
`#,##0.00`, текст не вычисляется как формула.
|
||||
10. **`warnings` — не контракт.** Тексты диагностики строки (расхождение `counterparty_id` между кварталами и т. п.) могут меняться;
|
||||
показывать как подсказку, не разбирать.
|
||||
11. **Q1.** ОСВ за `Q1` (парсер помечает её нарастающей) подходит версии любого квартала года, в версиях Q2+ служит квартальным
|
||||
источником Q1; ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14/R-14a, OQ-124). В `ConsolidatedRowOut.is_cumulative` —
|
||||
признак строки свода: `true` только для строк нарастающего документа за квартал версии.
|
||||
12. **Чтение не требует блокировки** и доступно для версии любого статуса, включая `CANCELLED`.
|
||||
13. **Все деньги — строки с двумя знаками**; склеенные квартальные строки не содержат `null` (пустое = `"0.00"`), строки нарастающего
|
||||
документа — как в файле (могут быть `null`).
|
||||
14. **`period_check` документа остаётся `UNKNOWN`**: подходящесть документа версии — свойство пары (документ, версия), показывайте её
|
||||
через чек-лист версии, а не через поле документа (OQ-120).
|
||||
15. **Создание не ставит блокировку.** После `POST /consolidations` версия свободна; «Взять в работу» — отдельное действие [35].
|
||||
ГСК в запросе не сохраняется (`related_group_id` в версии пока пуст); члены берутся из текущего состава ГСК на момент создания.
|
||||
|
||||
## 5. Сквозные сценарии экранов
|
||||
|
||||
| Экран | Вызовы |
|
||||
|---|---|
|
||||
| [42] Новая консолидация | справочники (ГК → её ГСК) → `POST /consolidations` |
|
||||
| [34] Консолидации ГК | `GET /company-groups/{id}/consolidations` (+ `in_work_only`), клик по строке — `GET …/versions/{id}`; «Взять в работу» — `POST …/lock`; меню «…» → «Отмена КФО» — `POST …/status {CANCELLED, comment}` |
|
||||
| [36] Состав | `GET …/versions/{id}` (члены), все ЮЛ ГК — справочники, «Сохранить» — `PUT …/members` |
|
||||
| [39]/[40] Документы организации | `GET /legal-entities/{id}/files`, `POST /legal-entities/{id}/files?consolidation_version_id=` |
|
||||
| [37] Чек-лист | `GET …/checklist` (CSV в Срезе 4 нет) |
|
||||
| [30] ОСВ | `GET …/osv` с фильтрами, `GET …/osv/summary`, «Экспорт» — `GET …/osv/export` |
|
||||
| [32-osv-no-inn], [32-osv-map-counterparty] | `GET …/osv?unmapped=true`, `GET …/counterparty-candidates`, «Связать» — `POST …/osv/entries/{entry_id}/counterparty` |
|
||||
| [31-00] Предупреждение при уходе с вкладки | `GET …/osv/summary` (`unmapped_total`) |
|
||||
@ -1,4 +1,4 @@
|
||||
# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c)
|
||||
# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 4)
|
||||
|
||||
Документ для ручного тестирования текущего `main`. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34,
|
||||
Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов.
|
||||
@ -18,11 +18,14 @@ Node 24, uv 0.10): от пустого каталога до экрана адм
|
||||
- Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор
|
||||
результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление
|
||||
файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
|
||||
- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых
|
||||
документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7). UI консолидации — другой
|
||||
исполнитель.
|
||||
|
||||
**Нет (это не баги):**
|
||||
|
||||
- Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (`/docs`) или curl.
|
||||
- Экрана загрузки и таблицы ОСВ (подсрез 2d), ББ/ОФР, распознавания PDF, консолидации, КФО — следующие срезы.
|
||||
- Экрана загрузки и таблицы ОСВ (подсрез 2d), экранов консолидации (API есть, раздел 7), ББ/ОФР, распознавания PDF, шахматки, КФО — следующие срезы.
|
||||
- Настоящего входа через SSO банка. Вход — dev-форма по email без пароля.
|
||||
|
||||
## 2. Что установить и проверить из ПО
|
||||
@ -208,7 +211,7 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
|
||||
### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger)
|
||||
|
||||
Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на свежем стенде или после
|
||||
сброса (раздел 8), иначе счётчики будут другими.
|
||||
сброса (раздел 9), иначе счётчики будут другими.
|
||||
|
||||
| № | Шаги | Ожидание |
|
||||
|---|---|---|
|
||||
@ -254,7 +257,7 @@ docker compose -f ../docs/dev/docker-compose.yml exec postgres \
|
||||
### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)
|
||||
|
||||
Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), файлы из `../fixtures/osv/`. Выполняйте на
|
||||
свежем стенде (раздел 8) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до
|
||||
свежем стенде (раздел 9) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до
|
||||
`PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды).
|
||||
|
||||
Подготовка: пользователь `ka_go` (права `DOCUMENT_EDIT`, `DOCUMENT_VIEW`, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ —
|
||||
@ -303,7 +306,67 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n
|
||||
(системное событие) и `description`, где перечислены документы заменённого файла; U12 — `FILE_DELETE` с карточкой файла
|
||||
в `before_value`. Смены статусов обмена (`PROCESSING`, `PROCESSED`, `ERROR`) аудитом не фиксируются — так задумано.
|
||||
|
||||
## 7. Известные ограничения (не заводить как баги)
|
||||
## 7. Консолидация через API (Срез 4)
|
||||
|
||||
Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001`
|
||||
(раздел 6.6); данные — в `../fixtures/osv/`. Токены: `ka_go` (`CONSOLIDATION_VIEW/EDIT`, видит всё), `ka_rf`
|
||||
(`CONSOLIDATION_VIEW/EDIT`, только ГК Ростовского РФ), `expert` (только `CONSOLIDATION_VIEW`), `client_manager`
|
||||
(`CONSOLIDATION_VIEW`, Краснодарский РФ), `admin` (прав на консолидации нет). Подробные схемы запросов и ответов, коды ошибок и
|
||||
нюансы для фронта — `dev/api-contract-slice-4.md`; Swagger — `/docs`.
|
||||
|
||||
**Подготовка.** Нужна ГК с ГСК не менее чем из двух ЮЛ (ГК — в Ростовском РФ, чтобы её видел `ka_rf`): создайте через API
|
||||
справочников (раздел 6.2; ГСК — `POST /directories/company-groups/{id}/related-groups`, участники — `POST
|
||||
/directories/related-groups/{id}/members`) либо импортом ЕЦП (I2). Для данных РБ: ЮЛ «АО "Русский бисквит"» (ИНН 3528052718) и
|
||||
«ООО "Пекарня Счастливочная"» (в ГСК обе). Ниже `V` — id версии, `G` — id ГК, `S` — id ГСК.
|
||||
|
||||
```bash
|
||||
curl -s -X POST localhost:8000/api/v1/consolidations -H "$H" -H 'Content-Type: application/json' \
|
||||
-d '{"company_group_id":"'$G'","related_group_id":"'$S'","year":2024,"quarter":"Q3"}'
|
||||
# 201: VersionCard; V = ответ.id, status = INITIATED, reporting_date = 2024-10-01
|
||||
curl -s -X POST localhost:8000/api/v1/consolidations/versions/$V/lock -H "$H" # 201 LockOut
|
||||
curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files?consolidation_version_id=$V" -H "$H" -F 'files=@…'
|
||||
curl -s localhost:8000/api/v1/consolidations/versions/$V/checklist -H "$H"
|
||||
curl -s "localhost:8000/api/v1/consolidations/versions/$V/osv?limit=50" -H "$H"
|
||||
curl -s localhost:8000/api/v1/consolidations/versions/$V/osv/summary -H "$H"
|
||||
curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/export" -H "$H" # Content-Disposition
|
||||
```
|
||||
|
||||
| № | Пользователь | Шаги | Ожидание |
|
||||
|---|---|---|---|
|
||||
| K1 | ka_go | создать консолидацию ГК/ГСК за 2024 Q3 (команда выше) | 201, `status = INITIATED`, `version_no = 1`, `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false`, `members` = текущие члены ГСК, `lock = null`; повтор — 409 `CONSOLIDATION_EXISTS` |
|
||||
| K2 | ka_go | то же с `year = 1999`; с ГСК другой ГК; с ГСК из одного ЮЛ; с чужим/несуществующим id ГК | 422 (валидация года); 422 `RELATED_GROUP_MISMATCH`; 422 `PERIMETER_TOO_SMALL`; 404 `NOT_FOUND` |
|
||||
| K3 | ka_go | `GET /company-groups/{G}/consolidations`, затем с `?in_work_only=true`; `GET …/versions/{V}` | период с версией; карточка с `members[]` (у каждого `checklist[]`), `history[0].from_status = null`; `ka_rf` для ГК вне своего филиала — 404 |
|
||||
| K4 | ka_go | `POST …/lock`; повтор | 201 `LockOut` (`locked_by`, `locked_by_name`); повтор — 200, блокировка та же |
|
||||
| K5 | ka_rf (ГК его филиала) | `POST …/lock` на версию, занятую `ka_go` | 409 `LOCKED_BY_OTHER` (`locked_by`, `locked_at`, `locked_by_name`); с телом `{"takeover_reason":"…"}` — 201, `taken_over_from` = id `ka_go`, `ka_go` теряет блокировку; в аудите `CONSOLIDATION_LOCK_TAKEOVER` |
|
||||
| K6 | ka_go | после K5 `PUT …/members` | 409 `LOCKED_BY_OTHER` (чужая блокировка), без блокировки вообще — 409 `LOCK_REQUIRED` |
|
||||
| K7 | владелец блокировки | `PUT …/members` с составом ≤ 1 ЮЛ, с ЮЛ другой ГК, с тем же составом | 422 `PERIMETER_TOO_SMALL`; 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`); 200 без изменений и без события `MEMBERSHIP_CHANGE` |
|
||||
| K8 | владелец блокировки | загрузить ОСВ ЮЛ периметра с `?consolidation_version_id=V` (раздел 6.6) | 201; версия `INITIATED` → `DOCUMENTS_PROCESSING`; ЮЛ вне периметра — 422 `ENTITY_NOT_IN_PERIMETER`; несуществующая версия — 404 |
|
||||
| K9 | ka_go | после `PROCESSED` и `NEEDS_CHECK` документа — `GET …/versions/{V}`; затем `POST /documents/{id}/confirm` | `NEEDS_CHECK` → версия `DATA_CORRECTION` (если файл загружал пользователь); подтверждение переводит `INITIATED`/`DOCUMENTS_PROCESSING` → `DATA_CORRECTION`; документ привязан к версии (виден в чек-листе: ячейка `CONFIRMED`) |
|
||||
| K10 | ka_go | `GET …/checklist` до и после подтверждения; для квартальных ОСВ — только Q3 | ячейка ОСВ: `MISSING` → `NEEDS_CHECK`/`RECOGNIZED` → `CONFIRMED`; при неполных кварталах — `missing_quarters`; ОСВ по субсчёту (58.03) не закрывает «ОСВ 58» |
|
||||
| K11 | ka_go | `GET …/osv`, `…/osv/summary` по данным РБ (раздел «Данные для K11») | строки свода по парам, где покрыты все кварталы `Q1..Qn`; пары без квартала — в `gaps[]` с `missing_quarters`; деньги — строки с двумя знаками; `unmapped_total` > 0 |
|
||||
| K12 | ka_go | фильтры: `legal_entity_id`, `account_root=60`, `counterparty_type=EXTERNAL`, `unmapped=true`, `match_kind=MANUAL&match_kind=PROPAGATED`, `q=бисквит`, `limit=2001` | отфильтрованные страницы с `total`; `limit=2001` — 422 |
|
||||
| K13 | ka_go | `GET …/counterparty-candidates?q=счастл&entry_id=<entry_ids[0]>`; `scope=all&q=ab` | список ЮЛ с `counterparty_type`; `recommended` — только при точном совпадении нормализованного имени; `scope=all` с `q` короче 3 — 422 `QUERY_TOO_SHORT` |
|
||||
| K14 | владелец блокировки | `POST …/osv/entries/{entry_id}/counterparty {legal_entity_id}` для строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» | 200: `row.match_kind = MANUAL` (слать **последнюю** `entry_id` строки свода), `propagated` ≥ 0, `alias_created = true`; в сводке `unmapped_total` уменьшился, у других строк с тем же именем — `PROPAGATED`; в аудите `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE` |
|
||||
| K15 | владелец блокировки | сопоставить другое имя с алиасом, уже закреплённым за другим ЮЛ; затем с `"move_alias": true` | 409 `ALIAS_TAKEN` (`owners[]`); с `move_alias` — 200, `alias_moved_from` = прежний владелец |
|
||||
| K16 | ka_go | `entry_id` строки `NU`/`ACCOUNT` или чужой версии | 404 `NOT_FOUND`; без блокировки — 409 `LOCK_REQUIRED` |
|
||||
| K17 | ka_go | `GET …/osv/export` (в т. ч. с фильтрами) | 200, `Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''…`; файл открывается в Excel: лист «Сводная ОСВ» (суммы — числа, формат `#,##0.00`, число строк как в API) и лист «Пробелы» (от фильтров не зависит); в аудите `EXPORT` |
|
||||
| K18 | владелец блокировки | `POST …/status {"to_status":"CANCELLED"}` без комментария; с комментарием | 422 `COMMENT_REQUIRED`; 200, `status = CANCELLED`, блокировка снята (`lock = null`), `history` пополнена |
|
||||
| K19 | ka_go | после K18: `PUT …/members`, `POST …/lock`, `POST …/status`, сопоставление | 409 `VERSION_NOT_EDITABLE`; чтение (`GET …/osv`, экспорт, чек-лист) — 200 |
|
||||
| K20 | владелец блокировки | `POST …/status {"to_status":"INITIATED"}` из `DATA_CORRECTION`; затем `{"to_status":"DATA_CORRECTION"}` и `{"to_status":"VERIFIED"}` | 200, привязки документов сохраняются; ручные `DATA_CORRECTION`/`VERIFIED` — 409 `TRANSITION_NOT_ALLOWED` (`from_status`, `to_status`) |
|
||||
| K21 | expert, client_manager | `GET …/osv`, `…/checklist`; `POST …/lock`, `PUT …/members` | чтение — 200 (client_manager — только ГК своего филиала, иначе 404); правки — 403 `недостаточно прав: требуется CONSOLIDATION_EDIT` |
|
||||
| K22 | ka_go | заменить файл (загрузить исправленную ОСВ того же счёта и периода) после подтверждения документа | старый файл `SUPERSEDED`, привязка его документов к версии снята (`FILE_REPLACE.after_value.unbound_version_ids` содержит `V`); новый документ привязывается только после подтверждения |
|
||||
|
||||
Данные для K11 (РБ 2024, как в сквозном тесте `test_consolidation_fixtures`): ОСВ 60 и 62 РБ и Счастливочной, ОСВ 76
|
||||
Счастливочной за Q1, Q2 и Q3 2024 (`../fixtures/osv/…`, подтвердить документы), плюс по одному документу за Q3 (РБ: 01, 58.03,
|
||||
66, 67, 76; Счастливочная: 01, 02). Для версии Q3 пары с одним Q3 попадают в `gaps` (пропущены Q1–Q2); ОСВ за Q1 (нарастающая
|
||||
по признаку парсера) подходит версии любого квартала (R-14).
|
||||
|
||||
Аудит (6.4): K1 — `CONSOLIDATION_CREATE`; K4 — `CONSOLIDATION_LOCK`; K5 — `CONSOLIDATION_LOCK_TAKEOVER`; K7 — `MEMBERSHIP_CHANGE`
|
||||
(только при изменении состава); переходы статуса — `CONSOLIDATION_STATUS_CHANGE`; K14 — `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE`;
|
||||
K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокировка не истекает по времени — освободить её можно только `DELETE …/lock`,
|
||||
перехватом или аннулированием версии (OQ-119).
|
||||
|
||||
## 8. Известные ограничения (не заводить как баги)
|
||||
|
||||
- UI только для администрирования; справочники — через API.
|
||||
- Вход без пароля, открытый `/docs`, CORS `*` — dev-режим.
|
||||
@ -315,10 +378,15 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n
|
||||
- В консоли браузера сообщение `Store does not have a valid reducer` — каркас, не влияет.
|
||||
- Таблица пользователей показывает первые 200.
|
||||
- При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт `INVALID_INN`.
|
||||
- Консолидация (Срез 4): только API; маршрут согласования (`CONSOLIDATION_RF` и далее), «Добавить версию», гудвилл, шахматка — следующие
|
||||
срезы; ББ/ОФР в своде не участвуют; блокировка версии не истекает по времени; `period_check` документов остаётся `UNKNOWN`;
|
||||
`PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122).
|
||||
- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только
|
||||
при точном совпадении нормализованного имени (OQ-127).
|
||||
|
||||
Полный список отложенных находок — Gitea `RSHB/arm-core`, issue «Follow-ups from #5».
|
||||
|
||||
## 8. Сброс стенда
|
||||
## 9. Сброс стенда
|
||||
|
||||
```bash
|
||||
docker compose -f ../docs/dev/docker-compose.yml down -v # БД и бакет с нуля
|
||||
@ -331,7 +399,7 @@ make migrate && make seed-dev
|
||||
Файлы, загруженные в 6.6, удаляются из БД командой `DELETE /files/{id}`, но объекты в MinIO остаются (им ставится только
|
||||
`expires_at`) — `down -v` стирает и их.
|
||||
|
||||
## 9. Как заводить баги
|
||||
## 10. Как заводить баги
|
||||
|
||||
Gitea, репозиторий `RSHB/arm-core` (для документации — `RSHB/arm-ka-docs`). Один дефект — одна issue.
|
||||
|
||||
@ -351,7 +419,7 @@ Gitea, репозиторий `RSHB/arm-core` (для документации
|
||||
|
||||
Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке.
|
||||
|
||||
## 10. Типичные проблемы при запуске
|
||||
## 11. Типичные проблемы при запуске
|
||||
|
||||
| Симптом | Причина и решение |
|
||||
|---|---|
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
- [x] 1 **DBML v5.16.** `model/data-model-v5.16.dbml` (copy v5.15 + changelog v5.16 + enum + колонка + notes),
|
||||
`model/entities-v5.16.md`, `04-data-model-guide.md` (DECISION-5, переименования без изменений), ссылки на v5.16 в
|
||||
`CLAUDE.md`, `repo-templates/*`, `arm-core/openspec/config.yaml` (в дочернем change).
|
||||
- [ ] 2 **Правила.** `05-domain-rules.md` §2, §3, §9.1, §9.3, §9.5, §10 по proposal; `03-architecture.md`.
|
||||
- [ ] 3 **Вопросы и план.** `07-open-questions.md` (закрытия и OQ-118…123), `09-delivery-plan.md`, `10-user-journey.md`.
|
||||
- [ ] 4 **Контракт и QA.** `dev/api-contract-slice-4.md`, `dev/qa-guide.md` §7 — после того как эндпоинты arm-core зафиксированы
|
||||
- [x] 2 **Правила.** `05-domain-rules.md` §2, §3, §9.1, §9.3, §9.5, §10 по proposal; `03-architecture.md`.
|
||||
- [x] 3 **Вопросы и план.** `07-open-questions.md` (закрытия и OQ-118…123), `09-delivery-plan.md`, `10-user-journey.md`.
|
||||
- [x] 4 **Контракт и QA.** `dev/api-contract-slice-4.md`, `dev/qa-guide.md` §7 — после того как эндпоинты arm-core зафиксированы
|
||||
(последняя задача, синхронно с 5.2 arm-core).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user