from __future__ import annotations import uuid from datetime import UTC, datetime from sqlalchemy import ( BigInteger, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint, text, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column SCHEMA = "bitrix_local" def now() -> datetime: return datetime.now(UTC) class Base(DeclarativeBase): pass class Common: id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now, onupdate=now) record_status: Mapped[str] = mapped_column(String(1), default="A") status_changed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) status_change_reason: Mapped[str | None] = mapped_column(String(255)) class PortalInstallation(Common, Base): __tablename__ = "portal_installations" __table_args__ = ( Index( "uq_portal_active_domain", "domain", unique=True, postgresql_where=text("record_status = 'A'"), ), {"schema": SCHEMA}, ) member_id: Mapped[str] = mapped_column(String(128), unique=True) domain: Mapped[str] = mapped_column(String(255)) client_endpoint: Mapped[str] = mapped_column(String(1024)) access_ciphertext: Mapped[str] = mapped_column(Text) access_nonce: Mapped[str] = mapped_column(String(64)) refresh_ciphertext: Mapped[str] = mapped_column(Text) refresh_nonce: Mapped[str] = mapped_column(String(64)) application_ciphertext: Mapped[str] = mapped_column(Text) application_nonce: Mapped[str] = mapped_column(String(64)) key_version: Mapped[str] = mapped_column(String(32)) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) scope: Mapped[str | None] = mapped_column(Text) install_status: Mapped[str] = mapped_column(String(32), default="installed") setup_status: Mapped[str] = mapped_column(String(32), default="pending") last_refresh_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) class ConnectorSetup(Common, Base): __tablename__ = "connector_setup" __table_args__ = ( UniqueConstraint("portal_id", "connector_id", "line_id"), Index("ix_setup_retry", "next_retry_at", "attempt_count"), {"schema": SCHEMA}, ) portal_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey(f"{SCHEMA}.portal_installations.id") ) connector_id: Mapped[str] = mapped_column(String(64)) line_id: Mapped[str] = mapped_column(String(32)) registered: Mapped[bool] = mapped_column(default=False) activated: Mapped[bool] = mapped_column(default=False) bindings_json: Mapped[dict] = mapped_column(JSON, default=dict) desired_version: Mapped[str] = mapped_column(String(32), default="1") observed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) attempt_count: Mapped[int] = mapped_column(Integer, default=0) lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) class DialogSession(Common, Base): __tablename__ = "dialog_sessions" __table_args__ = ( Index( "uq_dialog_active_external_chat", "external_chat_id", unique=True, postgresql_where=text("record_status = 'A'"), ), Index("ix_dialog_bitrix_chat", "bitrix_chat_id"), Index("ix_dialog_session", "session_id"), Index("ix_dialog_status_updated", "status", "updated_at"), {"schema": SCHEMA}, ) external_chat_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) bitrix_chat_id: Mapped[int | None] = mapped_column(BigInteger) session_id: Mapped[str | None] = mapped_column(String(255)) portal_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey(f"{SCHEMA}.portal_installations.id") ) status: Mapped[str] = mapped_column(String(16), default="open") class InboxEvent(Common, Base): __tablename__ = "inbox_events" __table_args__ = ( UniqueConstraint("event_id"), Index( "uq_inbox_chat_message", "external_chat_id", "bitrix_message_id", unique=True, postgresql_where=text("bitrix_message_id IS NOT NULL"), ), Index("ix_inbox_worker", "status", "next_attempt_at"), {"schema": SCHEMA}, ) event_id: Mapped[str] = mapped_column(String(255)) event_type: Mapped[str] = mapped_column(String(64)) external_chat_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) bitrix_message_id: Mapped[str | None] = mapped_column(String(255)) payload_fingerprint: Mapped[str] = mapped_column(String(64)) normalized_json: Mapped[dict] = mapped_column(JSON) status: Mapped[str] = mapped_column(String(32), default="received") attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) api_ack_status: Mapped[str | None] = mapped_column(String(32)) delivery_ack_status: Mapped[str | None] = mapped_column(String(32)) last_error_code: Mapped[str | None] = mapped_column(String(64)) class OutboundMessage(Common, Base): __tablename__ = "outbound_messages" __table_args__ = (Index("ix_outbound_worker", "status", "next_attempt_at"), {"schema": SCHEMA}) message_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), unique=True) external_chat_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) request_fingerprint: Mapped[str] = mapped_column(String(64)) payload_json: Mapped[dict] = mapped_column(JSON) status: Mapped[str] = mapped_column(String(32), default="received") bitrix_message_id: Mapped[str | None] = mapped_column(String(255)) response_json: Mapped[dict | None] = mapped_column(JSON) attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) class DeliveryAckOutbox(Common, Base): __tablename__ = "delivery_ack_outbox" __table_args__ = (Index("ix_ack_worker", "status", "next_attempt_at"), {"schema": SCHEMA}) inbox_event_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey(f"{SCHEMA}.inbox_events.id"), unique=True ) payload_json: Mapped[dict] = mapped_column(JSON) status: Mapped[str] = mapped_column(String(32), default="pending") attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now) lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_error_code: Mapped[str | None] = mapped_column(String(64)) class InstallRun(Common, Base): __tablename__ = "install_runs" __table_args__ = ({"schema": SCHEMA},) portal_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True)) status: Mapped[str] = mapped_column(String(32)) result_json: Mapped[dict] = mapped_column(JSON, default=dict) class AuditEvent(Common, Base): __tablename__ = "audit_events" __table_args__ = (Index("ix_audit_created", "created_at"), {"schema": SCHEMA}) event_type: Mapped[str] = mapped_column(String(128)) actor_type: Mapped[str] = mapped_column(String(32), default="system") safe_details: Mapped[dict] = mapped_column(JSON, default=dict)