arm-core/api/src/services/auto_confirm.py
Andrey Epifancev 2a9ed10a29 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>
2026-11-05 19:25:03 +04:00

145 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Автоподтверждение чистых документов при маппинге результата (osv-result-mapping «Автоподтверждение чистого
документа»; `05-domain-rules.md` §9.3, §9.5; OQ-157; design D-1–D-3 change `auto-confirm-clean-documents`).
Документ, получивший при маппинге `RECOGNIZED` (ТЗ 03_9 п. 4: «при отсутствии замечаний — „Загружен“»), становится
`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.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
AUTO_CONFIRM_DESCRIPTION = "автоподтверждение: без замечаний"
OBJECT_TYPE = "document"
def transition_comment(document: Document) -> str:
"""Комментарий перехода версии: «автоподтверждение документа <тип> [<счёт>]» (design решение №6)."""
parts = ["автоподтверждение документа", document.document_type.value]
if document.osv_account_code:
parts.append(document.osv_account_code)
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]:
"""Подтверждает документы `RECOGNIZED` файла (форма — только при `form_blocker is None`, D-3); `NEEDS_CHECK`
и прочие не трогает. Возвращает подтверждённые документы по возрастанию `id`."""
audit = AuditService(session)
confirmed: list[Document] = []
for document in sorted(documents, key=lambda item: item.id):
if document.status is not DocumentStatus.RECOGNIZED:
continue
meta = None
if document.document_type in FORM_TYPES:
meta = await statements_repo.get_meta(session, document.id, refresh=True)
if meta is not None and await form_blocker(session, document, meta) is not None:
continue
document.status = DocumentStatus.CONFIRMED
document.confirmed_by = None
document.confirmed_at = now
document.updated_at = now
if meta is not None:
meta.reviewed_by = None
meta.reviewed_at = now
await session.flush()
await consolidation_documents.on_document_confirmed(
session,
audit,
document=document,
changed_by=file.uploaded_by,
comment=transition_comment(document),
now=now,
)
await audit.record(
action=AuditAction.DOCUMENT_CONFIRM,
section=AuditSection.PRIMARY_DOCUMENTS,
object_type=OBJECT_TYPE,
object_id=document.id,
user_id=None,
description=AUTO_CONFIRM_DESCRIPTION,
before_value={"status": DocumentStatus.RECOGNIZED.value},
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