import uuid from collections.abc import AsyncIterator from datetime import datetime from typing import Any from sqlalchemy import ( JSON, BigInteger, Boolean, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import INET, UUID from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column SCHEMA = "han_app" class Base(DeclarativeBase): type_annotation_map = {dict[str, Any]: JSON} class Common: id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) record_status: Mapped[str] = mapped_column(String(1), default="A", server_default="A") status_changed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) status_change_reason: Mapped[str | None] = mapped_column(String(255)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) updater_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) class UserIdentity(Common, Base): __tablename__ = "user_identities" __table_args__ = (Index("ix_user_identities_phone", "phone_number"), {"schema": SCHEMA}) keycloak_sub: Mapped[str] = mapped_column(String(255), unique=True) phone_number: Mapped[str] = mapped_column(String(32)) last_login_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) class ClientProfile(Common, Base): __tablename__ = "client_profiles" __table_args__ = ({"schema": SCHEMA},) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey(f"{SCHEMA}.user_identities.id", ondelete="RESTRICT"), unique=True ) bitrix_contact_id: Mapped[str | None] = mapped_column(String(64)) full_name: Mapped[str | None] = mapped_column(String(255)) citizenship: Mapped[str | None] = mapped_column(String(128)) russian_phone: Mapped[str | None] = mapped_column(String(32)) foreign_phone: Mapped[str | None] = mapped_column(String(32)) email: Mapped[str | None] = mapped_column(String(320)) source_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class UserConsent(Common, Base): __tablename__ = "user_consents" __table_args__ = ( UniqueConstraint("user_id", "consent_type", "document_version"), CheckConstraint("consent_type IN ('personal_data','user_agreement','marketing')"), {"schema": SCHEMA}, ) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey(f"{SCHEMA}.user_identities.id", ondelete="RESTRICT") ) ux_session_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) consent_type: Mapped[str] = mapped_column(String(32)) document_version: Mapped[str] = mapped_column(String(64)) accepted: Mapped[bool] = mapped_column(Boolean) accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) client_ip: Mapped[str | None] = mapped_column(INET) user_agent_hash: Mapped[str | None] = mapped_column(String(64)) class UxSession(Common, Base): __tablename__ = "ux_sessions" __table_args__ = ( CheckConstraint("start_reason IN ('first_launch','cold_start','idle_timeout')"), Index("ix_ux_sessions_user_started", "user_id", "started_at"), {"schema": SCHEMA}, ) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey(f"{SCHEMA}.user_identities.id", ondelete="RESTRICT") ) start_reason: Mapped[str] = mapped_column(String(32)) platform: Mapped[str] = mapped_column(String(32)) app_version: Mapped[str] = mapped_column(String(64)) device_id_hash: Mapped[str | None] = mapped_column(String(64)) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) class Dialog(Common, Base): __tablename__ = "dialogs" __table_args__ = ( CheckConstraint("status IN ('open','waiting_for_company','waiting_for_client','closed')"), Index("ix_dialogs_user_updated", "user_id", "updated_at", "id"), {"schema": SCHEMA}, ) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey(f"{SCHEMA}.user_identities.id", ondelete="RESTRICT") ) status: Mapped[str] = mapped_column(String(32), default="open") last_message_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class Message(Common, Base): __tablename__ = "messages" __table_args__ = ( CheckConstraint("sender_type IN ('client','company')"), CheckConstraint("content_kind IN ('text','file')"), CheckConstraint("safety_status IN ('pending','allowed','blocked','needs_review')"), CheckConstraint( "delivery_status IN ('accepted','processing','delivered','rejected','failed')" ), Index("ix_messages_dialog_created", "dialog_id", "created_at", "id"), UniqueConstraint("dialog_id", "client_idempotency_key"), {"schema": SCHEMA}, ) dialog_id: Mapped[uuid.UUID] = mapped_column( ForeignKey(f"{SCHEMA}.dialogs.id", ondelete="RESTRICT") ) sender_type: Mapped[str] = mapped_column(String(16)) content_kind: Mapped[str] = mapped_column(String(16)) text: Mapped[str] = mapped_column(Text, default="") safety_status: Mapped[str] = mapped_column(String(16)) delivery_status: Mapped[str] = mapped_column(String(16)) external_message_id: Mapped[str | None] = mapped_column(String(255)) client_idempotency_key: Mapped[str | None] = mapped_column(String(128)) occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) class MessageAttachment(Common, Base): __tablename__ = "message_attachments" __table_args__ = ( CheckConstraint("direction IN ('client_upload','company_inbound')"), CheckConstraint("scan_status IN ('pending','clean','infected','failed')"), CheckConstraint("size_bytes > 0"), {"schema": SCHEMA}, ) dialog_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{SCHEMA}.dialogs.id")) message_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey(f"{SCHEMA}.messages.id"), unique=True ) owner_user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{SCHEMA}.user_identities.id")) direction: Mapped[str] = mapped_column(String(32)) original_file_name: Mapped[str] = mapped_column(String(255)) safe_file_name: Mapped[str] = mapped_column(String(255)) mime_type: Mapped[str] = mapped_column(String(128)) size_bytes: Mapped[int] = mapped_column(BigInteger) checksum_sha256: Mapped[str | None] = mapped_column(String(64)) scan_status: Mapped[str] = mapped_column(String(16), default="pending") storage_bucket: Mapped[str] = mapped_column(String(255)) object_key: Mapped[str] = mapped_column(String(1024)) quarantine_object_key: Mapped[str | None] = mapped_column(String(1024)) upload_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class Document(Common, Base): __tablename__ = "documents" __table_args__ = (UniqueConstraint("storage_bucket", "object_key"), {"schema": SCHEMA}) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{SCHEMA}.user_identities.id")) name: Mapped[str] = mapped_column(String(255)) mime_type: Mapped[str] = mapped_column(String(128)) size_bytes: Mapped[int] = mapped_column(BigInteger) checksum_sha256: Mapped[str] = mapped_column(String(64)) storage_bucket: Mapped[str] = mapped_column(String(255)) object_key: Mapped[str] = mapped_column(String(1024)) sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) class SafetyTask(Base): __tablename__ = "safety_tasks" __table_args__ = ({"schema": SCHEMA},) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) task_id: Mapped[str] = mapped_column(String(255), unique=True) message_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{SCHEMA}.messages.id"), unique=True) attachment_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) quarantine_object_key: Mapped[str | None] = mapped_column(String(1024)) status: Mapped[str] = mapped_column(String(16)) deadline_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) next_poll_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) attempt_count: Mapped[int] = mapped_column(Integer, default=0) last_error_code: Mapped[str | None] = mapped_column(String(64)) locked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) locked_by: Mapped[str | None] = mapped_column(String(128)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class DeliveryOutbox(Base): __tablename__ = "delivery_outbox" __table_args__ = ({"schema": SCHEMA},) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) message_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{SCHEMA}.messages.id"), unique=True) external_chat_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) payload_json: Mapped[dict[str, Any]] = mapped_column(JSON) status: Mapped[str] = mapped_column(String(16), default="pending") attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) locked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) locked_by: Mapped[str | None] = mapped_column(String(128)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class IdempotencyRecord(Base): __tablename__ = "idempotency_records" __table_args__ = ( UniqueConstraint("scope", "user_id", "idempotency_key"), {"schema": SCHEMA}, ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) scope: Mapped[str] = mapped_column(String(128)) user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) idempotency_key: Mapped[str] = mapped_column(String(128)) request_fingerprint: Mapped[str] = mapped_column(String(64)) status: Mapped[str] = mapped_column(String(16)) response_status: Mapped[int | None] = mapped_column(Integer) response_body_json: Mapped[dict[str, Any] | None] = mapped_column(JSON) resource_type: Mapped[str | None] = mapped_column(String(64)) resource_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class OpenLinesInboxReceipt(Base): __tablename__ = "openlines_inbox_receipts" __table_args__ = ( UniqueConstraint("event_id"), UniqueConstraint("external_chat_id", "bitrix_message_id"), {"schema": SCHEMA}, ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) event_id: Mapped[str] = mapped_column(String(255)) external_chat_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) bitrix_message_id: Mapped[str | None] = mapped_column(String(255)) event_type: Mapped[str] = mapped_column(String(32)) payload_fingerprint: Mapped[str] = mapped_column(String(64)) status: Mapped[str] = mapped_column(String(16)) message_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class AuditEvent(Base): __tablename__ = "audit_events" __table_args__ = ({"schema": SCHEMA},) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) event_type: Mapped[str] = mapped_column(String(128)) actor_type: Mapped[str] = mapped_column(String(32)) user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) ux_session_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) request_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str | None] = mapped_column(String(64)) resource_type: Mapped[str | None] = mapped_column(String(64)) resource_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) ip: Mapped[str | None] = mapped_column(INET) user_agent_hash: Mapped[str | None] = mapped_column(String(64)) outcome: Mapped[str] = mapped_column(String(32)) metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class AppSetting(Base): __tablename__ = "app_settings" __table_args__ = ({"schema": SCHEMA},) setting_key: Mapped[str] = mapped_column(String(255), primary_key=True) setting_value: Mapped[str] = mapped_column(Text) value_type: Mapped[str] = mapped_column(String(32)) is_public: Mapped[bool] = mapped_column(Boolean, default=False) description: Mapped[str | None] = mapped_column(Text) record_status: Mapped[str] = mapped_column(String(1), default="A") updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class TextResource(Common, Base): __tablename__ = "text_resources" __table_args__ = ( UniqueConstraint("mnemonic", "locale"), {"schema": SCHEMA}, ) mnemonic: Mapped[str] = mapped_column(String(255)) locale: Mapped[str] = mapped_column(String(16), default="ru") text_value: Mapped[str] = mapped_column(Text) sort_order: Mapped[int] = mapped_column(Integer, default=0) class PopularQuestion(Common, Base): __tablename__ = "popular_questions" __table_args__ = ( UniqueConstraint("mnemonic", "locale"), {"schema": SCHEMA}, ) mnemonic: Mapped[str] = mapped_column(String(255)) locale: Mapped[str] = mapped_column(String(16), default="ru") question_text: Mapped[str] = mapped_column(Text) sort_order: Mapped[int] = mapped_column(Integer, default=0) class SyncQueue(Base): __tablename__ = "sync_queue" __table_args__ = ({"schema": SCHEMA},) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) task_type: Mapped[str] = mapped_column(String(64)) entity_type: Mapped[str] = mapped_column(String(64)) entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) dedup_key: Mapped[str] = mapped_column(String(255), unique=True) payload_json: Mapped[dict[str, Any]] = mapped_column(JSON) status: Mapped[str] = mapped_column(String(16), default="pending") attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class EntityExternalMapping(Base): __tablename__ = "entity_external_mapping" __table_args__ = (UniqueConstraint("entity_type", "entity_id"), {"schema": SCHEMA}) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) entity_type: Mapped[str] = mapped_column(String(64)) entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) external_id: Mapped[str] = mapped_column(String(128)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Database: def __init__(self, url: str) -> None: self.engine: AsyncEngine = create_async_engine(url, pool_pre_ping=True) self.sessions = async_sessionmaker(self.engine, expire_on_commit=False) async def session(self) -> AsyncIterator[AsyncSession]: async with self.sessions() as session: yield session async def close(self) -> None: await self.engine.dispose()