Архив change quality-metrics (AURORA-1759, #229) #230

Merged
aep merged 1 commits from chore/archive-quality-metrics into main 2026-11-07 07:52:05 +03:00
21 changed files with 231 additions and 11 deletions

View File

@ -212,3 +212,14 @@ object_type = document, object_id документа, user_id инициатор
#### Scenario: Возврат к исходному
- **WHEN** аналитик возвращает исходные значения после предыдущей правки
- **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}`,
`business_segment`, `roles` (список `code`), `role_details` (те же активные роли списком `{code, name}`, `name` — `role.name`,
порядок как в `roles`), `permissions` (отсортированный список `code`). Это единственный маршрут
Среза 0, доступный любому аутентифицированному пользователю без `require`.
Среза 0, доступный любому аутентифицированному пользователю без `require`. Состав `permissions` определяется матрицей ролей (§10), включая `QUALITY_METRICS_VIEW`, который seed выдаёт всем ролям.
#### Scenario: Ответ для dev-пользователя
- **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
#### Scenario: Право метрик в профиле
- **WHEN** любой пользователь запрашивает `/api/v1/me` после `make seed`
- **THEN** `permissions` содержит `QUALITY_METRICS_VIEW`, отсортированно, перед `USERS_ADMIN`
### Requirement: Dev-эмитент токенов только в режиме разработки
При `AUTH_DEV_ISSUER=true` **и** `DEBUG=true` ядро SHALL предоставлять `POST /api/v1/dev/token` с телом `{"email"}`,
возвращающий HS256 JWT (срок 8 часов) для существующего активного пользователя. При любом другом сочетании флагов

View File

@ -58,7 +58,7 @@ v5.18 — консолидация, в контексте которой фай
`period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же
версии SHALL отвязываться (R-1). Каждая версия, получившая
привязку, из `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: Причина ошибки маппинга в карточке файла
- **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст)
@ -109,6 +109,10 @@ v5.18 — консолидация, в контексте которой фай
- **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй
- **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый
#### Scenario: Подтверждение с правкой суммы
- **WHEN** аналитик исправил сумму и подтвердил ББ
- **THEN** записан `quality_outcome_fact` `CONFIRMED` со счётчиком правки значения по источнику и `payload.fixes`
### Requirement: Подхват документов при создании версии и расширении периметра
При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из
незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`,

View File

