"""Bot-owned tables (bot_* prefix, idempotent DDL in db_init.py). These are separate from the ERP schema on purpose: the bot is an autonomous container and owns its own state (links, drafts, message audit log). """ from datetime import datetime from sqlalchemy import BigInteger, DateTime, Index, Integer, String, Text, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass def _uuid_pk() -> Mapped[str]: return mapped_column(UUID(as_uuid=False), primary_key=True, server_default=func.gen_random_uuid()) def _timestamptz(**kwargs) -> Mapped[datetime]: return mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False, **kwargs) class BotLink(Base): """Bale chat ↔ ERP user mapping (established via Casdoor login).""" __tablename__ = "bot_links" id: Mapped[str] = _uuid_pk() chat_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False, index=True) user_id: Mapped[str | None] = mapped_column(UUID(as_uuid=False)) # users.id casdoor_username: Mapped[str | None] = mapped_column(Text) display_name: Mapped[str | None] = mapped_column(Text) role: Mapped[str | None] = mapped_column(String(32)) created_at: Mapped[datetime] = _timestamptz() updated_at: Mapped[datetime] = _timestamptz(onupdate=func.now()) class BotDraft(Base): """A prepared-but-not-committed ERP action (quote / supplier / material).""" __tablename__ = "bot_drafts" __table_args__ = ( Index("ix_bot_drafts_chat_status", "chat_id", "status"), Index("ix_bot_drafts_review_msg", "review_message_id"), Index("ix_bot_drafts_source_msg", "source_message_id"), ) id: Mapped[str] = _uuid_pk() chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) kind: Mapped[str] = mapped_column(String(16), nullable=False) # quote|supplier|material payload: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) missing: Mapped[dict | None] = mapped_column(JSONB) status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") # active|committed|cancelled|expired|failed source: Mapped[str | None] = mapped_column(String(16)) # text|voice source_message_id: Mapped[int | None] = mapped_column(BigInteger) review_message_id: Mapped[int | None] = mapped_column(BigInteger) last_bale_update_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = _timestamptz() updated_at: Mapped[datetime] = _timestamptz(onupdate=func.now()) class BotMessage(Base): """Audit log of every inbound/outbound message (who asked for what).""" __tablename__ = "bot_messages" id: Mapped[str] = _uuid_pk() chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) direction: Mapped[str] = mapped_column(String(4), nullable=False) # in|out kind: Mapped[str] = mapped_column(String(16), nullable=False, default="text") # text|voice|callback|error body: Mapped[str | None] = mapped_column(Text) payload: Mapped[dict | None] = mapped_column(JSONB) created_at: Mapped[datetime] = _timestamptz() class BotMeta(Base): """Tiny KV store (e.g. last processed Bale update_id across restarts).""" __tablename__ = "bot_meta" key: Mapped[str] = mapped_column(String(64), primary_key=True) value: Mapped[str | None] = mapped_column(Text) updated_at: Mapped[datetime] = _timestamptz(onupdate=func.now())