diff --git a/api/alembic/versions/0007_dbml_v5_19.py b/api/alembic/versions/0007_dbml_v5_19.py index c293f8d..ac7e39d 100644 --- a/api/alembic/versions/0007_dbml_v5_19.py +++ b/api/alembic/versions/0007_dbml_v5_19.py @@ -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") diff --git a/api/src/domain/statement_status.py b/api/src/domain/statement_status.py index 0181b30..ece8a03 100644 --- a/api/src/domain/statement_status.py +++ b/api/src/domain/statement_status.py @@ -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) diff --git a/api/src/repositories/statements.py b/api/src/repositories/statements.py index ecd4c71..d72c52f 100644 --- a/api/src/repositories/statements.py +++ b/api/src/repositories/statements.py @@ -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() diff --git a/api/tests/test_auto_confirm.py b/api/tests/test_auto_confirm.py index 05ebed8..45c5e07 100644 --- a/api/tests/test_auto_confirm.py +++ b/api/tests/test_auto_confirm.py @@ -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" diff --git a/api/tests/test_domain_statement_status.py b/api/tests/test_domain_statement_status.py index 4c7d758..603cfde 100644 --- a/api/tests/test_domain_statement_status.py +++ b/api/tests/test_domain_statement_status.py @@ -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") == []