Compare commits
No commits in common. "2295d19f980cba87d63e33379b258c3f938123c4" and "631a80d5337bdb0fd4592419a942260e03ec2ef4" have entirely different histories.
2295d19f98
...
631a80d533
@ -225,8 +225,6 @@ docker compose -f ../docs/dev/docker-compose.yml down -v # полный сб
|
|||||||
|
|
||||||
Филиал в ответах ЮЛ и ГК приходит объектом рядом с `*_id`: `regional_office` (ЮЛ, их история) и `base_regional_office`
|
Филиал в ответах ЮЛ и ГК приходит объектом рядом с `*_id`: `regional_office` (ЮЛ, их история) и `base_regional_office`
|
||||||
(ГК, их история) — `{id, name, is_head_office}` или `null`; неактивный филиал тоже отдаётся (имя нужно для истории).
|
(ГК, их история) — `{id, name, is_head_office}` или `null`; неактивный филиал тоже отдаётся (имя нужно для истории).
|
||||||
Имя ГК в ответах ЮЛ (список, карточка, `POST`/`PATCH`/`move`/`withdraw`; не в истории) — `company_group_name` рядом с
|
|
||||||
`company_group_id`: текущее имя ГК, архивной тоже; у монозаёмщика `null` (arm-core#67).
|
|
||||||
|
|
||||||
| Метод и путь | Право | Что делает |
|
| Метод и путь | Право | Что делает |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@ -431,17 +431,6 @@ async def regional_offices_by_ids(
|
|||||||
return {office.id: office for office in rows}
|
return {office.id: office for office in rows}
|
||||||
|
|
||||||
|
|
||||||
async def company_group_names_by_ids(
|
|
||||||
session: AsyncSession, group_ids: Iterable[uuid.UUID | None]
|
|
||||||
) -> dict[uuid.UUID, str]:
|
|
||||||
"""Имена ГК по набору id одним запросом (включая архивные); `None` и дубли игнорируются."""
|
|
||||||
ids = {group_id for group_id in group_ids if group_id is not None}
|
|
||||||
if not ids:
|
|
||||||
return {}
|
|
||||||
rows = (await session.execute(select(CompanyGroup.id, CompanyGroup.name).where(CompanyGroup.id.in_(ids)))).all()
|
|
||||||
return {group_id: name for group_id, name in rows}
|
|
||||||
|
|
||||||
|
|
||||||
async def list_regional_offices(session: AsyncSession) -> list[RegionalOffice]:
|
async def list_regional_offices(session: AsyncSession) -> list[RegionalOffice]:
|
||||||
"""Все филиалы по имени."""
|
"""Все филиалы по имени."""
|
||||||
return list((await session.execute(select(RegionalOffice).order_by(RegionalOffice.name))).scalars().all())
|
return list((await session.execute(select(RegionalOffice).order_by(RegionalOffice.name))).scalars().all())
|
||||||
|
|||||||
@ -229,7 +229,6 @@ class LegalEntityOut(BaseModel):
|
|||||||
entity_type: EntityType | None
|
entity_type: EntityType | None
|
||||||
name: str
|
name: str
|
||||||
company_group_id: uuid.UUID | None
|
company_group_id: uuid.UUID | None
|
||||||
company_group_name: str | None = None # текущее имя ГК (архивной тоже); у монозаёмщика — null
|
|
||||||
regional_office_id: uuid.UUID | None
|
regional_office_id: uuid.UUID | None
|
||||||
regional_office: RegionalOfficeBrief | None = None # объект филиала; неактивный тоже отдаётся
|
regional_office: RegionalOfficeBrief | None = None # объект филиала; неактивный тоже отдаётся
|
||||||
close_date: datetime.date | None
|
close_date: datetime.date | None
|
||||||
|
|||||||
@ -252,9 +252,6 @@ class LegalEntityService:
|
|||||||
entity = await self._get_entity(entity_id)
|
entity = await self._get_entity(entity_id)
|
||||||
detail = LegalEntityDetail.model_validate(entity)
|
detail = LegalEntityDetail.model_validate(entity)
|
||||||
detail.regional_office = (await self.offices_by_ids([entity.regional_office_id])).get(entity.regional_office_id)
|
detail.regional_office = (await self.offices_by_ids([entity.regional_office_id])).get(entity.regional_office_id)
|
||||||
if entity.company_group_id is not None:
|
|
||||||
names = await repo.company_group_names_by_ids(self._session, [entity.company_group_id])
|
|
||||||
detail.company_group_name = names.get(entity.company_group_id)
|
|
||||||
detail.memberships = [
|
detail.memberships = [
|
||||||
MembershipOut(
|
MembershipOut(
|
||||||
related_group_id=group.id,
|
related_group_id=group.id,
|
||||||
@ -288,21 +285,18 @@ class LegalEntityService:
|
|||||||
return {office_id: RegionalOfficeBrief.model_validate(office) for office_id, office in offices.items()}
|
return {office_id: RegionalOfficeBrief.model_validate(office) for office_id, office in offices.items()}
|
||||||
|
|
||||||
async def to_outs(self, entities: Sequence[LegalEntity], counts: Mapping[uuid.UUID, int]) -> list[LegalEntityOut]:
|
async def to_outs(self, entities: Sequence[LegalEntity], counts: Mapping[uuid.UUID, int]) -> list[LegalEntityOut]:
|
||||||
"""Ответы по набору ЮЛ с числом документов; филиалы и имена ГК — по одному батч-запросу."""
|
"""Ответы по набору ЮЛ с числом документов; филиалы — одним батч-запросом."""
|
||||||
offices = await self.offices_by_ids(entity.regional_office_id for entity in entities)
|
offices = await self.offices_by_ids(entity.regional_office_id for entity in entities)
|
||||||
group_names = await repo.company_group_names_by_ids(self._session, (e.company_group_id for e in entities))
|
|
||||||
result: list[LegalEntityOut] = []
|
result: list[LegalEntityOut] = []
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
out = LegalEntityOut.model_validate(entity)
|
out = LegalEntityOut.model_validate(entity)
|
||||||
out.documents_count = counts.get(entity.id, 0)
|
out.documents_count = counts.get(entity.id, 0)
|
||||||
out.regional_office = offices.get(entity.regional_office_id)
|
out.regional_office = offices.get(entity.regional_office_id)
|
||||||
if entity.company_group_id is not None:
|
|
||||||
out.company_group_name = group_names.get(entity.company_group_id)
|
|
||||||
result.append(out)
|
result.append(out)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def to_out(self, entity: LegalEntity, documents_count: int) -> LegalEntityOut:
|
async def to_out(self, entity: LegalEntity, documents_count: int) -> LegalEntityOut:
|
||||||
"""Ответ по одному ЮЛ с объектом филиала и именем ГК."""
|
"""Ответ по одному ЮЛ с объектом филиала."""
|
||||||
return (await self.to_outs([entity], {entity.id: documents_count}))[0]
|
return (await self.to_outs([entity], {entity.id: documents_count}))[0]
|
||||||
|
|
||||||
async def page(self, flt: LegalEntityFilter, page: PageParams) -> tuple[list[LegalEntity], int]:
|
async def page(self, flt: LegalEntityFilter, page: PageParams) -> tuple[list[LegalEntity], int]:
|
||||||
|
|||||||
@ -1,116 +0,0 @@
|
|||||||
"""Имя ГК в ответах ЮЛ (arm-core#67): `company_group_name` рядом с `company_group_id`, без N+1."""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import event
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
||||||
|
|
||||||
from src.common.clock import moscow_date
|
|
||||||
from tests.conftest import AuthClientFactory
|
|
||||||
from tests.factories import make_company_group, make_legal_entity
|
|
||||||
|
|
||||||
HTTP_OK = 200
|
|
||||||
HTTP_CREATED = 201
|
|
||||||
SMALL = 2
|
|
||||||
ENTITIES = "/directories/legal-entities"
|
|
||||||
KA_GO = "ka_go@armka.local"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sql_log(db_engine: AsyncEngine) -> Iterator[list[str]]:
|
|
||||||
statements: list[str] = []
|
|
||||||
|
|
||||||
def record(_conn: Any, _cursor: Any, statement: str, *_rest: Any) -> None:
|
|
||||||
statements.append(statement)
|
|
||||||
|
|
||||||
event.listen(db_engine.sync_engine, "before_cursor_execute", record)
|
|
||||||
try:
|
|
||||||
yield statements
|
|
||||||
finally:
|
|
||||||
event.remove(db_engine.sync_engine, "before_cursor_execute", record)
|
|
||||||
|
|
||||||
|
|
||||||
def _group_selects(statements: list[str]) -> int:
|
|
||||||
return sum(1 for s in statements if s.lstrip().upper().startswith("SELECT") and "FROM dkb.company_group " in s)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
async def test_legal_entity_responses_carry_group_name(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
|
||||||
group = await make_company_group(seeded, name="ГК Имя")
|
|
||||||
new_group = await make_company_group(seeded, name="ГК Имя новая")
|
|
||||||
entity = await make_legal_entity(seeded, name="ООО С группой", group=group)
|
|
||||||
mono = await make_legal_entity(seeded, name="ООО Монозаёмщик")
|
|
||||||
client = await auth_client(KA_GO)
|
|
||||||
today = moscow_date(datetime.datetime.now(datetime.UTC)).isoformat()
|
|
||||||
|
|
||||||
card = (await client.get(f"{ENTITIES}/{entity.id}")).json()
|
|
||||||
assert card["company_group_name"] == "ГК Имя"
|
|
||||||
assert card["company_group_id"] == str(group.id)
|
|
||||||
no_group = (await client.get(f"{ENTITIES}/{mono.id}")).json()
|
|
||||||
assert no_group["company_group_name"] is None and no_group["company_group_id"] is None
|
|
||||||
|
|
||||||
listing = (await client.get(ENTITIES, params={"q": "ООО"})).json()["items"]
|
|
||||||
by_name = {item["name"]: item for item in listing}
|
|
||||||
assert by_name["ООО С группой"]["company_group_name"] == "ГК Имя"
|
|
||||||
assert by_name["ООО Монозаёмщик"]["company_group_name"] is None
|
|
||||||
|
|
||||||
patched = await client.patch(f"{ENTITIES}/{entity.id}", json={"name": "ООО С группой плюс"})
|
|
||||||
assert patched.status_code == HTTP_OK, patched.text
|
|
||||||
assert patched.json()["company_group_name"] == "ГК Имя"
|
|
||||||
|
|
||||||
move = await client.post(
|
|
||||||
f"{ENTITIES}/{entity.id}/move", json={"company_group_id": str(new_group.id), "effective_date": today}
|
|
||||||
)
|
|
||||||
assert move.status_code == HTTP_OK, move.text
|
|
||||||
assert move.json()["company_group_name"] == "ГК Имя новая"
|
|
||||||
|
|
||||||
withdraw = await client.post(
|
|
||||||
f"{ENTITIES}/{entity.id}/withdraw", json={"event": "REMOVE_FROM_GROUP", "effective_date": today}
|
|
||||||
)
|
|
||||||
assert withdraw.status_code == HTTP_OK, withdraw.text
|
|
||||||
assert withdraw.json()["company_group_name"] is None
|
|
||||||
|
|
||||||
created = await client.post(
|
|
||||||
ENTITIES,
|
|
||||||
json={"inn": "7701234567", "name": "ООО Новое", "entity_type": "ЮЛ", "company_group_id": str(group.id)},
|
|
||||||
)
|
|
||||||
assert created.status_code == HTTP_CREATED, created.text
|
|
||||||
assert created.json()["company_group_name"] == "ГК Имя"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
async def test_archived_group_name_is_still_returned(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
|
||||||
group = await make_company_group(seeded, name="ГК Архивная")
|
|
||||||
entity = await make_legal_entity(seeded, name="ООО В архивной", group=group)
|
|
||||||
client = await auth_client(KA_GO)
|
|
||||||
archived = await client.post(f"/directories/company-groups/{group.id}/archive")
|
|
||||||
assert archived.status_code == HTTP_OK, archived.text
|
|
||||||
|
|
||||||
assert (await client.get(f"{ENTITIES}/{entity.id}")).json()["company_group_name"] == "ГК Архивная"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
async def test_list_loads_group_names_in_one_query(
|
|
||||||
auth_client: AuthClientFactory, seeded: AsyncSession, sql_log: list[str]
|
|
||||||
) -> None:
|
|
||||||
client = await auth_client(KA_GO)
|
|
||||||
await client.get(ENTITIES) # прогрев (первый запрос сессии тянет лишнее)
|
|
||||||
total_before = 0
|
|
||||||
for size in (2, 12):
|
|
||||||
for n in range(size):
|
|
||||||
group = await make_company_group(seeded, name=f"ГК имя список {size}-{n}")
|
|
||||||
await make_legal_entity(seeded, name=f"ООО имя список {size}-{n}", group=group)
|
|
||||||
sql_log.clear()
|
|
||||||
response = await client.get(ENTITIES, params={"q": "имя список", "limit": 200})
|
|
||||||
assert response.status_code == HTTP_OK, response.text
|
|
||||||
items = response.json()["items"]
|
|
||||||
assert all(item["company_group_name"] is not None for item in items)
|
|
||||||
if size == SMALL:
|
|
||||||
total_before = len(sql_log)
|
|
||||||
else:
|
|
||||||
assert len(items) == 14 # noqa: PLR2004 - 2 + 12
|
|
||||||
assert len(sql_log) == total_before
|
|
||||||
assert _group_selects(sql_log) == 1
|
|
||||||
@ -50,8 +50,7 @@ API справочников ядра: группы компаний (ГК), ю
|
|||||||
`GET …` SHALL давать список с фильтрами `company_group_id`, `outside_group` (`company_group_id IS NULL`), `inn`
|
`GET …` SHALL давать список с фильтрами `company_group_id`, `outside_group` (`company_group_id IS NULL`), `inn`
|
||||||
(точно), `q` (подстрока имени/ИНН/КПП), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими
|
(точно), `q` (подстрока имени/ИНН/КПП), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими
|
||||||
членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число
|
членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число
|
||||||
документов файлов ЮЛ, кроме файлов `SUPERSEDED`. Во всех ответах ЮЛ, кроме снимков истории, SHALL быть `company_group_name` —
|
документов файлов ЮЛ, кроме файлов `SUPERSEDED`.
|
||||||
текущее имя ГК `company_group_id` (архивной тоже) или `null` у монозаёмщика; имена набора ЮЛ — одним запросом.
|
|
||||||
|
|
||||||
#### Scenario: Неверная длина ИНН
|
#### Scenario: Неверная длина ИНН
|
||||||
- **WHEN** создаётся ИП с ИНН из 10 цифр
|
- **WHEN** создаётся ИП с ИНН из 10 цифр
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user