feat(directories): crm_id ГСК/ГСЗ сохраняется, 409 CRM_ID_EXISTS (arm-core#112)
crm_id пишется в колонку related_group.crm_id (v5.19), попадает в ответы и в payload аудита; занятый значением другой ГСК/ГСЗ даёт 409 CRM_ID_EXISTS (pre-check и uq_related_group_crm_id на гонку). Смена crm_id пишется как GROUP_CHANGE с before/after всех изменённых полей (Р-41, P3-18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
4f21160cf5
commit
b7f45c6a47
@ -830,7 +830,7 @@ API: список счетов `GET /directories/accounts`, правила ве
|
|||||||
|
|
||||||
**Гонки уникальных ограничений.** Если параллельный запрос успел вставить запись между проверкой сервиса и INSERT,
|
**Гонки уникальных ограничений.** Если параллельный запрос успел вставить запись между проверкой сервиса и INSERT,
|
||||||
БД отклоняет вставку (`unique_violation`), и ответ тот же 409 с тем же кодом, что у проверки (реестр —
|
БД отклоняет вставку (`unique_violation`), и ответ тот же 409 с тем же кодом, что у проверки (реестр —
|
||||||
`src/common/constraint_conflicts.py`): `CRM_ID_EXISTS` (ГК и ЮЛ), `INN_EXISTS`, `ALIAS_EXISTS`, `EMAIL_EXISTS`,
|
`src/common/constraint_conflicts.py`): `CRM_ID_EXISTS` (ГК, ЮЛ и ГСК/ГСЗ), `INN_EXISTS`, `ALIAS_EXISTS`, `EMAIL_EXISTS`,
|
||||||
`MEMBER_EXISTS`/`MEMBER_OF_OTHER_GROUP`. В теле гонки — только код и постоянный текст, без дополнительных полей
|
`MEMBER_EXISTS`/`MEMBER_OF_OTHER_GROUP`. В теле гонки — только код и постоянный текст, без дополнительных полей
|
||||||
(`legal_entity_id`, `related_group_id`) и без значений ключей. Особенности: повторное добавление в ту же группу
|
(`legal_entity_id`, `related_group_id`) и без значений ключей. Особенности: повторное добавление в ту же группу
|
||||||
отдаёт `MEMBER_OF_OTHER_GROUP` с нейтральным текстом «ЮЛ уже входит в группу этого вида» (его раньше срабатывает индекс
|
отдаёт `MEMBER_OF_OTHER_GROUP` с нейтральным текстом «ЮЛ уже входит в группу этого вида» (его раньше срабатывает индекс
|
||||||
@ -853,7 +853,7 @@ email без учёта регистра проверяет только сер
|
|||||||
| `WRONG_GROUP_KIND` | 422 | в `gsk_id` формы ЮЛ передана ГСЗ или в `gsz_id` — ГСК (`POST`/`PATCH /directories/legal-entities`) |
|
| `WRONG_GROUP_KIND` | 422 | в `gsk_id` формы ЮЛ передана ГСЗ или в `gsz_id` — ГСК (`POST`/`PATCH /directories/legal-entities`) |
|
||||||
| `UNKNOWN_ROLE` / `UNKNOWN_PERMISSION` | 422 | неизвестная или неактивная роль / неизвестный код права (поля `roles` / `permissions`) |
|
| `UNKNOWN_ROLE` / `UNKNOWN_PERMISSION` | 422 | неизвестная или неактивная роль / неизвестный код права (поля `roles` / `permissions`) |
|
||||||
| `MISSING_COLUMNS` / `UNSUPPORTED_FILE` | 422 | файл импорта целиком (`missing` — перечень колонок) |
|
| `MISSING_COLUMNS` / `UNSUPPORTED_FILE` | 422 | файл импорта целиком (`missing` — перечень колонок) |
|
||||||
| `CRM_ID_EXISTS` | 409 | ГК или ЮЛ с таким `crm_id` уже есть |
|
| `CRM_ID_EXISTS` | 409 | ГК, ЮЛ или ГСК/ГСЗ с таким `crm_id` уже есть |
|
||||||
| `INN_EXISTS` | 409 | ЮЛ с таким ИНН уже есть (без дополнительных полей) |
|
| `INN_EXISTS` | 409 | ЮЛ с таким ИНН уже есть (без дополнительных полей) |
|
||||||
| `LEGAL_ENTITY_CLOSED` | 409 | перевод закрытого ЮЛ |
|
| `LEGAL_ENTITY_CLOSED` | 409 | перевод закрытого ЮЛ |
|
||||||
| `SAME_COMPANY_GROUP` | 409 | перевод в ту же ГК |
|
| `SAME_COMPANY_GROUP` | 409 | перевод в ту же ГК |
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class ConstraintConflict:
|
|||||||
UNIQUE_CONSTRAINT_CONFLICTS: dict[str, ConstraintConflict] = {
|
UNIQUE_CONSTRAINT_CONFLICTS: dict[str, ConstraintConflict] = {
|
||||||
"uq_company_group_crm_id": ConstraintConflict("CRM_ID_EXISTS", "ГК с таким crm_id уже существует"),
|
"uq_company_group_crm_id": ConstraintConflict("CRM_ID_EXISTS", "ГК с таким crm_id уже существует"),
|
||||||
"uq_legal_entity_crm_id": ConstraintConflict("CRM_ID_EXISTS", "ЮЛ с таким crm_id уже существует"),
|
"uq_legal_entity_crm_id": ConstraintConflict("CRM_ID_EXISTS", "ЮЛ с таким crm_id уже существует"),
|
||||||
|
"uq_related_group_crm_id": ConstraintConflict("CRM_ID_EXISTS", "ГСК/ГСЗ с таким crm_id уже существует"),
|
||||||
"uq_legal_entity_inn": ConstraintConflict("INN_EXISTS", "ЮЛ с таким ИНН уже существует"),
|
"uq_legal_entity_inn": ConstraintConflict("INN_EXISTS", "ЮЛ с таким ИНН уже существует"),
|
||||||
"uq_legal_entity_alias_legal_entity_id_normalized_alias": ConstraintConflict(
|
"uq_legal_entity_alias_legal_entity_id_normalized_alias": ConstraintConflict(
|
||||||
"ALIAS_EXISTS", "У этого ЮЛ уже есть такой алиас"
|
"ALIAS_EXISTS", "У этого ЮЛ уже есть такой алиас"
|
||||||
|
|||||||
@ -101,13 +101,23 @@ async def get_company_group(session: AsyncSession, scope: VisibilityScope, group
|
|||||||
|
|
||||||
|
|
||||||
async def crm_id_taken(session: AsyncSession, crm_id: str, *, except_group_id: uuid.UUID | None = None) -> bool:
|
async def crm_id_taken(session: AsyncSession, crm_id: str, *, except_group_id: uuid.UUID | None = None) -> bool:
|
||||||
"""Есть ли у какой-либо ГК (кроме `except_group_id`) такой `crm_id` — без фильтра видимости: уникален глобально."""
|
"""Есть ли у какой-либо ГК (кроме `except_group_id`) такой `crm_id`; без фильтра видимости (уникален глобально)."""
|
||||||
stmt = select(CompanyGroup.id).where(CompanyGroup.crm_id == crm_id)
|
stmt = select(CompanyGroup.id).where(CompanyGroup.crm_id == crm_id)
|
||||||
if except_group_id is not None:
|
if except_group_id is not None:
|
||||||
stmt = stmt.where(CompanyGroup.id != except_group_id)
|
stmt = stmt.where(CompanyGroup.id != except_group_id)
|
||||||
return (await session.execute(stmt.limit(1))).first() is not None
|
return (await session.execute(stmt.limit(1))).first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def related_group_crm_id_taken(
|
||||||
|
session: AsyncSession, crm_id: str, *, except_group_id: uuid.UUID | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""Есть ли у ГСК/ГСЗ (кроме `except_group_id`) такой `crm_id`; без фильтра видимости (уникален глобально)."""
|
||||||
|
stmt = select(RelatedGroup.id).where(RelatedGroup.crm_id == crm_id)
|
||||||
|
if except_group_id is not None:
|
||||||
|
stmt = stmt.where(RelatedGroup.id != except_group_id)
|
||||||
|
return (await session.execute(stmt.limit(1))).first() is not None
|
||||||
|
|
||||||
|
|
||||||
async def count_legal_entities(session: AsyncSession, group_id: uuid.UUID) -> int:
|
async def count_legal_entities(session: AsyncSession, group_id: uuid.UUID) -> int:
|
||||||
"""Число ЮЛ/ИП с `company_group_id = group_id`."""
|
"""Число ЮЛ/ИП с `company_group_id = group_id`."""
|
||||||
stmt = select(func.count(LegalEntity.id)).where(LegalEntity.company_group_id == group_id)
|
stmt = select(func.count(LegalEntity.id)).where(LegalEntity.company_group_id == group_id)
|
||||||
|
|||||||
@ -363,7 +363,7 @@ class LegalEntityFilter(BaseModel):
|
|||||||
class RelatedGroupCreate(BaseModel):
|
class RelatedGroupCreate(BaseModel):
|
||||||
"""Создание ГСК/ГСЗ в ГК: `code` (`GSK-`/`GSZ-NNNNNN`) назначает сервис.
|
"""Создание ГСК/ГСЗ в ГК: `code` (`GSK-`/`GSZ-NNNNNN`) назначает сервис.
|
||||||
|
|
||||||
`crm_id` («ID ГСК/ГСЗ в CRM» форм 51-03/51-04) принимается и отбрасывается: колонки в DBML нет (arm-core #112).
|
`crm_id` («ID ГСК/ГСЗ в CRM» форм 51-03/51-04, v5.19) сохраняется; пустая строка — «нет значения».
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
@ -387,7 +387,7 @@ class RelatedGroupCreate(BaseModel):
|
|||||||
class RelatedGroupUpdate(BaseModel):
|
class RelatedGroupUpdate(BaseModel):
|
||||||
"""Изменение ГСК/ГСЗ: `name`, `created_date` (вид и ГК не меняются); переданные поля — `model_fields_set`.
|
"""Изменение ГСК/ГСЗ: `name`, `created_date` (вид и ГК не меняются); переданные поля — `model_fields_set`.
|
||||||
|
|
||||||
`crm_id` (форма 77-02) принимается и отбрасывается, как при создании (arm-core #112).
|
`crm_id` (форма 77-02) сохраняется; явный `null` очищает значение.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
@ -422,7 +422,7 @@ class MemberAdd(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RelatedGroupOut(BaseModel):
|
class RelatedGroupOut(BaseModel):
|
||||||
"""ГСК/ГСЗ; `members_count` — число текущих членов; `crm_id` — всегда `null`, пока нет колонки (arm-core #112)."""
|
"""ГСК/ГСЗ; `members_count` — число текущих членов; `crm_id` — из БД (v5.19)."""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ MEMBERSHIP_NOT_FOUND = "ЮЛ не входит в эту группу"
|
|||||||
OBJECT_TYPE = "related_group"
|
OBJECT_TYPE = "related_group"
|
||||||
MEMBER_OBJECT_TYPE = "related_group_member"
|
MEMBER_OBJECT_TYPE = "related_group_member"
|
||||||
CODE_PREFIXES = {RelatedGroupKind.gsk: "GSK", RelatedGroupKind.gsz: "GSZ"}
|
CODE_PREFIXES = {RelatedGroupKind.gsk: "GSK", RelatedGroupKind.gsz: "GSZ"}
|
||||||
TRACKED_FIELDS = ("name", "created_date") # поля группы, изменяемые PATCH
|
TRACKED_FIELDS = ("name", "created_date", "crm_id") # поля группы, изменяемые PATCH
|
||||||
|
|
||||||
|
|
||||||
def member_payload(member: RelatedGroupMember) -> dict[str, Any]:
|
def member_payload(member: RelatedGroupMember) -> dict[str, Any]:
|
||||||
@ -115,12 +115,15 @@ class RelatedGroupService:
|
|||||||
async def create(self, company_group_id: uuid.UUID, data: RelatedGroupCreate) -> RelatedGroup:
|
async def create(self, company_group_id: uuid.UUID, data: RelatedGroupCreate) -> RelatedGroup:
|
||||||
"""Новая ГСК/ГСЗ видимой ГК с кодом `GSK-`/`GSZ-NNNNNN`; аудит `MEMBERSHIP_CHANGE`."""
|
"""Новая ГСК/ГСЗ видимой ГК с кодом `GSK-`/`GSZ-NNNNNN`; аудит `MEMBERSHIP_CHANGE`."""
|
||||||
company_group = await self._visible_company_group(company_group_id)
|
company_group = await self._visible_company_group(company_group_id)
|
||||||
|
if data.crm_id is not None:
|
||||||
|
await self._check_crm_id_free(data.crm_id)
|
||||||
group = RelatedGroup(
|
group = RelatedGroup(
|
||||||
code=await repo.next_code(self._session, RELATED_GROUP_CODE_SEQ, CODE_PREFIXES[data.kind]),
|
code=await repo.next_code(self._session, RELATED_GROUP_CODE_SEQ, CODE_PREFIXES[data.kind]),
|
||||||
company_group_id=company_group.id,
|
company_group_id=company_group.id,
|
||||||
kind=data.kind,
|
kind=data.kind,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
created_date=data.created_date,
|
created_date=data.created_date,
|
||||||
|
crm_id=data.crm_id,
|
||||||
)
|
)
|
||||||
self._session.add(group)
|
self._session.add(group)
|
||||||
await self._session.flush()
|
await self._session.flush()
|
||||||
@ -134,7 +137,11 @@ class RelatedGroupService:
|
|||||||
return group
|
return group
|
||||||
|
|
||||||
async def update(self, group_id: uuid.UUID, data: RelatedGroupUpdate) -> RelatedGroup:
|
async def update(self, group_id: uuid.UUID, data: RelatedGroupUpdate) -> RelatedGroup:
|
||||||
"""Изменение `name`/`created_date`; без фактических изменений ничего не пишется."""
|
"""Изменение `name`/`created_date`/`crm_id`; без фактических изменений ничего не пишется.
|
||||||
|
|
||||||
|
Смена `crm_id` пишется как изменение группы — `GROUP_CHANGE` с `before`/`after` всех изменённых полей
|
||||||
|
(Р-41, P3-18); прочие правки — `MEMBERSHIP_CHANGE`, как раньше.
|
||||||
|
"""
|
||||||
group = await self._get_group(group_id)
|
group = await self._get_group(group_id)
|
||||||
changes: dict[str, Any] = {
|
changes: dict[str, Any] = {
|
||||||
field: getattr(data, field)
|
field: getattr(data, field)
|
||||||
@ -143,12 +150,14 @@ class RelatedGroupService:
|
|||||||
}
|
}
|
||||||
if not changes:
|
if not changes:
|
||||||
return group
|
return group
|
||||||
|
if changes.get("crm_id") is not None:
|
||||||
|
await self._check_crm_id_free(changes["crm_id"], except_group_id=group.id)
|
||||||
before = {field: getattr(group, field) for field in changes}
|
before = {field: getattr(group, field) for field in changes}
|
||||||
for field, value in changes.items():
|
for field, value in changes.items():
|
||||||
setattr(group, field, value)
|
setattr(group, field, value)
|
||||||
await self._session.flush()
|
await self._session.flush()
|
||||||
await self._audit.record(
|
await self._audit.record(
|
||||||
action=AuditAction.MEMBERSHIP_CHANGE,
|
action=AuditAction.GROUP_CHANGE if "crm_id" in changes else AuditAction.MEMBERSHIP_CHANGE,
|
||||||
section=AuditSection.DIRECTORIES,
|
section=AuditSection.DIRECTORIES,
|
||||||
object_type=OBJECT_TYPE,
|
object_type=OBJECT_TYPE,
|
||||||
object_id=group.id,
|
object_id=group.id,
|
||||||
@ -249,4 +258,9 @@ class RelatedGroupService:
|
|||||||
"kind": group.kind,
|
"kind": group.kind,
|
||||||
"name": group.name,
|
"name": group.name,
|
||||||
"created_date": group.created_date,
|
"created_date": group.created_date,
|
||||||
|
"crm_id": group.crm_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def _check_crm_id_free(self, crm_id: str, *, except_group_id: uuid.UUID | None = None) -> None:
|
||||||
|
if await repo.related_group_crm_id_taken(self._session, crm_id, except_group_id=except_group_id):
|
||||||
|
raise ConflictError("CRM_ID_EXISTS", "ГСК/ГСЗ с таким crm_id уже существует")
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
"""`crm_id` ГСК/ГСЗ в API без колонки в БД (arm-core #111; колонка — #112).
|
"""`crm_id` ГСК/ГСЗ (v5.19, arm-core #112): сохраняется, уникален, смена пишется в аудит как `GROUP_CHANGE` (Р-41)."""
|
||||||
|
|
||||||
Формы 51-03 / 51-04 / 77-02 шлют «ID ГСК/ГСЗ в CRM»: POST/PATCH его принимают, значение отбрасывается,
|
|
||||||
в ответах `crm_id` всегда `null`, в аудит не попадает.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -19,16 +15,16 @@ from tests.factories import make_company_group
|
|||||||
|
|
||||||
HTTP_OK = 200
|
HTTP_OK = 200
|
||||||
HTTP_CREATED = 201
|
HTTP_CREATED = 201
|
||||||
|
HTTP_CONFLICT = 409
|
||||||
HTTP_UNPROCESSABLE = 422
|
HTTP_UNPROCESSABLE = 422
|
||||||
GROUPS = "/directories/company-groups"
|
GROUPS = "/directories/company-groups"
|
||||||
RELATED = "/directories/related-groups"
|
RELATED = "/directories/related-groups"
|
||||||
GO = "ka_go@armka.local"
|
GO = "ka_go@armka.local"
|
||||||
|
CONFLICT_DETAIL = {"code": "CRM_ID_EXISTS", "message": "ГСК/ГСЗ с таким crm_id уже существует"}
|
||||||
|
|
||||||
|
|
||||||
async def _events(session: AsyncSession) -> Sequence[AuditEvent]:
|
async def _events(session: AsyncSession, action: AuditAction) -> Sequence[AuditEvent]:
|
||||||
stmt = select(AuditEvent).where(
|
stmt = select(AuditEvent).where(AuditEvent.action == action, AuditEvent.object_type == "related_group")
|
||||||
AuditEvent.action == AuditAction.MEMBERSHIP_CHANGE, AuditEvent.object_type == "related_group"
|
|
||||||
)
|
|
||||||
return (await session.execute(stmt.order_by(AuditEvent.occurred_at, AuditEvent.id))).scalars().all()
|
return (await session.execute(stmt.order_by(AuditEvent.occurred_at, AuditEvent.id))).scalars().all()
|
||||||
|
|
||||||
|
|
||||||
@ -37,49 +33,79 @@ async def _create(client: httpx.AsyncClient, group_id: Any, **extra: Any) -> htt
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_create_accepts_crm_id_and_drops_it(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
async def test_create_related_group_stores_crm_id(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
group = await make_company_group(seeded)
|
group = await make_company_group(seeded)
|
||||||
client = await auth_client(GO)
|
client = await auth_client(GO)
|
||||||
|
|
||||||
response = await _create(client, group.id, crm_id=" 3-9CGSK01 ")
|
response = await _create(client, group.id, crm_id=" 51-03-0001 ")
|
||||||
assert response.status_code == HTTP_CREATED, response.text
|
assert response.status_code == HTTP_CREATED, response.text
|
||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["crm_id"] is None
|
assert body["crm_id"] == "51-03-0001"
|
||||||
assert body["name"] == "ГСК А"
|
assert (await client.get(f"{RELATED}/{body['id']}")).json()["crm_id"] == "51-03-0001"
|
||||||
events = await _events(seeded)
|
listed = await client.get(f"{GROUPS}/{group.id}/related-groups")
|
||||||
assert len(events) == 1
|
assert [row["crm_id"] for row in listed.json()] == ["51-03-0001"]
|
||||||
assert "crm_id" not in events[0].after_value
|
(event,) = await _events(seeded, AuditAction.MEMBERSHIP_CHANGE)
|
||||||
|
assert event.after_value["crm_id"] == "51-03-0001"
|
||||||
|
|
||||||
for value in ("", None):
|
for value in ("", None):
|
||||||
assert (await _create(client, group.id, crm_id=value)).status_code == HTTP_CREATED
|
created = await _create(client, group.id, crm_id=value)
|
||||||
|
assert created.status_code == HTTP_CREATED
|
||||||
|
assert created.json()["crm_id"] is None
|
||||||
assert (await _create(client, group.id, crm_id=123)).status_code == HTTP_UNPROCESSABLE
|
assert (await _create(client, group.id, crm_id=123)).status_code == HTTP_UNPROCESSABLE
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_patch_accepts_crm_id_without_changes(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
async def test_patch_related_group_crm_id_audited(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
group = await make_company_group(seeded)
|
group = await make_company_group(seeded)
|
||||||
client = await auth_client(GO)
|
client = await auth_client(GO)
|
||||||
gsk = (await _create(client, group.id)).json()
|
gsk = (await _create(client, group.id)).json()
|
||||||
|
|
||||||
only_crm = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": "3-9CGSK01"})
|
response = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": "77-02-0002", "name": "ГСК Б"})
|
||||||
assert only_crm.status_code == HTTP_OK, only_crm.text
|
assert response.status_code == HTTP_OK, response.text
|
||||||
assert only_crm.json()["crm_id"] is None
|
assert response.json()["crm_id"] == "77-02-0002"
|
||||||
assert len(await _events(seeded)) == 1 # только создание: crm_id не изменение
|
(event,) = await _events(seeded, AuditAction.GROUP_CHANGE)
|
||||||
|
assert event.before_value["crm_id"] is None
|
||||||
|
assert event.after_value == {"crm_id": "77-02-0002", "name": "ГСК Б"}
|
||||||
|
assert event.before_value["name"] == "ГСК А"
|
||||||
|
|
||||||
with_name = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": None, "name": "ГСК Б"})
|
# смена без crm_id остаётся изменением состава/реквизитов прежнего вида
|
||||||
assert with_name.status_code == HTTP_OK, with_name.text
|
await client.patch(f"{RELATED}/{gsk['id']}", json={"name": "ГСК В"})
|
||||||
assert (with_name.json()["name"], with_name.json()["crm_id"]) == ("ГСК Б", None)
|
assert len(await _events(seeded, AuditAction.GROUP_CHANGE)) == 1
|
||||||
events = await _events(seeded)
|
|
||||||
assert len(events) == 2 # noqa: PLR2004 - создание и смена имени
|
|
||||||
assert events[1].after_value == {"name": "ГСК Б"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_list_and_detail_return_null_crm_id(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
async def test_crm_id_taken_is_409(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
|
group = await make_company_group(seeded)
|
||||||
|
other = await make_company_group(seeded)
|
||||||
|
client = await auth_client(GO)
|
||||||
|
assert (await _create(client, group.id, crm_id="77-02-0002")).status_code == HTTP_CREATED
|
||||||
|
|
||||||
|
same_group = await _create(client, group.id, crm_id="77-02-0002")
|
||||||
|
other_group = await _create(client, other.id, crm_id="77-02-0002", kind="gsz")
|
||||||
|
for response in (same_group, other_group):
|
||||||
|
assert response.status_code == HTTP_CONFLICT, response.text
|
||||||
|
assert response.json()["detail"] == CONFLICT_DETAIL
|
||||||
|
assert len((await client.get(f"{GROUPS}/{group.id}/related-groups")).json()) == 1
|
||||||
|
assert (await client.get(f"{GROUPS}/{other.id}/related-groups")).json() == []
|
||||||
|
|
||||||
|
second = (await _create(client, group.id, crm_id="77-02-0003")).json()
|
||||||
|
patched = await client.patch(f"{RELATED}/{second['id']}", json={"crm_id": "77-02-0002"})
|
||||||
|
assert patched.status_code == HTTP_CONFLICT
|
||||||
|
assert patched.json()["detail"] == CONFLICT_DETAIL
|
||||||
|
# своё же значение — не конфликт
|
||||||
|
same = await client.patch(f"{RELATED}/{second['id']}", json={"crm_id": "77-02-0003"})
|
||||||
|
assert same.status_code == HTTP_OK
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_patch_crm_id_null_clears(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||||
group = await make_company_group(seeded)
|
group = await make_company_group(seeded)
|
||||||
client = await auth_client(GO)
|
client = await auth_client(GO)
|
||||||
gsk = (await _create(client, group.id, crm_id="3-9CGSK01")).json()
|
gsk = (await _create(client, group.id, crm_id="51-03-0001")).json()
|
||||||
|
|
||||||
listed = await client.get(f"{GROUPS}/{group.id}/related-groups")
|
response = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": None})
|
||||||
assert [row["crm_id"] for row in listed.json()] == [None]
|
assert response.status_code == HTTP_OK, response.text
|
||||||
assert (await client.get(f"{RELATED}/{gsk['id']}")).json()["crm_id"] is None
|
assert response.json()["crm_id"] is None
|
||||||
|
(event,) = await _events(seeded, AuditAction.GROUP_CHANGE)
|
||||||
|
assert event.before_value == {"crm_id": "51-03-0001"}
|
||||||
|
assert event.after_value == {"crm_id": None}
|
||||||
|
|||||||
@ -98,6 +98,7 @@ async def test_create_gsk_and_gsz_codes_and_audit(auth_client: AuthClientFactory
|
|||||||
"kind": "gsk",
|
"kind": "gsk",
|
||||||
"name": "ГСК Бисквит",
|
"name": "ГСК Бисквит",
|
||||||
"created_date": "2025-01-15",
|
"created_date": "2025-01-15",
|
||||||
|
"crm_id": None,
|
||||||
}
|
}
|
||||||
assert first.user_id is not None
|
assert first.user_id is not None
|
||||||
|
|
||||||
|
|||||||
@ -180,6 +180,7 @@ def test_registry_codes_and_messages_match_service_precheck_texts() -> None:
|
|||||||
assert codes == {
|
assert codes == {
|
||||||
"uq_company_group_crm_id": "CRM_ID_EXISTS",
|
"uq_company_group_crm_id": "CRM_ID_EXISTS",
|
||||||
"uq_legal_entity_crm_id": "CRM_ID_EXISTS",
|
"uq_legal_entity_crm_id": "CRM_ID_EXISTS",
|
||||||
|
"uq_related_group_crm_id": "CRM_ID_EXISTS",
|
||||||
"uq_legal_entity_inn": "INN_EXISTS",
|
"uq_legal_entity_inn": "INN_EXISTS",
|
||||||
"uq_legal_entity_alias_legal_entity_id_normalized_alias": "ALIAS_EXISTS",
|
"uq_legal_entity_alias_legal_entity_id_normalized_alias": "ALIAS_EXISTS",
|
||||||
"uq_app_user_email": "EMAIL_EXISTS",
|
"uq_app_user_email": "EMAIL_EXISTS",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user