I-2: delta MODIFIED consolidations-api «Статусы Среза 4» (загрузка в консолидацию). T1.1: шапки моделей — DBML v5.18. T3.1: lock_versions_of_consolidation — двухшаговая блокировка (FOR UPDATE без членства, затем выборка членов); тесты гонки и выборки. M-1: тест миграции 0006 — порядок кварталов внутри года. M-2: тест 404 для ЮЛ чужого филиала на столе своей консолидации. T4.1: docstring supersede_previous — перенос строк. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
198 lines
10 KiB
Python
198 lines
10 KiB
Python
"""Рабочий стол консолидации для ЮЛ и `consolidation_id` в карточке файла (spec documents-api «Рабочий стол консолидации
|
||
для ЮЛ», «Файлы организации и карточка файла»; design D-6, D-7). Поверх фикстуры `app`: данные — фабрики в транзакции
|
||
теста."""
|
||
|
||
import uuid
|
||
|
||
import pytest
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from src.models.access import AppUser
|
||
from src.models.consolidation import ConsolidationDocument
|
||
from src.models.directories import LegalEntity, RegionalOffice
|
||
from src.models.enums import DocumentStatus, FileStatus, PeriodQuarter, RecognitionStatus
|
||
from tests.conftest import AuthClientFactory
|
||
from tests.factories import make_company_group, make_consolidation, make_document, make_legal_entity, make_stored_file
|
||
|
||
HTTP_OK = 200
|
||
HTTP_NOT_FOUND = 404
|
||
HTTP_FORBIDDEN = 403
|
||
KA_RF = "ka_rf@armka.local" # Ростовский филиал, DOCUMENT_VIEW + DOCUMENT_EDIT
|
||
CLIENT_MANAGER = "client_manager@armka.local" # Краснодарский филиал
|
||
CURATOR_GO = "curator_go@armka.local" # головной офис, права DOCUMENT_VIEW нет
|
||
ROSTOV = "Ростовский региональный филиал"
|
||
KRASNODAR = "Краснодарский региональный филиал"
|
||
|
||
pytestmark = pytest.mark.integration
|
||
|
||
|
||
async def _group(session: AsyncSession, office_name: str = ROSTOV, name: str = "ГК Стол"):
|
||
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == office_name))).scalar_one()
|
||
group = await make_company_group(session, name=name, office=office)
|
||
entity = await make_legal_entity(session, name=f"ООО {name}", group=group)
|
||
initiator = (await session.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||
return group, entity, initiator
|
||
|
||
|
||
async def _consolidation( # noqa: PLR0913, PLR0917 - тестовый помощник
|
||
session: AsyncSession, group, entity, initiator, year: int, quarter: PeriodQuarter
|
||
):
|
||
consolidation, version = await make_consolidation(
|
||
session, group=group, year=year, quarter=quarter, members=[entity], initiator=initiator
|
||
)
|
||
return consolidation, version
|
||
|
||
|
||
async def _file(
|
||
session: AsyncSession, entity: LegalEntity, name: str, *, consolidation_id=None, status=FileStatus.PROCESSED
|
||
):
|
||
file, job = await make_stored_file(
|
||
session, entity=entity, status=status, job_status=RecognitionStatus.done, original_name=name
|
||
)
|
||
file.consolidation_id = consolidation_id
|
||
await session.flush()
|
||
return file, job
|
||
|
||
|
||
def _desk_url(consolidation_id: uuid.UUID, entity_id: uuid.UUID, **query: str) -> str:
|
||
base = f"/consolidations/{consolidation_id}/legal-entities/{entity_id}/files"
|
||
return base + ("?" + "&".join(f"{k}={v}" for k, v in query.items()) if query else "")
|
||
|
||
|
||
async def test_desk_shows_only_files_of_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
"""Сценарий «Стол показывает только файлы консоли»: три файла ЮЛ — 2025 Q1, 2025 Q2, без контекста."""
|
||
group, entity, initiator = await _group(seeded)
|
||
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
c2, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||
f1, _ = await _file(seeded, entity, "q1.xls", consolidation_id=c1.id)
|
||
f2, _ = await _file(seeded, entity, "q2.xls", consolidation_id=c2.id)
|
||
f0, _ = await _file(seeded, entity, "none.xls")
|
||
client = await auth_client(KA_RF)
|
||
|
||
desk1 = await client.get(_desk_url(c1.id, entity.id))
|
||
desk2 = await client.get(_desk_url(c2.id, entity.id))
|
||
all_files = await client.get(f"/legal-entities/{entity.id}/files")
|
||
|
||
assert desk1.status_code == HTTP_OK, desk1.text
|
||
assert [item["id"] for item in desk1.json()["items"]] == [str(f1.id)]
|
||
assert desk1.json()["total"] == 1
|
||
assert desk1.json()["items"][0]["consolidation_id"] == str(c1.id)
|
||
assert [item["id"] for item in desk2.json()["items"]] == [str(f2.id)]
|
||
assert {item["id"] for item in all_files.json()["items"]} == {str(f1.id), str(f2.id), str(f0.id)}
|
||
by_id = {item["id"]: item["consolidation_id"] for item in all_files.json()["items"]}
|
||
assert by_id[str(f0.id)] is None
|
||
|
||
|
||
async def test_desk_includes_file_bound_to_version(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
"""Сценарий «Подхваченный файл виден через привязку»: файл без контекста, документ привязан к версии консоли."""
|
||
group, entity, initiator = await _group(seeded)
|
||
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||
file, job = await _file(seeded, entity, "q1-confirmed.xls")
|
||
document = await make_document(
|
||
seeded,
|
||
file=file,
|
||
job=job,
|
||
account="60",
|
||
year=2025,
|
||
quarter=PeriodQuarter.Q1,
|
||
status=DocumentStatus.CONFIRMED,
|
||
is_cumulative=False,
|
||
)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
|
||
await seeded.flush()
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.get(_desk_url(c2.id, entity.id))
|
||
|
||
assert response.status_code == HTTP_OK, response.text
|
||
assert [item["id"] for item in response.json()["items"]] == [str(file.id)]
|
||
assert response.json()["items"][0]["consolidation_id"] is None
|
||
|
||
|
||
async def test_desk_excludes_other_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
"""Review Focus 2: документ привязан к версии ДРУГОЙ консолидации — файл в этом столе не показывается."""
|
||
group, entity, initiator = await _group(seeded)
|
||
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||
file, job = await _file(seeded, entity, "bound-elsewhere.xls", consolidation_id=c2.id)
|
||
document = await make_document(seeded, file=file, job=job, status=DocumentStatus.CONFIRMED)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
|
||
await seeded.flush()
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.get(_desk_url(c1.id, entity.id))
|
||
|
||
assert response.status_code == HTTP_OK, response.text
|
||
assert response.json()["items"] == []
|
||
assert response.json()["total"] == 0
|
||
|
||
|
||
async def test_desk_superseded_hidden_by_default(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
"""Сценарий «Заменённый файл скрыт по умолчанию» для DESK и BOUND; `include_superseded=true` — оба."""
|
||
group, entity, initiator = await _group(seeded)
|
||
c1, v1 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
new, _ = await _file(seeded, entity, "new.xls", consolidation_id=c1.id)
|
||
old_desk, _ = await _file(seeded, entity, "old-desk.xls", consolidation_id=c1.id, status=FileStatus.SUPERSEDED)
|
||
old_desk.superseded_by_file_id = new.id
|
||
old_bound, job = await _file(seeded, entity, "old-bound.xls", status=FileStatus.SUPERSEDED)
|
||
old_bound.superseded_by_file_id = new.id
|
||
document = await make_document(seeded, file=old_bound, job=job, status=DocumentStatus.CONFIRMED)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=v1.id, document_id=document.id))
|
||
await seeded.flush()
|
||
client = await auth_client(KA_RF)
|
||
|
||
default = await client.get(_desk_url(c1.id, entity.id))
|
||
with_superseded = await client.get(_desk_url(c1.id, entity.id, include_superseded="true"))
|
||
|
||
assert [item["id"] for item in default.json()["items"]] == [str(new.id)]
|
||
assert {item["id"] for item in with_superseded.json()["items"]} == {
|
||
str(new.id),
|
||
str(old_desk.id),
|
||
str(old_bound.id),
|
||
}
|
||
assert with_superseded.json()["total"] == 3 # noqa: PLR2004
|
||
|
||
|
||
async def test_desk_foreign_and_missing_consolidation_are_same_404(
|
||
seeded: AsyncSession, auth_client: AuthClientFactory
|
||
) -> None:
|
||
"""Сценарий «Чужая консолидация»: ГК другого филиала и несуществующий id — одинаковый 404; чужое ЮЛ — тоже 404."""
|
||
group, entity, initiator = await _group(seeded, office_name=KRASNODAR, name="ГК Чужая")
|
||
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
own_group, own_entity, own_initiator = await _group(seeded, name="ГК Своя")
|
||
own_c, _ = await _consolidation(seeded, own_group, own_entity, own_initiator, 2025, PeriodQuarter.Q1)
|
||
client = await auth_client(KA_RF)
|
||
|
||
foreign = await client.get(_desk_url(c.id, own_entity.id))
|
||
missing = await client.get(_desk_url(uuid.uuid4(), own_entity.id))
|
||
foreign_entity = await client.get(_desk_url(own_c.id, entity.id)) # своя консолидация, ЮЛ чужого филиала
|
||
|
||
assert foreign.status_code == missing.status_code == foreign_entity.status_code == HTTP_NOT_FOUND
|
||
assert foreign.json() == missing.json()
|
||
assert foreign.json()["detail"]["code"] == "NOT_FOUND"
|
||
assert foreign_entity.json()["detail"]["code"] == "NOT_FOUND"
|
||
|
||
|
||
async def test_desk_requires_document_view(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
group, entity, initiator = await _group(seeded)
|
||
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
client = await auth_client(CURATOR_GO)
|
||
|
||
response = await client.get(_desk_url(c.id, entity.id))
|
||
|
||
assert response.status_code == HTTP_FORBIDDEN
|
||
|
||
|
||
async def test_file_card_has_consolidation_id(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||
"""Сценарий «Консолидация в карточке»: `GET /files/{id}` отдаёт `consolidation_id`."""
|
||
group, entity, initiator = await _group(seeded)
|
||
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||
file, _ = await _file(seeded, entity, "card.xls", consolidation_id=c.id)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.get(f"/files/{file.id}")
|
||
|
||
assert response.status_code == HTTP_OK, response.text
|
||
assert response.json()["consolidation_id"] == str(c.id)
|