diff --git a/api/src/services/auto_confirm.py b/api/src/services/auto_confirm.py index f639b89..ab1187e 100644 --- a/api/src/services/auto_confirm.py +++ b/api/src/services/auto_confirm.py @@ -6,10 +6,13 @@ пользователя. Привязка к версиям и переход версии — тот же хук, что у ручного подтверждения, с автором `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`. +§14.3 п. 5 (design `quality-metrics` §4.2, AURORA-1759): на каждый подтверждённый документ типа `BALANCE`/ +`INCOME`/`OSV` (`OUTCOME_FACT_DOCUMENT_TYPES`, тот же набор, что у факта приёма — `INTAKE_FACT_DOCUMENT_TYPES`, +Task 5) пишется факт исхода `quality_outcome_fact` с `outcome = AUTO_CONFIRMED`. `DISCLOSURE`/`OTHER` +автоподтверждаются как прежде (без факта приёма — факта исхода у них тоже нет, иначе `document_id` факта исхода +не нашёл бы пары при соединении с фактом приёма в матрице §14). Правок при автоподтверждении не бывает (D-3 +блокирует формы с нарушениями) — `primary_cause` всегда `WITHOUT_PARTICIPATION`, без вычисления приоритета §14.3 +(он нужен только общему случаю `CONFIRMED`, Task 14). Запись — в той же транзакции, без `flush`/`commit`. """ import datetime @@ -41,6 +44,10 @@ from src.services.statements import FORM_TYPES, form_blocker AUTO_CONFIRM_DESCRIPTION = "автоподтверждение: без замечаний" OBJECT_TYPE = "document" +# §14.1/§14.3 (design quality-metrics §4.1/§4.2): факт качества (приёма и исхода) пишется только для этих типов; +# тот же набор, что `INTAKE_FACT_DOCUMENT_TYPES` в services/result_mapping.py (Task 5) — не импортируется оттуда +# во избежание цикла result_mapping → auto_confirm (там этот импорт уже отложенный) +OUTCOME_FACT_DOCUMENT_TYPES = frozenset({DocumentType.BALANCE, DocumentType.INCOME, DocumentType.OSV}) def transition_comment(document: Document) -> str: @@ -56,11 +63,12 @@ async def _outcome_fact_fields( ) -> 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` факта приёма, который фильтрует только - предупреждения сервиса).""" + Вызывается только для `document.document_type ∈ OUTCOME_FACT_DOCUMENT_TYPES` (гейт — в `auto_confirm`, + `DISCLOSURE`/`OTHER` сюда не попадают). Строение `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 = ( @@ -82,8 +90,8 @@ async def _outcome_fact_fields( document, lines=lines, values=values, issues=issues, header_source=meta.header_source, fixes=() ) else: - # DISCLOSURE/OTHER и форма без шапки (в текущем `map_file` не возникает — см. Task 5): снимок без построчных - # данных дешевле, чем прерванная транзакция маппинга + # BALANCE/INCOME без шапки (в текущем `map_file` не возникает — см. Task 5): снимок без построчных данных + # дешевле, чем прерванная транзакция маппинга payload = build_outcome_payload(document, issues=issues, fixes=()) return {"issue_count_at_outcome": len(issues), "payload": payload} @@ -130,15 +138,16 @@ 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, - ) + if document.document_type in OUTCOME_FACT_DOCUMENT_TYPES: + 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 diff --git a/api/tests/test_auto_confirm.py b/api/tests/test_auto_confirm.py index 56e0f22..d9635c9 100644 --- a/api/tests/test_auto_confirm.py +++ b/api/tests/test_auto_confirm.py @@ -386,6 +386,27 @@ async def test_recognized_disclosure_is_auto_confirmed(seeded: AsyncSession, fix assert disclosure.status is DocumentStatus.CONFIRMED +@pytest.mark.fixtures +async def test_recognized_disclosure_is_auto_confirmed_without_outcome_fact( + seeded: AsyncSession, fixtures_dir: Path +) -> None: + """`DISCLOSURE`/`OTHER` вне матрицы §14 (как и у факта приёма, `INTAKE_FACT_DOCUMENT_TYPES`, Task 5): + автоподтверждение документа не меняется, но `quality_outcome_fact` не пишется — иначе `document_id` факта + исхода не нашёл бы пары с фактом приёма.""" + 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 + + facts = ( + await seeded.execute(select(QualityOutcomeFact).where(QualityOutcomeFact.document_id == disclosure.id)) + ).scalars().all() + assert facts == [] + + @pytest.mark.fixtures async def test_recognized_disclosure_is_manually_confirmable( seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory