refactor(schemas): одна модель филиала в schemas/directories.py (R-13)

RegionalOfficeOut больше не дублируется в schemas/me.py: в schemas/directories.py
определены RegionalOfficeBrief (id, name, is_head_office — форма /me Среза 0) и
RegionalOfficeOut(RegionalOfficeBrief) с is_active. /me импортирует RegionalOfficeBrief,
тело ответа /me не меняется; в OpenAPI нет двух одноимённых компонентов.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-04 23:54:59 +04:00
parent 83746e694e
commit 4dd966b0b7
5 changed files with 18 additions and 18 deletions

View File

@ -9,7 +9,8 @@ from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from src.api.dependencies.auth import Principal, current_user from src.api.dependencies.auth import Principal, current_user
from src.schemas.me import MeResponse, RegionalOfficeOut from src.schemas.directories import RegionalOfficeBrief
from src.schemas.me import MeResponse
router = APIRouter(tags=["me"]) router = APIRouter(tags=["me"])
@ -24,7 +25,7 @@ async def get_me(principal: Annotated[Principal, Depends(current_user)]) -> MeRe
# пользователь найден по email, поэтому он заполнен; пустая строка — лишь для типов (колонка nullable) # пользователь найден по email, поэтому он заполнен; пустая строка — лишь для типов (колонка nullable)
email=user.email or "", email=user.email or "",
full_name=user.full_name, full_name=user.full_name,
regional_office=RegionalOfficeOut(id=office.id, name=office.name, is_head_office=office.is_head_office), regional_office=RegionalOfficeBrief.model_validate(office),
business_segment=user.business_segment, business_segment=user.business_segment,
roles=sorted(principal.roles), roles=sorted(principal.roles),
permissions=sorted(principal.permissions), permissions=sorted(principal.permissions),

View File

@ -19,14 +19,19 @@ def _blank_to_none(value: str | None) -> str | None:
return stripped or None return stripped or None
class RegionalOfficeOut(BaseModel): class RegionalOfficeBrief(BaseModel):
"""Филиал (`GET /directories/regional-offices`).""" """Филиал кратко: профиль `GET /me` (форма ответа Среза 0 — без `is_active`)."""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: uuid.UUID id: uuid.UUID
name: str name: str
is_head_office: bool is_head_office: bool
class RegionalOfficeOut(RegionalOfficeBrief):
"""Филиал целиком (`GET /directories/regional-offices`, пользователи администратора)."""
is_active: bool is_active: bool

View File

@ -5,14 +5,7 @@ import uuid
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from src.models.enums import BusinessSegment from src.models.enums import BusinessSegment
from src.schemas.directories import RegionalOfficeBrief
class RegionalOfficeOut(BaseModel):
"""Филиал пользователя."""
id: uuid.UUID
name: str
is_head_office: bool
class MeResponse(BaseModel): class MeResponse(BaseModel):
@ -23,7 +16,7 @@ class MeResponse(BaseModel):
id: uuid.UUID id: uuid.UUID
email: str email: str
full_name: str full_name: str
regional_office: RegionalOfficeOut regional_office: RegionalOfficeBrief
business_segment: BusinessSegment | None business_segment: BusinessSegment | None
roles: list[str] roles: list[str]
permissions: list[str] permissions: list[str]

View File

@ -34,7 +34,8 @@ from src.models.access import AppUser, Role, UserRole
from src.models.directories import RegionalOffice from src.models.directories import RegionalOffice
from src.models.enums import BusinessSegment from src.models.enums import BusinessSegment
from src.repositories.users import AmbiguousUserError, load_principal_data from src.repositories.users import AmbiguousUserError, load_principal_data
from src.schemas.me import MeResponse, RegionalOfficeOut from src.schemas.directories import RegionalOfficeBrief
from src.schemas.me import MeResponse
SECRET = "test-secret-for-auth-deps-0123456789abcdef" SECRET = "test-secret-for-auth-deps-0123456789abcdef"
OTHER_SECRET = "foreign-secret-for-auth-deps-0123456789ab" OTHER_SECRET = "foreign-secret-for-auth-deps-0123456789ab"
@ -487,7 +488,7 @@ async def test_repository_raises_on_ambiguity(seeded: AsyncSession) -> None:
def test_me_response_schema_serializes_enum_and_null_segment() -> None: def test_me_response_schema_serializes_enum_and_null_segment() -> None:
office = RegionalOfficeOut(id=uuid.uuid4(), name="ГО", is_head_office=True) office = RegionalOfficeBrief(id=uuid.uuid4(), name="ГО", is_head_office=True)
fields: dict[str, Any] = { fields: dict[str, Any] = {
"id": uuid.uuid4(), "id": uuid.uuid4(),
"email": "a@b.c", "email": "a@b.c",

View File

@ -36,7 +36,7 @@ from src.db.db import get_async_session
from src.models.directories import RegionalOffice from src.models.directories import RegionalOffice
from src.models.enums import AuditAction, AuditSection from src.models.enums import AuditAction, AuditSection
from src.schemas.common import ErrorDetail, Page, PageParams from src.schemas.common import ErrorDetail, Page, PageParams
from src.schemas.me import RegionalOfficeOut from src.schemas.directories import RegionalOfficeBrief
from src.services.audit import AuditService from src.services.audit import AuditService
from tests.pg_helpers import alembic_async from tests.pg_helpers import alembic_async
@ -141,8 +141,8 @@ def test_domain_error_extra_cannot_shadow_code_or_message() -> None:
def test_page_serializes() -> None: def test_page_serializes() -> None:
office = RegionalOfficeOut(id=CONFLICT_ID, name="ГО", is_head_office=True) office = RegionalOfficeBrief(id=CONFLICT_ID, name="ГО", is_head_office=True)
page = Page[RegionalOfficeOut](items=[office], total=7, limit=1, offset=3) page = Page[RegionalOfficeBrief](items=[office], total=7, limit=1, offset=3)
assert page.model_dump(mode="json") == { assert page.model_dump(mode="json") == {
"items": [{"id": str(CONFLICT_ID), "name": "ГО", "is_head_office": True}], "items": [{"id": str(CONFLICT_ID), "name": "ГО", "is_head_office": True}],
"total": 7, "total": 7,