arm-core/api/tests/test_api_osv_header.py

229 lines
10 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.

"""Правки шапки ОСВ с сохранением первичного снимка и повторным подтверждением."""
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