Andrey Epifancev 4e42a93f54 chore(openspec): архив unified-normalization-core-a и -core-b (#126, #131)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:26:03 +04:00

51 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 и выполнить автозамену. Результат без документов 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

Scenario: Алиас приоритетнее ЮЛ-тёзки

  • WHEN в справочнике есть ЮЛ «ООО Гамма Сервис» и другое ЮЛ с алиасом гамма сервис, строка — «Гамма Сервис» без ИНН
  • THEN counterparty_id — владелец алиаса, counterparty_match_kind = ALIAS; тот же результат при дорезолве свода на чтение

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). Документ RECOGNIZED в той же транзакции SHALL подтверждаться автоматически (требование «Автоподтверждение чистого документа»).

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 = CONFIRMED (автоподтверждение), 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 для документа NEEDS_CHECK при маппинге MUST NOT создаваться (привязку при маппинге создаёт только автоподтверждение).

Scenario: Предупреждение маппинга двигает статус

  • WHEN версия 2024 Q3 в DOCUMENTS_PROCESSING; смапплен файл ЮЛ периметра с ОСВ 62 за 2024 Q3 в NEEDS_CHECK
  • THEN версия в DATA_CORRECTION, запись истории с changed_by = загрузивший; consolidation_document нет

Scenario: Документ RECOGNIZED

  • WHEN версия 2024 Q3 в DOCUMENTS_PROCESSING; смапплен файл с единственным чистым документом за 2024 Q3
  • THEN документ CONFIRMED и привязан; переход версии в DATA_CORRECTION записан автоподтверждением (комментарий «автоподтверждение документа …»), этот хук второго перехода не делает

Scenario: Документ не подходит версии

  • WHEN версия 2024 Q3 в DOCUMENTS_PROCESSING; смапплен файл с единственным документом NEEDS_CHECK за 2025 Q1
  • 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, okei, address — как пришли (шапка 1.x — по полям 1.x); statement_type — balance/income. Колонки — по columns[], is_current = true у колонки с самой поздней period_date. Строки — по lines[] с исходниками кода и названия; повтор кода сохраняется как отдельная строка (Р-4), порядок строк — row_index (с 0). Значение SHALL создаваться на каждую пару «строка × колонка»: из результата — amount, string_value, confidence, is_recognized; отсутствующая ячейка — пустая запись с needs_review = false; is_modified = false, original_amount = NULL. Сохранение результата — требование «Хранение результата формы v2.0 (DBML v5.19)». Нарушения checks результата 2.0 — в document_issue; для 1.x §7 считает ядро при приёме (требование «Формы результата 1.x — §7 при приёме»). 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 результата SHALL давать отдельный document с собственным file_ordinal; вторая форма того же типа в файле хранится (Р-10). Формы одного типа и одной пары period_year, period_quarter получают предупреждение маппинга «в файле есть другая форма <тип> за тот же период», документ — NEEDS_CHECK, автоподтверждения нет (Р-18). Документы DISCLOSURE и OTHER результата SHALL давать по одному document каждого типа на файл (file_ordinal — индекс первого такого документа в documents[]) без данных (locator — все locator этого типа через запятую, статус при маппинге RECOGNIZED, затем автоподтверждение → CONFIRMED). Форма BALANCE/INCOME без строк SHALL создаваться с шапкой и колонками (если колонки есть в результате).

Scenario: Расшифровки

  • WHEN смаплен синтетический снимок st06_header_variants (форма INCOME и пять DISCLOSURE)
  • THEN у файла два документа: INCOME и один DISCLOSURE с пятью locator через запятую; DISCLOSURE — CONFIRMED

Scenario: Форма без колонки кодов

  • WHEN смаплен синтетический снимок st03_no_codes (BALANCE, строк 0)
  • THEN документ BALANCE NEEDS_CHECK с шапкой, строк нет (колонок в снимке тоже нет — создаются те, что есть в результате)

Scenario: Две формы одного типа и периода

  • WHEN результат содержит два BALANCE с одной парой period_year, period_quarter
  • THEN создаются два документа с разными file_ordinal, оба NEEDS_CHECK с предупреждением маппинга; ни один не автоподтверждается

Requirement: Статус документа формы

