From 5f1001c808ba0e42c954e0cd1212fe01ef8d7dc2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 5 Nov 2026 16:07:45 +0400 Subject: [PATCH] =?UTF-8?q?fix(#190):=20header=5Fsource=20=D0=B8=20=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=9E=D0=A1=D0=92=20=D0=B2=20payload=20=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BA=D0=B0=D1=87=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B0=20(=C2=A714.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Снимки приёма и исхода получают необязательные kwargs: header_source (сырой текст шапки из financial_statement_meta.header_source) кладётся в header для ББ/ОФР, osv_row_count — в payload ОСВ как row_count. Без kwargs ключи не эмитятся. Co-Authored-By: Claude Sonnet 5 --- api/src/domain/quality_facts.py | 45 +++++++++++++++++++++----- api/tests/test_domain_quality_facts.py | 38 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/api/src/domain/quality_facts.py b/api/src/domain/quality_facts.py index a013204..a702689 100644 --- a/api/src/domain/quality_facts.py +++ b/api/src/domain/quality_facts.py @@ -10,7 +10,7 @@ Объекты строк, значений и нарушений читаются атрибутами без привязки к сессии: отсутствующий атрибут даёт `None`. """ -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Mapping, Sequence from datetime import date, datetime from decimal import Decimal from enum import Enum @@ -37,22 +37,33 @@ _VALUE_FIELDS = ( _ISSUE_FIELDS = ("kind", "code", "severity", "source", "field", "relation", "codes", "line_ids") -def build_intake_payload( +def build_intake_payload( # noqa: PLR0913 - сигнатура снимка фиксирована (design §4.1) document: Document, *, lines: Sequence[Any] | None = None, values: Sequence[Any] | None = None, issues: Iterable[Any] = (), osv_warnings: Iterable[str] = (), + header_source: Mapping[str, Any] | None = None, + osv_row_count: int | None = None, ) -> dict[str, Any]: """Снимок «как распознано» для `quality_intake_fact.payload` (§14.1, design §4.1). - `BALANCE` / `INCOME`: `{"header", "lines", "values", "issues"}`. `OSV`: `{"osv_warnings"}` без `lines` и `values`. + `BALANCE` / `INCOME`: `{"header", "lines", "values", "issues"}`; при переданном `header_source` (сырой текст и + уверенность по полям шапки, `financial_statement_meta.header_source`) он кладётся в `header["header_source"]`. + `OSV`: `{"osv_warnings"}` без `lines` и `values`; при переданном `osv_row_count` добавляется `row_count`. + Без необязательных аргументов ключей `header_source` и `row_count` в payload нет. """ if document.document_type is DocumentType.OSV: - return {"osv_warnings": list(osv_warnings)} + payload: dict[str, Any] = {"osv_warnings": list(osv_warnings)} + if osv_row_count is not None: + payload["row_count"] = osv_row_count + return payload + header = _pick(document, _HEADER_FIELDS) + if header_source is not None: + header["header_source"] = {key: _plain_mapping(entry) for key, entry in header_source.items()} return { - "header": _pick(document, _HEADER_FIELDS), + "header": header, "lines": [_line(line) for line in lines or ()], "values": [_value(value) for value in values or ()], "issues": [_pick(issue, _ISSUE_FIELDS) for issue in issues], @@ -67,13 +78,24 @@ def build_outcome_payload( # noqa: PLR0913 - сигнатура снимка ф issues: Iterable[Any] = (), osv_warnings: Iterable[str] = (), fixes: Iterable[Any] = (), + header_source: Mapping[str, Any] | None = None, + osv_row_count: int | None = None, ) -> dict[str, Any]: """Подтверждённый результат и правки для `quality_outcome_fact.payload` (§14.1, design §4.2). - Строение — как у снимка приёма (`build_intake_payload`), плюс `fixes`: список правок с отнесением, каждая запись - строится вызывающим (состав записи — задача отнесения правок, не этой функции). + Строение — как у снимка приёма (`build_intake_payload`, с теми же `header_source` и `osv_row_count`), плюс `fixes`: + список правок с отнесением, каждая запись строится вызывающим (состав записи — задача отнесения правок, не этой + функции). """ - payload = build_intake_payload(document, lines=lines, values=values, issues=issues, osv_warnings=osv_warnings) + payload = build_intake_payload( + document, + lines=lines, + values=values, + issues=issues, + osv_warnings=osv_warnings, + header_source=header_source, + osv_row_count=osv_row_count, + ) payload["fixes"] = list(fixes) return payload @@ -86,6 +108,13 @@ def _line(line: Any) -> dict[str, Any]: } +def _plain_mapping(entry: Any) -> Any: + """Приводит запись `header_source` (`{"text", "confidence"}`) к JSON-совместимому виду; Decimal → строка.""" + if isinstance(entry, Mapping): + return {key: _plain(item) for key, item in entry.items()} + return _plain(entry) + + def _value(value: Any) -> dict[str, Any]: return _pick(value, _VALUE_FIELDS) diff --git a/api/tests/test_domain_quality_facts.py b/api/tests/test_domain_quality_facts.py index 6bae63a..796cd9f 100644 --- a/api/tests/test_domain_quality_facts.py +++ b/api/tests/test_domain_quality_facts.py @@ -15,6 +15,8 @@ from src.models.enums import ( from src.models.files import Document from src.models.statements import DocumentIssue, FinancialStatementLine, FinancialStatementValue +ROW_COUNT = 4_200 + def make_document(document_type: DocumentType = DocumentType.BALANCE) -> Document: return Document( @@ -125,3 +127,39 @@ def test_build_outcome_payload_osv_has_fixes_and_no_lines(): assert payload["osv_warnings"] == ["OSV_SOMETHING"] assert payload["fixes"] == [fix] assert "lines" not in payload and "values" not in payload + + +def test_build_intake_payload_includes_header_source_for_statement(): + document = make_document(document_type=DocumentType.BALANCE) + header_source = {"inn": {"text": "7701234567", "confidence": Decimal("100.00")}} + payload = build_intake_payload(document, lines=[], values=[], header_source=header_source) + assert payload["header"]["header_source"] == {"inn": {"text": "7701234567", "confidence": "100.00"}} + + +def test_build_outcome_payload_includes_header_source_for_statement(): + document = make_document(document_type=DocumentType.INCOME) + header_source = {"okpo": {"text": "12345678", "confidence": None}} + payload = build_outcome_payload(document, lines=[], values=[], fixes=[], header_source=header_source) + assert payload["header"]["header_source"] == {"okpo": {"text": "12345678", "confidence": None}} + + +def test_build_intake_payload_includes_osv_row_count(): + document = make_document(document_type=DocumentType.OSV) + payload = build_intake_payload(document, osv_warnings=["OSV_SOMETHING"], osv_row_count=ROW_COUNT) + assert payload["row_count"] == ROW_COUNT + assert payload["osv_warnings"] == ["OSV_SOMETHING"] + + +def test_build_outcome_payload_includes_osv_row_count(): + document = make_document(document_type=DocumentType.OSV) + payload = build_outcome_payload(document, osv_warnings=[], fixes=[], osv_row_count=0) + assert payload["row_count"] == 0 + + +def test_header_source_and_row_count_absent_when_kwargs_omitted(): + balance = build_intake_payload(make_document(document_type=DocumentType.BALANCE), lines=[], values=[]) + assert "header_source" not in balance["header"] + osv = build_intake_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[]) + assert "row_count" not in osv + outcome_osv = build_outcome_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[], fixes=[]) + assert "row_count" not in outcome_osv