Ссылки README и CLAUDE.md переведены на архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
19 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 и выполнить автозамену. Результат без документов 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 = []): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызовmap_fileдаётMappingFatal - THEN файл
ERROR, причина вmetrics.mapping_error(при проверке схемы — ошибка схемы, вmap_file— «в результате нет документов»), повтор не выполняется
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 статус версии не изменился
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_meta37 строк и по значению на каждую пару строка × колонка
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,innNULL
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 документ
BALANCENEEDS_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 документ
INCOMERECOGNIZED
Scenario: Форма OCR
- WHEN смаплен эталон
ocr/expected_normalize/aep_3(уверенность значений < 100, ИНН шапки ≠ ИНН владельца) - THEN документ
BALANCENEEDS_CHECK,counterparty_check = MISMATCH
Scenario: Форма двигает версию
- WHEN версия ГК 2025 Q3 в
DOCUMENTS_PROCESSING, в периметре ЮЛ файла; смаплена формаBALANCEза 2025 Q3 вNEEDS_CHECK - THEN версия в
DATA_CORRECTION, привязкиconsolidation_documentнет