"""Адаптер `StatementsEngine` поверх реального пакета `arm_statements` (часть Б, B1).""" import datetime import json from decimal import Decimal from pathlib import Path import pytest from arm_statements import DocStatus, DocumentType, Edition, FormVariant, PeriodKind, default_rules, statements_version from src.contract import StatementDocumentV2 from src.domain.stored_form import build_stored_form, edition_codes from src.models.enums import StatementLineStatus from src.services.statements_engine import ArmStatementsEngine, default_engine from tests.fake_engine import FakeEngine, empty_result from tests.test_domain_stored_form import make_column, make_document, make_line, make_meta, make_value EXAMPLES = Path(__file__).resolve().parent.parent / "contracts" / "examples" ENGINE = default_engine() def _documents(name: str) -> list[dict[str, object]]: raw = json.loads((EXAMPLES / name).read_text(encoding="utf-8")) return [d for d in raw["documents"] if d["document_type"] in ("BALANCE", "INCOME", "DISCLOSURE")] @pytest.mark.parametrize("name", ["result-statement-minimal.json", "result-statement-excel.json"]) def test_contract_document_status_matches_examples(name: str) -> None: documents = _documents(name) assert documents for doc in documents: assert ENGINE.contract_document_status(doc) == DocStatus(doc["status"]) if name == "result-statement-minimal.json": assert ENGINE.contract_document_status(documents[0]) is DocStatus.NEEDS_CHECK # строка unreadable def test_edition_codes_and_engine_edition() -> None: doc = StatementDocumentV2.model_validate(_documents("result-statement-minimal.json")[0]) pairs = edition_codes(doc) effective = datetime.date(2025, 9, 30) edition = ENGINE.edition(DocumentType.BALANCE, pairs, effective, FormVariant.FULL) assert isinstance(edition, Edition) # та же форма, собранная из этого результата в строки БД: редакция пересчёта совпадает с редакцией приёма column = make_column() lines = [] for ln in doc.lines: line = make_line( ln.line_code, row_index=ln.row_index, status=StatementLineStatus(ln.line_status), confidence=None if ln.line_code_confidence is None else str(ln.line_code_confidence), ) lines.append((line, [make_value(line, column, v.amount, confidence=str(v.confidence)) for v in ln.values])) form = build_stored_form(make_document(effective), make_meta(organization_name="ООО"), [column], lines, []) assert ENGINE.recompute(form).edition == edition @pytest.mark.parametrize(("modified", "confirmed"), [(False, False), (True, False), (False, True)]) def test_edition_parity_for_unsure_fixed_code(modified: bool, confirmed: bool) -> None: """T1.1: строка `ok` с `line_code_confidence < 100` и `code_fixed` (назначенная / подтверждённая) — редакция приёма (`edition_codes` + `engine.edition`) совпадает с редакцией пересчёта из БД.""" doc = StatementDocumentV2.model_validate(_documents("result-statement-minimal.json")[0]) first_ok = next(ln for ln in doc.lines if ln.line_status == "ok") doc = doc.model_copy( update={ "lines": [ ln.model_copy(update={"line_code_confidence": Decimal(50)}) if ln is first_ok else ln for ln in doc.lines ] } ) unsure = next(ln for ln in doc.lines if ln.line_status == "ok") effective = datetime.date(2025, 9, 30) edition = ENGINE.edition(DocumentType.BALANCE, edition_codes(doc), effective, FormVariant.FULL) column = make_column() lines = [] for ln in doc.lines: fixed = ln is unsure line = make_line( ln.line_code, row_index=ln.row_index, status=StatementLineStatus(ln.line_status), confidence=None if ln.line_code_confidence is None else str(ln.line_code_confidence), modified=modified and fixed, confirmed=confirmed and fixed, ) lines.append((line, [make_value(line, column, v.amount, confidence=str(v.confidence)) for v in ln.values])) form = build_stored_form(make_document(effective), make_meta(organization_name="ООО"), [column], lines, []) assert ENGINE.recompute(form).edition == edition def test_edition_codes_skip_ok_line_without_code() -> None: """T1.2: строка `ok` без кода не даёт пару `("None", …)`.""" doc = StatementDocumentV2.model_validate(_documents("result-statement-minimal.json")[0]) ok_line = next(ln for ln in doc.lines if ln.line_status == "ok") count = len(edition_codes(doc)) doc = doc.model_copy( update={"lines": [ln.model_copy(update={"line_code": None}) if ln is ok_line else ln for ln in doc.lines]} ) pairs = edition_codes(doc) assert len(pairs) == count - 1 assert all(code != "None" for code, _ in pairs) def test_engine_scenario_recompute_after_edit() -> None: column = make_column() effective = datetime.date(2025, 9, 30) l1210 = make_line("1210", row_index=0) l1230 = make_line("1230", row_index=1) l1250 = make_line("1250", row_index=2, confidence="25", modified=True) # бывшая unreadable с назначенным кодом l1260 = make_line("1260", row_index=None, confidence=None, modified=None) # шаблонная строка l1200 = make_line("1200", row_index=3) detail = make_line(None, row_index=4, status=StatementLineStatus.detail, parent_row_index=3) pairs = [ (l1210, [make_value(l1210, column, "100")]), (l1230, [make_value(l1230, column, "428311", confidence="60", modified=True)]), (l1250, [make_value(l1250, column, "50", confidence="67", modified=True)]), (l1260, [make_value(l1260, column, "10", confidence=None, recognized=None, modified=True)]), (l1200, [make_value(l1200, column, "428471")]), (detail, [make_value(detail, column, "5", confidence="60")]), ] form = build_stored_form(make_document(effective), make_meta(organization_name="ООО"), [column], pairs, []) result = ENGINE.recompute(form) assert result.edition is Edition.FSBU4 assert result.form_variant is FormVariant.FULL key = str(column.id) assert result.value_needs_review[(str(l1230.id), key)] is False assert result.value_needs_review[(str(detail.id), key)] is False assert result.line_needs_review[str(l1250.id)] is False assert result.detail_parents[str(detail.id)] == str(l1200.id) # ближайшая выше строка ok # 1200 = … + 1250 + 1260 сошлось только с учётом строки 1250 и шаблонной 1260 assert [f for f in result.checks.relations_failed if f.relation.startswith("1200")] == [] # без шаблонной строки 1260 сумма не сходится: соотношение нарушено с ключом колонки БД without_template = build_stored_form( make_document(effective), make_meta(organization_name="ООО"), [column], [p for p in pairs if p[0] is not l1260], [], ) failed = [f for f in ENGINE.recompute(without_template).checks.relations_failed if f.relation.startswith("1200")] assert [(f.column_key, f.expected, f.actual) for f in failed] == [(key, Decimal(428471), Decimal(428461))] def test_engine_version_reads_file(tmp_path: Path) -> None: assert ENGINE.version == statements_version() assert statements_version(tmp_path / "VERSION") is None def test_engine_rules_accessors() -> None: engine = ArmStatementsEngine(default_rules()) assert engine.tolerance() == Decimal(1) assert engine.editions() == (Edition.RAS_66N, Edition.FSBU4) assert engine.code_order(Edition.FSBU4, DocumentType.DISCLOSURE, FormVariant.FULL) == () assert "1200" in engine.code_order(Edition.FSBU4, DocumentType.BALANCE, FormVariant.FULL) assert engine.resolve_relative(PeriodKind.ABSOLUTE, datetime.date(2025, 9, 30)) is None assert engine.resolve_relative(PeriodKind.NONE, datetime.date(2025, 9, 30)) is None assert engine.resolve_relative(PeriodKind.PREV_YEAR_END, datetime.date(2025, 9, 30)) == datetime.date(2024, 12, 31) def test_fake_engine_records_form() -> None: fake = FakeEngine(empty_result(DocStatus.NEEDS_CHECK), document_status=DocStatus.NEEDS_CHECK, edition=None) form = build_stored_form(make_document(), make_meta(), [], [], []) assert fake.recompute(form).status is DocStatus.NEEDS_CHECK assert fake.forms == [form] assert fake.edition(DocumentType.BALANCE, [("1200", True)], None, None) is None assert fake.edition_calls[0][1] == [("1200", True)] assert fake.contract_document_status({}) is DocStatus.NEEDS_CHECK