Merge pull request 'Архив change quality-metrics (AURORA-1759, #229)' (#230) from chore/archive-quality-metrics into main

This commit is contained in:
aep 2026-11-07 07:52:02 +03:00
commit 371c38c17c
21 changed files with 231 additions and 11 deletions

View File

@ -212,3 +212,14 @@ object_type = document, object_id документа, user_id инициатор
#### Scenario: Возврат к исходному #### Scenario: Возврат к исходному
- **WHEN** аналитик возвращает исходные значения после предыдущей правки - **WHEN** аналитик возвращает исходные значения после предыдущей правки
- **THEN** новое RECOGNIZED_DATA_EDIT отражает эту правку; старые события не меняются - **THEN** новое RECOGNIZED_DATA_EDIT отражает эту правку; старые события не меняются
### Requirement: Аудит выгрузки метрик качества
Каждый успешный `GET /quality-metrics/export` SHALL добавлять отдельное неизменяемое событие `audit_event` с
`action = EXPORT`, `section = EXPORT`, `object_type = quality_metrics`, `object_id = null`, `user_id` инициатора,
`before_value = null` и `after_value = {report, format, filters, rows}`: `report` — тип выгрузки, `format` — `csv` или
`json`, `filters` — применённые фильтры, `rows` — число выгруженных строк. Выгрузка сводной ОСВ и шахматки продолжают
писать собственные события `EXPORT`.
#### Scenario: Выгрузка метрик оставляет след
- **WHEN** пользователь с `QUALITY_METRICS_VIEW` выгружает факты за период в CSV
- **THEN** записано одно событие `EXPORT` с `object_type = quality_metrics`, форматом, фильтрами и числом строк

View File

@ -49,12 +49,16 @@ claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) и найти `app_us
Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`, Маршрут SHALL возвращать текущего пользователя: `id`, `email`, `full_name`, `regional_office {id, name, is_head_office}`,
`business_segment`, `roles` (список `code`), `role_details` (те же активные роли списком `{code, name}`, `name` — `role.name`, `business_segment`, `roles` (список `code`), `role_details` (те же активные роли списком `{code, name}`, `name` — `role.name`,
порядок как в `roles`), `permissions` (отсортированный список `code`). Это единственный маршрут порядок как в `roles`), `permissions` (отсортированный список `code`). Это единственный маршрут
Среза 0, доступный любому аутентифицированному пользователю без `require`. Среза 0, доступный любому аутентифицированному пользователю без `require`. Состав `permissions` определяется матрицей ролей (§10), включая `QUALITY_METRICS_VIEW`, который seed выдаёт всем ролям.
#### Scenario: Ответ для dev-пользователя #### Scenario: Ответ для dev-пользователя
- **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me` - **WHEN** `ka_rf@armka.local` (роль `KA_RF`) запрашивает `/api/v1/me`
- **THEN** ответ 200 содержит `roles = ["KA_RF"]`, `role_details` с одним элементом `{code: "KA_RF", name: <непустое название>}` и `permissions`, равные правам `KA_RF` из матрицы §10 - **THEN** ответ 200 содержит `roles = ["KA_RF"]`, `role_details` с одним элементом `{code: "KA_RF", name: <непустое название>}` и `permissions`, равные правам `KA_RF` из матрицы §10
#### Scenario: Право метрик в профиле
- **WHEN** любой пользователь запрашивает `/api/v1/me` после `make seed`
- **THEN** `permissions` содержит `QUALITY_METRICS_VIEW`, отсортированно, перед `USERS_ADMIN`
### Requirement: Dev-эмитент токенов только в режиме разработки ### Requirement: Dev-эмитент токенов только в режиме разработки
При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`, При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`,
возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов

View File

@ -58,7 +58,7 @@ v5.18 — консолидация, в контексте которой фай
`period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же `period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же
версии SHALL отвязываться (R-1). Каждая версия, получившая версии SHALL отвязываться (R-1). Каждая версия, получившая
привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` = привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` =
подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться. подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться. В той же транзакции ручное подтверждение документа `BALANCE` / `INCOME` / `OSV` SHALL записывать `quality_outcome_fact` (`outcome = CONFIRMED`) с отнесением правок по живым колонкам документа (`is_modified`, `original_*`, `source_text`), а не по `payload` приёма; `checks_accepted` — признак акцепта проверок шапкой (на ячейку матрицы не влияет: при правках приоритет у «сервиса» и «нашего разбора»). `DISCLOSURE` / `OTHER` факта не получают.
#### Scenario: Причина ошибки маппинга в карточке файла #### Scenario: Причина ошибки маппинга в карточке файла
- **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст) - **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст)
@ -109,6 +109,10 @@ v5.18 — консолидация, в контексте которой фай
- **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй - **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй
- **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый - **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый
#### Scenario: Подтверждение с правкой суммы
- **WHEN** аналитик исправил сумму и подтвердил ББ
- **THEN** записан `quality_outcome_fact` `CONFIRMED` со счётчиком правки значения по источнику и `payload.fixes`
### Requirement: Подхват документов при создании версии и расширении периметра ### Requirement: Подхват документов при создании версии и расширении периметра
При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из
незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`, незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`,

View File

@ -15,7 +15,7 @@ LOCKED`), проверять, что `result_s3_object` относится к б
в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`, в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`,
перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку
маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать
файл результата и MUST NOT создавать документы. файл результата и MUST NOT создавать документы. После `map_file` и до автоподтверждения транзакция маппинга SHALL писать факты приёма `quality_intake_fact` по документам `BALANCE` / `INCOME` / `OSV`; неповторяемая ошибка маппинга (файл → `ERROR`) SHALL писать `quality_failure_fact` (`failure_kind = MAPPING`, `error_code = MAPPING_FAILED`).
#### Scenario: Результат смапплен #### Scenario: Результат смапплен
- **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING` - **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING`
@ -44,6 +44,14 @@ LOCKED`), проверять, что `result_s3_object` относится к б
- **WHEN** результат без документов (`documents = []`): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызов `map_file` даёт `MappingFatal` - **WHEN** результат без документов (`documents = []`): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызов `map_file` даёт `MappingFatal`
- **THEN** файл `ERROR`, причина в `metrics.mapping_error` (при проверке схемы — ошибка схемы, в `map_file` — «в результате нет документов»), повтор не выполняется - **THEN** файл `ERROR`, причина в `metrics.mapping_error` (при проверке схемы — ошибка схемы, в `map_file` — «в результате нет документов»), повтор не выполняется
#### Scenario: Факт приёма в транзакции маппинга
- **WHEN** результат смаплен и создан документ ОСВ
- **THEN** в той же транзакции записан `quality_intake_fact` документа до перехода в `CONFIRMED`
#### Scenario: Неповторяемая ошибка маппинга
- **WHEN** маппинг завершился неповторяемой ошибкой
- **THEN** файл `ERROR` и записан `quality_failure_fact` с `failure_kind = MAPPING`
### Requirement: Документ по листу ОСВ ### Requirement: Документ по листу ОСВ
На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный
код из шапки (`60.1 → 60.01`, `58.03`, буквенные сегменты как есть), `period_year`, `period_quarter`, `is_cumulative` код из шапки (`60.1 → 60.01`, `58.03`, буквенные сегменты как есть), `period_year`, `period_quarter`, `is_cumulative`
@ -222,7 +230,7 @@ object_type = document)` с `user_id = NULL`, `before_value.status = RECOGNIZED`
получившая привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` с `changed_by = получившая привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` с `changed_by =
stored_file.uploaded_by` и `comment = «автоподтверждение документа <document_type> <osv_account_code>»` (счёт — только у stored_file.uploaded_by` и `comment = «автоподтверждение документа <document_type> <osv_account_code>»` (счёт — только у
ОСВ). Если `uploaded_by` NULL, привязка SHALL создаваться, а переход — пропускаться с записью в лог. Пересчёт статуса ОСВ). Если `uploaded_by` NULL, привязка SHALL создаваться, а переход — пропускаться с записью в лог. Пересчёт статуса
формы после правки или акцепта MUST NOT подтверждать документ автоматически. формы после правки или акцепта MUST NOT подтверждать документ автоматически. Для каждого автоподтверждённого документа `BALANCE` / `INCOME` / `OSV` SHALL записываться `quality_outcome_fact` (`AUTO_CONFIRMED`, `primary_cause = WITHOUT_PARTICIPATION`, `confirmed_by` пуст); автозамена прежних версий SHALL писать `SUPERSEDED` / `SUPERSEDED_AFTER_CONFIRM` по вытесненным документам.
#### Scenario: Чистая ОСВ сразу «Загружен» #### Scenario: Чистая ОСВ сразу «Загружен»
- **WHEN** смаплен результат `fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx` для ЮЛ АО «Русский бисквит» - **WHEN** смаплен результат `fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx` для ЮЛ АО «Русский бисквит»
@ -257,6 +265,10 @@ stored_file.uploaded_by` и `comment = «автоподтверждение до
(чистый), A заменён автозаменой (чистый), A заменён автозаменой
- **THEN** привязка документа A снята, документ B `CONFIRMED` и привязан к версии - **THEN** привязка документа A снята, документ B `CONFIRMED` и привязан к версии
#### Scenario: Факт автоподтверждения
- **WHEN** документ автоподтверждён
- **THEN** есть `quality_outcome_fact` `AUTO_CONFIRMED` для этого документа
### Requirement: Хранение результата формы v2.0 (DBML v5.19) ### Requirement: Хранение результата формы v2.0 (DBML v5.19)
Маппер результата `2.0` SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не Маппер результата `2.0` SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не
дедуплицируются (повтор кода — отдельные строки), каждая строка результата — `financial_statement_line` (`row_index`, дедуплицируются (повтор кода — отдельные строки), каждая строка результата — `financial_statement_line` (`row_index`,

View File

@ -22,7 +22,7 @@
запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`. запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`.
Query-параметр `consolidation_version_id` MUST NOT приниматься (удалён в v5.18): загрузка этим маршрутом — без Query-параметр `consolidation_version_id` MUST NOT приниматься (удалён в v5.18): загрузка этим маршрутом — без
контекста консолидации и не меняет статусы версий; загрузка в консолидацию — требование «Загрузка файлов в контекста консолидации и не меняет статусы версий; загрузка в консолидацию — требование «Загрузка файлов в
консолидацию». консолидацию». Для PDF `stored_file.is_scan` SHALL определяться при загрузке по наличию текстового слоя в первых 10 страницах (разбор pypdf — вне event loop, `asyncio.to_thread`): нет текста — скан (`true`), есть — цифровой (`false`); для не-PDF — NULL. PDF со скрытым OCR-слоем считается цифровым.
#### Scenario: Реальная ОСВ принята и ушла в outbox #### Scenario: Реальная ОСВ принята и ушла в outbox
- **WHEN** `ka_rf` загружает `rb/2024-FY/ОСВ_66 за 2024.xls` для ЮЛ своего филиала - **WHEN** `ka_rf` загружает `rb/2024-FY/ОСВ_66 за 2024.xls` для ЮЛ своего филиала
@ -51,6 +51,10 @@ Query-параметр `consolidation_version_id` MUST NOT приниматьс
- **WHEN** ЮЛ не входит в периметр ни одной версии, файл загружен маршрутом ЮЛ - **WHEN** ЮЛ не входит в периметр ни одной версии, файл загружен маршрутом ЮЛ
- **THEN** 201 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию - **THEN** 201 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию
#### Scenario: Признак скана
- **WHEN** загружается PDF без текстового слоя
- **THEN** `stored_file.is_scan = true`; у xlsx `is_scan IS NULL`
### Requirement: Дедуп по содержимому ### Requirement: Дедуп по содержимому
Файл с тем же sha256, что у существующего `stored_file` того же ЮЛ со статусом не `SUPERSEDED`, SHALL отклоняться с Файл с тем же sha256, что у существующего `stored_file` того же ЮЛ со статусом не `SUPERSEDED`, SHALL отклоняться с
кодом `DUPLICATE_FILE` и `file_id` существующего. Тот же файл у другого ЮЛ SHALL приниматься. кодом `DUPLICATE_FILE` и `file_id` существующего. Тот же файл у другого ЮЛ SHALL приниматься.
@ -147,7 +151,7 @@ NULL), или если автоподтверждённый документ ф
файл ссылаются `superseded_by_file_id`. При удалении файла с автоподтверждёнными документами их привязки файл ссылаются `superseded_by_file_id`. При удалении файла с автоподтверждёнными документами их привязки
`consolidation_document` SHALL сниматься, статус версий MUST NOT меняться, `after_value.unbound_version_ids` — id версий, `consolidation_document` SHALL сниматься, статус версий MUST NOT меняться, `after_value.unbound_version_ids` — id версий,
где привязки сняты (по возрастанию; пустой список, если привязок не было). Блокировки: задания → файл → документы → где привязки сняты (по возрастанию; пустой список, если привязок не было). Блокировки: задания → файл → документы →
advisory-блокировка привязок ГК → версии по `id`. Файл `SUPERSEDED` удалить можно. advisory-блокировка привязок ГК → версии по `id`. Файл `SUPERSEDED` удалить можно. Если у файла есть `quality_intake_fact` (по `file_id`), объект `uploads` файла MUST NOT получать `expires_at` — он нужен эталонному прогону качества; сырой ответ ИС (`raw`) при удалении `expires_at` не получает никогда. Для документов с исходом удаление SHALL писать факт исхода `DELETED` / `DELETED_AFTER_CONFIRM` в той же транзакции (спека `quality-metrics`).
#### Scenario: Удаление ошибочного файла #### Scenario: Удаление ошибочного файла
- **WHEN** файл в `ERROR` удаляется владельцем - **WHEN** файл в `ERROR` удаляется владельцем
@ -170,6 +174,10 @@ advisory-блокировка привязок ГК → версии по `id`.
- **WHEN** удаляется файл, все документы которого автоподтверждены и ни к одной версии не привязаны - **WHEN** удаляется файл, все документы которого автоподтверждены и ни к одной версии не привязаны
- **THEN** 204; `after_value.unbound_version_ids = []` - **THEN** 204; `after_value.unbound_version_ids = []`
#### Scenario: Файл с фактом приёма
- **WHEN** удаляется файл, по документам которого записан `quality_intake_fact`
- **THEN** 204; объект `uploads` и сырой ответ остаются без `expires_at`, результат (`results`) экспирится; записан факт исхода
### Requirement: Загрузка файлов в консолидацию ### Requirement: Загрузка файлов в консолидацию
`POST /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart `files`, право `POST /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart `files`, право
`DOCUMENT_EDIT`; §9.5 п. 8 v5.18) SHALL до приёма файлов проверить: консолидация видима (ГК в области пользователя; `DOCUMENT_EDIT`; §9.5 п. 8 v5.18) SHALL до приёма файлов проверить: консолидация видима (ГК в области пользователя;

View File

@ -0,0 +1,42 @@
# quality-metrics-ui Specification
## Purpose
Экран «Качество первички» веб-интерфейса: матрица «источник × причина», динамика по месяцам, список документов ячейки,
выгрузка фактов; пункт меню по праву.
## Requirements
### Requirement: Экран «Качество первички»
Веб-интерфейс SHALL содержать экран «Качество первички» с фильтрами (период, ГК, тип документа, источник, скан/цифровой),
матрицей «источник × причина» (колонки PDF, Excel, Всего; технические отказы — отдельным столбцом по файлам с пометкой,
что фильтр типа на них не действует), динамикой по месяцам выбранной причины и кнопкой выгрузки фактов (CSV / JSON).
Фильтра по ЮЛ на экране нет (API его поддерживает). Справочник ГК для фильтра SHALL запрашиваться один раз за
жизнь экрана, в том числе при пустом ответе или ошибке.
#### Scenario: Пустой справочник ГК
- **WHEN** справочник ГК пуст или запрос завершился ошибкой
- **THEN** экран не повторяет запрос бесконечно
#### Scenario: Выбран источник
- **WHEN** в фильтре выбран PDF
- **THEN** матрица показывает одну колонку
### Requirement: Переход из ячейки в документ
Клик по ячейке матрицы SHALL открывать список документов ячейки (постранично, со счётчиками правок «сервис» и «наш
разбор» и исходом); из строки списка, если документ ещё существует, SHALL быть переход в экран проверки документа.
#### Scenario: Документ удалён
- **WHEN** документ ячейки уже удалён
- **THEN** строка показана без ссылки на документ
### Requirement: Пункт меню по праву
Боковое меню SHALL показывать пункт «Качество первички» только при наличии `QUALITY_METRICS_VIEW` и снова SHALL
скрывать пункты по полю `permission`: «Пользователи и роли» скрыт без `USERS_ADMIN`. Доступ по-прежнему решает сервер.
#### Scenario: Нет права
- **WHEN** в `/me` нет `QUALITY_METRICS_VIEW`
- **THEN** пункта «Качество первички» в меню нет
#### Scenario: Нет USERS_ADMIN
- **WHEN** в `/me` нет `USERS_ADMIN`
- **THEN** пункт «Пользователи и роли» скрыт

View File

@ -0,0 +1,123 @@
# quality-metrics Specification
## Purpose
Факты качества первички (приём «как распознано», исход, технический отказ), отнесение правок аналитика к источнику
ошибки, матрица причин и API `/quality-metrics` с отдельным правом. Правила подсчёта — `05-domain-rules.md` §14,
модель — DBML v5.25, дизайн — `design.md` этого change.
## Requirements
### Requirement: Факты качества — отдельная схема, только добавление
Ядро SHALL хранить факты качества тремя плоскими таблицами `quality_intake_fact`, `quality_outcome_fact`,
`quality_failure_fact` в отдельной схеме PostgreSQL (`QUALITY_METRICS_SCHEMA`, по умолчанию `dkb_metrics`; значение MUST
отличаться от `DB_SCHEMA`). Таблицы MUST NOT иметь внешних ключей на рабочие таблицы: идентификаторы документов, файлов,
заданий, ЮЛ и ГК — просто `uuid`, имена ЮЛ и ГК копируются на момент факта. Записи SHALL только добавляться (UPDATE и
DELETE по фактам MUST NOT выполняться); `payload` — `jsonb` с `payload_schema_version`, форма по контракту
`quality-facts.schema.json`. Факты пишутся только для документов, принятых после выкладки.
#### Scenario: Факт переживает удаление файла
- **WHEN** файл с документом, у которого есть факт приёма, удаляется
- **THEN** строки документа и файла удалены, а `quality_intake_fact` остаётся нетронутым
#### Scenario: Схема метрик совпала со схемой модели
- **WHEN** `DB_SCHEMA` и `QUALITY_METRICS_SCHEMA` заданы одним значением
- **THEN** приложение не стартует с ошибкой конфигурации
### Requirement: Факт приёма «как распознано»
В транзакции маппинга результата, после `map_file` и до автоподтверждения, ядро SHALL записать по одному
`quality_intake_fact` на каждый созданный документ `BALANCE`, `INCOME`, `OSV` (`DISCLOSURE` и `OTHER` факта не
получают): измерения (ЮЛ и ГК с именами, тип документа, `source_kind` pdf/excel, `is_scan` для PDF, сервис и версии,
`intake_at`, `intake_status`), счётчики строк, значений, нарушений и предупреждений, ключи S3, и `payload` со снимком
«как распознано». Запись факта MUST NOT менять документы, статусы и результат маппинга.
#### Scenario: Приём ББ
- **WHEN** маппинг создал документ `BALANCE` из PDF
- **THEN** есть ровно один `quality_intake_fact` с `document_id`, `source_kind = pdf`, `intake_status` как у документа после
маппинга и снимком строк и значений в `payload`
#### Scenario: Расшифровка не учитывается
- **WHEN** маппинг создал документ `DISCLOSURE`
- **THEN** факта приёма по нему нет
### Requirement: Факт исхода документа
Ядро SHALL записать `quality_outcome_fact` в транзакции события, которое завершает документ: ручное подтверждение
(`CONFIRMED`), автоподтверждение (`AUTO_CONFIRMED`), удаление файла (`DELETED`), автозамена файла (`SUPERSEDED`). Если
документ на момент удаления или замены уже `CONFIRMED`, исход SHALL быть `DELETED_AFTER_CONFIRM` /
`SUPERSEDED_AFTER_CONFIRM`. Факт содержит `primary_cause`, счётчики правок по источнику (`service` / `ours` / `client`),
`checks_accepted`, число нарушений на момент исхода, подтверждённую отчётную дату. Повторное подтверждение ОСВ после
правки SHALL писать новый факт; метрики берут последний факт документа.
#### Scenario: Автоподтверждение
- **WHEN** документ без замечаний автоподтверждён
- **THEN** факт с `outcome = AUTO_CONFIRMED`, `primary_cause = WITHOUT_PARTICIPATION`, `confirmed_by` пуст
#### Scenario: Удаление после подтверждения
- **WHEN** удаляется файл с автоподтверждённым документом
- **THEN** новый факт `DELETED_AFTER_CONFIRM`; прежний факт `AUTO_CONFIRMED` не изменён
### Requirement: Факт технического отказа
Ядро SHALL записывать `quality_failure_fact` при переходе задания распознавания в `error`, `timeout`, `dlq` и при
неповторяемой ошибке маппинга (файл → `ERROR`). `failure_kind` — `SERVICE_ERROR` / `TIMEOUT` / `NO_ACK` / `MAPPING`;
`error_code` отказа маппинга — константа `MAPPING_FAILED`. Повторная обработка создаёт новое задание: отказ первой
попытки остаётся фактом.
#### Scenario: Таймаут задания
- **WHEN** вахтёр переводит задание в `timeout`
- **THEN** есть факт `failure_kind = TIMEOUT` с `job_id`, `file_id`, `failed_at`
### Requirement: Отнесение правки к источнику ошибки
Правка аналитика (отличие подтверждённого результата от снимка приёма) SHALL относиться к источнику чистой функцией без
БД по правилу «верный ответ есть в сыром тексте сервиса — ошибка наша, иначе сервиса»: сумма, код строки, добавленная
строка, поля шапки (отчётная дата сравнивается как дата после разбора `parse_date`), период и счёт ОСВ. Для Excel любая
ошибка — «наш разбор». Спорные случаи и случаи без сырого текста SHALL относиться к «нашему разбору». Правки колонок дат
и набор кодов сырого ответа для добавленных строк PDF в этом change не учитываются (отложено).
#### Scenario: Сервис прочитал верно
- **WHEN** аналитик исправил сумму, цифры которой совпадают с цифрами сырого текста
- **THEN** правка относится к «нашему разбору»
#### Scenario: Сервис ошибся
- **WHEN** подтверждённый код строки не извлекается из сырого текста кода
- **THEN** правка относится к «ошибке сервиса»
### Requirement: Причина документа и матрица
Каждый документ с исходом SHALL попадать ровно в одну ячейку по первому сработавшему правилу: удалён/заменён до
подтверждения; удалён/заменён после подтверждения; есть правки «сервис» → ошибка сервиса; есть правки «наш разбор» →
наш разбор; ручное подтверждение с акцептом жёстких нарушений → документ клиента; `AUTO_CONFIRMED` → без участия;
иначе → как есть. Документы без исхода — «в работе». Доли считаются от документов с исходом `AUTO_CONFIRMED` /
`CONFIRMED`; удалённые, заменённые и «в работе» в знаменатель не входят. Технические отказы считаются по файлам своей
долей. Расчёт — чистая функция от фактов, в БД не хранится; последний факт документа выбирается по времени исхода, при
равенстве — детерминированно.
#### Scenario: Документ с правкой сервиса и клиента
- **WHEN** у подтверждённого документа есть правка «сервис» и акцепт нарушений
- **THEN** документ в ячейке «ошибка сервиса», счётчики обеих сторон сохранены в факте
#### Scenario: Нулевой знаменатель
- **WHEN** за период нет документов с исходом
- **THEN** доли равны 0, `denominator = 0`
### Requirement: API метрик качества
Система SHALL предоставлять под правом `QUALITY_METRICS_VIEW` маршруты `/api/v1/quality-metrics`: `GET /matrix`
(матрица за период с фильтрами период по дате приёма, ГК, ЮЛ, тип, источник, скан/цифровой, отказы по файлам, счётчик
уникальных отчётов), `GET /series` (ряд по месяцам даты приёма для причины, пропущенные месяцы — нули), `GET
/cells/{cell}/documents` (постраничный список документов ячейки со счётчиками правок, ссылкой на документ, если он
существует, и `confirmed_by`), `GET /export` (факты за период, CSV с BOM и JSON; значения, начинающиеся с символов
формулы, экранируются). Выгрузка SHALL писать `audit_event(EXPORT)` с `object_type = quality_metrics` и фильтрами. Агрегаты
MUST NOT содержать фамилий пользователей.
#### Scenario: Матрица без права
- **WHEN** пользователь без `QUALITY_METRICS_VIEW` запрашивает `/quality-metrics/matrix`
- **THEN** 403
#### Scenario: Выгрузка оставляет след
- **WHEN** пользователь выгружает факты за период
- **THEN** записан `audit_event(EXPORT)` с форматом, фильтрами и числом строк
### Requirement: Право QUALITY_METRICS_VIEW
Объект `QUALITY_METRICS` в `permission_object` и право `QUALITY_METRICS_VIEW` SHALL быть в seed и выдаваться всем
ролям; позже права сужаются данными (`role_permission`) без изменения кода. Видимость по филиалу и ГК не применяется.
#### Scenario: Право у всех ролей
- **WHEN** выполнен `make seed` на чистой БД
- **THEN** `GET /me` любой роли содержит `QUALITY_METRICS_VIEW`

View File

@ -58,7 +58,7 @@
заданию) SHALL получить `stored_file.status = ERROR`; при `done`/`needs_check` файл остаётся `PROCESSING` и SHALL быть заданию) SHALL получить `stored_file.status = ERROR`; при `done`/`needs_check` файл остаётся `PROCESSING` и SHALL быть
обработан циклом маппинга (спека `osv-result-mapping`); inbox MUST NOT читать файл результата. Неизвестный `job_id` SHALL обработан циклом маппинга (спека `osv-result-mapping`); inbox MUST NOT читать файл результата. Неизвестный `job_id` SHALL
быть залогирован. Результат для задания, уже находящегося в финальном статусе (`done`, `needs_check`, `error`, быть залогирован. Результат для задания, уже находящегося в финальном статусе (`done`, `needs_check`, `error`,
`timeout`, `dlq`), SHALL логироваться и не применяться. `timeout`, `dlq`), SHALL логироваться и не применяться. Если в конверте есть `raw_s3_key`, ядро SHALL независимо от `status` зарегистрировать сырой ответ ИС в `s3_object` (`bucket_role = raw`, ключ `raw/{job_id}.json`) и проставить `recognition_job.raw_s3_object_id`. При `status = error` в той же транзакции SHALL записываться `quality_failure_fact` (`failure_kind = SERVICE_ERROR`, `error_code` из конверта).
#### Scenario: Успешный результат #### Scenario: Успешный результат
- **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results/<file_id>/<job_id>/result.json}` и - **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results/<file_id>/<job_id>/result.json}` и
@ -79,15 +79,27 @@
- **WHEN** задание уже `timeout`, и приходит конверт `done` - **WHEN** задание уже `timeout`, и приходит конверт `done`
- **THEN** задание не меняется, событие залогировано - **THEN** задание не меняется, событие залогировано
#### Scenario: Сырой ответ зарегистрирован
- **WHEN** конверт `done` содержит `raw_s3_key = raw/<job_id>.json`
- **THEN** создан `s3_object` с `bucket_role = raw`, `recognition_job.raw_s3_object_id` указывает на него
#### Scenario: Факт отказа сервиса
- **WHEN** конверт содержит `status = error`
- **THEN** кроме `ERROR` файла записан `quality_failure_fact` с `failure_kind = SERVICE_ERROR`
### Requirement: Таймаут ожидания результата ### Requirement: Таймаут ожидания результата
Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в
`status = timeout`, `error_code = TIMEOUT`, `finished_at = now()`, а их файлы (по `current_recognition_job_id`) — в `status = timeout`, `error_code = TIMEOUT`, `finished_at = now()`, а их файлы (по `current_recognition_job_id`) — в
`stored_file.status = ERROR`. Публикатор при переводе задания в `dlq` SHALL так же переводить файл в `ERROR`. Ручной повтор после TIMEOUT/NO_ACK SHALL создавать новое задание по требованию «Новая попытка обработки файла». `stored_file.status = ERROR`. Публикатор при переводе задания в `dlq` SHALL так же переводить файл в `ERROR`. Ручной повтор после TIMEOUT/NO_ACK SHALL создавать новое задание по требованию «Новая попытка обработки файла». Каждый перевод в `timeout` и в `dlq` SHALL писать `quality_failure_fact` (`TIMEOUT` / `NO_ACK`) в той же транзакции.
#### Scenario: Сервис взял, но не ответил #### Scenario: Сервис взял, но не ответил
- **WHEN** задание в `processing` и `reply_deadline` прошёл без результата - **WHEN** задание в `processing` и `reply_deadline` прошёл без результата
- **THEN** не позднее чем через 30 с задание имеет `status = timeout`, файл — `ERROR` с `error_code = TIMEOUT` - **THEN** не позднее чем через 30 с задание имеет `status = timeout`, файл — `ERROR` с `error_code = TIMEOUT`
#### Scenario: Факт таймаута
- **WHEN** вахтёр перевёл задание в `timeout`
- **THEN** записан `quality_failure_fact` с `failure_kind = TIMEOUT`
### Requirement: Соединение с NATS по шаблону, без JetStream ### Requirement: Соединение с NATS по шаблону, без JetStream
Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных, Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных,
если они пусты; подписываться на subject ответа `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`) в queue group `arm-core`; переподключаться автоматически. если они пусты; подписываться на subject ответа `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`) в queue group `arm-core`; переподключаться автоматически.