Документ BALANCE/INCOME, принятый по результату 1.x, SHALL быть RECOGNIZED, только если одновременно: задание done; нет записей document_issue вида relation без акцепта (§7, посчитанный ядром при приёме, statement-checks); counterparty_check ≠ MISMATCH; period_year и period_quarter определены; ни у одного значения нет needs_review = true (при приёме 1.x needs_review = НЕ values_ok); нет предупреждений маппинга по документу; в файле нет второй формы того же типа и периода (Р-18); есть хотя бы одна строка. Иначе NEEDS_CHECK. Для результата 2.0 условие — требование «Условие RECOGNIZED при приёме 2.0». Статус SHALL пересчитываться при маппинге, после правки формы и после акцепта, кроме документа CONFIRMED. Документ RECOGNIZED после маппинга SHALL подтверждаться автоматически (требование «Автоподтверждение чистого документа»); RECOGNIZED, полученный пересчётом после правки или акцепта, — только вручную. Документ формы NEEDS_CHECK SHALL участвовать в переходе версии в DATA_CORRECTION наравне с ОСВ (требование «Документ NEEDS_CHECK переводит версию в корректировку»).

Scenario: Чистая форма Excel

  • WHEN смаплен mezheninovskaya/2025-Q3/1_Форма2_бух_09 (ОФР, done, соотношения сходятся, ИНН шапки = ИНН владельца файла)
  • THEN документ INCOME CONFIRMED (автоподтверждение)

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 нет

Scenario: Чистая после правки — вручную

  • WHEN форма NEEDS_CHECK исправлена аналитиком (PATCH …/statement) и пересчёт дал RECOGNIZED
  • THEN документ остаётся RECOGNIZED до POST …/confirm

Requirement: Автоподтверждение чистого документа

В транзакции маппинга после автозамены прежних версий файла и до перехода версий по NEEDS_CHECK цикл маппинга SHALL перевести каждый документ файла, получивший при маппинге статус RECOGNIZED, в CONFIRMED (ТЗ 03_9 п. 4, 10_10; решение владельца 08.10.2026, OQ-157): confirmed_by = NULL, confirmed_at = now; у формы BALANCE/INCOME — financial_statement_meta.reviewed_by = NULL, reviewed_at = now; audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS, object_type = document) с user_id = NULL, before_value.status = RECOGNIZED, after_value.status = CONFIRMED и description = «автоподтверждение: без замечаний». Форма BALANCE/INCOME с шапкой SHALL подтверждаться автоматически, только если выполнены условия ручного подтверждения (period_year и period_quarter определены, нет непройденных неакцептованных жёстких проверок §7); иначе она MUST оставаться RECOGNIZED без ошибки. Документы NEEDS_CHECK MUST NOT подтверждаться автоматически.

Автоподтверждённый документ SHALL привязываться к версиям так же, как при ручном подтверждении (documents-api «Подтверждение документа»: все подходящие версии «в работе», замена привязки того же ключа R-1), и каждая версия, получившая привязку, из INITIATED или DOCUMENTS_PROCESSING SHALL переходить в DATA_CORRECTION с changed_by = stored_file.uploaded_by и comment = «автоподтверждение документа <document_type> <osv_account_code>» (счёт — только у ОСВ). Если uploaded_by NULL, привязка SHALL создаваться, а переход — пропускаться с записью в лог. Пересчёт статуса формы после правки или акцепта MUST NOT подтверждать документ автоматически.

Scenario: Чистая ОСВ сразу «Загружен»

  • WHEN смаплен результат fixtures/osv/rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsx для ЮЛ АО «Русский бисквит» (задание done, шапка совпадает с владельцем, нарушений и предупреждений нет)
  • THEN документ OSV 60 — CONFIRMED, confirmed_by NULL, confirmed_at заполнен; есть audit_event(DOCUMENT_CONFIRM) с user_id NULL и описанием «автоподтверждение: без замечаний»

Scenario: Привязка и переход версии

  • WHEN версия ГК «Русский Бисквит» 2025 Q1 в INITIATED, РБ в периметре; смаплен чистый результат ОСВ 60 РБ за 2025 Q1, файл загрузил ka_go
  • THEN consolidation_document(version, document) создан; версия в DATA_CORRECTION; запись истории INITIATED → DATA_CORRECTION с changed_by = ka_go и комментарием «автоподтверждение документа OSV 60»

