"""Phase 6.1 supporting CRM entities — lookups, tags, addresses, notes, attachments, audit.""" from __future__ import annotations import uuid from datetime import date, datetime from decimal import Decimal from sqlalchemy import ( Boolean, Date, DateTime, Index, Integer, Numeric, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( AddressType, AttachmentKind, AuditAction, CrmEntityType, CustomFieldType, GUID, TagScope, ) class LeadSource(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "lead_sources" code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(150), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_lead_sources_tenant_code"), ) class LeadStatusDefinition(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "lead_statuses" code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(150), nullable=False) position: Mapped[int] = mapped_column(Integer, default=0, nullable=False) is_terminal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_converted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) color: Mapped[str | None] = mapped_column(String(20), nullable=True) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_lead_statuses_tenant_code"), ) class ContactType(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "contact_types" code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(150), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint("tenant_id", "code", name="uq_contact_types_tenant_code"), ) class OrganizationType(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "organization_types" code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(150), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_organization_types_tenant_code" ), ) class OrganizationIndustry(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "organization_industries" code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(150), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_organization_industries_tenant_code" ), ) class LeadAssignment(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "lead_assignments" lead_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) assigned_user_id: Mapped[str] = mapped_column(String(100), nullable=False) assigned_by: Mapped[str | None] = mapped_column(String(100), nullable=True) assigned_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False ) note: Mapped[str | None] = mapped_column(Text, nullable=True) is_current: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( Index("ix_lead_assignments_lead", "tenant_id", "lead_id"), Index("ix_lead_assignments_user", "tenant_id", "assigned_user_id"), ) class CrmTag(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "crm_tags" name: Mapped[str] = mapped_column(String(100), nullable=False) color: Mapped[str | None] = mapped_column(String(20), nullable=True) group_name: Mapped[str | None] = mapped_column(String(100), nullable=True) scope: Mapped[TagScope] = mapped_column(default=TagScope.ANY, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_crm_tags_tenant_name"), ) class LeadTag(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "lead_tags" lead_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) tag_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) __table_args__ = ( UniqueConstraint("tenant_id", "lead_id", "tag_id", name="uq_lead_tags"), Index("ix_lead_tags_lead", "tenant_id", "lead_id"), ) class ContactTag(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "contact_tags" contact_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) tag_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) __table_args__ = ( UniqueConstraint("tenant_id", "contact_id", "tag_id", name="uq_contact_tags"), Index("ix_contact_tags_contact", "tenant_id", "contact_id"), ) class OrganizationTag(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "organization_tags" organization_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) tag_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) __table_args__ = ( UniqueConstraint( "tenant_id", "organization_id", "tag_id", name="uq_organization_tags" ), Index("ix_organization_tags_org", "tenant_id", "organization_id"), ) class CrmCustomField(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "crm_custom_fields" entity_type: Mapped[CrmEntityType] = mapped_column(nullable=False) field_key: Mapped[str] = mapped_column(String(100), nullable=False) label: Mapped[str] = mapped_column(String(150), nullable=False) field_type: Mapped[CustomFieldType] = mapped_column(nullable=False) is_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) options: Mapped[list | None] = mapped_column(JSON, nullable=True) validation_rules: Mapped[dict | None] = mapped_column(JSON, nullable=True) position: Mapped[int] = mapped_column(Integer, default=0, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) __table_args__ = ( UniqueConstraint( "tenant_id", "entity_type", "field_key", name="uq_crm_custom_fields_key", ), Index("ix_crm_custom_fields_entity", "tenant_id", "entity_type"), ) class CrmCustomFieldValue(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "crm_custom_field_values" custom_field_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) entity_type: Mapped[CrmEntityType] = mapped_column(nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) value_text: Mapped[str | None] = mapped_column(Text, nullable=True) value_number: Mapped[Decimal | None] = mapped_column(Numeric(18, 4), nullable=True) value_boolean: Mapped[bool | None] = mapped_column(Boolean, nullable=True) value_date: Mapped[date | None] = mapped_column(Date, nullable=True) value_json: Mapped[list | dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "custom_field_id", "entity_id", name="uq_crm_custom_field_values", ), Index( "ix_crm_custom_field_values_entity", "tenant_id", "entity_type", "entity_id", ), ) class CrmAddress( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "crm_addresses" entity_type: Mapped[CrmEntityType] = mapped_column(nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) address_type: Mapped[AddressType] = mapped_column(nullable=False) label: Mapped[str | None] = mapped_column(String(100), nullable=True) line1: Mapped[str] = mapped_column(String(255), nullable=False) line2: Mapped[str | None] = mapped_column(String(255), nullable=True) city: Mapped[str | None] = mapped_column(String(100), nullable=True) state: Mapped[str | None] = mapped_column(String(100), nullable=True) postal_code: Mapped[str | None] = mapped_column(String(30), nullable=True) country: Mapped[str | None] = mapped_column(String(100), nullable=True) latitude: Mapped[Decimal | None] = mapped_column(Numeric(10, 7), nullable=True) longitude: Mapped[Decimal | None] = mapped_column(Numeric(10, 7), nullable=True) is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) __table_args__ = ( Index( "ix_crm_addresses_entity", "tenant_id", "entity_type", "entity_id", ), ) class CrmNote( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): __tablename__ = "crm_notes" entity_type: Mapped[CrmEntityType] = mapped_column(nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False) body_format: Mapped[str] = mapped_column(String(20), default="richtext", nullable=False) version: Mapped[int] = mapped_column(Integer, default=1, nullable=False) is_pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) __table_args__ = ( Index( "ix_crm_notes_entity", "tenant_id", "entity_type", "entity_id", ), ) class CrmNoteVersion(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "crm_note_versions" note_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) version: Mapped[int] = mapped_column(Integer, nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False) created_by: Mapped[str | None] = mapped_column(String(100), nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "note_id", "version", name="uq_crm_note_versions" ), Index("ix_crm_note_versions_note", "tenant_id", "note_id"), ) class CrmAttachment( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Reference to File Storage service — no binary bytes stored in CRM.""" __tablename__ = "crm_attachments" entity_type: Mapped[CrmEntityType] = mapped_column(nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) file_storage_id: Mapped[str] = mapped_column(String(100), nullable=False) file_name: Mapped[str] = mapped_column(String(255), nullable=False) content_type: Mapped[str | None] = mapped_column(String(150), nullable=True) size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True) kind: Mapped[AttachmentKind] = mapped_column( default=AttachmentKind.DOCUMENT, nullable=False ) description: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index( "ix_crm_attachments_entity", "tenant_id", "entity_type", "entity_id", ), Index("ix_crm_attachments_file", "tenant_id", "file_storage_id"), ) class CrmAuditLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin): __tablename__ = "crm_audit" entity_type: Mapped[str] = mapped_column(String(50), nullable=False) entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) action: Mapped[AuditAction] = mapped_column(nullable=False) actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True) changes: Mapped[dict | None] = mapped_column(JSON, nullable=True) message: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index( "ix_crm_audit_entity", "tenant_id", "entity_type", "entity_id", ), Index("ix_crm_audit_action", "tenant_id", "action"), )