View File

@ -9,7 +9,7 @@ dev-данные загружаются из файлов репозитория
### Requirement: Идемпотентный seed справочников продукта ### Requirement: Идемпотентный seed справочников продукта
Команда `python -m scripts.seed` (из `api/`) SHALL загрузить из `api/seed/core/`: `regional_office` (Головной офис, Команда `python -m scripts.seed` (из `api/`) SHALL загрузить из `api/seed/core/`: `regional_office` (Головной офис,
`is_head_office = true`), `role` — 7 системных ролей ТЗ 8.1 (`KA_RF`, `KA_GO`, `RKA`, `CLIENT_MANAGER`, `CURATOR_GO`, `is_head_office = true`), `role` — 7 системных ролей ТЗ 8.1 (`KA_RF`, `KA_GO`, `RKA`, `CLIENT_MANAGER`, `CURATOR_GO`,
`EXPERT`, `ADMIN`, `is_system = true`), `permission` — 16 сочетаний объект × действие из §10 правил с кодами `EXPERT`, `ADMIN`, `is_system = true`), `permission` — 17 сочетаний объект × действие из §10 правил с кодами
`<OBJECT>_<ACTION>` (`EXPORT` при совпадении), `role_permission` по матрице §10 (ASSUMPTION(OQ-16)), `chart_of_account`, `<OBJECT>_<ACTION>` (`EXPORT` при совпадении), `role_permission` по матрице §10 (ASSUMPTION(OQ-16)), `chart_of_account`,
`elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`. `elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`.
Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться. Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться.
@ -18,11 +18,11 @@ dev-данные загружаются из файлов репозитория
матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право
вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право, вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право,
снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что
матрица пропущена (или сколько добавлено пар новых ролей и прав). матрица пропущена (или сколько добавлено пар новых ролей и прав). Право `QUALITY_METRICS_VIEW` (объект `QUALITY_METRICS`, действие `VIEW`) SHALL выдаваться всем ролям; на стенде, где матрица уже заполнена, пара добавляется только как пара впервые созданного права — для появления пункта меню нужен повторный `make seed`.
#### Scenario: Первый запуск на пустой схеме #### Scenario: Первый запуск на пустой схеме
- **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed` - **WHEN** после `alembic upgrade head` выполняется `python -m scripts.seed`
- **THEN** в `role` 7 строк, в `permission` 16, в `role_permission` — число строк равно сумме прав по матрице §10, - **THEN** в `role` 7 строк, в `permission` 17, в `role_permission` — число строк равно сумме прав по матрице §10,
в `elimination_rule` 8 строк с `statement_type = balance`, `version_no = 1`, в `required_document_rule` для `BASIC` в `elimination_rule` 8 строк с `statement_type = balance`, `version_no = 1`, в `required_document_rule` для `BASIC`
10 строк (BALANCE, INCOME, OSV × 01, 58, 60, 62, 66, 67, 76, 90) и для `EXTENDED` 27 строк 10 строк (BALANCE, INCOME, OSV × 01, 58, 60, 62, 66, 67, 76, 90) и для `EXTENDED` 27 строк
@ -48,6 +48,10 @@ dev-данные загружаются из файлов репозитория
- **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова - **WHEN** в `roles.yaml` изменено `name` роли `EXPERT` и seed запущен снова
- **THEN** строка `role` с `code = EXPERT` обновлена, новая не создана - **THEN** строка `role` с `code = EXPERT` обновлена, новая не создана
#### Scenario: Право метрик на заполненной матрице
- **WHEN** матрица заполнена, право `QUALITY_METRICS_VIEW` создаётся впервые и seed запущен
- **THEN** добавлены пары права со всеми ролями; ранее снятые администратором связи не возвращены
### Requirement: Счета правил и чек-листа существуют в плане счетов ### Requirement: Счета правил и чек-листа существуют в плане счетов
Все коды счетов, на которые ссылаются `elimination_rules_v1.yaml` и `required_document_rules.yaml`, SHALL резолвиться в Все коды счетов, на которые ссылаются `elimination_rules_v1.yaml` и `required_document_rules.yaml`, SHALL резолвиться в
`chart_of_account.code` после seed; `elimination_rule_account.is_excluded = true` SHALL быть у исключений вида `chart_of_account.code` после seed; `elimination_rule_account.is_excluded = true` SHALL быть у исключений вида