Scenario: Документ с замечанием ждёт аналитика

  • WHEN файл ОСВ загружен для ЮЛ «Пекарня Счастливочная», а в шапке АО "Русский бисквит"
  • THEN документ NEEDS_CHECK, confirmed_at пуст, события DOCUMENT_CONFIRM нет, привязки нет

Scenario: Чистая форма Excel

  • WHEN смаплен statements-xlsx/mezheninovskaya/2025-Q3/1_Форма2_бух_09 (ОФР, done, соотношения сходятся, период определён)
  • THEN документ INCOME CONFIRMED, reviewed_by NULL, reviewed_at заполнен

Scenario: Форма OCR не подтверждается сама

  • WHEN смаплен эталон ocr/expected_normalize/БО 1 кв 2025 (уверенность значений < 100, дата отчёта не разобрана)
  • THEN документы BALANCE и INCOME — NEEDS_CHECK, не CONFIRMED

Scenario: Файл без загрузившего

  • WHEN версия в INITIATED, ЮЛ в периметре; смаплен чистый результат файла с uploaded_by NULL
  • THEN документ CONFIRMED и привязан к версии; версия осталась INITIATED; в логе — строка о пропуске перехода

Scenario: Автозамена чистой версией файла

  • WHEN документ ОСВ 60 РБ за 2025 Q1 из файла A привязан к версии 2025 Q1; загружен и смаплен файл B того же ключа (чистый), A заменён автозаменой
  • THEN привязка документа A снята, документ B CONFIRMED и привязан к версии

Requirement: Хранение результата формы v2.0 (DBML v5.19)

Маппер результата 2.0 SHALL писать документ формы как пришёл: строки, коды и значения не сливаются и не дедуплицируются (повтор кода — отдельные строки), каждая строка результата — financial_statement_line (row_index, source_ref = ref, line_status, line_code, line_code_original_value = line_code результата, line_code_text, line_code_confidence, line_name, line_name_confidence, parent_row_index — row_index строки с ref = parent_ref, needs_review, is_confirmed = false); каждая пара «строка × колонка» — одна financial_statement_value (amount, string_value, source_text = values[].text, confidence, is_recognized, needs_review; значения, которого нет в результате, — пустая запись с needs_review = false); колонки — financial_statement_column с period_kind, label_confidence; шапка — financial_statement_meta с form_variant, needs_review_fields и header_source (раздел «Шапка формы»); предупреждения и нарушения документа — document_issue (источник service); версия общего кода — recognition_job.statements_version. Единственные случаи, когда элемент результата не записывается, — нарушение уникальности ключей хранения, и каждый из них MUST фиксироваться document_issue MAPPING_WARNING (info, источник core): значение с column_index вне колонок формы; повтор column_index колонки — остаётся первая колонка, остальные пропускаются; повтор row_index или ref у строк — остаётся первая строка, остальные пропускаются; detail с parent_ref, которого нет, хранится с parent_row_index = null. Уникальность (meta_id, line_code) снята; (meta_id, source_ref) уникален там, где source_ref не null. Каждый BALANCE / INCOME результата SHALL становиться отдельным document; вторая форма того же типа в файле хранится (Р-10); ОСВ по счёту, DISCLOSURE и OTHER — по-прежнему один документ на тип (и счёт) на файл. Каждый создаваемый document (ОСВ, формы, DISCLOSURE, OTHER) при приёме результата любого MAJOR (мапперы 1.x и 2.0) SHALL получать file_ordinal = индекс документа в documents[] результата (с 0); документ, который не создаётся (второй лист ОСВ с тем же счётом, второй DISCLOSURE), номер не занимает, но и не сдвигает остальные — номера уникальны в файле и могут идти с пропусками.

Scenario: Книга из двух ОСВ

  • WHEN результат содержит две ОСВ по счетам 60 и 62 (documents[0], documents[1]), принятый маппером 1.x или 2.0
  • THEN два документа с file_ordinal 0 и 1; уникальность (stored_file_id, file_ordinal) не нарушена, файл не ERROR

Scenario: PDF с ББ и ОФР по 1.x

  • WHEN результат 1.2 содержит BALANCE и INCOME
  • THEN документы с file_ordinal 0 и 1

