Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
8.9 KiB
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