urmi-ai/backend/app/models.py
2026-10-02 21:59:13 +05:30

621 lines
32 KiB
Python

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"
)
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)
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 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()
)