Scenario: Повтор кода сохраняется

  • WHEN в результате две строки ok с кодом 2410 (пустая и с (649))
  • THEN в БД две строки 2410 с разными row_index и source_ref, обе со своими значениями

Scenario: Форма 2.0 хранится как пришла

  • WHEN результат 2.0 содержит две строки с кодом 1230 и строку без кода (ref с parent_ref)
  • THEN создано три строки с их row_index, source_ref и parent_row_index; повтор кода не сливается

Scenario: Строка без кода

  • WHEN в результате строка unreadable с line_code_text = "ИЧ,250" и суммой
  • THEN строка хранится с line_code = null, line_code_original_value = null, line_status = unreadable, line_code_text = "ИЧ,250" и row_index из результата

Scenario: Два баланса одного периода в одном файле

  • WHEN результат содержит два BALANCE с одной парой period_year, period_quarter (лист-продолжение другого типа или двойная печать)
  • THEN создаются два документа с разными file_ordinal; ни один не автоподтверждается (Р-18)

Scenario: Два баланса разных периодов в одном файле

  • WHEN результат содержит BALANCE на 30.09.2025 и BALANCE на 31.12.2024
  • THEN два документа с разными file_ordinal; каждый получает статус по своим условиям, правило Р-18 их не касается

Scenario: Повтор column_index колонки

  • WHEN в columns[] результата два элемента с column_index = 1
  • THEN сохраняется первая колонка, вторая пропускается, у документа MAPPING_WARNING

Scenario: Повтор ref строки

  • WHEN в результате две строки с одним ref
  • THEN сохраняется первая, вторая пропускается с MAPPING_WARNING; приём не падает

Requirement: Признак строки, созданной правкой

Строка формы SHALL считаться созданной правкой аналитика (шаблонной, origin = template) тогда и только тогда, когда row_index IS NULL. Маппер 1.x и маппер 2.0 SHALL всегда писать row_index (у 1.x — по порядку строк результата с 0), поэтому у строки из источника он не null. line_code_original_value SHALL хранить код строки из результата как пришёл (null у unreadable / missing / detail и у строк, созданных правкой) и правками кода MUST NOT меняться. Признак line_code_original_value IS NULL для origin MUST NOT использоваться.

Scenario: Строка источника без кода — не шаблонная

  • WHEN у строки unreadable из результата line_code_original_value = null и row_index = 7
  • THEN origin = source; правила шаблонных строк (удаление без сумм, 409) к ней не применяются

Scenario: Назначение одного кода двум строкам источника

  • WHEN аналитик назначил код 1250 строке unreadable (row_index = 7, без сумм), а затем тот же код — строке missing (row_index = 2) с суммой
  • THEN обе строки ok с кодом 1250, ни одна не удалена, 409 нет (повтор кода законен, Р-4)

Requirement: Шапка формы — сырой текст и уверенность

