"""Правки шапки ОСВ с сохранением первичного снимка и повторным подтверждением.""" import asyncio import json import uuid from http import HTTPStatus from pathlib import Path from typing import Any import pytest from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from src.api.dependencies.audit import RequestAudit from src.contract.versions import parse_result from src.core.config import get_settings from src.domain.policy import VisibilityScope from src.models.audit_ex import AuditEvent from src.models.consolidation import Consolidation, ConsolidationDocument from src.models.enums import ConsolidationStatus, DocumentStatus, DocumentType, FileStatus, PeriodQuarter from src.models.files import Document, StoredFile from src.schemas.documents import OsvHeaderPatch from src.services.audit import AuditService from src.services.documents import DocumentService from src.services.osv_header import OsvHeaderService from tests.conftest import AuthClientFactory from tests.factories import make_document, make_stored_file, make_version from tests.fixtures_helpers import map_golden_snapshot from tests.test_api_consolidation_members import _user, _version from tests.test_api_files_read import KA_RF, _rostov_entity pytestmark = [pytest.mark.integration, pytest.mark.fixtures] def _result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> Any: data = json.loads(path.read_text()) data.update(job_id=str(job_id), file_id=str(file_id)) data["service"]["version"] = "golden" return parse_result(json.dumps(data).encode()) async def test_header_keeps_original_and_entries( seeded: AsyncSession, auth_client: AuthClientFactory, fixtures_dir: Path ) -> None: entity = await _rostov_entity(seeded) snapshots = sorted((fixtures_dir / "golden/expected_osv_parse/rb").rglob("*.json")) assert snapshots, "Нужны реальные golden-снимки ОСВ РБ" documents = await map_golden_snapshot( seeded, entity=entity, path=snapshots[0], uploaded_by=None, result_builder=_result ) document = documents[0] file = await seeded.get(StoredFile, document.stored_file_id) assert file is not None file.status = FileStatus.PROCESSED document.status = DocumentStatus.CONFIRMED original = { key: getattr(document, key) for key in ("period_year", "period_quarter", "is_cumulative", "osv_account_code") } await seeded.flush() client = await auth_client(KA_RF) url = f"/documents/{document.id}" entries = (await client.get(f"{url}/entries")).json() ordinal = document.file_ordinal response = await client.patch(f"{url}/osv-header", json={"osv_account_code": "99.1"}) assert response.status_code == HTTPStatus.OK, response.text body = response.json() assert body["osv_account_code"] == "99.01" assert body["original_values"] == original assert body["is_modified"] is True and body["status"] == "NEEDS_CHECK" response = await client.patch(f"{url}/osv-header", json={"osv_account_code": original["osv_account_code"]}) assert response.status_code == HTTPStatus.OK, response.text assert response.json()["is_modified"] is False assert response.json()["original_values"] == original await seeded.refresh(document) assert document.file_ordinal == ordinal assert (await client.get(f"{url}/entries")).json() == entries async def test_header_unbinds_working_version_but_keeps_cancelled( seeded: AsyncSession, auth_client: AuthClientFactory ) -> None: version, members, _, _ = await _version(seeded, members=1, locked_by=None) file, job = await make_stored_file(seeded, entity=members[0], status=FileStatus.PROCESSED) doc = await make_document(seeded, file=file, job=job, status=DocumentStatus.CONFIRMED) consolidation = await seeded.get(Consolidation, version.consolidation_id) assert consolidation is not None cancelled = await make_version( seeded, consolidation=consolidation, version_no=2, members=members, initiator=await _user(seeded, KA_RF), status=ConsolidationStatus.CANCELLED, ) seeded.add_all( [ConsolidationDocument(consolidation_version_id=v.id, document_id=doc.id) for v in (version, cancelled)] ) await seeded.flush() client = await auth_client(KA_RF) response = await client.patch(f"/documents/{doc.id}/osv-header", json={"osv_account_code": "62"}) assert response.status_code == HTTPStatus.OK, response.text bound = list( ( await seeded.scalars( select(ConsolidationDocument.consolidation_version_id).where( ConsolidationDocument.document_id == doc.id ) ) ).all() ) assert bound == [cancelled.id] assert response.json()["status"] == "NEEDS_CHECK" async def test_header_validation_order_duplicate_and_noop(seeded: AsyncSession, auth_client: AuthClientFactory) -> None: entity = await _rostov_entity(seeded) file, job = await make_stored_file(seeded, entity=entity, status=FileStatus.PROCESSED) doc = await make_document(seeded, file=file, job=job, status=DocumentStatus.CONFIRMED) twin = await make_document(seeded, file=file, job=job, account="62") client = await auth_client(KA_RF) url = f"/documents/{doc.id}/osv-header" response = await client.patch(url, json={"period_quarter": None, "osv_account_code": "62"}) assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY assert response.json()["detail"]["code"] == "INVALID_PERIOD" response = await client.patch(url, json={"osv_account_code": "62"}) assert response.status_code == HTTPStatus.CONFLICT assert response.json()["detail"]["document_id"] == str(twin.id) response = await client.patch(url, json={"osv_account_code": "60"}) assert response.status_code == HTTPStatus.OK assert response.json()["status"] == "CONFIRMED" assert response.json()["original_values"] is None assert ( await seeded.scalar( select(func.count()) .select_from(AuditEvent) .where(AuditEvent.object_id == doc.id, AuditEvent.action == "RECOGNIZED_DATA_EDIT") ) == 0 ) async def test_header_type_validation_precedes_domain_checks( seeded: AsyncSession, auth_client: AuthClientFactory ) -> None: entity = await _rostov_entity(seeded) file, job = await make_stored_file(seeded, entity=entity, status=FileStatus.PROCESSED) doc = await make_document(seeded, file=file, job=job) doc.document_type = DocumentType.BALANCE doc.osv_account_code = None await seeded.flush() client = await auth_client(KA_RF) url = f"/documents/{doc.id}/osv-header" response = await client.patch(url, json={"period_year": "abc"}) assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY assert isinstance(response.json()["detail"], list) assert response.json()["detail"][0]["loc"] == ["body", "period_year"] response = await client.patch(url, json={"period_year": 2100}) assert response.status_code == HTTPStatus.CONFLICT assert response.json()["detail"]["code"] == "DOCUMENT_NOT_OSV" @pytest.mark.parametrize("period_from_version", [False, True]) async def test_corrected_account_in_version_card( seeded: AsyncSession, auth_client: AuthClientFactory, monkeypatch: pytest.MonkeyPatch, period_from_version: bool, ) -> None: monkeypatch.setattr(get_settings(), "CONSOLIDATION_PERIOD_FROM_VERSION", period_from_version) version, members, _, _ = await _version(seeded, members=1, locked_by=None) file, job = await make_stored_file(seeded, entity=members[0], status=FileStatus.PROCESSED) file.consolidation_id = version.consolidation_id doc = await make_document(seeded, file=file, job=job, year=2024, quarter=PeriodQuarter.Q3) old_checks = doc.period_check, doc.counterparty_check await seeded.flush() client = await auth_client(KA_RF) response = await client.patch(f"/documents/{doc.id}/osv-header", json={"osv_account_code": "62"}) assert response.status_code == HTTPStatus.OK, response.text card = (await client.get(f"/consolidations/versions/{version.id}")).json() assert card["members"][0]["osv_accounts"] == ["62"] await seeded.refresh(doc) assert (doc.period_check, doc.counterparty_check) == old_checks response = await client.post(f"/documents/{doc.id}/confirm") assert response.status_code == HTTPStatus.OK, response.text assert ( await seeded.scalar( select(ConsolidationDocument.document_id).where( ConsolidationDocument.consolidation_version_id == version.id ) ) == doc.id ) async def test_concurrent_header_patch_and_confirm(committed_sessionmaker) -> None: async with committed_sessionmaker() as session, session.begin(): version, members, _, _ = await _version(session, members=1, locked_by=None) file, job = await make_stored_file(session, entity=members[0], status=FileStatus.PROCESSED) doc = await make_document(session, file=file, job=job, year=2024, quarter=PeriodQuarter.Q3) user = await _user(session, KA_RF) doc_id, user_id, version_id = doc.id, user.id, version.id async def change(confirm: bool): async with committed_sessionmaker() as session, session.begin(): audit = RequestAudit(AuditService(session), user_id, None, None) if confirm: return await DocumentService(session, audit, VisibilityScope(None)).confirm(doc_id) return await OsvHeaderService(session, audit, VisibilityScope(None)).patch( doc_id, OsvHeaderPatch(osv_account_code="62") ) await asyncio.wait_for(asyncio.gather(change(True), change(False)), timeout=10) async with committed_sessionmaker() as session: doc = await session.get(Document, doc_id) assert doc is not None and doc.osv_account_code == "62" bound = await session.scalar( select(ConsolidationDocument.document_id).where( ConsolidationDocument.consolidation_version_id == version_id ) ) if doc.status is DocumentStatus.CONFIRMED: assert bound == doc_id else: assert doc.status is DocumentStatus.NEEDS_CHECK and bound is None