8.9 KiB
Raw Blame History

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 выполняться по всему справочнику без фильтра видимости.

Scenario: По ИНН, по алиасу, неоднозначно

  • WHEN в справочнике есть ЮЛ с ИНН 3528052718, ЮЛ с алиасом русский бисквит, и два ЮЛ с одинаковым нормализованным именем альфа
  • THEN строка с ИНН 3528052718 → это ЮЛ; строка «Русский бисквит» без ИНН → ЮЛ по алиасу; строка «ООО Альфа» → counterparty_id = 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