Маппер 2.0 SHALL писать в financial_statement_meta.header_source объект {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для всех полей шапки результата (organization_name, inn, report_date, units, okud, okpo, okopf, okfs, okei, address); разобранные значения — в одноимённые колонки; *_original_value — как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; organization_name_original_value = organization_name, inn_confidence = inn_confidence. header_source и *_original_value правками MUST NOT меняться. Маппер 1.x заполняет header_source из полей шапки 1.x. StatementOut.header.source SHALL отдавать header_source.

Scenario: Подсказка у поля шапки

  • WHEN в результате organization_name_text = "ООО «Ромашкa»", organization_name_confidence = 67
  • THEN header_source.organization_name = {"text": "ООО «Ромашкa»", "confidence": 67}; экран показывает их в подсказке у поля

Requirement: Условие RECOGNIZED при приёме 2.0

При приёме 2.0 статус документа формы SHALL быть RECOGNIZED ⇔ document.status = done и он согласован с содержимым документа ∧ в metrics.warnings файла нет предупреждений уровня error ∧ counterparty_check ≠ MISMATCH ∧ период — конец квартала (period_year и period_quarter определены) ∧ у каждой строки ok с неуверенным кодом (line_code_confidence < 100) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того же типа и периода (та же пара period_year, period_quarter; формы без квартала не считаются); иначе NEEDS_CHECK. Согласованность: ядро SHALL вызывать общую функцию статуса пакета (statement-normalization, «Шаг 8 — статус документа и конверта») по входу, собранному из результата: layout_found — lines[] непуст, has_organization_or_inn и period_known — по шапке и колонкам результата, строки и значения с их needs_review (значения detail функция не учитывает), число checks.relations_failed, accepted = false, уровни warnings[] документа. done согласован, если функция тоже даёт done; иначе ядро считает документ needs_check и пишет document_issue RESULT_STATUS_INCONSISTENT (info, источник core). Это вызов той же функции, а не пересчёт §7 и needs_review. Статус конверта и needs_check соседнего документа в условие MUST NOT входить (Р-19). Проверки §7, needs_review и предупреждения сервиса при загрузке MUST NOT пересчитываться.

Совпадение наименований: обе строки приводятся к нижнему регистру, ё → е, всё, кроме букв и цифр, — пробел, пробелы схлопываются, крайние пробелы обрезаются (strip); совпало, если строки равны, или более короткая (не короче 5 символов) входит в более длинную, или отношение сходства difflib.SequenceMatcher ≥ 0,8; сравнение — с любым справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются номера раздела: номер — начальная часть первого слова после фразы из букв i, v, x (латиница; кириллические «і», «х» приводятся к ним), остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени — совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало. Несовпадение — document_issue NAME_MISMATCH (info, источник core, строка). RECOGNIZED SHALL, как и прежде, автоподтверждаться (OQ-157).

Scenario: Ячейка соседнего документа

  • WHEN ББ done, ОФР того же файла needs_check, предупреждений файла уровня error нет, ИНН совпал, период — 30.09.2025
  • THEN ББ RECOGNIZED и автоподтверждается, ОФР — NEEDS_CHECK

Scenario: Потерянный json

  • WHEN все документы done, но в metrics.warnings файла JSON_FAILED (или TABLE_PARSE_ERROR) уровня error
  • THEN все документы формы файла NEEDS_CHECK

Scenario: Подмена неуверенного кода на соседа

  • WHEN строка с кодом 1120 (уверенность 50) и наименованием «Нематериальные активы»
  • THEN наименование не совпало со справочным «Результаты исследований и разработок», документ NEEDS_CHECK, NAME_MISMATCH у строки

Scenario: Законный повтор с другим именем

  • WHEN строка 1220 (уверенность 50) с наименованием «НДС, начисленный по отгрузке», а в словаре имён fsbu4 у 1220 есть это имя вторым
  • THEN наименование совпало, NAME_MISMATCH нет

Scenario: Подмена итога раздела

  • WHEN строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
  • THEN номер раздела I не равен II у справочного «Итого по разделу II», NAME_MISMATCH

Scenario: Хвост OCR после номера раздела (Счастливочная, РБ)

  • WHEN строка 1500 (уверенность 56, разлепленная) с наименованием «Итого по разделу V1», или строка с наименованием «Итого по разделу V /»
  • THEN номер раздела V равен номеру справочного «Итого по разделу V», наименование совпало, NAME_MISMATCH нет

Scenario: Сервис прислал done при неразобранной строке без значений

  • WHEN документ пришёл done, но в нём строка unreadable без значений
  • THEN функция статуса даёт needs_check (не все строки ok / detail), документ NEEDS_CHECK, RESULT_STATUS_INCONSISTENT; автоподтверждения нет

Scenario: Сервис ошибся со статусом

  • WHEN документ пришёл done, но в нём строка unreadable с суммой
  • THEN документ NEEDS_CHECK, RESULT_STATUS_INCONSISTENT у документа; автоподтверждения нет

Scenario: Период не конец квартала

  • WHEN документ done с датой отчёта 15.08.2025
  • THEN квартал не определён, документ NEEDS_CHECK (как в D-6) и не автоподтверждается; ручное подтверждение — 409 PERIOD_REQUIRED, пока аналитик не поправит дату отчёта

Requirement: Формы результата 1.x — §7 при приёме

Для формы, принятой по результату 1.x (окно перехода после выкладки ядра; checks контракта 2.0 у неё нет), ядро SHALL один раз при приёме посчитать §7 точкой входа пакета для пересчёта (statement-normalization; вариант формы — по структуре кодов, как у 2.0) по суммам результата и записать нарушения в document_issue (источник core, виды relation / soft, как в требовании о document_issue) и form_variant в financial_statement_meta; собственного пересчёта §7 ядро MUST NOT выполнять. Условие D-6 «проверки сошлись» для 1.x SHALL браться из этого расчёта; needs_review значения при приёме 1.x = НЕ values_ok (values_ok — is_recognized не false и confidence равна 100 или пуста), needs_review строки — false. Предупреждения маппера 1.x по документу (повтор кода, значение вне колонок) — document_issue MAPPING_WARNING (info, источник core); строки metrics.warnings результата 1.x — в recognition_job.metrics.warnings структурами {code: "LEGACY_WARNING", severity: "info", message}. После правки такая форма пересчитывается общим кодом, как форма 2.0; MAPPING_WARNING и LEGACY_WARNING статус не держат. Формы, существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед выкладкой»).

