PUT /consolidations/versions/{id}/members: замена состава ЮЛ версии, проверка принадлежности ГК
(ENTITY_NOT_IN_GROUP), минимум два ЮЛ, diff с отвязкой документов ушедших (unbind_entity) и подхватом
добавленных (bind_confirmed, заглушка до Task 6), аудит MEMBERSHIP_CHANGE. Общий пролог
«lock → видимость → редактируемость» вынесен в services/consolidation_guard.py (T4.2). Решения #25–#29.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
494 lines
22 KiB
Python
494 lines
22 KiB
Python
"""Периметр версии: `PUT /consolidations/versions/{id}/members` (spec consolidations-api «Периметр версии»;
|
||
directory-policy «редактируемость версии»; audit-trail `MEMBERSHIP_CHANGE`; design D-5, D-8, D-9).
|
||
|
||
Подхват подтверждённых документов добавленных ЮЛ (`bind_confirmed`) до Task 6 — заглушка: здесь проверяется ВЫЗОВ
|
||
(шпион на функцию модуля), а настоящие привязки появятся с реализацией и ячейкой `CONFIRMED` (Task 6, Task 8).
|
||
Отвязка документов удалённых ЮЛ (`unbind_entity`) работает уже сейчас — прямое удаление `consolidation_document`.
|
||
"""
|
||
|
||
import uuid
|
||
from collections.abc import Sequence
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from sqlalchemy import func, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from src.models.access import AppUser
|
||
from src.models.audit_ex import AuditEvent
|
||
from src.models.consolidation import ConsolidationDocument, ConsolidationMember, ConsolidationVersion
|
||
from src.models.directories import CompanyGroup, LegalEntity, RegionalOffice, RelatedGroupMember
|
||
from src.models.enums import AuditAction, AuditSection, ConsolidationStatus, PeriodQuarter
|
||
from src.services import consolidation_documents
|
||
from tests.conftest import AuthClientFactory
|
||
from tests.factories import (
|
||
make_company_group,
|
||
make_confirmed_osv,
|
||
make_consolidation,
|
||
make_legal_entity,
|
||
make_lock,
|
||
make_membership,
|
||
make_related_group,
|
||
)
|
||
|
||
HTTP_OK = 200
|
||
HTTP_FORBIDDEN = 403
|
||
HTTP_NOT_FOUND = 404
|
||
HTTP_CONFLICT = 409
|
||
HTTP_UNPROCESSABLE = 422
|
||
ROSTOV = "Ростовский региональный филиал"
|
||
KRASNODAR = "Краснодарский региональный филиал"
|
||
KA_RF = "ka_rf@armka.local"
|
||
RKA = "rka@armka.local"
|
||
CURATOR = "curator_go@armka.local"
|
||
EXPERT = "expert@armka.local"
|
||
VERSIONS = "/consolidations/versions"
|
||
ACCOUNT = "60"
|
||
THREE = 3
|
||
|
||
|
||
async def _user(session: AsyncSession, email: str) -> AppUser:
|
||
return (await session.execute(select(AppUser).where(AppUser.email == email))).scalar_one()
|
||
|
||
|
||
async def _version( # noqa: PLR0913 - тестовая фабрика: состав периметра, статус, филиал и владелец блокировки
|
||
session: AsyncSession,
|
||
*,
|
||
members: int = 2,
|
||
spare: int = 1,
|
||
status: ConsolidationStatus = ConsolidationStatus.DATA_CORRECTION,
|
||
office_name: str = ROSTOV,
|
||
locked_by: str | None = KA_RF,
|
||
) -> tuple[ConsolidationVersion, list[LegalEntity], list[LegalEntity], uuid.UUID]:
|
||
"""Версия ГК филиала за 2024 Q3: `members` ЮЛ в периметре и `spare` ЮЛ той же ГК вне периметра.
|
||
|
||
Возвращает версию, члены, запасные ЮЛ и id ГК; `locked_by` — владелец активной блокировки.
|
||
"""
|
||
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == office_name))).scalar_one()
|
||
group = await make_company_group(session, name=f"ГК {uuid.uuid4().hex[:6]}", office=office)
|
||
inside = [await make_legal_entity(session, name=f"ООО Член {n}", group=group) for n in range(members)]
|
||
outside = [await make_legal_entity(session, name=f"ООО Запас {n}", group=group) for n in range(spare)]
|
||
_, version = await make_consolidation(
|
||
session,
|
||
group=group,
|
||
year=2024,
|
||
quarter=PeriodQuarter.Q3,
|
||
members=inside,
|
||
initiator=await _user(session, KA_RF),
|
||
status=status,
|
||
)
|
||
if locked_by is not None:
|
||
await make_lock(session, version=version, user=await _user(session, locked_by))
|
||
return version, inside, outside, group.id
|
||
|
||
|
||
def _url(version_id: uuid.UUID) -> str:
|
||
return f"{VERSIONS}/{version_id}/members"
|
||
|
||
|
||
def _body(entities: Sequence[LegalEntity | uuid.UUID]) -> dict[str, Any]:
|
||
return {"legal_entity_ids": [str(e.id if isinstance(e, LegalEntity) else e) for e in entities]}
|
||
|
||
|
||
async def _member_ids(session: AsyncSession, version_id: uuid.UUID) -> set[uuid.UUID]:
|
||
stmt = select(ConsolidationMember.legal_entity_id).where(ConsolidationMember.consolidation_version_id == version_id)
|
||
return set((await session.execute(stmt)).scalars())
|
||
|
||
|
||
async def _bound_documents(session: AsyncSession, version_id: uuid.UUID) -> set[uuid.UUID]:
|
||
stmt = select(ConsolidationDocument.document_id).where(ConsolidationDocument.consolidation_version_id == version_id)
|
||
return set((await session.execute(stmt)).scalars())
|
||
|
||
|
||
async def _events(session: AsyncSession) -> list[AuditEvent]:
|
||
stmt = select(AuditEvent).where(AuditEvent.action == AuditAction.MEMBERSHIP_CHANGE)
|
||
return list((await session.execute(stmt)).scalars())
|
||
|
||
|
||
# --- добавление ----------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_add_member_updates_perimeter_and_picks_up_documents(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
"""Добавленное ЮЛ — в периметре и в карточке; подхват подтверждённых документов вызван ровно для добавленных."""
|
||
version, inside, outside, _ = await _version(seeded)
|
||
await make_confirmed_osv(
|
||
seeded,
|
||
entity=outside[0],
|
||
account=ACCOUNT,
|
||
year=2024,
|
||
quarter=PeriodQuarter.Q3,
|
||
is_cumulative=True,
|
||
rows=[{"name": "ООО Контрагент", "inn": "7701234567", "closing_debit": 10}],
|
||
)
|
||
calls: list[tuple[uuid.UUID, list[uuid.UUID]]] = []
|
||
|
||
async def spy(session: AsyncSession, bound_version: ConsolidationVersion, entity_ids: Sequence[uuid.UUID]) -> int:
|
||
calls.append((bound_version.id, list(entity_ids)))
|
||
return 0
|
||
|
||
monkeypatch.setattr(consolidation_documents, "bind_confirmed", spy)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, outside[0]]))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
card = response.json()
|
||
assert {m["id"] for m in card["members"]} == {str(e.id) for e in [*inside, outside[0]]}
|
||
assert card["members_count"] == THREE
|
||
assert calls == [(version.id, [outside[0].id])]
|
||
assert await _member_ids(seeded, version.id) == {e.id for e in [*inside, outside[0]]}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_unchanged_members_are_not_touched_by_picking_up(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||
) -> None:
|
||
"""Подхват не вызывается для ЮЛ, которые уже в периметре, и не вызывается, если добавлять некого."""
|
||
version, inside, _, _ = await _version(seeded, members=THREE, spare=0)
|
||
calls: list[list[uuid.UUID]] = []
|
||
|
||
async def spy(session: AsyncSession, bound_version: ConsolidationVersion, entity_ids: Sequence[uuid.UUID]) -> int:
|
||
calls.append(list(entity_ids))
|
||
return 0
|
||
|
||
monkeypatch.setattr(consolidation_documents, "bind_confirmed", spy)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body(inside[:2]))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
assert calls == []
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_duplicates_are_collapsed(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, outside, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
ids = [outside[0], inside[0], outside[0], inside[1], inside[0]]
|
||
|
||
response = await client.put(_url(version.id), json=_body(ids))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
assert response.json()["members_count"] == THREE
|
||
assert await _member_ids(seeded, version.id) == {inside[0].id, inside[1].id, outside[0].id}
|
||
count = await seeded.execute(
|
||
select(func.count())
|
||
.select_from(ConsolidationMember)
|
||
.where(ConsolidationMember.consolidation_version_id == version.id)
|
||
)
|
||
assert count.scalar_one() == THREE
|
||
|
||
|
||
# --- удаление ------------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_removing_member_drops_its_bindings_only(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Удалённое ЮЛ теряет привязки документов к этой версии; документы оставшихся и других версий не затронуты."""
|
||
version, inside, _, _ = await _version(seeded, members=THREE, spare=0)
|
||
documents = {}
|
||
for entity in inside:
|
||
documents[entity.id] = await make_confirmed_osv(
|
||
seeded,
|
||
entity=entity,
|
||
account=ACCOUNT,
|
||
year=2024,
|
||
quarter=PeriodQuarter.Q3,
|
||
is_cumulative=True,
|
||
rows=[{"name": "ООО Контрагент", "inn": "7701234567", "closing_debit": 10}],
|
||
)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=version.id, document_id=documents[entity.id].id))
|
||
other, other_entities, _, _ = await _version(seeded, spare=0, locked_by=None)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=other.id, document_id=documents[inside[2].id].id))
|
||
await seeded.flush()
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body(inside[:2]))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
assert await _member_ids(seeded, version.id) == {inside[0].id, inside[1].id}
|
||
assert await _bound_documents(seeded, version.id) == {documents[inside[0].id].id, documents[inside[1].id].id}
|
||
assert await _bound_documents(seeded, other.id) == {documents[inside[2].id].id}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_unbind_entity_removes_only_that_entitys_documents_of_that_version(seeded: AsyncSession) -> None:
|
||
"""`unbind_entity` (Task 6 заменит тело, имя и сигнатура — прежние) возвращает число снятых привязок."""
|
||
version, inside, _, _ = await _version(seeded, locked_by=None)
|
||
docs = []
|
||
for entity in inside:
|
||
document = await make_confirmed_osv(
|
||
seeded,
|
||
entity=entity,
|
||
account=ACCOUNT,
|
||
year=2024,
|
||
quarter=PeriodQuarter.Q3,
|
||
is_cumulative=True,
|
||
rows=[{"name": "ООО Контрагент", "inn": "7701234567", "closing_debit": 10}],
|
||
)
|
||
docs.append(document)
|
||
seeded.add(ConsolidationDocument(consolidation_version_id=version.id, document_id=document.id))
|
||
await seeded.flush()
|
||
|
||
removed = await consolidation_documents.unbind_entity(seeded, version_id=version.id, entity_id=inside[0].id)
|
||
|
||
assert removed == 1
|
||
assert await _bound_documents(seeded, version.id) == {docs[1].id}
|
||
|
||
|
||
# --- проверки ------------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_entity_of_other_group_is_rejected(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, _, _ = await _version(seeded)
|
||
office = (await seeded.execute(select(RegionalOffice).where(RegionalOffice.name == ROSTOV))).scalar_one()
|
||
stranger_group = await make_company_group(seeded, name="ГК Чужая", office=office)
|
||
stranger = await make_legal_entity(seeded, name="ООО Чужое", group=stranger_group)
|
||
ungrouped = await make_legal_entity(seeded, name="ООО Без ГК")
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, stranger, ungrouped]))
|
||
|
||
assert response.status_code == HTTP_UNPROCESSABLE
|
||
detail = response.json()["detail"]
|
||
assert detail["code"] == "ENTITY_NOT_IN_GROUP"
|
||
assert set(detail["legal_entity_ids"]) == {str(stranger.id), str(ungrouped.id)}
|
||
assert await _member_ids(seeded, version.id) == {e.id for e in inside}
|
||
assert await _events(seeded) == []
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_unknown_entity_is_rejected_as_not_in_group(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, _, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
ghost = uuid.uuid4()
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, ghost]))
|
||
|
||
assert response.status_code == HTTP_UNPROCESSABLE
|
||
assert response.json()["detail"]["code"] == "ENTITY_NOT_IN_GROUP"
|
||
assert response.json()["detail"]["legal_entity_ids"] == [str(ghost)]
|
||
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.parametrize("count", [0, 1])
|
||
async def test_perimeter_smaller_than_two_is_rejected(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession, count: int
|
||
) -> None:
|
||
version, inside, _, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body(inside[:count]))
|
||
|
||
assert response.status_code == HTTP_UNPROCESSABLE
|
||
assert response.json()["detail"]["code"] == "PERIMETER_TOO_SMALL"
|
||
assert await _member_ids(seeded, version.id) == {e.id for e in inside}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_duplicates_do_not_make_a_perimeter_of_two(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Два одинаковых id — это один ЮЛ: меньше двух."""
|
||
version, inside, _, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([inside[0], inside[0]]))
|
||
|
||
assert response.status_code == HTTP_UNPROCESSABLE
|
||
assert response.json()["detail"]["code"] == "PERIMETER_TOO_SMALL"
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_foreign_entity_is_reported_before_size(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Порядок проверок: принадлежность ГК раньше размера (одно чужое ЮЛ → `ENTITY_NOT_IN_GROUP`)."""
|
||
version, _, _, _ = await _version(seeded)
|
||
stranger = await make_legal_entity(seeded, name="ООО Чужое")
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([stranger]))
|
||
|
||
assert response.json()["detail"]["code"] == "ENTITY_NOT_IN_GROUP"
|
||
|
||
|
||
# --- редактируемость, видимость, право -----------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_members_without_lock_is_conflict(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, outside, _ = await _version(seeded, locked_by=None)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, outside[0]]))
|
||
|
||
assert response.status_code == HTTP_CONFLICT
|
||
assert response.json()["detail"]["code"] == "LOCK_REQUIRED"
|
||
assert await _member_ids(seeded, version.id) == {e.id for e in inside}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_members_with_foreign_lock_is_conflict(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, outside, _ = await _version(seeded, locked_by=RKA)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, outside[0]]))
|
||
|
||
assert response.status_code == HTTP_CONFLICT
|
||
detail = response.json()["detail"]
|
||
assert detail["code"] == "LOCKED_BY_OTHER"
|
||
assert detail["locked_by"] == str((await _user(seeded, RKA)).id)
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_members_of_cancelled_version_is_not_editable(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
version, inside, outside, _ = await _version(seeded, status=ConsolidationStatus.CANCELLED)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, outside[0]]))
|
||
|
||
assert response.status_code == HTTP_CONFLICT
|
||
assert response.json()["detail"]["code"] == "VERSION_NOT_EDITABLE"
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_editability_is_checked_before_body_validation(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
"""Без блокировки даже заведомо неверный состав даёт `LOCK_REQUIRED` (порядок спеки: редактируемость раньше)."""
|
||
version, _, _, _ = await _version(seeded, locked_by=None)
|
||
stranger = await make_legal_entity(seeded, name="ООО Чужое")
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([stranger]))
|
||
|
||
assert response.status_code == HTTP_CONFLICT
|
||
assert response.json()["detail"]["code"] == "LOCK_REQUIRED"
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_members_of_foreign_version_is_not_found(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, inside, outside, _ = await _version(seeded, office_name=KRASNODAR, locked_by=None)
|
||
client = await auth_client(KA_RF)
|
||
body = _body([*inside, outside[0]])
|
||
|
||
foreign = await client.put(_url(version.id), json=body)
|
||
missing = await client.put(_url(uuid.uuid4()), json=body)
|
||
|
||
assert foreign.status_code == HTTP_NOT_FOUND
|
||
assert foreign.json() == missing.json()
|
||
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.parametrize("email", [CURATOR, EXPERT])
|
||
async def test_members_requires_consolidation_edit(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession, email: str
|
||
) -> None:
|
||
version, inside, outside, _ = await _version(seeded)
|
||
client = await auth_client(email)
|
||
|
||
response = await client.put(_url(version.id), json=_body([*inside, outside[0]]))
|
||
|
||
assert response.status_code == HTTP_FORBIDDEN
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_members_rejects_malformed_body(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
version, _, _, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json={"legal_entity_ids": ["не-uuid"]})
|
||
|
||
assert response.status_code == HTTP_UNPROCESSABLE
|
||
|
||
|
||
# --- аудит и независимость от ГСК ----------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_membership_change_is_audited_with_full_sorted_lists(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
version, inside, outside, _ = await _version(seeded, members=THREE, spare=1)
|
||
client = await auth_client(KA_RF)
|
||
new = [inside[0], inside[1], outside[0]] # inside[2] ушёл, outside[0] пришёл
|
||
|
||
response = await client.put(_url(version.id), json=_body(new))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
(event,) = await _events(seeded)
|
||
assert event.section == AuditSection.CONSOLIDATION
|
||
assert event.object_type == "consolidation_version"
|
||
assert event.object_id == version.id
|
||
assert event.consolidation_version_id == version.id
|
||
assert event.user_id == (await _user(seeded, KA_RF)).id
|
||
assert event.before_value == {"legal_entity_ids": sorted(str(e.id) for e in inside)}
|
||
assert event.after_value == {"legal_entity_ids": sorted(str(e.id) for e in new)}
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_same_perimeter_is_ok_without_audit(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||
"""Состав не изменился (в том числе порядок id другой): 200 с карточкой, события нет (design #25)."""
|
||
version, inside, _, _ = await _version(seeded)
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body(list(reversed(inside))))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
assert response.json()["members_count"] == len(inside)
|
||
assert await _events(seeded) == []
|
||
|
||
|
||
@pytest.mark.integration
|
||
async def test_kept_members_and_related_group_are_untouched(
|
||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||
) -> None:
|
||
"""Оставшиеся члены не пересоздаются (`is_head_entity`, `id`), `related_group_member` не меняется."""
|
||
version, inside, outside, group_id = await _version(seeded, members=THREE, spare=1)
|
||
head = (
|
||
await seeded.execute(
|
||
select(ConsolidationMember).where(
|
||
ConsolidationMember.consolidation_version_id == version.id,
|
||
ConsolidationMember.legal_entity_id == inside[0].id,
|
||
)
|
||
)
|
||
).scalar_one()
|
||
head.is_head_entity = True
|
||
group = await seeded.get(CompanyGroup, group_id)
|
||
assert group is not None
|
||
related = await make_related_group(seeded, group=group)
|
||
for entity in inside:
|
||
await make_membership(seeded, related_group=related, entity=entity)
|
||
await seeded.flush()
|
||
before = await _related_state(seeded, related.id)
|
||
kept_id = head.id
|
||
client = await auth_client(KA_RF)
|
||
|
||
response = await client.put(_url(version.id), json=_body([inside[0], inside[1], outside[0]]))
|
||
|
||
assert response.status_code == HTTP_OK
|
||
assert await _related_state(seeded, related.id) == before
|
||
kept = (
|
||
await seeded.execute(
|
||
select(ConsolidationMember)
|
||
.where(ConsolidationMember.id == kept_id)
|
||
.execution_options(populate_existing=True)
|
||
)
|
||
).scalar_one()
|
||
assert kept.is_head_entity is True
|
||
head_flags = {m["id"]: m["is_head_entity"] for m in response.json()["members"]}
|
||
assert head_flags[str(inside[0].id)] is True
|
||
assert head_flags[str(outside[0].id)] is False
|
||
|
||
|
||
async def _related_state(session: AsyncSession, related_group_id: uuid.UUID) -> list[tuple[uuid.UUID, bool, Any]]:
|
||
stmt = (
|
||
select(RelatedGroupMember.legal_entity_id, RelatedGroupMember.is_current, RelatedGroupMember.valid_to)
|
||
.where(RelatedGroupMember.related_group_id == related_group_id)
|
||
.order_by(RelatedGroupMember.legal_entity_id)
|
||
)
|
||
return [(row[0], row[1], row[2]) for row in await session.execute(stmt)]
|