from __future__ import annotations from datetime import UTC, datetime from decimal import Decimal from typing import Any from uuid import uuid4 from sqlalchemy import ( JSON, Boolean, DateTime, ForeignKey, Index, Integer, Numeric, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base def new_id() -> str: return str(uuid4()) def now_utc() -> datetime: return datetime.now(UTC) class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, onupdate=now_utc, nullable=False, ) class Tenant(Base, TimestampMixin): __tablename__ = "crm_tenants" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) slug: Mapped[str] = mapped_column(String(80), unique=True, nullable=False) name: Mapped[str] = mapped_column(String(160), nullable=False) mode: Mapped[str] = mapped_column(String(24), default="standalone", nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class User(Base, TimestampMixin): __tablename__ = "crm_users" __table_args__ = ( UniqueConstraint("tenant_id", "email", name="uq_crm_users_tenant_email"), Index("ix_crm_users_tenant_role", "tenant_id", "role"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) email: Mapped[str] = mapped_column(String(255), nullable=False) full_name: Mapped[str] = mapped_column(String(160), nullable=False) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column(String(24), default="member", nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) tenant: Mapped[Tenant] = relationship() class Organization(Base, TimestampMixin): __tablename__ = "crm_organizations" __table_args__ = ( Index("ix_crm_organizations_tenant_name", "tenant_id", "name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) name: Mapped[str] = mapped_column(String(200), nullable=False) legal_name: Mapped[str | None] = mapped_column(String(240)) website: Mapped[str | None] = mapped_column(String(500)) primary_email: Mapped[str | None] = mapped_column(String(255)) phone: Mapped[str | None] = mapped_column(String(80)) industry: Mapped[str | None] = mapped_column(String(120)) address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) owner_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) owner: Mapped[User | None] = relationship() contacts: Mapped[list[Contact]] = relationship(back_populates="organization") class Contact(Base, TimestampMixin): __tablename__ = "crm_contacts" __table_args__ = ( UniqueConstraint( "tenant_id", "primary_email", name="uq_crm_contacts_tenant_primary_email", ), Index("ix_crm_contacts_tenant_name", "tenant_id", "last_name", "first_name"), Index("ix_crm_contacts_tenant_score", "tenant_id", "score"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) first_name: Mapped[str] = mapped_column(String(100), nullable=False) last_name: Mapped[str] = mapped_column(String(100), default="", nullable=False) job_title: Mapped[str | None] = mapped_column(String(160)) primary_email: Mapped[str | None] = mapped_column(String(255)) emails: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False) phones: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False) lifecycle_stage: Mapped[str] = mapped_column(String(40), default="lead", nullable=False) lead_source: Mapped[str | None] = mapped_column(String(120)) score: Mapped[int] = mapped_column(Integer, default=0, nullable=False) organization_id: Mapped[str | None] = mapped_column( ForeignKey("crm_organizations.id", ondelete="SET NULL"), index=True, ) owner_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) organization: Mapped[Organization | None] = relationship(back_populates="contacts") owner: Mapped[User | None] = relationship() @property def name(self) -> str: return f"{self.first_name} {self.last_name}".strip() class Pipeline(Base, TimestampMixin): __tablename__ = "crm_pipelines" __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_crm_pipelines_tenant_name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) name: Mapped[str] = mapped_column(String(140), nullable=False) is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) stages: Mapped[list[Stage]] = relationship( back_populates="pipeline", cascade="all, delete-orphan", order_by="Stage.position", ) class Stage(Base, TimestampMixin): __tablename__ = "crm_stages" __table_args__ = ( UniqueConstraint( "pipeline_id", "name", name="uq_crm_stages_pipeline_name", ), Index("ix_crm_stages_pipeline_position", "pipeline_id", "position"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) pipeline_id: Mapped[str] = mapped_column( ForeignKey("crm_pipelines.id", ondelete="CASCADE"), nullable=False, ) name: Mapped[str] = mapped_column(String(140), nullable=False) position: Mapped[int] = mapped_column(Integer, nullable=False) probability: Mapped[int] = mapped_column(Integer, default=0, nullable=False) color: Mapped[str] = mapped_column(String(16), default="#64748b", nullable=False) pipeline: Mapped[Pipeline] = relationship(back_populates="stages") class LeadSource(Base, TimestampMixin): __tablename__ = "crm_lead_sources" __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_crm_lead_sources_tenant_name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) name: Mapped[str] = mapped_column(String(120), nullable=False) class LeadType(Base, TimestampMixin): __tablename__ = "crm_lead_types" __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_crm_lead_types_tenant_name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) name: Mapped[str] = mapped_column(String(120), nullable=False) class Lead(Base, TimestampMixin): __tablename__ = "crm_leads" __table_args__ = ( Index("ix_crm_leads_tenant_stage", "tenant_id", "stage_id", "position"), Index("ix_crm_leads_tenant_status", "tenant_id", "status"), Index("ix_crm_leads_tenant_owner", "tenant_id", "owner_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) title: Mapped[str] = mapped_column(String(240), nullable=False) description: Mapped[str | None] = mapped_column(Text) value: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False) status: Mapped[str] = mapped_column(String(24), default="open", nullable=False) score: Mapped[int] = mapped_column(Integer, default=0, nullable=False) position: Mapped[int] = mapped_column(Integer, default=0, nullable=False) lost_reason: Mapped[str | None] = mapped_column(Text) expected_close_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) contact_id: Mapped[str | None] = mapped_column( ForeignKey("crm_contacts.id", ondelete="SET NULL"), index=True, ) organization_id: Mapped[str | None] = mapped_column( ForeignKey("crm_organizations.id", ondelete="SET NULL"), index=True, ) owner_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) pipeline_id: Mapped[str] = mapped_column( ForeignKey("crm_pipelines.id", ondelete="RESTRICT"), nullable=False, ) stage_id: Mapped[str] = mapped_column( ForeignKey("crm_stages.id", ondelete="RESTRICT"), nullable=False, ) source_id: Mapped[str | None] = mapped_column( ForeignKey("crm_lead_sources.id", ondelete="SET NULL"), ) type_id: Mapped[str | None] = mapped_column( ForeignKey("crm_lead_types.id", ondelete="SET NULL"), ) attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) contact: Mapped[Contact | None] = relationship() organization: Mapped[Organization | None] = relationship() owner: Mapped[User | None] = relationship() pipeline: Mapped[Pipeline] = relationship() stage: Mapped[Stage] = relationship() source: Mapped[LeadSource | None] = relationship() lead_type: Mapped[LeadType | None] = relationship() class Activity(Base, TimestampMixin): __tablename__ = "crm_activities" __table_args__ = ( Index("ix_crm_activities_tenant_due", "tenant_id", "is_done", "due_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) activity_type: Mapped[str] = mapped_column(String(32), nullable=False) title: Mapped[str] = mapped_column(String(240), nullable=False) details: Mapped[str | None] = mapped_column(Text) starts_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) is_done: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) owner_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) contact_id: Mapped[str | None] = mapped_column( ForeignKey("crm_contacts.id", ondelete="CASCADE"), index=True, ) organization_id: Mapped[str | None] = mapped_column( ForeignKey("crm_organizations.id", ondelete="CASCADE"), index=True, ) lead_id: Mapped[str | None] = mapped_column( ForeignKey("crm_leads.id", ondelete="CASCADE"), index=True, ) additional: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) owner: Mapped[User | None] = relationship() contact: Mapped[Contact | None] = relationship() organization: Mapped[Organization | None] = relationship() lead: Mapped[Lead | None] = relationship() class Product(Base, TimestampMixin): __tablename__ = "crm_products" __table_args__ = ( UniqueConstraint("tenant_id", "sku", name="uq_crm_products_tenant_sku"), Index("ix_crm_products_tenant_name", "tenant_id", "name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) sku: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text) quantity: Mapped[int] = mapped_column(Integer, default=0, nullable=False) price: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) class Quote(Base, TimestampMixin): __tablename__ = "crm_quotes" __table_args__ = ( UniqueConstraint("tenant_id", "number", name="uq_crm_quotes_tenant_number"), Index("ix_crm_quotes_tenant_status", "tenant_id", "status"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) number: Mapped[str] = mapped_column(String(80), nullable=False) subject: Mapped[str] = mapped_column(String(240), nullable=False) description: Mapped[str | None] = mapped_column(Text) status: Mapped[str] = mapped_column(String(32), default="draft", nullable=False) currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False) billing_address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) shipping_address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) discount_amount: Mapped[Decimal] = mapped_column( Numeric(14, 2), default=0, nullable=False, ) tax_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) adjustment_amount: Mapped[Decimal] = mapped_column( Numeric(14, 2), default=0, nullable=False, ) subtotal: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) grand_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) contact_id: Mapped[str | None] = mapped_column( ForeignKey("crm_contacts.id", ondelete="SET NULL"), ) organization_id: Mapped[str | None] = mapped_column( ForeignKey("crm_organizations.id", ondelete="SET NULL"), ) lead_id: Mapped[str | None] = mapped_column( ForeignKey("crm_leads.id", ondelete="SET NULL"), ) owner_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) items: Mapped[list[QuoteItem]] = relationship( back_populates="quote", cascade="all, delete-orphan", ) class QuoteItem(Base, TimestampMixin): __tablename__ = "crm_quote_items" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) quote_id: Mapped[str] = mapped_column( ForeignKey("crm_quotes.id", ondelete="CASCADE"), nullable=False, ) product_id: Mapped[str | None] = mapped_column( ForeignKey("crm_products.id", ondelete="SET NULL"), ) name: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text) quantity: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=1, nullable=False) unit_price: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) tax_rate: Mapped[Decimal] = mapped_column(Numeric(6, 3), default=0, nullable=False) line_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False) quote: Mapped[Quote] = relationship(back_populates="items") class AuditEvent(Base): __tablename__ = "crm_audit_events" __table_args__ = ( Index("ix_crm_audit_tenant_entity", "tenant_id", "entity_type", "entity_id"), Index("ix_crm_audit_tenant_created", "tenant_id", "created_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, ) actor_id: Mapped[str | None] = mapped_column( ForeignKey("crm_users.id", ondelete="SET NULL"), ) action: Mapped[str] = mapped_column(String(100), nullable=False) entity_type: Mapped[str] = mapped_column(String(80), nullable=False) entity_id: Mapped[str] = mapped_column(String(80), nullable=False) payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, nullable=False, ) class IntegrationEvent(Base): __tablename__ = "crm_integration_events" __table_args__ = ( Index("ix_crm_integration_events_delivery", "status", "available_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, ) topic: Mapped[str] = mapped_column(String(140), nullable=False) payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) status: Mapped[str] = mapped_column(String(24), default="pending", nullable=False) attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False) available_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, nullable=False, ) delivered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, nullable=False, ) class IntegrationCredential(Base, TimestampMixin): __tablename__ = "crm_integration_credentials" __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_crm_integration_key_name"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, index=True, ) name: Mapped[str] = mapped_column(String(120), nullable=False) key_prefix: Mapped[str] = mapped_column(String(20), nullable=False) key_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class ExternalLink(Base, TimestampMixin): __tablename__ = "crm_external_links" __table_args__ = ( UniqueConstraint( "tenant_id", "provider", "entity_type", "external_id", name="uq_crm_external_links_provider_entity", ), Index("ix_crm_external_links_local", "tenant_id", "entity_type", "entity_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, ) provider: Mapped[str] = mapped_column(String(80), nullable=False) entity_type: Mapped[str] = mapped_column(String(80), nullable=False) entity_id: Mapped[str] = mapped_column(String(36), nullable=False) external_id: Mapped[str] = mapped_column(String(255), nullable=False) metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) class IdempotencyRecord(Base): __tablename__ = "crm_idempotency_records" __table_args__ = ( UniqueConstraint( "tenant_id", "scope", "idempotency_key", name="uq_crm_idempotency_scope_key", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id) tenant_id: Mapped[str] = mapped_column( ForeignKey("crm_tenants.id", ondelete="CASCADE"), nullable=False, ) scope: Mapped[str] = mapped_column(String(100), nullable=False) idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False) response_code: Mapped[int] = mapped_column(Integer, nullable=False) response_body: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=now_utc, nullable=False, )