Scenario: Полярность needs_review при приёме 1.x

  • WHEN в результате 1.2 у значения confidence = 100, is_recognized = true, у другого — confidence = 80
  • THEN у первого needs_review = false, у второго — true

Scenario: Форма 1.x с нарушением

  • WHEN результат 1.2 содержит ББ с несошедшимся 1600 = 1700
  • THEN у формы запись document_issue relation RELATION_FAILED с codes = ["1600", "1700"] (источник core), документ NEEDS_CHECK; POST …/confirm без акцепта — 409 CHECKS_NOT_PASSED

Scenario: 1.x-документ после PATCH

  • WHEN форма принята по результату 1.2, аналитик правит сумму 1230 так, что 1200 = 1210 + … + 1260 сходится
  • THEN ядро пересчитывает §7 и статус общим кодом, запись relation по 1200 заменена (её больше нет), статус пересчитан; STATEMENTS_VERSION_MISMATCH не пишется (у задания 1.x версии нет)

Requirement: Проверки ядра при приёме результата 2.0

При приёме результата 2.0 ядро SHALL писать в document_issue формы предупреждения и checks результата (источник service) и проверки ядра (источник core): CODE_NOT_IN_DICTIONARY и CODE_NOT_IN_EDITION (редакция — edition точки входа пакета, а не по дате; одна запись на код документа, line_ids — все строки ok кода; только для кодов из ровно четырёх цифр — пятизначные подстроки Excel не проверяются; запись не пишется, если пакет не определил редакцию), NAME_MISMATCH, RESULT_STATUS_INCONSISTENT (согласованность done — вызовом engine.contract_document_status пакета) и STATEMENTS_VERSION_MISMATCH (при отличии statements_version задания от версии копии ядра; у 1.x не пишется). Проверки §7, needs_review и предупреждения сервиса MUST NOT пересчитываться при загрузке. Статус документа формы — по требованию «Условие RECOGNIZED при приёме 2.0».

Scenario: Пятизначный код не даёт info

  • WHEN в ББ строка с кодом 12301 (подстрока Excel), которого нет в словаре
  • THEN CODE_NOT_IN_DICTIONARY и CODE_NOT_IN_EDITION для него не пишутся

Scenario: Четырёхзначный код вне перечня

  • WHEN в ББ строка 1120 на бланке fsbu4, где такого кода нет
  • THEN запись CODE_NOT_IN_EDITION (info, core) с line_ids строк ok этого кода

Scenario: Версия копии отличается

  • WHEN результат 2.0 принят с statements_version, отличной от версии копии ядра
  • THEN запись STATEMENTS_VERSION_MISMATCH (info, core) уже при приёме

Requirement: Ведомость ОСВ по контракту 2.0

Для листа ОСВ результата 2.0 ядро SHALL создавать документ ОСВ по osvDocument, предупреждения листа (osvDocument.warnings, OSV_*) писать в document_issue (вид warning, источник service), а статус документа определять по требованию «Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0». Статус ОСВ MUST NOT зависеть от статуса соседнего документа книги и от статуса конверта.

Scenario: ОСВ в книге с ОФР, требующим проверки

  • WHEN книга из ОСВ (status = done, контрольные суммы сошлись) и ОФР (needs_check)
  • THEN документ ОСВ RECOGNIZED, документ ОФР NEEDS_CHECK