Архив change quality-metrics (AURORA-1759, #229) #230
@ -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`, форматом, фильтрами и числом строк
|
||||
|
||||
@ -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 часов) для существующего активного пользователя. При любом другом сочетании флагов
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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 до приёма файлов проверить: консолидация видима (ГК в области пользователя;
|
||||
|
||||
42
openspec/specs/quality-metrics-ui/spec.md
Normal file
42
openspec/specs/quality-metrics-ui/spec.md
Normal 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** пункт «Пользователи и роли» скрыт
|
||||
123
openspec/specs/quality-metrics/spec.md
Normal file
123
openspec/specs/quality-metrics/spec.md
Normal 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`
|
||||
@ -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`; переподключаться автоматически.
|
||||
|
||||
@ -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 быть у исключений вида
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user