chore(statements): комментарии, README, типы, тесты по итогам финального ревью (M-1, M-2, M-3, M-5, M-7, M-9, T2.1, T4.5, T10.1, T11.1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-18 18:05:28 +04:00
parent 16b53b0bdb
commit 1ab7f2238d
5 changed files with 50 additions and 3 deletions

View File

@ -262,6 +262,7 @@ def _log_lost_values() -> None:
def downgrade() -> None:
"""Откат v5.19. Нужно онлайн-соединение (проверки читают данные): offline-режим `--sql` не поддерживается (T2.1)."""
_check_downgrade_allowed()
_log_lost_values()
op.execute(f"DROP TABLE {S}.document_issue")

View File

@ -51,13 +51,14 @@ class NameCheckLine:
def name_mismatches(lines: Iterable[NameCheckLine], catalog: LineCatalog, form: str) -> list[Hashable]:
"""Ключи строк `ok` с неуверенным кодом (`line_code_confidence < 100`), без закреплённого кода (`code_fixed`),
чьё наименование не совпало ни с одним справочным именем кода (Р-16). Кода нет в словаре — несовпадение."""
чьё наименование не совпало ни с одним справочным именем кода (Р-16). Кода нет в словаре — несовпадение.
Строка без уверенности (`None`) считается уверенной и не проверяется: спека требует `confidence < 100`."""
result: list[Hashable] = []
for line in lines:
if line.line_status != StatementLineStatus.ok or line.code_fixed:
continue
confidence = line.line_code_confidence
if confidence is None or confidence >= _CONFIDENT:
if confidence is None or confidence >= _CONFIDENT: # None — уверенной (T10.1)
continue
if not line.line_code or not names_match(line.line_name, catalog.names(form, line.line_code)):
result.append(line.key)

View File

@ -10,6 +10,7 @@ from typing import Any
from sqlalchemy import delete, exists, func, insert, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql.expression import ScalarSelect
from src.domain.statement_aggregate import AggLine, CodeAggregate, aggregate_by_code
from src.domain.statement_issues import IssueDraft
@ -213,7 +214,7 @@ async def delete_documents_statements(session: AsyncSession, document_ids: Seque
await _delete_statements(session, list(document_ids))
async def _delete_statements(session: AsyncSession, documents: Any) -> None:
async def _delete_statements(session: AsyncSession, documents: ScalarSelect[uuid.UUID] | list[uuid.UUID]) -> None:
await session.execute(delete(DocumentIssue).where(DocumentIssue.document_id.in_(documents)))
metas = select(FinancialStatementMeta.id).where(FinancialStatementMeta.document_id.in_(documents)).scalar_subquery()
lines = select(FinancialStatementLine.id).where(FinancialStatementLine.meta_id.in_(metas)).scalar_subquery()

View File

@ -259,3 +259,42 @@ async def test_manual_confirm_of_auto_confirmed_is_409(seeded: AsyncSession, aut
assert response.status_code == HTTP_CONFLICT, response.text
assert response.json()["detail"]["code"] == "ALREADY_CONFIRMED"
assert len(await _confirm_events(seeded)) == 1
async def _synthetic_disclosure(session: AsyncSession, fixtures_dir: Path, *, office: RegionalOffice) -> Document:
"""RECOGNIZED `DISCLOSURE` из синтетического снимка st06 (T11.1: форм-проверки к не-форме не применяются)."""
import json # noqa: PLC0415 - нужен только здесь
path = fixtures_dir / GOLDEN_STATEMENTS / "synthetic" / "st06_header_variants.json"
data = json.loads(path.read_text("utf-8"))
inn = next((d["header"].get("inn") for d in data["documents"] if d.get("header", {}).get("inn")), None)
owner = await make_legal_entity(session, name="ООО Синтетика", inn=inn, office=office)
documents = await map_golden_snapshot(session, entity=owner, path=path, uploaded_by=None)
disclosure = next(d for d in documents if d.document_type is DocumentType.DISCLOSURE)
assert disclosure.status is DocumentStatus.RECOGNIZED
return disclosure
@pytest.mark.fixtures
async def test_recognized_disclosure_is_auto_confirmed(seeded: AsyncSession, fixtures_dir: Path) -> None:
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
disclosure = await _synthetic_disclosure(seeded, fixtures_dir, office=office)
file = await seeded.get(StoredFile, disclosure.stored_file_id)
assert file is not None
assert await auto_confirm(seeded, file=file, documents=[disclosure], now=utc_now()) == [disclosure]
assert disclosure.status is DocumentStatus.CONFIRMED
@pytest.mark.fixtures
async def test_recognized_disclosure_is_manually_confirmable(
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
) -> None:
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
disclosure = await _synthetic_disclosure(seeded, fixtures_dir, office=office)
client = await auth_client(KA_RF)
response = await client.post(f"/documents/{disclosure.id}/confirm")
assert response.status_code == 200, response.text # noqa: PLR2004
assert response.json()["status"] == "CONFIRMED"

View File

@ -120,3 +120,8 @@ def test_name_mismatches() -> None:
_line(10, None, None, "Без кода", status="unreadable"),
]
assert name_mismatches(lines, CATALOG, "BALANCE") == [1, 3, 8, 9]
def test_name_mismatches_missing_confidence_is_confident() -> None:
"""T10.1: `line_code_confidence = None` не даёт `NAME_MISMATCH` (спека: `confidence < 100`)."""
assert name_mismatches([_line(1, "1120", None, "Чужое имя")], CATALOG, "BALANCE") == []