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