"""Рабочий стол консолидации для ЮЛ и `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)