@ -15,7 +15,7 @@ LOCKED`), проверять, что `result_s3_object` относится к б
в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`,
перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку
маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой 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: Результат смапплен
- **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`
- **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: Документ по листу ОСВ
На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный
код из шапки (`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 =
stored_file.uploaded_by` и `comment = «автоподтверждение документа <document_type> <osv_account_code>»` (счёт — только у
ОСВ). Если `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: Чистая ОСВ сразу «Загружен»
- **WHEN** смаплен результат `fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx` для ЮЛ АО «Русский бисквит»
@ -257,6 +265,10 @@ stored_file.uploaded_by` и `comment = «автоподтверждение до
(чистый), A заменён автозаменой
- **THEN** привязка документа A снята, документ B `CONFIRMED` и привязан к версии
#### Scenario: Факт автоподтверждения
- **WHEN** документ автоподтверждён
- **THEN** есть `quality_outcome_fact` `AUTO_CONFIRMED` для этого документа
### Requirement: Хранение результата формы v2.0 (DBML v5.19)
Маппер результата `2.0` SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не
дедуплицируются (повтор кода — отдельные строки), каждая строка результата — `financial_statement_line` (`row_index`,

View File

@ -22,7 +22,7 @@
запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`.
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
- **WHEN** `ka_rf` загружает `rb/2024-FY/ОСВ_66 за 2024.xls` для ЮЛ своего филиала
@ -51,6 +51,10 @@ Query-параметр `consolidation_version_id` MUST NOT приниматьс
- **WHEN** ЮЛ не входит в периметр ни одной версии, файл загружен маршрутом ЮЛ
- **THEN** 201 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию
#### Scenario: Признак скана
- **WHEN** загружается PDF без текстового слоя
- **THEN** `stored_file.is_scan = true`; у xlsx `is_scan IS NULL`
### Requirement: Дедуп по содержимому
Файл с тем же sha256, что у существующего `stored_file` того же ЮЛ со статусом не `SUPERSEDED`, SHALL отклоняться с
кодом `DUPLICATE_FILE` и `file_id` существующего. Тот же файл у другого ЮЛ SHALL приниматься.
@ -147,7 +151,7 @@ NULL), или если автоподтверждённый документ ф
файл ссылаются `superseded_by_file_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: Удаление ошибочного файла
- **WHEN** файл в `ERROR` удаляется владельцем
@ -170,6 +174,10 @@ advisory-блокировка привязок ГК → версии по `id`.
- **WHEN** удаляется файл, все документы которого автоподтверждены и ни к одной версии не привязаны
- **THEN** 204; `after_value.unbound_version_ids = []`
#### Scenario: Файл с фактом приёма
- **WHEN** удаляется файл, по документам которого записан `quality_intake_fact`
- **THEN** 204; объект `uploads` и сырой ответ остаются без `expires_at`, результат (`results`) экспирится; записан факт исхода
### Requirement: Загрузка файлов в консолидацию
`POST /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart `files`, право
`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 быть
обработан циклом маппинга (спека `osv-result-mapping`); inbox MUST NOT читать файл результата. Неизвестный `job_id` SHALL
быть залогирован. Результат для задания, уже находящегося в финальном статусе (`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: Успешный результат
- **WHEN** сервис публикует конверт `status = done` с `result_s3 = {arm-ka, results/<file_id>/<job_id>/result.json}` и
@ -79,15 +79,27 @@
- **WHEN** задание уже `timeout`, и приходит конверт `done`
- **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: Таймаут ожидания результата
Фоновый вахтёр SHALL с периодом 30 с переводить задания `processing` и `published` с `reply_deadline < now()` в
`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: Сервис взял, но не ответил
- **WHEN** задание в `processing` и `reply_deadline` прошёл без результата
- **THEN** не позднее чем через 30 с задание имеет `status = timeout`, файл — `ERROR` с `error_code = TIMEOUT`
#### Scenario: Факт таймаута
- **WHEN** вахтёр перевёл задание в `timeout`
- **THEN** записан `quality_failure_fact` с `failure_kind = TIMEOUT`
### Requirement: Соединение с NATS по шаблону, без JetStream
Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы `OPENBAO__NATS__NATS_SEED/NATS_JWT`, и без учётных данных,
если они пусты; подписываться на subject ответа `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`) в queue group `arm-core`; переподключаться автоматически.

View File

@ -9,7 +9,7 @@ dev-данные загружаются из файлов репозитория
### Requirement: Идемпотентный seed справочников продукта
Команда `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`,
`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`,
`elimination_rule` + `elimination_rule_account` версии 1 для ББ (§4), `required_document_rule` для `BASIC` и `EXTENDED`.
Записи SHALL вставляться или обновляться по натуральному ключу; удаление MUST NOT выполняться.
@ -18,11 +18,11 @@ dev-данные загружаются из файлов репозитория
матрица файла SHALL загружаться целиком; в заполненную SHALL вставляться только пары, у которых роль или право
вставлены этим же запуском seed (их администратор снять не мог), а остальные пары файла MUST NOT добавляться — право,
снятое администратором, не возвращается, добавленное сверх файла не отзывается. Отчёт команды SHALL сообщать, что
матрица пропущена (или сколько добавлено пар новых ролей и прав).
матрица пропущена (или сколько добавлено пар новых ролей и прав). Право `QUALITY_METRICS_VIEW` (объект `QUALITY_METRICS`, действие `VIEW`) SHALL выдаваться всем ролям; на стенде, где матрица уже заполнена, пара добавляется только как пара впервые созданного права — для появления пункта меню нужен повторный `make seed`.
#### Scenario: Первый запуск на пустой схеме
- **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`
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 запущен снова
- **THEN** строка `role` с `code = EXPERT` обновлена, новая не создана
#### Scenario: Право метрик на заполненной матрице
- **WHEN** матрица заполнена, право `QUALITY_METRICS_VIEW` создаётся впервые и seed запущен
- **THEN** добавлены пары права со всеми ролями; ранее снятые администратором связи не возвращены
### Requirement: Счета правил и чек-листа существуют в плане счетов
Все коды счетов, на которые ссылаются `elimination_rules_v1.yaml` и `required_document_rules.yaml`, SHALL резолвиться в
`chart_of_account.code` после seed; `elimination_rule_account.is_excluded = true` SHALL быть у исключений вида