"""Автоподтверждение чистых документов при маппинге результата (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