# Spec Delta: osv-result-mapping ## MODIFIED 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` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. 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)` нет #### Scenario: Файл только с формами - **WHEN** смаплен golden-снимок `mezheninovskaya/2025-Q3/1_Форма1_бух_09` (одна форма `BALANCE`, ОСВ нет) - **THEN** файл `PROCESSED`, создан документ `BALANCE`, ошибки «нет документов ОСВ» нет #### Scenario: Пустой результат - **WHEN** результат валиден по схеме, но `documents = []` - **THEN** файл `ERROR` с `metrics.mapping_error = "в результате нет документов"`, повтор не выполняется ## ADDED Requirements ### Requirement: Документ формы ББ/ОФР Каждый `statementDocument` с `document_type ∈ {BALANCE, INCOME}` SHALL давать `document` (`osv_account_code = NULL`, `is_cumulative = NULL`, `locator` из результата, `period_check = UNKNOWN`) и `financial_statement_meta` 1:1. Дата периода — `header.report_date`, при её отсутствии — самая поздняя `columns[].period_date`; `period_year` — год даты, `period_quarter` — квартал, если дата — последний день марта, июня, сентября или декабря, иначе `NULL` с предупреждением; `reporting_date` — эта дата. Шапка: `inn`, `inn_original_value` = `header.inn`, `inn_confidence`, `inn_verified = (inn_confidence = 100)`; `organization_name`; `report_date` = `header.report_date`, `report_date_original_value` = `header.report_date_text`; `units` = `units_original_value` = `header.units`; `okud`, `okpo`, `okopf`, `okfs`, `address` — как пришли; `statement_type` — `balance`/`income`. Колонки — по `columns[]`, `is_current = true` у колонки с самой поздней `period_date`. Строки — по `lines[]` с исходниками кода и названия; повтор кода — первая строка и предупреждение. Значение SHALL создаваться на каждую пару «строка × колонка»: из результата — `amount`, `string_value`, `confidence`, `is_recognized`; отсутствующая ячейка — все поля `NULL`; `is_modified = false`, `original_amount = NULL`. `checks` результата MUST NOT использоваться для статуса. `counterparty_check` — тем же правилом, что у листа ОСВ (ИНН шапки, иначе имя или алиас владельца). #### Scenario: Форма из Excel - **WHEN** смаплен golden-снимок `mezheninovskaya/2025-Q3/1_Форма1_бух_09` - **THEN** документ `BALANCE`, `period_year = 2025`, `period_quarter = Q3`, `reporting_date = 2025-09-30`; у `financial_statement_meta` 37 строк и по значению на каждую пару строка × колонка #### Scenario: Книга с двумя формами - **WHEN** смаплен golden-снимок книги НПЗ `npz/2025-Q3/…НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД` - **THEN** у файла документы `BALANCE` (132 строки) и `INCOME` (43 строки), оба за 2025 Q3 #### Scenario: Форма из OCR без даты отчёта - **WHEN** смаплен эталон нормализатора `ocr/expected_normalize/aep_6` (дата отчёта `null`, колонки баланса 2025-06-30, 2024-12-31, 2023-12-31) - **THEN** документ `BALANCE` за 2025 Q2, `is_current` — у колонки 2025-06-30, `report_date` в шапке `NULL`, `inn` `NULL` #### Scenario: Дата не конец квартала - **WHEN** дата отчёта формы — 2025-10-31 - **THEN** `period_year = 2025`, `period_quarter = NULL`, предупреждение маппинга, документ `NEEDS_CHECK` ### Requirement: Повтор формы в файле и прочие документы Второй документ `BALANCE` или `INCOME` в одном файле MUST NOT создаваться: предупреждение маппинга, первый документ — `NEEDS_CHECK`. Документы `DISCLOSURE` и `OTHER` результата SHALL давать по одному `document` каждого типа на файл без данных (`locator` — все `locator` этого типа через запятую, статус `RECOGNIZED`). Форма `BALANCE`/`INCOME` без строк SHALL создаваться с шапкой и колонками (если колонки есть в результате). #### Scenario: Расшифровки - **WHEN** смаплен синтетический снимок `st06_header_variants` (форма `INCOME` и пять `DISCLOSURE`) - **THEN** у файла два документа: `INCOME` и один `DISCLOSURE` с пятью `locator` через запятую #### Scenario: Форма без колонки кодов - **WHEN** смаплен синтетический снимок `st03_no_codes` (`BALANCE`, строк 0) - **THEN** документ `BALANCE` `NEEDS_CHECK` с шапкой, строк нет (колонок в снимке тоже нет — создаются те, что есть в результате) ### Requirement: Статус документа формы Документ `BALANCE`/`INCOME` SHALL быть `RECOGNIZED`, только если одновременно: задание `done`; нет непройденных неакцептованных жёстких проверок §7 (`statement-checks`); `counterparty_check ≠ MISMATCH`; `period_year` и `period_quarter` определены; у каждого значения `is_modified = true` либо (`is_recognized` не `false` и `confidence` равна 100 или `NULL`); нет предупреждений маппинга по документу; есть хотя бы одна строка. Иначе `NEEDS_CHECK`. Статус SHALL пересчитываться при маппинге, после правки формы и после акцепта, кроме документа `CONFIRMED`. Документ формы `NEEDS_CHECK` SHALL участвовать в переходе версии в `DATA_CORRECTION` наравне с ОСВ (требование «Документ NEEDS_CHECK переводит версию в корректировку»). #### Scenario: Чистая форма Excel - **WHEN** смаплен `mezheninovskaya/2025-Q3/1_Форма2_бух_09` (ОФР, `done`, соотношения сходятся, ИНН шапки = ИНН владельца файла) - **THEN** документ `INCOME` `RECOGNIZED` #### Scenario: Форма OCR - **WHEN** смаплен эталон `ocr/expected_normalize/aep_3` (уверенность значений < 100, ИНН шапки ≠ ИНН владельца) - **THEN** документ `BALANCE` `NEEDS_CHECK`, `counterparty_check = MISMATCH` #### Scenario: Форма двигает версию - **WHEN** версия ГК 2025 Q3 в `DOCUMENTS_PROCESSING`, в периметре ЮЛ файла; смаплена форма `BALANCE` за 2025 Q3 в `NEEDS_CHECK` - **THEN** версия в `DATA_CORRECTION`, привязки `consolidation_document` нет