- модели LegalEntity.kpp / LegalEntityHistory.kpp (последней колонкой), миграция 0005_dbml_v5_17 (upgrade/downgrade); - domain/kpp.py: 9 символов (4 цифры, 2 цифры/латинские A-Z, 3 цифры), у ИП запрещён, иначе 422 INVALID_KPP; - схемы Create/Update/Out/Detail/HistoryOut, PATCH пишет снимок history и аудит before/after с kpp, поиск q по КПП; - импорт справочника: необязательная колонка КПП (синоним kpp), код INVALID_KPP, update обновляет непустой КПП; - dev-seed: необязательный ключ kpp (значений в данных нет, не выдумываем); - сверка с DBML, Makefile, README, CLAUDE.md, openspec/config.yaml — на v5.17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
76 lines
3.9 KiB
Python
76 lines
3.9 KiB
Python
"""Часть 02 DBML v5.17 «Пользователи и доступ»: пользователи, роли, права как данные (`effective_permissions`)."""
|
||
|
||
import datetime
|
||
import uuid
|
||
|
||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, text
|
||
from sqlalchemy.dialects.postgresql import UUID
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from .base import Base, UUIDPrimaryMixin
|
||
from .enums import BusinessSegment, PermissionAction, PermissionObject, pg_enum
|
||
|
||
|
||
class AppUser(UUIDPrimaryMixin, Base):
|
||
"""Пользователь; аутентификация внешняя (JWT РАИСА, email → `app_user`)."""
|
||
|
||
__tablename__ = "app_user"
|
||
|
||
external_id: Mapped[str | None] = mapped_column(String, unique=True)
|
||
email: Mapped[str | None] = mapped_column(String, unique=True)
|
||
full_name: Mapped[str] = mapped_column(String, nullable=False)
|
||
employee_no: Mapped[str | None] = mapped_column(String)
|
||
regional_office_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("regional_office.id"), nullable=False
|
||
)
|
||
business_segment: Mapped[BusinessSegment | None] = mapped_column(pg_enum("business_segment"))
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
|
||
created_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
|
||
class Role(UUIDPrimaryMixin, Base):
|
||
"""Роль; полномочия — через `role_permission`, код роли в бизнес-логике не проверяется."""
|
||
|
||
__tablename__ = "role"
|
||
|
||
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text)
|
||
is_system: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false"))
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
|
||
|
||
|
||
class Permission(UUIDPrimaryMixin, Base):
|
||
"""Справочник прав «объект × действие» (seed, привязан к коду)."""
|
||
|
||
__tablename__ = "permission"
|
||
__table_args__ = (Index("uq_permission_object_type_action", "object_type", "action", unique=True),)
|
||
|
||
code: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||
object_type: Mapped[PermissionObject] = mapped_column(pg_enum("permission_object"), nullable=False)
|
||
action: Mapped[PermissionAction] = mapped_column(pg_enum("permission_action"), nullable=False)
|
||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(Text)
|
||
|
||
|
||
class RolePermission(Base):
|
||
"""Матрица доступов как данные; составной PK `(role_id, permission_id)`, суррогатного `id` нет."""
|
||
|
||
__tablename__ = "role_permission"
|
||
|
||
role_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("role.id"), primary_key=True)
|
||
permission_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("permission.id"), primary_key=True)
|
||
granted_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"))
|
||
granted_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
|
||
class UserRole(UUIDPrimaryMixin, Base):
|
||
"""Назначение ролей пользователю (M:N, current-state); история — `audit_event(USER_ROLE_CHANGE)`."""
|
||
|
||
__tablename__ = "user_role"
|
||
__table_args__ = (Index("uq_user_role_user_id_role_id", "user_id", "role_id", unique=True),)
|
||
|
||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("app_user.id"), nullable=False)
|
||
role_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("role.id"), nullable=False)
|