Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
113 lines
11 KiB
Markdown
113 lines
11 KiB
Markdown
# 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** статус версии не изменился
|