import uuid from datetime import datetime from sqlalchemy import Boolean, DateTime, ForeignKey, Numeric, String, Text, UniqueConstraint, func from sqlalchemy.dialects.postgresql import JSON, UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from .database import Base class Organization(Base): __tablename__ = "organizations" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(160), nullable=False) slug: Mapped[str] = mapped_column(String(100), unique=True, index=True) home_region: Mapped[str] = mapped_column(String(32), default="ap-south-1") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) projects: Mapped[list["Project"]] = relationship(back_populates="organization") class Project(Base): __tablename__ = "projects" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) organization_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("organizations.id")) name: Mapped[str] = mapped_column(String(160), nullable=False) environment: Mapped[str] = mapped_column(String(24), default="test") status: Mapped[str] = mapped_column(String(24), default="active") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) organization: Mapped[Organization] = relationship(back_populates="projects") class ActivityEvent(Base): __tablename__ = "activity_events" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) project_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("projects.id"), index=True) event_type: Mapped[str] = mapped_column(String(80), index=True) title: Mapped[str] = mapped_column(String(200)) detail: Mapped[str] = mapped_column(String(500), default="") status: Mapped[str] = mapped_column(String(24), default="success") occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) class UserAccount(Base): """Canonical person record, independent of any login provider.""" __tablename__ = "user_accounts" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) email: Mapped[str] = mapped_column(String(320), unique=True, index=True) display_name: Mapped[str] = mapped_column(String(160)) avatar_url: Mapped[str | None] = mapped_column(String(1000), nullable=True) email_verified: Mapped[bool] = mapped_column(Boolean, default=False) status: Mapped[str] = mapped_column(String(24), default="active") 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() ) password_credential: Mapped["PasswordCredential | None"] = relationship( back_populates="user", cascade="all, delete-orphan", uselist=False ) oidc_identities: Mapped[list["OidcIdentity"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) kyc_application: Mapped["KycApplication | None"] = relationship( back_populates="user", cascade="all, delete-orphan", uselist=False ) dlt_application: Mapped["DltApplication | None"] = relationship( back_populates="user", cascade="all, delete-orphan", uselist=False ) sender_configs: Mapped[list["SenderConfig"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) template_configs: Mapped[list["TemplateConfig"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) api_keys: Mapped[list["ApiKey"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) urmiotp_projects: Mapped[list["UrmiOtpProject"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) campaigns: Mapped[list["Campaign"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) phone_numbers: Mapped[list["PhoneNumber"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) voice_calls: Mapped[list["VoiceCall"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) sms_message_logs: Mapped[list["SmsMessageLog"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) otp_message_logs: Mapped[list["OtpMessageLog"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) voice_socket_endpoints: Mapped[list["VoiceSocketEndpoint"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) sms_delivery_webhooks: Mapped[list["SmsDeliveryWebhook"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) agent_template_subscriptions: Mapped[list["AgentTemplateSubscription"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) agent_message_dispatches: Mapped[list["AgentMessageDispatchLog"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) urmiport_report_groups: Mapped[list["UrmiportReportGroup"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) urmiport_reports: Mapped[list["UrmiportReport"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) class PasswordCredential(Base): """First-party credential kept separate from the canonical user.""" __tablename__ = "password_credentials" user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), primary_key=True ) password_hash: Mapped[str] = mapped_column(String(500)) 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() ) user: Mapped[UserAccount] = relationship(back_populates="password_credential") class OidcIdentity(Base): """Provider identity; no provider claims are stored on the user table.""" __tablename__ = "oidc_identities" __table_args__ = ( UniqueConstraint("issuer", "subject", name="uq_oidc_issuer_subject"), UniqueConstraint("user_id", "issuer", name="uq_oidc_user_issuer"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) issuer: Mapped[str] = mapped_column(String(500)) subject: Mapped[str] = mapped_column(String(500)) provider_email: Mapped[str] = mapped_column(String(320)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) last_login_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[UserAccount] = relationship(back_populates="oidc_identities") class KycApplication(Base): __tablename__ = "kyc_applications" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), unique=True, index=True ) pan_number: Mapped[str | None] = mapped_column(String(10), nullable=True) gst_number: Mapped[str | None] = mapped_column(String(15), nullable=True) status: Mapped[str] = mapped_column(String(24), default="not_started", index=True) submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="kyc_application") documents: Mapped[list["KycDocument"]] = relationship( back_populates="application", cascade="all, delete-orphan" ) class KycDocument(Base): __tablename__ = "kyc_documents" __table_args__ = (UniqueConstraint("application_id", "document_type", name="uq_kyc_app_doc_type"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) application_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("kyc_applications.id", ondelete="CASCADE"), index=True ) document_type: Mapped[str] = mapped_column(String(40)) original_filename: Mapped[str | None] = mapped_column(String(255), nullable=True) stored_path: Mapped[str | None] = mapped_column(String(500), nullable=True) status: Mapped[str] = mapped_column(String(24), default="missing") review_note: Mapped[str | None] = mapped_column(String(500), nullable=True) uploaded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) application: Mapped[KycApplication] = relationship(back_populates="documents") class DltApplication(Base): __tablename__ = "dlt_applications" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), unique=True, index=True ) status: Mapped[str] = mapped_column(String(24), default="not_started", index=True) submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="dlt_application") documents: Mapped[list["DltDocument"]] = relationship( back_populates="application", cascade="all, delete-orphan" ) class DltDocument(Base): __tablename__ = "dlt_documents" __table_args__ = (UniqueConstraint("application_id", "document_type", name="uq_dlt_app_doc_type"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) application_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("dlt_applications.id", ondelete="CASCADE"), index=True ) document_type: Mapped[str] = mapped_column(String(40)) original_filename: Mapped[str | None] = mapped_column(String(255), nullable=True) stored_path: Mapped[str | None] = mapped_column(String(500), nullable=True) status: Mapped[str] = mapped_column(String(24), default="missing") review_note: Mapped[str | None] = mapped_column(String(500), nullable=True) uploaded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) application: Mapped[DltApplication] = relationship(back_populates="documents") class SenderConfig(Base): __tablename__ = "sender_configs" __table_args__ = (UniqueConstraint("user_id", "sender_id", name="uq_sender_user_sender_id"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) sender_id: Mapped[str] = mapped_column(String(11), index=True) sender_type: Mapped[str] = mapped_column(String(20)) purpose: Mapped[str] = mapped_column(String(500)) dlt_entity_id: Mapped[str] = mapped_column(String(50)) status: Mapped[str] = mapped_column(String(24), default="pending", index=True) review_note: Mapped[str | None] = mapped_column(String(500), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) user: Mapped[UserAccount] = relationship(back_populates="sender_configs") templates: Mapped[list["TemplateConfig"]] = relationship( back_populates="sender_config", cascade="all, delete-orphan" ) class TemplateConfig(Base): __tablename__ = "template_configs" __table_args__ = (UniqueConstraint("user_id", "template_name", name="uq_template_user_name"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) sender_config_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("sender_configs.id", ondelete="CASCADE"), index=True ) template_name: Mapped[str] = mapped_column(String(40), index=True) template_text: Mapped[str] = mapped_column(String(1000)) dlt_template_id: Mapped[str] = mapped_column(String(50)) status: Mapped[str] = mapped_column(String(24), default="pending", index=True) review_note: Mapped[str | None] = mapped_column(String(500), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) user: Mapped[UserAccount] = relationship(back_populates="template_configs") sender_config: Mapped[SenderConfig] = relationship(back_populates="templates") class ApiKey(Base): __tablename__ = "api_keys" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) name: Mapped[str] = mapped_column(String(60)) key_prefix: Mapped[str] = mapped_column(String(24)) key_hash: Mapped[str] = mapped_column(String(500)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) user: Mapped[UserAccount] = relationship(back_populates="api_keys") class UrmiOtpProject(Base): __tablename__ = "urmiotp_projects" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) project_name: Mapped[str] = mapped_column(String(30), unique=True, index=True) message_label: Mapped[str] = mapped_column(String(20), server_default="URMI") app_type: Mapped[str] = mapped_column(String(16)) app_url: Mapped[str] = mapped_column(String(1000)) purpose: Mapped[str] = mapped_column(String(1000)) terms_accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) status: Mapped[str] = mapped_column(String(24), default="pending", index=True) review_note: Mapped[str | None] = mapped_column(String(500), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) user: Mapped[UserAccount] = relationship(back_populates="urmiotp_projects") class Campaign(Base): __tablename__ = "campaigns" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) name: Mapped[str] = mapped_column(String(80)) sender_config_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("sender_configs.id", ondelete="CASCADE"), index=True ) template_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("template_configs.id", ondelete="CASCADE"), index=True ) number_column: Mapped[str] = mapped_column(String(80)) variable_columns: Mapped[list] = mapped_column(JSON) run_mode: Mapped[str] = mapped_column(String(20), index=True) scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) status: Mapped[str] = mapped_column(String(24), default="active", index=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="campaigns") sender_config: Mapped["SenderConfig"] = relationship() template: Mapped["TemplateConfig"] = relationship() csv_uploads: Mapped[list["CampaignCsvUpload"]] = relationship( back_populates="campaign", cascade="all, delete-orphan" ) class CampaignCsvUpload(Base): __tablename__ = "campaign_csv_uploads" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) campaign_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("campaigns.id", ondelete="CASCADE"), index=True ) original_filename: Mapped[str] = mapped_column(String(255)) stored_path: Mapped[str] = mapped_column(String(500)) total_rows: Mapped[int] = mapped_column(default=0) status: Mapped[str] = mapped_column(String(24), default="validated", index=True) run_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) run_completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) campaign: Mapped[Campaign] = relationship(back_populates="csv_uploads") class PhoneNumber(Base): __tablename__ = "phone_numbers" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) phone_number: Mapped[str] = mapped_column(String(20), unique=True, index=True) user_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("user_accounts.id", ondelete="SET NULL"), index=True, nullable=True ) inbound_price: Mapped[float] = mapped_column(Numeric(10, 4)) outbound_price: Mapped[float] = mapped_column(Numeric(10, 4)) monthly_number_price: Mapped[float] = mapped_column(Numeric(10, 2), server_default="0") currency: Mapped[str] = mapped_column(String(8), default="INR") status: Mapped[str] = mapped_column(String(20), default="available", index=True) acquisition_type: Mapped[str | None] = mapped_column(String(20), nullable=True) assigned_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[UserAccount | None] = relationship(back_populates="phone_numbers") voice_calls: Mapped[list["VoiceCall"]] = relationship(back_populates="phone_number") voice_socket_endpoint: Mapped["VoiceSocketEndpoint | None"] = relationship( back_populates="phone_number", cascade="all, delete-orphan", uselist=False ) sms_delivery_webhooks: Mapped[list["SmsDeliveryWebhook"]] = relationship( back_populates="phone_number", cascade="all, delete-orphan" ) class VoiceSocketEndpoint(Base): """Customer WebSocket destination associated with one owned phone number.""" __tablename__ = "voice_socket_endpoints" __table_args__ = ( UniqueConstraint("phone_number_id", name="uq_voice_socket_phone_number"), UniqueConstraint("user_id", "name", name="uq_voice_socket_user_name"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) phone_number_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("phone_numbers.id", ondelete="CASCADE"), index=True ) name: Mapped[str] = mapped_column(String(120)) url: Mapped[str] = mapped_column(String(500)) enabled: Mapped[bool] = mapped_column(Boolean, default=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="voice_socket_endpoints") phone_number: Mapped[PhoneNumber] = relationship(back_populates="voice_socket_endpoint") class SmsDeliveryWebhook(Base): """Named SMS delivery-report destination associated with an owned number.""" __tablename__ = "sms_delivery_webhooks" __table_args__ = ( UniqueConstraint("user_id", "name", name="uq_sms_webhook_user_name"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) phone_number_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("phone_numbers.id", ondelete="CASCADE"), index=True ) name: Mapped[str] = mapped_column(String(120)) url: Mapped[str] = mapped_column(String(500)) description: Mapped[str | None] = mapped_column(String(500), nullable=True) enabled: Mapped[bool] = mapped_column(Boolean, default=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="sms_delivery_webhooks") phone_number: Mapped[PhoneNumber] = relationship(back_populates="sms_delivery_webhooks") class VoiceCall(Base): __tablename__ = "voice_calls" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) phone_number_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("phone_numbers.id", ondelete="CASCADE"), index=True ) direction: Mapped[str] = mapped_column(String(16), index=True) from_did: Mapped[str] = mapped_column(String(20)) to_number: Mapped[str] = mapped_column(String(20)) status: Mapped[str] = mapped_column(String(24), default="initiated", index=True) provider_event_id: Mapped[str | None] = mapped_column(String(64), nullable=True) session_token: Mapped[str] = mapped_column(String(64), unique=True, index=True) stream_id: Mapped[str | None] = mapped_column(String(32), nullable=True) call_id: Mapped[str | None] = mapped_column(String(32), nullable=True) bridge_mode: Mapped[str] = mapped_column(String(16), default="console") upstream_ws: Mapped[str | None] = mapped_column(String(500), nullable=True) error_message: Mapped[str | None] = mapped_column(String(500), nullable=True) answered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[UserAccount] = relationship(back_populates="voice_calls") phone_number: Mapped[PhoneNumber] = relationship(back_populates="voice_calls") class SmsMessageLog(Base): __tablename__ = "sms_message_logs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) sender_id: Mapped[str] = mapped_column(String(11), index=True) to_number: Mapped[str] = mapped_column(String(20), index=True) template_name: Mapped[str] = mapped_column(String(40)) template_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) status: Mapped[str] = mapped_column(String(24), default="sent", index=True) job_id: Mapped[str | None] = mapped_column(String(64), nullable=True) message_id: Mapped[str | None] = mapped_column(String(64), nullable=True) error_message: Mapped[str | None] = mapped_column(String(500), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) user: Mapped[UserAccount] = relationship(back_populates="sms_message_logs") class OtpMessageLog(Base): __tablename__ = "otp_message_logs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) project_name: Mapped[str] = mapped_column(String(30), index=True) template_type: Mapped[str] = mapped_column(String(40), index=True) to_number: Mapped[str] = mapped_column(String(20), index=True) status: Mapped[str] = mapped_column(String(24), default="sent", index=True) job_id: Mapped[str | None] = mapped_column(String(64), nullable=True) message_id: Mapped[str | None] = mapped_column(String(64), nullable=True) error_message: Mapped[str | None] = mapped_column(String(500), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) user: Mapped[UserAccount] = relationship(back_populates="otp_message_logs") class WhatsAppWebhookEvent(Base): """Inbound webhook payloads from Meta WhatsApp Business Platform.""" __tablename__ = "whatsapp_webhook_events" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) waba_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) phone_number_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) field: Mapped[str] = mapped_column(String(80), index=True) event_type: Mapped[str] = mapped_column(String(80), index=True) from_wa_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True) message_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) status: Mapped[str | None] = mapped_column(String(32), nullable=True) payload: Mapped[dict] = mapped_column(JSON) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) class WhatsAppMessage(Base): """Inbound and outbound WhatsApp messages for Urmi's business number.""" __tablename__ = "whatsapp_messages" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) direction: Mapped[str] = mapped_column(String(16), index=True) wa_message_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) from_number: Mapped[str] = mapped_column(String(32), index=True) to_number: Mapped[str] = mapped_column(String(32), index=True) body: Mapped[str | None] = mapped_column(String(4096), nullable=True) message_type: Mapped[str] = mapped_column(String(24), default="text") status: Mapped[str] = mapped_column(String(24), default="received", index=True) payload: Mapped[dict | None] = mapped_column(JSON, nullable=True) user_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), nullable=True, index=True) phone_number_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) contact_name: Mapped[str | None] = mapped_column(String(200), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) class WhatsAppMetaConnection(Base): """A customer's Meta authorization. Tokens stay server-side.""" __tablename__ = "whatsapp_meta_connections" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True) status: Mapped[str] = mapped_column(String(24), default="pending", index=True) meta_business_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) business_name: Mapped[str | None] = mapped_column(String(200), nullable=True) granted_scopes: Mapped[str | None] = mapped_column(String(500), nullable=True) access_token: Mapped[str | None] = mapped_column(Text, nullable=True) meta_app_id: Mapped[str | None] = mapped_column(String(64), nullable=True) meta_app_secret: Mapped[str | None] = mapped_column(Text, nullable=True) last_error: Mapped[str | None] = mapped_column(String(500), nullable=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(), onupdate=func.now() ) disconnected_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) accounts: Mapped[list["WhatsAppBusinessAccount"]] = relationship(back_populates="connection") class WhatsAppBusinessAccount(Base): """One WhatsApp Business Account (WABA) owned by a single Urmi user.""" __tablename__ = "whatsapp_business_accounts" __table_args__ = (UniqueConstraint("waba_id", name="uq_whatsapp_waba_id"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True) connection_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("whatsapp_meta_connections.id", ondelete="CASCADE"), index=True ) waba_id: Mapped[str] = mapped_column(String(64), index=True) name: Mapped[str] = mapped_column(String(200), default="WhatsApp Business") status: Mapped[str] = mapped_column(String(24), default="provisioning", index=True) messaging_status: Mapped[str] = mapped_column(String(24), default="unavailable") management_status: Mapped[str] = mapped_column(String(24), default="unavailable") templates_status: Mapped[str] = mapped_column(String(24), default="unavailable") webhooks_status: Mapped[str] = mapped_column(String(24), default="unavailable") template_count: Mapped[int] = mapped_column(default=0) last_error: Mapped[str | None] = mapped_column(String(500), nullable=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(), onupdate=func.now() ) connection: Mapped[WhatsAppMetaConnection] = relationship(back_populates="accounts") phones: Mapped[list["WhatsAppPhoneAsset"]] = relationship( back_populates="account", cascade="all, delete-orphan" ) class WhatsAppPhoneAsset(Base): __tablename__ = "whatsapp_phone_assets" __table_args__ = (UniqueConstraint("phone_number_id", name="uq_whatsapp_phone_number_id"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True) account_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("whatsapp_business_accounts.id", ondelete="CASCADE"), index=True ) phone_number_id: Mapped[str] = mapped_column(String(64), index=True) display_phone: Mapped[str] = mapped_column(String(32), default="") verified_name: Mapped[str | None] = mapped_column(String(200), nullable=True) quality: Mapped[str | None] = mapped_column(String(24), nullable=True) status: Mapped[str] = mapped_column(String(32), default="pending") code_verification_status: Mapped[str | None] = mapped_column(String(32), nullable=True) two_step_pin: Mapped[str | None] = mapped_column(String(6), nullable=True) account: Mapped[WhatsAppBusinessAccount] = relationship(back_populates="phones") class WhatsAppMessageTemplate(Base): """A WhatsApp message template owned by one WABA. Meta remains the source of approval.""" __tablename__ = "whatsapp_message_templates" __table_args__ = (UniqueConstraint("waba_id", "name", "language", name="uq_wa_template_name_lang"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True) account_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("whatsapp_business_accounts.id", ondelete="CASCADE"), index=True ) waba_id: Mapped[str] = mapped_column(String(64), index=True) phone_number_id: Mapped[str | None] = mapped_column(String(64), nullable=True) meta_template_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) name: Mapped[str] = mapped_column(String(512), index=True) language: Mapped[str] = mapped_column(String(16), default="en_US") category: Mapped[str] = mapped_column(String(32), default="MARKETING") subtype: Mapped[str] = mapped_column(String(40), default="default") status: Mapped[str] = mapped_column(String(32), default="PENDING", index=True) quality: Mapped[str | None] = mapped_column(String(24), nullable=True) rejected_reason: Mapped[str | None] = mapped_column(Text, nullable=True) parameter_format: Mapped[str] = mapped_column(String(16), default="POSITIONAL") definition: Mapped[dict] = mapped_column(JSON, default=dict) components: Mapped[list] = mapped_column(JSON, default=list) 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() ) class WhatsAppConnectionAudit(Base): __tablename__ = "whatsapp_connection_audits" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True) connection_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True, index=True) waba_id: Mapped[str | None] = mapped_column(String(64), nullable=True) event: Mapped[str] = mapped_column(String(40), index=True) detail: Mapped[str] = mapped_column(String(500), default="") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) class AgentMessageTemplate(Base): """Platform WhatsApp agent message templates available for user subscription.""" __tablename__ = "agent_message_templates" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) slug: Mapped[str] = mapped_column(String(64), unique=True, index=True) name: Mapped[str] = mapped_column(String(120)) description: Mapped[str] = mapped_column(String(1000)) header_text: Mapped[str | None] = mapped_column(String(120), nullable=True) body_preview: Mapped[str] = mapped_column(String(2000)) footer_text: Mapped[str | None] = mapped_column(String(120), nullable=True) meta_template_name: Mapped[str] = mapped_column(String(80), index=True) language: Mapped[str] = mapped_column(String(12), default="en") category: Mapped[str] = mapped_column(String(32), default="utility") parameters: Mapped[list] = mapped_column(JSON, default=list) button_label: Mapped[str | None] = mapped_column(String(40), nullable=True) button_url_base: Mapped[str | None] = mapped_column(String(500), nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) subscriptions: Mapped[list["AgentTemplateSubscription"]] = relationship( back_populates="template", cascade="all, delete-orphan" ) class AgentTemplateSubscription(Base): __tablename__ = "agent_template_subscriptions" __table_args__ = (UniqueConstraint("user_id", "template_id", name="uq_agent_template_subscription"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) template_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True ) subscribed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[UserAccount] = relationship(back_populates="agent_template_subscriptions") template: Mapped[AgentMessageTemplate] = relationship(back_populates="subscriptions") class AgentTemplateTestLog(Base): __tablename__ = "agent_template_test_logs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) template_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True ) recipient: Mapped[str] = mapped_column(String(20)) success: Mapped[bool] = mapped_column(Boolean, default=True) tested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) class AgentMessageDispatchLog(Base): __tablename__ = "agent_message_dispatch_logs" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) template_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("agent_message_templates.id", ondelete="CASCADE"), index=True ) recipient: Mapped[str] = mapped_column(String(20), index=True) parameters: Mapped[dict] = mapped_column(JSON, default=dict) wa_message_id: Mapped[str | None] = mapped_column(String(128), nullable=True) status: Mapped[str] = mapped_column(String(24), default="sent", index=True) source: Mapped[str] = mapped_column(String(16), default="api", index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) user: Mapped[UserAccount] = relationship(back_populates="agent_message_dispatches") template: Mapped[AgentMessageTemplate] = relationship() class UrmiportReportGroup(Base): __tablename__ = "urmiport_report_groups" __table_args__ = (UniqueConstraint("user_id", "slug", name="uq_urmiport_group_user_slug"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) slug: Mapped[str] = mapped_column(String(50), index=True) display_name: Mapped[str | None] = mapped_column(String(120), nullable=True) default_access: Mapped[str] = mapped_column(String(16), default="public", index=True) password_hash: Mapped[str | None] = mapped_column(String(500), nullable=True) id_mode: Mapped[str] = mapped_column(String(16), default="auto") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[UserAccount] = relationship(back_populates="urmiport_report_groups") reports: Mapped[list["UrmiportReport"]] = relationship( back_populates="group", cascade="all, delete-orphan" ) class UrmiportReport(Base): __tablename__ = "urmiport_reports" __table_args__ = (UniqueConstraint("group_id", "report_id", name="uq_urmiport_group_report_id"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("user_accounts.id", ondelete="CASCADE"), index=True ) group_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("urmiport_report_groups.id", ondelete="CASCADE"), index=True ) report_id: Mapped[str] = mapped_column(String(120), index=True) html_body: Mapped[str] = mapped_column(Text) access_mode: Mapped[str] = mapped_column(String(16), default="public", index=True) password_hash: Mapped[str | None] = mapped_column(String(500), nullable=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(), onupdate=func.now() ) user: Mapped[UserAccount] = relationship(back_populates="urmiport_reports") group: Mapped[UrmiportReportGroup] = relationship(back_populates="reports") class MetaConnection(Base): """One Meta Login connection per customer. The user token never leaves the server.""" __tablename__ = "meta_connections" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id"), unique=True, index=True) meta_user_id: Mapped[str] = mapped_column(String(64), default="") meta_user_name: Mapped[str] = mapped_column(String(255), default="") granted_scopes: Mapped[str] = mapped_column(Text, default="") access_token: Mapped[str] = mapped_column(Text, default="") token_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) status: Mapped[str] = mapped_column(String(32), default="disconnected") selected_page_id: Mapped[str] = mapped_column(String(64), default="") selected_page_name: Mapped[str] = mapped_column(String(255), default="") selected_ad_account_id: Mapped[str] = mapped_column(String(64), default="") selected_ad_account_name: Mapped[str] = mapped_column(String(255), default="") last_error: Mapped[str] = mapped_column(Text, default="") last_graph_code: Mapped[int | None] = mapped_column(nullable=True) connected_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class MetaAuditEvent(Base): """Customer-visible record of Meta connect, publish, and status changes.""" __tablename__ = "meta_audit_events" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("user_accounts.id"), index=True) action: Mapped[str] = mapped_column(String(64)) asset_type: Mapped[str] = mapped_column(String(32), default="") asset_label: Mapped[str] = mapped_column(String(255), default="") asset_id: Mapped[str] = mapped_column(String(64), default="") detail: Mapped[str] = mapped_column(Text, default="") status: Mapped[str] = mapped_column(String(16), default="success") error_message: Mapped[str] = mapped_column(Text, default="") graph_code: Mapped[int | None] = mapped_column(nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class PlatformSetting(Base): """Runtime overrides for platform credentials (admin-editable).""" __tablename__ = "platform_settings" key: Mapped[str] = mapped_column(String(64), primary_key=True) value: Mapped[str] = mapped_column(Text, default="") updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() )