feat(#190): факт исхода AUTO_CONFIRMED (§14.3 п.5)
Хук в auto_confirm: на каждый документ, подтверждённый системой, пишется quality_outcome_fact (outcome=AUTO_CONFIRMED, primary_cause=WITHOUT_PARTICIPATION всегда — правок при автоподтверждении не бывает, D-3 блокирует формы с нарушениями). payload строится build_outcome_payload по тому же шаблону, что факт приёма (Task 5): ОСВ — osv_warnings/row_count, формы — lines/values/header_source, issue_count_at_outcome — все нарушения документа без фильтра. Запись в транзакции маппинга, без flush/commit (Task 4 insert_outcome_fact). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
12ea8acd42
commit
2a9ed10a29
@ -5,16 +5,36 @@
|
||||
`CONFIRMED` от имени системы: `confirmed_by = NULL`, у формы `reviewed_by = NULL`, `audit_event(DOCUMENT_CONFIRM)` без
|
||||
пользователя. Привязка к версиям и переход версии — тот же хук, что у ручного подтверждения, с автором
|
||||
`stored_file.uploaded_by`. Вызывается в транзакции маппинга под заданием, файлом и документами (порядок блокировок D-5).
|
||||
|
||||
§14.3 п. 5 (design `quality-metrics` §4.2, AURORA-1759): на каждый подтверждённый документ пишется факт исхода
|
||||
`quality_outcome_fact` с `outcome = AUTO_CONFIRMED`. Правок при автоподтверждении не бывает (D-3 блокирует формы
|
||||
с нарушениями) — `primary_cause` всегда `WITHOUT_PARTICIPATION`, без вычисления приоритета §14.3 (он нужен только
|
||||
общему случаю `CONFIRMED`, Task 14). Запись — в той же транзакции, без `flush`/`commit`.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models.enums import AuditAction, AuditSection, DocumentStatus
|
||||
from src.domain.quality_facts import build_outcome_payload
|
||||
from src.models.enums import (
|
||||
AuditAction,
|
||||
AuditSection,
|
||||
DocumentIssueKind,
|
||||
DocumentStatus,
|
||||
DocumentType,
|
||||
IssueSource,
|
||||
QualityCause,
|
||||
QualityOutcome,
|
||||
)
|
||||
from src.models.files import Document, StoredFile
|
||||
from src.models.osv import OsvEntry
|
||||
from src.models.statements import FinancialStatementMeta
|
||||
from src.repositories import statements as statements_repo
|
||||
from src.repositories.quality_facts import insert_outcome_fact
|
||||
from src.services import consolidation_documents
|
||||
from src.services.audit import AuditService
|
||||
from src.services.statements import FORM_TYPES, form_blocker
|
||||
@ -31,6 +51,43 @@ def transition_comment(document: Document) -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
async def _outcome_fact_fields(
|
||||
session: AsyncSession, document: Document, *, meta: FinancialStatementMeta | None
|
||||
) -> dict[str, Any]:
|
||||
"""`payload` и `issue_count_at_outcome` факта исхода `AUTO_CONFIRMED` (§14.3 п. 5, design §4.2).
|
||||
|
||||
Строение `payload` повторяет факт приёма (Task 5, `services/result_mapping.py`:
|
||||
`_osv_intake_fields`/`_statement_intake_fields`), но как снимок ИСХОДА (`build_outcome_payload`, `fixes=()` —
|
||||
правок при автоподтверждении не бывает, D-3). `issue_count_at_outcome` — ВСЕ нарушения документа на этот момент,
|
||||
без фильтра по `kind`/`source` (в отличие от `warning_count` факта приёма, который фильтрует только
|
||||
предупреждения сервиса)."""
|
||||
issues = await statements_repo.issues(session, document.id)
|
||||
if document.document_type is DocumentType.OSV:
|
||||
row_count = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(OsvEntry).where(OsvEntry.document_id == document.id)
|
||||
)
|
||||
).scalar_one()
|
||||
osv_warnings = [
|
||||
issue.code
|
||||
for issue in issues
|
||||
if issue.kind is DocumentIssueKind.warning and issue.source is IssueSource.service
|
||||
]
|
||||
payload = build_outcome_payload(document, osv_warnings=osv_warnings, osv_row_count=row_count, fixes=())
|
||||
elif meta is not None:
|
||||
_, pairs = await statements_repo.load_form(session, meta.id)
|
||||
lines = [line for line, _ in pairs]
|
||||
values = [value for _, line_values in pairs for value in line_values]
|
||||
payload = build_outcome_payload(
|
||||
document, lines=lines, values=values, issues=issues, header_source=meta.header_source, fixes=()
|
||||
)
|
||||
else:
|
||||
# DISCLOSURE/OTHER и форма без шапки (в текущем `map_file` не возникает — см. Task 5): снимок без построчных
|
||||
# данных дешевле, чем прерванная транзакция маппинга
|
||||
payload = build_outcome_payload(document, issues=issues, fixes=())
|
||||
return {"issue_count_at_outcome": len(issues), "payload": payload}
|
||||
|
||||
|
||||
async def auto_confirm(
|
||||
session: AsyncSession, *, file: StoredFile, documents: Sequence[Document], now: datetime.datetime
|
||||
) -> list[Document]:
|
||||
@ -73,4 +130,15 @@ async def auto_confirm(
|
||||
after_value={"status": DocumentStatus.CONFIRMED.value},
|
||||
)
|
||||
confirmed.append(document)
|
||||
fields = await _outcome_fact_fields(session, document, meta=meta)
|
||||
await insert_outcome_fact(
|
||||
session,
|
||||
document_id=document.id,
|
||||
outcome=QualityOutcome.AUTO_CONFIRMED,
|
||||
outcome_at=now,
|
||||
primary_cause=QualityCause.WITHOUT_PARTICIPATION,
|
||||
report_date=document.reporting_date,
|
||||
confirmed_by=None,
|
||||
**fields,
|
||||
)
|
||||
return confirmed
|
||||
|
||||
@ -18,15 +18,22 @@ from src.models.directories import CompanyGroup, LegalEntity, RegionalOffice
|
||||
from src.models.enums import (
|
||||
AuditAction,
|
||||
ConsolidationStatus,
|
||||
DocumentIssueKind,
|
||||
DocumentStatus,
|
||||
DocumentType,
|
||||
FileStatus,
|
||||
IssueSeverity,
|
||||
IssueSource,
|
||||
PeriodQuarter,
|
||||
QualityCause,
|
||||
QualityOutcome,
|
||||
RecognitionStatus,
|
||||
StatementLineStatus,
|
||||
)
|
||||
from src.models.files import Document, StoredFile
|
||||
from src.models.quality_facts import QualityOutcomeFact
|
||||
from src.models.statements import (
|
||||
DocumentIssue,
|
||||
FinancialStatementColumn,
|
||||
FinancialStatementLine,
|
||||
FinancialStatementMeta,
|
||||
@ -177,6 +184,99 @@ async def test_mixed_documents_confirm_only_clean(seeded: AsyncSession) -> None:
|
||||
assert [d.status for d in documents] == [DocumentStatus.CONFIRMED, DocumentStatus.NEEDS_CHECK]
|
||||
|
||||
|
||||
async def test_auto_confirm_writes_outcome_fact_without_participation(seeded: AsyncSession) -> None:
|
||||
"""§14.3 п. 5 (design `quality-metrics` §4.2): `AUTO_CONFIRMED` без правок ⇒ всегда `WITHOUT_PARTICIPATION`."""
|
||||
group, entities = await _group(seeded)
|
||||
user = await _user(seeded)
|
||||
file, [document] = await _file_with(seeded, entities[0], [DocumentStatus.RECOGNIZED], user.id)
|
||||
now = utc_now()
|
||||
|
||||
confirmed = await auto_confirm(seeded, file=file, documents=[document], now=now)
|
||||
|
||||
assert confirmed == [document]
|
||||
fact = (
|
||||
await seeded.execute(select(QualityOutcomeFact).where(QualityOutcomeFact.document_id == document.id))
|
||||
).scalar_one()
|
||||
assert fact.outcome == QualityOutcome.AUTO_CONFIRMED
|
||||
assert fact.primary_cause == QualityCause.WITHOUT_PARTICIPATION
|
||||
assert fact.outcome_at == now
|
||||
assert fact.confirmed_by is None
|
||||
assert fact.report_date == document.reporting_date
|
||||
|
||||
|
||||
async def test_auto_confirm_outcome_fact_osv_payload_shape(seeded: AsyncSession) -> None:
|
||||
"""Payload исхода ОСВ — как у факта приёма (Task 5 `_osv_intake_fields`): без `lines`/`values`, предупреждения
|
||||
сервиса `OSV_*` в `osv_warnings` (ядровые `MAPPING_WARNING` — нет), число строк `osv_entry` — `row_count`;
|
||||
`issue_count_at_outcome` — ВСЕ нарушения документа, без фильтра по `source`/`kind`."""
|
||||
group, entities = await _group(seeded)
|
||||
user = await _user(seeded)
|
||||
file, [document] = await _file_with(seeded, entities[0], [DocumentStatus.RECOGNIZED], user.id)
|
||||
now = utc_now()
|
||||
seeded.add(
|
||||
DocumentIssue(
|
||||
document_id=document.id,
|
||||
kind=DocumentIssueKind.warning,
|
||||
code="OSV_ROUNDING",
|
||||
severity=IssueSeverity.info,
|
||||
message="тестовое предупреждение сервиса",
|
||||
source=IssueSource.service,
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
seeded.add(
|
||||
DocumentIssue(
|
||||
document_id=document.id,
|
||||
kind=DocumentIssueKind.warning,
|
||||
code="MAPPING_WARNING",
|
||||
severity=IssueSeverity.info,
|
||||
message="тестовое предупреждение ядра",
|
||||
source=IssueSource.core,
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
await seeded.flush()
|
||||
|
||||
await auto_confirm(seeded, file=file, documents=[document], now=now)
|
||||
|
||||
fact = (
|
||||
await seeded.execute(select(QualityOutcomeFact).where(QualityOutcomeFact.document_id == document.id))
|
||||
).scalar_one()
|
||||
assert "lines" not in fact.payload
|
||||
assert "values" not in fact.payload
|
||||
assert fact.payload["osv_warnings"] == ["OSV_ROUNDING"]
|
||||
assert fact.payload["row_count"] == 0
|
||||
assert fact.payload["fixes"] == []
|
||||
assert fact.issue_count_at_outcome == 2 # noqa: PLR2004 - оба созданных нарушения, без фильтра
|
||||
|
||||
|
||||
@pytest.mark.fixtures
|
||||
async def test_auto_confirm_outcome_fact_for_form_has_lines(seeded: AsyncSession, fixtures_dir: Path) -> None:
|
||||
"""Payload исхода формы ББ/ОФР — строки и значения формы, как у факта приёма (Task 5 `_statement_intake_fields`),
|
||||
`header_source` из `meta`; правок при автоподтверждении нет (`fixes=[]`)."""
|
||||
path = fixtures_dir / GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json"
|
||||
import json # noqa: PLC0415 - нужен только здесь
|
||||
|
||||
data = json.loads(path.read_text("utf-8"))
|
||||
inn = next(d["header"]["inn"] for d in data["documents"] if d.get("header", {}).get("inn"))
|
||||
owner = await make_legal_entity(seeded, name="ООО Владелец", inn=inn)
|
||||
(income,) = await map_golden_snapshot(seeded, entity=owner, path=path, uploaded_by=None)
|
||||
file = await seeded.get(StoredFile, income.stored_file_id)
|
||||
assert file is not None
|
||||
now = utc_now()
|
||||
|
||||
assert await auto_confirm(seeded, file=file, documents=[income], now=now) == [income]
|
||||
|
||||
fact = (
|
||||
await seeded.execute(select(QualityOutcomeFact).where(QualityOutcomeFact.document_id == income.id))
|
||||
).scalar_one()
|
||||
assert fact.outcome == QualityOutcome.AUTO_CONFIRMED
|
||||
assert fact.primary_cause == QualityCause.WITHOUT_PARTICIPATION
|
||||
assert fact.report_date == income.reporting_date
|
||||
assert fact.payload["fixes"] == []
|
||||
assert fact.payload["lines"]
|
||||
assert "osv_warnings" not in fact.payload
|
||||
|
||||
|
||||
@pytest.mark.fixtures
|
||||
async def test_clean_form_sets_reviewed_at(seeded: AsyncSession, fixtures_dir: Path) -> None:
|
||||
path = fixtures_dir / GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма2_бух_09.json"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user