Merge pull request 'ГСК/ГСЗ: POST/PATCH принимают crm_id без сохранения (#111)' (#113) from feat/111-rg-crm-id into main
Reviewed-on: #113
This commit is contained in:
commit
8b86735828
@ -335,33 +335,51 @@ class LegalEntityFilter(BaseModel):
|
||||
|
||||
|
||||
class RelatedGroupCreate(BaseModel):
|
||||
"""Создание ГСК/ГСЗ в ГК: `code` (`GSK-`/`GSZ-NNNNNN`) назначает сервис."""
|
||||
"""Создание ГСК/ГСЗ в ГК: `code` (`GSK-`/`GSZ-NNNNNN`) назначает сервис.
|
||||
|
||||
`crm_id` («ID ГСК/ГСЗ в CRM» форм 51-03/51-04) принимается и отбрасывается: колонки в DBML нет (arm-core #112).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
kind: RelatedGroupKind
|
||||
name: str = Field(min_length=1, max_length=NAME_MAX_LENGTH)
|
||||
created_date: datetime.date | None = None
|
||||
crm_id: str | None = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _strip_name(cls, value: str) -> str | None:
|
||||
return _strip_required_name(value)
|
||||
|
||||
@field_validator("crm_id")
|
||||
@classmethod
|
||||
def _normalize_crm_id(cls, value: str | None) -> str | None:
|
||||
return _blank_to_none(value)
|
||||
|
||||
|
||||
class RelatedGroupUpdate(BaseModel):
|
||||
"""Изменение ГСК/ГСЗ: `name`, `created_date` (вид и ГК не меняются); переданные поля — `model_fields_set`."""
|
||||
"""Изменение ГСК/ГСЗ: `name`, `created_date` (вид и ГК не меняются); переданные поля — `model_fields_set`.
|
||||
|
||||
`crm_id` (форма 77-02) принимается и отбрасывается, как при создании (arm-core #112).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(default=None, min_length=1, max_length=NAME_MAX_LENGTH)
|
||||
created_date: datetime.date | None = None
|
||||
crm_id: str | None = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _strip_name(cls, value: str | None) -> str | None:
|
||||
return _strip_required_name(value)
|
||||
|
||||
@field_validator("crm_id")
|
||||
@classmethod
|
||||
def _normalize_crm_id(cls, value: str | None) -> str | None:
|
||||
return _blank_to_none(value)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _name_is_not_null(self) -> Self:
|
||||
if "name" in self.model_fields_set and self.name is None:
|
||||
@ -378,7 +396,7 @@ class MemberAdd(BaseModel):
|
||||
|
||||
|
||||
class RelatedGroupOut(BaseModel):
|
||||
"""ГСК/ГСЗ; `members_count` — число текущих членов."""
|
||||
"""ГСК/ГСЗ; `members_count` — число текущих членов; `crm_id` — всегда `null`, пока нет колонки (arm-core #112)."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@ -388,6 +406,7 @@ class RelatedGroupOut(BaseModel):
|
||||
kind: RelatedGroupKind
|
||||
name: str
|
||||
created_date: datetime.date | None
|
||||
crm_id: str | None = None
|
||||
members_count: int = 0
|
||||
|
||||
|
||||
|
||||
85
api/tests/test_api_related_group_crm_id.py
Normal file
85
api/tests/test_api_related_group_crm_id.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""`crm_id` ГСК/ГСЗ в API без колонки в БД (arm-core #111; колонка — #112).
|
||||
|
||||
Формы 51-03 / 51-04 / 77-02 шлют «ID ГСК/ГСЗ в CRM»: POST/PATCH его принимают, значение отбрасывается,
|
||||
в ответах `crm_id` всегда `null`, в аудит не попадает.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models.audit_ex import AuditEvent
|
||||
from src.models.enums import AuditAction
|
||||
from tests.conftest import AuthClientFactory
|
||||
from tests.factories import make_company_group
|
||||
|
||||
HTTP_OK = 200
|
||||
HTTP_CREATED = 201
|
||||
HTTP_UNPROCESSABLE = 422
|
||||
GROUPS = "/directories/company-groups"
|
||||
RELATED = "/directories/related-groups"
|
||||
GO = "ka_go@armka.local"
|
||||
|
||||
|
||||
async def _events(session: AsyncSession) -> Sequence[AuditEvent]:
|
||||
stmt = select(AuditEvent).where(
|
||||
AuditEvent.action == AuditAction.MEMBERSHIP_CHANGE, AuditEvent.object_type == "related_group"
|
||||
)
|
||||
return (await session.execute(stmt.order_by(AuditEvent.occurred_at, AuditEvent.id))).scalars().all()
|
||||
|
||||
|
||||
async def _create(client: httpx.AsyncClient, group_id: Any, **extra: Any) -> httpx.Response:
|
||||
return await client.post(f"{GROUPS}/{group_id}/related-groups", json={"kind": "gsk", "name": "ГСК А", **extra})
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_create_accepts_crm_id_and_drops_it(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
group = await make_company_group(seeded)
|
||||
client = await auth_client(GO)
|
||||
|
||||
response = await _create(client, group.id, crm_id=" 3-9CGSK01 ")
|
||||
assert response.status_code == HTTP_CREATED, response.text
|
||||
body = response.json()
|
||||
assert body["crm_id"] is None
|
||||
assert body["name"] == "ГСК А"
|
||||
events = await _events(seeded)
|
||||
assert len(events) == 1
|
||||
assert "crm_id" not in events[0].after_value
|
||||
|
||||
for value in ("", None):
|
||||
assert (await _create(client, group.id, crm_id=value)).status_code == HTTP_CREATED
|
||||
assert (await _create(client, group.id, crm_id=123)).status_code == HTTP_UNPROCESSABLE
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_patch_accepts_crm_id_without_changes(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
group = await make_company_group(seeded)
|
||||
client = await auth_client(GO)
|
||||
gsk = (await _create(client, group.id)).json()
|
||||
|
||||
only_crm = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": "3-9CGSK01"})
|
||||
assert only_crm.status_code == HTTP_OK, only_crm.text
|
||||
assert only_crm.json()["crm_id"] is None
|
||||
assert len(await _events(seeded)) == 1 # только создание: crm_id не изменение
|
||||
|
||||
with_name = await client.patch(f"{RELATED}/{gsk['id']}", json={"crm_id": None, "name": "ГСК Б"})
|
||||
assert with_name.status_code == HTTP_OK, with_name.text
|
||||
assert (with_name.json()["name"], with_name.json()["crm_id"]) == ("ГСК Б", None)
|
||||
events = await _events(seeded)
|
||||
assert len(events) == 2 # noqa: PLR2004 - создание и смена имени
|
||||
assert events[1].after_value == {"name": "ГСК Б"}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_list_and_detail_return_null_crm_id(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
group = await make_company_group(seeded)
|
||||
client = await auth_client(GO)
|
||||
gsk = (await _create(client, group.id, crm_id="3-9CGSK01")).json()
|
||||
|
||||
listed = await client.get(f"{GROUPS}/{group.id}/related-groups")
|
||||
assert [row["crm_id"] for row in listed.json()] == [None]
|
||||
assert (await client.get(f"{RELATED}/{gsk['id']}")).json()["crm_id"] is None
|
||||
@ -83,6 +83,7 @@ async def test_create_gsk_and_gsz_codes_and_audit(auth_client: AuthClientFactory
|
||||
"kind": "gsk",
|
||||
"name": "ГСК Бисквит",
|
||||
"created_date": "2025-01-15",
|
||||
"crm_id": None,
|
||||
"members_count": 0,
|
||||
}
|
||||
assert gsz_body["created_date"] is None
|
||||
|
||||
@ -152,7 +152,13 @@ SHALL установить `company_group_id = NULL`, при `CLOSE` — ещё
|
||||
`DIFFERENT_COMPANY_GROUP`); если у ЮЛ уже есть текущее членство в группе того же вида — 409 `MEMBER_OF_OTHER_GROUP`
|
||||
(с `related_group_id`); повтор в той же группе — 409 `MEMBER_EXISTS`. `DELETE …/members/{legal_entity_id}` SHALL закрывать
|
||||
членство (`valid_to`, `is_current = false`). Все операции пишут `MEMBERSHIP_CHANGE`. `GET …/related-groups` и
|
||||
`GET …/related-groups/{id}` SHALL отдавать группы с числом текущих членов и списком членов.
|
||||
`GET …/related-groups/{id}` SHALL отдавать группы с числом текущих членов и списком членов. Тела `POST` и `PATCH` SHALL
|
||||
принимать необязательный `crm_id` («ID ГСК/ГСЗ в CRM» форм 51-03/51-04/77-02) и отбрасывать его: колонки у `related_group`
|
||||
нет (arm-core #112); в ответах `crm_id` всегда `null`, в аудит и в список изменений `PATCH` не попадает.
|
||||
|
||||
#### Scenario: `crm_id` ГСК принимается и не сохраняется
|
||||
- **WHEN** ГСК создаётся с `crm_id = "3-9CGSK01"`, затем `PATCH` передаёт только `crm_id`
|
||||
- **THEN** создание — 201 с `crm_id = null`; `PATCH` — 200 без изменений и без события аудита
|
||||
|
||||
#### Scenario: Одновременно в ГСК и ГСЗ
|
||||
- **WHEN** ЮЛ добавлено в ГСК своей ГК, затем в ГСЗ той же ГК
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user