113 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# osv-result-mapping Specification
## Purpose
Маппинг файла результата распознавания (`recognition-result.schema.json` v1.x, `osvDocument`) в `document` и
`osv_entry` отдельным фоновым циклом (§1.3, §1.5, §3 п. 4, §9.3); резолв счетов и контрагентов; проверки шапки;
статус документа.
## Requirements
### Requirement: Цикл маппинга отделён от inbox
Фоновая задача ядра SHALL с периодом `MAPPING_POLL_SECONDS` (5) выбирать одно задание со `status ∈ {done,
needs_check}`, чей файл имеет `status = PROCESSING` и `current_recognition_job_id` = это задание (`FOR UPDATE SKIP
LOCKED`), проверять, что `result_s3_object` относится к бакету хранилища и его ключ начинается с
`results/{file_id}/{job_id}/` (OQ-70), читать объект из S3 **вне** транзакции, валидировать `RecognitionResult` v1.x и
в одной транзакции создать документы и строки, записать `recognition_job.group_schema`, перевести файл в `PROCESSED`
и выполнить автозамену. Inbox MUST NOT читать файл результата и MUST NOT создавать документы.
#### Scenario: Результат смапплен
- **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING`
- **THEN** не позднее чем через `MAPPING_POLL_SECONDS` + время маппинга файл — `PROCESSED`, создан один `document`
(`OSV`, `osv_account_code = "66"`, `period_year = 2024`, `period_quarter = Q4`, `is_cumulative = true`, `locator = sheet:Лист_1`),
`osv_entry` — все строки результата кроме `TOTAL`, `group_schema` задания содержит `[{"locator": "sheet:Лист_1", "groups": [...]}]`
#### Scenario: Ключ результата вне папки задания
- **WHEN** `result_s3_object.key = results/<другой file_id>/.../result.json`
- **THEN** файл — `ERROR` с ошибкой маппинга, документы не созданы, повтор не выполняется
#### Scenario: Сбой маппинга с повтором
- **WHEN** вставка строк падает по ошибке БД
- **THEN** транзакция откачена, `metrics.mapping_attempts = 1`, файл остаётся `PROCESSING`; на третьей неудаче — `ERROR`
с `metrics.mapping_error`
#### Scenario: Повторный маппинг идемпотентен
- **WHEN** маппинг прерван после частичной вставки (имитация) и выполнен снова
- **THEN** у файла ровно один набор документов и строк, дублей по `(document_id, row_number)` нет
### Requirement: Документ по листу ОСВ
На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный
код из шапки (`60.1 → 60.01`, `58.03`, буквенные сегменты как есть), `period_year`, `period_quarter`, `is_cumulative`
по шапке, `locator`, `legal_entity_id` владельца файла. Второй лист того же файла с тем же счётом MUST NOT создавать
документ: в `metrics.mapping_warnings` пишется предупреждение, первый документ получает `NEEDS_CHECK`.
#### Scenario: Два листа одного счёта
- **WHEN** результат содержит два `osvDocument` со счётом `60`
- **THEN** один `document`, статус `NEEDS_CHECK`, предупреждение называет оба `locator`
### Requirement: Строки ОСВ
Ядро SHALL сохранить все строки `osvDocument.rows` кроме `group_type = TOTAL` (OQ-01) как `osv_entry`: `row_number`,
`level`, `row_value`, `group_type`, `group_name`, `data_type` (включая `NU`), шесть сумм без изменений,
`counterparty_name`/`counterparty_inn` как в результате, `parent_entry_id` по `parent_row_number`, `account_code`
(как в результате), `account_root` (первый сегмент, `None` если он не цифровой — `ASSUMPTION(OQ-103)`), `account_id`
(точное совпадение нормализованного кода с `chart_of_account.code`, иначе NULL), `owner_legal_entity_id`,
`recognition_job_id`, `is_modified = false`.
#### Scenario: Дерево и счета
- **WHEN** смапплен результат `rb/2024-FY/ОСВ_66 за 2024.xls`
- **THEN** строк `osv_entry` = строк результата − 1 (TOTAL); у «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» `parent_entry_id`
указывает на строку `66.03`, `account_code = "66.03"`, `account_root = "66"`, `account_id` = строка плана счетов `66.03`
#### Scenario: Все golden-снимки парсера
- **WHEN** в БД маппятся все `../fixtures/golden/expected_osv_parse/**` (82 результата, маркер `fixtures`)
- **THEN** для каждого: число строк = `rows` без TOTAL, у каждой строки уровня > 0 есть родитель, суммы равны снимку,
`account_id` заполнен у всех строк-счетов, код которых есть в плане счетов seed
### Requirement: Резолв контрагента
Для строк `group_type = COUNTERPARTY` с `data_type ∈ {BU, SUM}` ядро SHALL искать `counterparty_id` по §3 п. 4: по
`counterparty_inn` в `legal_entity.inn`; иначе по точному `counterparty_name` в `legal_entity.name`; иначе по
`normalize_alias(counterparty_name)` среди нормализованных имён ЮЛ и `legal_entity_alias.normalized_alias`; больше
одного кандидата на шаге → `NULL`. Резолв SHALL выполняться по всему справочнику без фильтра видимости. Вместе с
`counterparty_id` SHALL записываться `counterparty_match_kind`: `INN`, `NAME` или `ALIAS` по шагу, на котором нашёлся
единственный кандидат (нормализованное имя ЮЛ и алиас — оба `ALIAS`); при `NULL` — NULL.
#### Scenario: По ИНН, по алиасу, неоднозначно
- **WHEN** в справочнике есть ЮЛ с ИНН `3528052718`, ЮЛ с алиасом `русский бисквит`, и два ЮЛ с одинаковым нормализованным именем `альфа`
- **THEN** строка с ИНН `3528052718` → это ЮЛ, `counterparty_match_kind = INN`; строка «Русский бисквит» без ИНН → ЮЛ по
алиасу, `ALIAS`; строка «ООО Альфа» → `counterparty_id = NULL`, `counterparty_match_kind = NULL`
### Requirement: Проверки шапки и статус документа
`counterparty_check` SHALL быть `MATCH`, если ИНН шапки равен ИНН владельца, или (без ИНН) нормализованное имя шапки
совпадает с нормализованным именем или алиасом владельца; `MISMATCH` иначе; `UNKNOWN` при пустой шапке.
`period_check` SHALL быть `UNKNOWN` до появления версии консолидации (Срез 4). Для `period_quarter ∈ {Q2, Q3, Q4}` с
`is_cumulative = false` ядро SHALL сверить сальдо на начало корневой строки с документом `Q1` того же года, ЮЛ и
счёта (файл не `SUPERSEDED`) и при совпадении (±0.01) установить `is_cumulative = true` с предупреждением
(`ASSUMPTION(OQ-02)`). Статус документа SHALL быть `RECOGNIZED`, если задание `done`, `checks` без нарушений,
`counterparty_check ≠ MISMATCH` и нет предупреждений маппинга; иначе `NEEDS_CHECK` (OQ-05).
#### Scenario: Шапка другой организации
- **WHEN** файл загружен для ЮЛ «Пекарня Счастливочная», а в шапке `АО "Русский бисквит"`
- **THEN** `counterparty_check = MISMATCH`, `status = NEEDS_CHECK`
#### Scenario: Нарастающая по данным
- **WHEN** у ЮЛ есть документ ОСВ 60 за 2024 Q1 (`opening_*` корня = X) и маппится ОСВ 60 «за 3 квартал 2024» с `opening_*` корня = X
- **THEN** `is_cumulative = true`, предупреждение «нарастающая по данным Q1»
#### Scenario: Чистый результат
- **WHEN** задание `done`, шапка совпадает с владельцем, нарушений нет
- **THEN** `status = RECOGNIZED`, `counterparty_check = MATCH`, `period_check = UNKNOWN`
### Requirement: Документ NEEDS_CHECK переводит версию в корректировку
После фиксации документов файла цикл маппинга SHALL для каждой версии в `DOCUMENTS_PROCESSING`, в периметр которой
входит ЮЛ файла, проверить документы файла: если среди них есть документ `NEEDS_CHECK`, подходящий версии по периоду
(правила привязки `documents-api`), версия SHALL перейти в `DATA_CORRECTION` с `changed_by = stored_file.uploaded_by`
в той же транзакции маппинга (версии блокируются `FOR UPDATE` по `id` после файла). Если `uploaded_by` NULL, переход
SHALL пропускаться с записью в лог. Привязка `consolidation_document` при маппинге MUST NOT создаваться.
#### Scenario: Предупреждение маппинга двигает статус
- **WHEN** версия 2024 Q3 в `DOCUMENTS_PROCESSING`; смапплен файл ЮЛ периметра с ОСВ 62 за 2024 Q3 в `NEEDS_CHECK`
- **THEN** версия в `DATA_CORRECTION`, запись истории с `changed_by` = загрузивший; `consolidation_document` нет
#### Scenario: Документ RECOGNIZED
- **WHEN** смапплен файл с единственным документом `RECOGNIZED`
- **THEN** статус версии не изменился